add dependency
This commit is contained in:
parent
e1f48721a4
commit
04ef2c4158
|
|
@ -0,0 +1,44 @@
|
|||
package themes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var defaultSymbolsFinished = []rune("█◎▭☢≈⋮─━═=╸")
|
||||
var defaultTheme = []rune("█ ||")
|
||||
|
||||
// New returns a new theme
|
||||
func New(symbols ...string) []string {
|
||||
symbols = append(symbols, make([]string, 4-len(symbols))...)
|
||||
for i, _ := range symbols {
|
||||
if len(symbols[i]) == 0 {
|
||||
symbols[i] = string(defaultTheme[i])
|
||||
}
|
||||
}
|
||||
return []string{
|
||||
symbols[0],
|
||||
symbols[1],
|
||||
symbols[2],
|
||||
symbols[3],
|
||||
}
|
||||
}
|
||||
|
||||
// NewDefault returns nth theme from default themes
|
||||
func NewDefault(n uint8) ([]string, error) {
|
||||
if n > uint8(len(defaultSymbolsFinished)) {
|
||||
return nil, errors.New("n must be less than defined themes")
|
||||
}
|
||||
return New(string(defaultSymbolsFinished[n])), nil
|
||||
}
|
||||
|
||||
func NewFromRunes(symbols []rune) ([]string, error) {
|
||||
if len(symbols) != 4 {
|
||||
return []string{}, errors.New("symbols lenght must be exactly 4")
|
||||
}
|
||||
return []string{
|
||||
string(symbols[0]),
|
||||
string(symbols[1]),
|
||||
string(symbols[2]),
|
||||
string(symbols[3]),
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package themes
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNewDefault(t *testing.T) {
|
||||
if _, err := NewDefault(uint8(len(defaultSymbolsFinished) + 1)); err == nil {
|
||||
t.Error("should have an error if n > default themes")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
// +build appengine
|
||||
|
||||
package logrus
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
func checkIfTerminal(w io.Writer) bool {
|
||||
return true
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
// +build !appengine
|
||||
|
||||
package logrus
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
func checkIfTerminal(w io.Writer) bool {
|
||||
switch v := w.(type) {
|
||||
case *os.File:
|
||||
return terminal.IsTerminal(int(v.Fd()))
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,283 @@
|
|||
// 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 argon2 implements the key derivation function Argon2.
|
||||
// Argon2 was selected as the winner of the Password Hashing Competition and can
|
||||
// be used to derive cryptographic keys from passwords.
|
||||
//
|
||||
// For a detailed specification of Argon2 see [1].
|
||||
//
|
||||
// If you aren't sure which function you need, use Argon2id (IDKey) and
|
||||
// the parameter recommendations for your scenario.
|
||||
//
|
||||
//
|
||||
// Argon2i
|
||||
//
|
||||
// Argon2i (implemented by Key) is the side-channel resistant version of Argon2.
|
||||
// It uses data-independent memory access, which is preferred for password
|
||||
// hashing and password-based key derivation. Argon2i requires more passes over
|
||||
// memory than Argon2id to protect from trade-off attacks. The recommended
|
||||
// parameters (taken from [2]) for non-interactive operations are time=3 and to
|
||||
// use the maximum available memory.
|
||||
//
|
||||
//
|
||||
// Argon2id
|
||||
//
|
||||
// Argon2id (implemented by IDKey) is a hybrid version of Argon2 combining
|
||||
// Argon2i and Argon2d. It uses data-independent memory access for the first
|
||||
// half of the first iteration over the memory and data-dependent memory access
|
||||
// for the rest. Argon2id is side-channel resistant and provides better brute-
|
||||
// force cost savings due to time-memory tradeoffs than Argon2i. The recommended
|
||||
// parameters for non-interactive operations (taken from [2]) are time=1 and to
|
||||
// use the maximum available memory.
|
||||
//
|
||||
// [1] https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf
|
||||
// [2] https://tools.ietf.org/html/draft-irtf-cfrg-argon2-03#section-9.3
|
||||
package argon2
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/crypto/blake2b"
|
||||
)
|
||||
|
||||
// The Argon2 version implemented by this package.
|
||||
const Version = 0x13
|
||||
|
||||
const (
|
||||
argon2d = iota
|
||||
argon2i
|
||||
argon2id
|
||||
)
|
||||
|
||||
// Key derives a key from the password, salt, and cost parameters using Argon2i
|
||||
// returning a byte slice of length keyLen that can be used as cryptographic
|
||||
// key. The CPU cost and parallism degree must be greater than zero.
|
||||
//
|
||||
// For example, you can get a derived key for e.g. AES-256 (which needs a
|
||||
// 32-byte key) by doing: `key := argon2.Key([]byte("some password"), salt, 3,
|
||||
// 32*1024, 4, 32)`
|
||||
//
|
||||
// The draft RFC recommends[2] time=3, and memory=32*1024 is a sensible number.
|
||||
// If using that amount of memory (32 MB) is not possible in some contexts then
|
||||
// the time parameter can be increased to compensate.
|
||||
//
|
||||
// The time parameter specifies the number of passes over the memory and the
|
||||
// memory parameter specifies the size of the memory in KiB. For example
|
||||
// memory=32*1024 sets the memory cost to ~32 MB. The number of threads can be
|
||||
// adjusted to the number of available CPUs. The cost parameters should be
|
||||
// increased as memory latency and CPU parallelism increases. Remember to get a
|
||||
// good random salt.
|
||||
func Key(password, salt []byte, time, memory uint32, threads uint8, keyLen uint32) []byte {
|
||||
return deriveKey(argon2i, password, salt, nil, nil, time, memory, threads, keyLen)
|
||||
}
|
||||
|
||||
// IDKey derives a key from the password, salt, and cost parameters using
|
||||
// Argon2id returning a byte slice of length keyLen that can be used as
|
||||
// cryptographic key. The CPU cost and parallism degree must be greater than
|
||||
// zero.
|
||||
//
|
||||
// For example, you can get a derived key for e.g. AES-256 (which needs a
|
||||
// 32-byte key) by doing: `key := argon2.IDKey([]byte("some password"), salt, 1,
|
||||
// 64*1024, 4, 32)`
|
||||
//
|
||||
// The draft RFC recommends[2] time=1, and memory=64*1024 is a sensible number.
|
||||
// If using that amount of memory (64 MB) is not possible in some contexts then
|
||||
// the time parameter can be increased to compensate.
|
||||
//
|
||||
// The time parameter specifies the number of passes over the memory and the
|
||||
// memory parameter specifies the size of the memory in KiB. For example
|
||||
// memory=64*1024 sets the memory cost to ~64 MB. The number of threads can be
|
||||
// adjusted to the numbers of available CPUs. The cost parameters should be
|
||||
// increased as memory latency and CPU parallelism increases. Remember to get a
|
||||
// good random salt.
|
||||
func IDKey(password, salt []byte, time, memory uint32, threads uint8, keyLen uint32) []byte {
|
||||
return deriveKey(argon2id, password, salt, nil, nil, time, memory, threads, keyLen)
|
||||
}
|
||||
|
||||
func deriveKey(mode int, password, salt, secret, data []byte, time, memory uint32, threads uint8, keyLen uint32) []byte {
|
||||
if time < 1 {
|
||||
panic("argon2: number of rounds too small")
|
||||
}
|
||||
if threads < 1 {
|
||||
panic("argon2: parallelism degree too low")
|
||||
}
|
||||
h0 := initHash(password, salt, secret, data, time, memory, uint32(threads), keyLen, mode)
|
||||
|
||||
memory = memory / (syncPoints * uint32(threads)) * (syncPoints * uint32(threads))
|
||||
if memory < 2*syncPoints*uint32(threads) {
|
||||
memory = 2 * syncPoints * uint32(threads)
|
||||
}
|
||||
B := initBlocks(&h0, memory, uint32(threads))
|
||||
processBlocks(B, time, memory, uint32(threads), mode)
|
||||
return extractKey(B, memory, uint32(threads), keyLen)
|
||||
}
|
||||
|
||||
const (
|
||||
blockLength = 128
|
||||
syncPoints = 4
|
||||
)
|
||||
|
||||
type block [blockLength]uint64
|
||||
|
||||
func initHash(password, salt, key, data []byte, time, memory, threads, keyLen uint32, mode int) [blake2b.Size + 8]byte {
|
||||
var (
|
||||
h0 [blake2b.Size + 8]byte
|
||||
params [24]byte
|
||||
tmp [4]byte
|
||||
)
|
||||
|
||||
b2, _ := blake2b.New512(nil)
|
||||
binary.LittleEndian.PutUint32(params[0:4], threads)
|
||||
binary.LittleEndian.PutUint32(params[4:8], keyLen)
|
||||
binary.LittleEndian.PutUint32(params[8:12], memory)
|
||||
binary.LittleEndian.PutUint32(params[12:16], time)
|
||||
binary.LittleEndian.PutUint32(params[16:20], uint32(Version))
|
||||
binary.LittleEndian.PutUint32(params[20:24], uint32(mode))
|
||||
b2.Write(params[:])
|
||||
binary.LittleEndian.PutUint32(tmp[:], uint32(len(password)))
|
||||
b2.Write(tmp[:])
|
||||
b2.Write(password)
|
||||
binary.LittleEndian.PutUint32(tmp[:], uint32(len(salt)))
|
||||
b2.Write(tmp[:])
|
||||
b2.Write(salt)
|
||||
binary.LittleEndian.PutUint32(tmp[:], uint32(len(key)))
|
||||
b2.Write(tmp[:])
|
||||
b2.Write(key)
|
||||
binary.LittleEndian.PutUint32(tmp[:], uint32(len(data)))
|
||||
b2.Write(tmp[:])
|
||||
b2.Write(data)
|
||||
b2.Sum(h0[:0])
|
||||
return h0
|
||||
}
|
||||
|
||||
func initBlocks(h0 *[blake2b.Size + 8]byte, memory, threads uint32) []block {
|
||||
var block0 [1024]byte
|
||||
B := make([]block, memory)
|
||||
for lane := uint32(0); lane < threads; lane++ {
|
||||
j := lane * (memory / threads)
|
||||
binary.LittleEndian.PutUint32(h0[blake2b.Size+4:], lane)
|
||||
|
||||
binary.LittleEndian.PutUint32(h0[blake2b.Size:], 0)
|
||||
blake2bHash(block0[:], h0[:])
|
||||
for i := range B[j+0] {
|
||||
B[j+0][i] = binary.LittleEndian.Uint64(block0[i*8:])
|
||||
}
|
||||
|
||||
binary.LittleEndian.PutUint32(h0[blake2b.Size:], 1)
|
||||
blake2bHash(block0[:], h0[:])
|
||||
for i := range B[j+1] {
|
||||
B[j+1][i] = binary.LittleEndian.Uint64(block0[i*8:])
|
||||
}
|
||||
}
|
||||
return B
|
||||
}
|
||||
|
||||
func processBlocks(B []block, time, memory, threads uint32, mode int) {
|
||||
lanes := memory / threads
|
||||
segments := lanes / syncPoints
|
||||
|
||||
processSegment := func(n, slice, lane uint32, wg *sync.WaitGroup) {
|
||||
var addresses, in, zero block
|
||||
if mode == argon2i || (mode == argon2id && n == 0 && slice < syncPoints/2) {
|
||||
in[0] = uint64(n)
|
||||
in[1] = uint64(lane)
|
||||
in[2] = uint64(slice)
|
||||
in[3] = uint64(memory)
|
||||
in[4] = uint64(time)
|
||||
in[5] = uint64(mode)
|
||||
}
|
||||
|
||||
index := uint32(0)
|
||||
if n == 0 && slice == 0 {
|
||||
index = 2 // we have already generated the first two blocks
|
||||
if mode == argon2i || mode == argon2id {
|
||||
in[6]++
|
||||
processBlock(&addresses, &in, &zero)
|
||||
processBlock(&addresses, &addresses, &zero)
|
||||
}
|
||||
}
|
||||
|
||||
offset := lane*lanes + slice*segments + index
|
||||
var random uint64
|
||||
for index < segments {
|
||||
prev := offset - 1
|
||||
if index == 0 && slice == 0 {
|
||||
prev += lanes // last block in lane
|
||||
}
|
||||
if mode == argon2i || (mode == argon2id && n == 0 && slice < syncPoints/2) {
|
||||
if index%blockLength == 0 {
|
||||
in[6]++
|
||||
processBlock(&addresses, &in, &zero)
|
||||
processBlock(&addresses, &addresses, &zero)
|
||||
}
|
||||
random = addresses[index%blockLength]
|
||||
} else {
|
||||
random = B[prev][0]
|
||||
}
|
||||
newOffset := indexAlpha(random, lanes, segments, threads, n, slice, lane, index)
|
||||
processBlockXOR(&B[offset], &B[prev], &B[newOffset])
|
||||
index, offset = index+1, offset+1
|
||||
}
|
||||
wg.Done()
|
||||
}
|
||||
|
||||
for n := uint32(0); n < time; n++ {
|
||||
for slice := uint32(0); slice < syncPoints; slice++ {
|
||||
var wg sync.WaitGroup
|
||||
for lane := uint32(0); lane < threads; lane++ {
|
||||
wg.Add(1)
|
||||
go processSegment(n, slice, lane, &wg)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func extractKey(B []block, memory, threads, keyLen uint32) []byte {
|
||||
lanes := memory / threads
|
||||
for lane := uint32(0); lane < threads-1; lane++ {
|
||||
for i, v := range B[(lane*lanes)+lanes-1] {
|
||||
B[memory-1][i] ^= v
|
||||
}
|
||||
}
|
||||
|
||||
var block [1024]byte
|
||||
for i, v := range B[memory-1] {
|
||||
binary.LittleEndian.PutUint64(block[i*8:], v)
|
||||
}
|
||||
key := make([]byte, keyLen)
|
||||
blake2bHash(key, block[:])
|
||||
return key
|
||||
}
|
||||
|
||||
func indexAlpha(rand uint64, lanes, segments, threads, n, slice, lane, index uint32) uint32 {
|
||||
refLane := uint32(rand>>32) % threads
|
||||
if n == 0 && slice == 0 {
|
||||
refLane = lane
|
||||
}
|
||||
m, s := 3*segments, ((slice+1)%syncPoints)*segments
|
||||
if lane == refLane {
|
||||
m += index
|
||||
}
|
||||
if n == 0 {
|
||||
m, s = slice*segments, 0
|
||||
if slice == 0 || lane == refLane {
|
||||
m += index
|
||||
}
|
||||
}
|
||||
if index == 0 || lane == refLane {
|
||||
m--
|
||||
}
|
||||
return phi(rand, uint64(m), uint64(s), refLane, lanes)
|
||||
}
|
||||
|
||||
func phi(rand, m, s uint64, lane, lanes uint32) uint32 {
|
||||
p := rand & 0xFFFFFFFF
|
||||
p = (p * p) >> 32
|
||||
p = (p * m) >> 32
|
||||
return lane*lanes + uint32((s+m-(p+1))%uint64(lanes))
|
||||
}
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
// 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 argon2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
genKatPassword = []byte{
|
||||
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
|
||||
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
|
||||
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
|
||||
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
|
||||
}
|
||||
genKatSalt = []byte{0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02}
|
||||
genKatSecret = []byte{0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03}
|
||||
genKatAAD = []byte{0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04}
|
||||
)
|
||||
|
||||
func TestArgon2(t *testing.T) {
|
||||
defer func(sse4 bool) { useSSE4 = sse4 }(useSSE4)
|
||||
|
||||
if useSSE4 {
|
||||
t.Log("SSE4.1 version")
|
||||
testArgon2i(t)
|
||||
testArgon2d(t)
|
||||
testArgon2id(t)
|
||||
useSSE4 = false
|
||||
}
|
||||
t.Log("generic version")
|
||||
testArgon2i(t)
|
||||
testArgon2d(t)
|
||||
testArgon2id(t)
|
||||
}
|
||||
|
||||
func testArgon2d(t *testing.T) {
|
||||
want := []byte{
|
||||
0x51, 0x2b, 0x39, 0x1b, 0x6f, 0x11, 0x62, 0x97,
|
||||
0x53, 0x71, 0xd3, 0x09, 0x19, 0x73, 0x42, 0x94,
|
||||
0xf8, 0x68, 0xe3, 0xbe, 0x39, 0x84, 0xf3, 0xc1,
|
||||
0xa1, 0x3a, 0x4d, 0xb9, 0xfa, 0xbe, 0x4a, 0xcb,
|
||||
}
|
||||
hash := deriveKey(argon2d, genKatPassword, genKatSalt, genKatSecret, genKatAAD, 3, 32, 4, 32)
|
||||
if !bytes.Equal(hash, want) {
|
||||
t.Errorf("derived key does not match - got: %s , want: %s", hex.EncodeToString(hash), hex.EncodeToString(want))
|
||||
}
|
||||
}
|
||||
|
||||
func testArgon2i(t *testing.T) {
|
||||
want := []byte{
|
||||
0xc8, 0x14, 0xd9, 0xd1, 0xdc, 0x7f, 0x37, 0xaa,
|
||||
0x13, 0xf0, 0xd7, 0x7f, 0x24, 0x94, 0xbd, 0xa1,
|
||||
0xc8, 0xde, 0x6b, 0x01, 0x6d, 0xd3, 0x88, 0xd2,
|
||||
0x99, 0x52, 0xa4, 0xc4, 0x67, 0x2b, 0x6c, 0xe8,
|
||||
}
|
||||
hash := deriveKey(argon2i, genKatPassword, genKatSalt, genKatSecret, genKatAAD, 3, 32, 4, 32)
|
||||
if !bytes.Equal(hash, want) {
|
||||
t.Errorf("derived key does not match - got: %s , want: %s", hex.EncodeToString(hash), hex.EncodeToString(want))
|
||||
}
|
||||
}
|
||||
|
||||
func testArgon2id(t *testing.T) {
|
||||
want := []byte{
|
||||
0x0d, 0x64, 0x0d, 0xf5, 0x8d, 0x78, 0x76, 0x6c,
|
||||
0x08, 0xc0, 0x37, 0xa3, 0x4a, 0x8b, 0x53, 0xc9,
|
||||
0xd0, 0x1e, 0xf0, 0x45, 0x2d, 0x75, 0xb6, 0x5e,
|
||||
0xb5, 0x25, 0x20, 0xe9, 0x6b, 0x01, 0xe6, 0x59,
|
||||
}
|
||||
hash := deriveKey(argon2id, genKatPassword, genKatSalt, genKatSecret, genKatAAD, 3, 32, 4, 32)
|
||||
if !bytes.Equal(hash, want) {
|
||||
t.Errorf("derived key does not match - got: %s , want: %s", hex.EncodeToString(hash), hex.EncodeToString(want))
|
||||
}
|
||||
}
|
||||
|
||||
func TestVectors(t *testing.T) {
|
||||
password, salt := []byte("password"), []byte("somesalt")
|
||||
for i, v := range testVectors {
|
||||
want, err := hex.DecodeString(v.hash)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: failed to decode hash: %v", i, err)
|
||||
}
|
||||
hash := deriveKey(v.mode, password, salt, nil, nil, v.time, v.memory, v.threads, uint32(len(want)))
|
||||
if !bytes.Equal(hash, want) {
|
||||
t.Errorf("Test %d - got: %s want: %s", i, hex.EncodeToString(hash), hex.EncodeToString(want))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func benchmarkArgon2(mode int, time, memory uint32, threads uint8, keyLen uint32, b *testing.B) {
|
||||
password := []byte("password")
|
||||
salt := []byte("choosing random salts is hard")
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
deriveKey(mode, password, salt, nil, nil, time, memory, threads, keyLen)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkArgon2i(b *testing.B) {
|
||||
b.Run(" Time: 3 Memory: 32 MB, Threads: 1", func(b *testing.B) { benchmarkArgon2(argon2i, 3, 32*1024, 1, 32, b) })
|
||||
b.Run(" Time: 4 Memory: 32 MB, Threads: 1", func(b *testing.B) { benchmarkArgon2(argon2i, 4, 32*1024, 1, 32, b) })
|
||||
b.Run(" Time: 5 Memory: 32 MB, Threads: 1", func(b *testing.B) { benchmarkArgon2(argon2i, 5, 32*1024, 1, 32, b) })
|
||||
b.Run(" Time: 3 Memory: 64 MB, Threads: 4", func(b *testing.B) { benchmarkArgon2(argon2i, 3, 64*1024, 4, 32, b) })
|
||||
b.Run(" Time: 4 Memory: 64 MB, Threads: 4", func(b *testing.B) { benchmarkArgon2(argon2i, 4, 64*1024, 4, 32, b) })
|
||||
b.Run(" Time: 5 Memory: 64 MB, Threads: 4", func(b *testing.B) { benchmarkArgon2(argon2i, 5, 64*1024, 4, 32, b) })
|
||||
}
|
||||
|
||||
func BenchmarkArgon2d(b *testing.B) {
|
||||
b.Run(" Time: 3, Memory: 32 MB, Threads: 1", func(b *testing.B) { benchmarkArgon2(argon2d, 3, 32*1024, 1, 32, b) })
|
||||
b.Run(" Time: 4, Memory: 32 MB, Threads: 1", func(b *testing.B) { benchmarkArgon2(argon2d, 4, 32*1024, 1, 32, b) })
|
||||
b.Run(" Time: 5, Memory: 32 MB, Threads: 1", func(b *testing.B) { benchmarkArgon2(argon2d, 5, 32*1024, 1, 32, b) })
|
||||
b.Run(" Time: 3, Memory: 64 MB, Threads: 4", func(b *testing.B) { benchmarkArgon2(argon2d, 3, 64*1024, 4, 32, b) })
|
||||
b.Run(" Time: 4, Memory: 64 MB, Threads: 4", func(b *testing.B) { benchmarkArgon2(argon2d, 4, 64*1024, 4, 32, b) })
|
||||
b.Run(" Time: 5, Memory: 64 MB, Threads: 4", func(b *testing.B) { benchmarkArgon2(argon2d, 5, 64*1024, 4, 32, b) })
|
||||
}
|
||||
|
||||
func BenchmarkArgon2id(b *testing.B) {
|
||||
b.Run(" Time: 3, Memory: 32 MB, Threads: 1", func(b *testing.B) { benchmarkArgon2(argon2id, 3, 32*1024, 1, 32, b) })
|
||||
b.Run(" Time: 4, Memory: 32 MB, Threads: 1", func(b *testing.B) { benchmarkArgon2(argon2id, 4, 32*1024, 1, 32, b) })
|
||||
b.Run(" Time: 5, Memory: 32 MB, Threads: 1", func(b *testing.B) { benchmarkArgon2(argon2id, 5, 32*1024, 1, 32, b) })
|
||||
b.Run(" Time: 3, Memory: 64 MB, Threads: 4", func(b *testing.B) { benchmarkArgon2(argon2id, 3, 64*1024, 4, 32, b) })
|
||||
b.Run(" Time: 4, Memory: 64 MB, Threads: 4", func(b *testing.B) { benchmarkArgon2(argon2id, 4, 64*1024, 4, 32, b) })
|
||||
b.Run(" Time: 5, Memory: 64 MB, Threads: 4", func(b *testing.B) { benchmarkArgon2(argon2id, 5, 64*1024, 4, 32, b) })
|
||||
}
|
||||
|
||||
// Generated with the CLI of https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf
|
||||
var testVectors = []struct {
|
||||
mode int
|
||||
time, memory uint32
|
||||
threads uint8
|
||||
hash string
|
||||
}{
|
||||
{
|
||||
mode: argon2i, time: 1, memory: 64, threads: 1,
|
||||
hash: "b9c401d1844a67d50eae3967dc28870b22e508092e861a37",
|
||||
},
|
||||
{
|
||||
mode: argon2d, time: 1, memory: 64, threads: 1,
|
||||
hash: "8727405fd07c32c78d64f547f24150d3f2e703a89f981a19",
|
||||
},
|
||||
{
|
||||
mode: argon2id, time: 1, memory: 64, threads: 1,
|
||||
hash: "655ad15eac652dc59f7170a7332bf49b8469be1fdb9c28bb",
|
||||
},
|
||||
{
|
||||
mode: argon2i, time: 2, memory: 64, threads: 1,
|
||||
hash: "8cf3d8f76a6617afe35fac48eb0b7433a9a670ca4a07ed64",
|
||||
},
|
||||
{
|
||||
mode: argon2d, time: 2, memory: 64, threads: 1,
|
||||
hash: "3be9ec79a69b75d3752acb59a1fbb8b295a46529c48fbb75",
|
||||
},
|
||||
{
|
||||
mode: argon2id, time: 2, memory: 64, threads: 1,
|
||||
hash: "068d62b26455936aa6ebe60060b0a65870dbfa3ddf8d41f7",
|
||||
},
|
||||
{
|
||||
mode: argon2i, time: 2, memory: 64, threads: 2,
|
||||
hash: "2089f3e78a799720f80af806553128f29b132cafe40d059f",
|
||||
},
|
||||
{
|
||||
mode: argon2d, time: 2, memory: 64, threads: 2,
|
||||
hash: "68e2462c98b8bc6bb60ec68db418ae2c9ed24fc6748a40e9",
|
||||
},
|
||||
{
|
||||
mode: argon2id, time: 2, memory: 64, threads: 2,
|
||||
hash: "350ac37222f436ccb5c0972f1ebd3bf6b958bf2071841362",
|
||||
},
|
||||
{
|
||||
mode: argon2i, time: 3, memory: 256, threads: 2,
|
||||
hash: "f5bbf5d4c3836af13193053155b73ec7476a6a2eb93fd5e6",
|
||||
},
|
||||
{
|
||||
mode: argon2d, time: 3, memory: 256, threads: 2,
|
||||
hash: "f4f0669218eaf3641f39cc97efb915721102f4b128211ef2",
|
||||
},
|
||||
{
|
||||
mode: argon2id, time: 3, memory: 256, threads: 2,
|
||||
hash: "4668d30ac4187e6878eedeacf0fd83c5a0a30db2cc16ef0b",
|
||||
},
|
||||
{
|
||||
mode: argon2i, time: 4, memory: 4096, threads: 4,
|
||||
hash: "a11f7b7f3f93f02ad4bddb59ab62d121e278369288a0d0e7",
|
||||
},
|
||||
{
|
||||
mode: argon2d, time: 4, memory: 4096, threads: 4,
|
||||
hash: "935598181aa8dc2b720914aa6435ac8d3e3a4210c5b0fb2d",
|
||||
},
|
||||
{
|
||||
mode: argon2id, time: 4, memory: 4096, threads: 4,
|
||||
hash: "145db9733a9f4ee43edf33c509be96b934d505a4efb33c5a",
|
||||
},
|
||||
{
|
||||
mode: argon2i, time: 4, memory: 1024, threads: 8,
|
||||
hash: "0cdd3956aa35e6b475a7b0c63488822f774f15b43f6e6e17",
|
||||
},
|
||||
{
|
||||
mode: argon2d, time: 4, memory: 1024, threads: 8,
|
||||
hash: "83604fc2ad0589b9d055578f4d3cc55bc616df3578a896e9",
|
||||
},
|
||||
{
|
||||
mode: argon2id, time: 4, memory: 1024, threads: 8,
|
||||
hash: "8dafa8e004f8ea96bf7c0f93eecf67a6047476143d15577f",
|
||||
},
|
||||
{
|
||||
mode: argon2i, time: 2, memory: 64, threads: 3,
|
||||
hash: "5cab452fe6b8479c8661def8cd703b611a3905a6d5477fe6",
|
||||
},
|
||||
{
|
||||
mode: argon2d, time: 2, memory: 64, threads: 3,
|
||||
hash: "22474a423bda2ccd36ec9afd5119e5c8949798cadf659f51",
|
||||
},
|
||||
{
|
||||
mode: argon2id, time: 2, memory: 64, threads: 3,
|
||||
hash: "4a15b31aec7c2590b87d1f520be7d96f56658172deaa3079",
|
||||
},
|
||||
{
|
||||
mode: argon2i, time: 3, memory: 1024, threads: 6,
|
||||
hash: "d236b29c2b2a09babee842b0dec6aa1e83ccbdea8023dced",
|
||||
},
|
||||
{
|
||||
mode: argon2d, time: 3, memory: 1024, threads: 6,
|
||||
hash: "a3351b0319a53229152023d9206902f4ef59661cdca89481",
|
||||
},
|
||||
{
|
||||
mode: argon2id, time: 3, memory: 1024, threads: 6,
|
||||
hash: "1640b932f4b60e272f5d2207b9a9c626ffa1bd88d2349016",
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
// 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 argon2
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"hash"
|
||||
|
||||
"golang.org/x/crypto/blake2b"
|
||||
)
|
||||
|
||||
// blake2bHash computes an arbitrary long hash value of in
|
||||
// and writes the hash to out.
|
||||
func blake2bHash(out []byte, in []byte) {
|
||||
var b2 hash.Hash
|
||||
if n := len(out); n < blake2b.Size {
|
||||
b2, _ = blake2b.New(n, nil)
|
||||
} else {
|
||||
b2, _ = blake2b.New512(nil)
|
||||
}
|
||||
|
||||
var buffer [blake2b.Size]byte
|
||||
binary.LittleEndian.PutUint32(buffer[:4], uint32(len(out)))
|
||||
b2.Write(buffer[:4])
|
||||
b2.Write(in)
|
||||
|
||||
if len(out) <= blake2b.Size {
|
||||
b2.Sum(out[:0])
|
||||
return
|
||||
}
|
||||
|
||||
outLen := len(out)
|
||||
b2.Sum(buffer[:0])
|
||||
b2.Reset()
|
||||
copy(out, buffer[:32])
|
||||
out = out[32:]
|
||||
for len(out) > blake2b.Size {
|
||||
b2.Write(buffer[:])
|
||||
b2.Sum(buffer[:0])
|
||||
copy(out, buffer[:32])
|
||||
out = out[32:]
|
||||
b2.Reset()
|
||||
}
|
||||
|
||||
if outLen%blake2b.Size > 0 { // outLen > 64
|
||||
r := ((outLen + 31) / 32) - 2 // ⌈τ /32⌉-2
|
||||
b2, _ = blake2b.New(outLen-32*r, nil)
|
||||
}
|
||||
b2.Write(buffer[:])
|
||||
b2.Sum(out[:0])
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
// 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.
|
||||
|
||||
// +build amd64,!gccgo,!appengine
|
||||
|
||||
package argon2
|
||||
|
||||
func init() {
|
||||
useSSE4 = supportsSSE4()
|
||||
}
|
||||
|
||||
//go:noescape
|
||||
func supportsSSE4() bool
|
||||
|
||||
//go:noescape
|
||||
func mixBlocksSSE2(out, a, b, c *block)
|
||||
|
||||
//go:noescape
|
||||
func xorBlocksSSE2(out, a, b, c *block)
|
||||
|
||||
//go:noescape
|
||||
func blamkaSSE4(b *block)
|
||||
|
||||
func processBlockSSE(out, in1, in2 *block, xor bool) {
|
||||
var t block
|
||||
mixBlocksSSE2(&t, in1, in2, &t)
|
||||
if useSSE4 {
|
||||
blamkaSSE4(&t)
|
||||
} else {
|
||||
for i := 0; i < blockLength; i += 16 {
|
||||
blamkaGeneric(
|
||||
&t[i+0], &t[i+1], &t[i+2], &t[i+3],
|
||||
&t[i+4], &t[i+5], &t[i+6], &t[i+7],
|
||||
&t[i+8], &t[i+9], &t[i+10], &t[i+11],
|
||||
&t[i+12], &t[i+13], &t[i+14], &t[i+15],
|
||||
)
|
||||
}
|
||||
for i := 0; i < blockLength/8; i += 2 {
|
||||
blamkaGeneric(
|
||||
&t[i], &t[i+1], &t[16+i], &t[16+i+1],
|
||||
&t[32+i], &t[32+i+1], &t[48+i], &t[48+i+1],
|
||||
&t[64+i], &t[64+i+1], &t[80+i], &t[80+i+1],
|
||||
&t[96+i], &t[96+i+1], &t[112+i], &t[112+i+1],
|
||||
)
|
||||
}
|
||||
}
|
||||
if xor {
|
||||
xorBlocksSSE2(out, in1, in2, &t)
|
||||
} else {
|
||||
mixBlocksSSE2(out, in1, in2, &t)
|
||||
}
|
||||
}
|
||||
|
||||
func processBlock(out, in1, in2 *block) {
|
||||
processBlockSSE(out, in1, in2, false)
|
||||
}
|
||||
|
||||
func processBlockXOR(out, in1, in2 *block) {
|
||||
processBlockSSE(out, in1, in2, true)
|
||||
}
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
// 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.
|
||||
|
||||
// +build amd64,!gccgo,!appengine
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
DATA ·c40<>+0x00(SB)/8, $0x0201000706050403
|
||||
DATA ·c40<>+0x08(SB)/8, $0x0a09080f0e0d0c0b
|
||||
GLOBL ·c40<>(SB), (NOPTR+RODATA), $16
|
||||
|
||||
DATA ·c48<>+0x00(SB)/8, $0x0100070605040302
|
||||
DATA ·c48<>+0x08(SB)/8, $0x09080f0e0d0c0b0a
|
||||
GLOBL ·c48<>(SB), (NOPTR+RODATA), $16
|
||||
|
||||
#define SHUFFLE(v2, v3, v4, v5, v6, v7, t1, t2) \
|
||||
MOVO v4, t1; \
|
||||
MOVO v5, v4; \
|
||||
MOVO t1, v5; \
|
||||
MOVO v6, t1; \
|
||||
PUNPCKLQDQ v6, t2; \
|
||||
PUNPCKHQDQ v7, v6; \
|
||||
PUNPCKHQDQ t2, v6; \
|
||||
PUNPCKLQDQ v7, t2; \
|
||||
MOVO t1, v7; \
|
||||
MOVO v2, t1; \
|
||||
PUNPCKHQDQ t2, v7; \
|
||||
PUNPCKLQDQ v3, t2; \
|
||||
PUNPCKHQDQ t2, v2; \
|
||||
PUNPCKLQDQ t1, t2; \
|
||||
PUNPCKHQDQ t2, v3
|
||||
|
||||
#define SHUFFLE_INV(v2, v3, v4, v5, v6, v7, t1, t2) \
|
||||
MOVO v4, t1; \
|
||||
MOVO v5, v4; \
|
||||
MOVO t1, v5; \
|
||||
MOVO v2, t1; \
|
||||
PUNPCKLQDQ v2, t2; \
|
||||
PUNPCKHQDQ v3, v2; \
|
||||
PUNPCKHQDQ t2, v2; \
|
||||
PUNPCKLQDQ v3, t2; \
|
||||
MOVO t1, v3; \
|
||||
MOVO v6, t1; \
|
||||
PUNPCKHQDQ t2, v3; \
|
||||
PUNPCKLQDQ v7, t2; \
|
||||
PUNPCKHQDQ t2, v6; \
|
||||
PUNPCKLQDQ t1, t2; \
|
||||
PUNPCKHQDQ t2, v7
|
||||
|
||||
#define HALF_ROUND(v0, v1, v2, v3, v4, v5, v6, v7, t0, c40, c48) \
|
||||
MOVO v0, t0; \
|
||||
PMULULQ v2, t0; \
|
||||
PADDQ v2, v0; \
|
||||
PADDQ t0, v0; \
|
||||
PADDQ t0, v0; \
|
||||
PXOR v0, v6; \
|
||||
PSHUFD $0xB1, v6, v6; \
|
||||
MOVO v4, t0; \
|
||||
PMULULQ v6, t0; \
|
||||
PADDQ v6, v4; \
|
||||
PADDQ t0, v4; \
|
||||
PADDQ t0, v4; \
|
||||
PXOR v4, v2; \
|
||||
PSHUFB c40, v2; \
|
||||
MOVO v0, t0; \
|
||||
PMULULQ v2, t0; \
|
||||
PADDQ v2, v0; \
|
||||
PADDQ t0, v0; \
|
||||
PADDQ t0, v0; \
|
||||
PXOR v0, v6; \
|
||||
PSHUFB c48, v6; \
|
||||
MOVO v4, t0; \
|
||||
PMULULQ v6, t0; \
|
||||
PADDQ v6, v4; \
|
||||
PADDQ t0, v4; \
|
||||
PADDQ t0, v4; \
|
||||
PXOR v4, v2; \
|
||||
MOVO v2, t0; \
|
||||
PADDQ v2, t0; \
|
||||
PSRLQ $63, v2; \
|
||||
PXOR t0, v2; \
|
||||
MOVO v1, t0; \
|
||||
PMULULQ v3, t0; \
|
||||
PADDQ v3, v1; \
|
||||
PADDQ t0, v1; \
|
||||
PADDQ t0, v1; \
|
||||
PXOR v1, v7; \
|
||||
PSHUFD $0xB1, v7, v7; \
|
||||
MOVO v5, t0; \
|
||||
PMULULQ v7, t0; \
|
||||
PADDQ v7, v5; \
|
||||
PADDQ t0, v5; \
|
||||
PADDQ t0, v5; \
|
||||
PXOR v5, v3; \
|
||||
PSHUFB c40, v3; \
|
||||
MOVO v1, t0; \
|
||||
PMULULQ v3, t0; \
|
||||
PADDQ v3, v1; \
|
||||
PADDQ t0, v1; \
|
||||
PADDQ t0, v1; \
|
||||
PXOR v1, v7; \
|
||||
PSHUFB c48, v7; \
|
||||
MOVO v5, t0; \
|
||||
PMULULQ v7, t0; \
|
||||
PADDQ v7, v5; \
|
||||
PADDQ t0, v5; \
|
||||
PADDQ t0, v5; \
|
||||
PXOR v5, v3; \
|
||||
MOVO v3, t0; \
|
||||
PADDQ v3, t0; \
|
||||
PSRLQ $63, v3; \
|
||||
PXOR t0, v3
|
||||
|
||||
#define LOAD_MSG_0(block, off) \
|
||||
MOVOU 8*(off+0)(block), X0; \
|
||||
MOVOU 8*(off+2)(block), X1; \
|
||||
MOVOU 8*(off+4)(block), X2; \
|
||||
MOVOU 8*(off+6)(block), X3; \
|
||||
MOVOU 8*(off+8)(block), X4; \
|
||||
MOVOU 8*(off+10)(block), X5; \
|
||||
MOVOU 8*(off+12)(block), X6; \
|
||||
MOVOU 8*(off+14)(block), X7
|
||||
|
||||
#define STORE_MSG_0(block, off) \
|
||||
MOVOU X0, 8*(off+0)(block); \
|
||||
MOVOU X1, 8*(off+2)(block); \
|
||||
MOVOU X2, 8*(off+4)(block); \
|
||||
MOVOU X3, 8*(off+6)(block); \
|
||||
MOVOU X4, 8*(off+8)(block); \
|
||||
MOVOU X5, 8*(off+10)(block); \
|
||||
MOVOU X6, 8*(off+12)(block); \
|
||||
MOVOU X7, 8*(off+14)(block)
|
||||
|
||||
#define LOAD_MSG_1(block, off) \
|
||||
MOVOU 8*off+0*8(block), X0; \
|
||||
MOVOU 8*off+16*8(block), X1; \
|
||||
MOVOU 8*off+32*8(block), X2; \
|
||||
MOVOU 8*off+48*8(block), X3; \
|
||||
MOVOU 8*off+64*8(block), X4; \
|
||||
MOVOU 8*off+80*8(block), X5; \
|
||||
MOVOU 8*off+96*8(block), X6; \
|
||||
MOVOU 8*off+112*8(block), X7
|
||||
|
||||
#define STORE_MSG_1(block, off) \
|
||||
MOVOU X0, 8*off+0*8(block); \
|
||||
MOVOU X1, 8*off+16*8(block); \
|
||||
MOVOU X2, 8*off+32*8(block); \
|
||||
MOVOU X3, 8*off+48*8(block); \
|
||||
MOVOU X4, 8*off+64*8(block); \
|
||||
MOVOU X5, 8*off+80*8(block); \
|
||||
MOVOU X6, 8*off+96*8(block); \
|
||||
MOVOU X7, 8*off+112*8(block)
|
||||
|
||||
#define BLAMKA_ROUND_0(block, off, t0, t1, c40, c48) \
|
||||
LOAD_MSG_0(block, off); \
|
||||
HALF_ROUND(X0, X1, X2, X3, X4, X5, X6, X7, t0, c40, c48); \
|
||||
SHUFFLE(X2, X3, X4, X5, X6, X7, t0, t1); \
|
||||
HALF_ROUND(X0, X1, X2, X3, X4, X5, X6, X7, t0, c40, c48); \
|
||||
SHUFFLE_INV(X2, X3, X4, X5, X6, X7, t0, t1); \
|
||||
STORE_MSG_0(block, off)
|
||||
|
||||
#define BLAMKA_ROUND_1(block, off, t0, t1, c40, c48) \
|
||||
LOAD_MSG_1(block, off); \
|
||||
HALF_ROUND(X0, X1, X2, X3, X4, X5, X6, X7, t0, c40, c48); \
|
||||
SHUFFLE(X2, X3, X4, X5, X6, X7, t0, t1); \
|
||||
HALF_ROUND(X0, X1, X2, X3, X4, X5, X6, X7, t0, c40, c48); \
|
||||
SHUFFLE_INV(X2, X3, X4, X5, X6, X7, t0, t1); \
|
||||
STORE_MSG_1(block, off)
|
||||
|
||||
// func blamkaSSE4(b *block)
|
||||
TEXT ·blamkaSSE4(SB), 4, $0-8
|
||||
MOVQ b+0(FP), AX
|
||||
|
||||
MOVOU ·c40<>(SB), X10
|
||||
MOVOU ·c48<>(SB), X11
|
||||
|
||||
BLAMKA_ROUND_0(AX, 0, X8, X9, X10, X11)
|
||||
BLAMKA_ROUND_0(AX, 16, X8, X9, X10, X11)
|
||||
BLAMKA_ROUND_0(AX, 32, X8, X9, X10, X11)
|
||||
BLAMKA_ROUND_0(AX, 48, X8, X9, X10, X11)
|
||||
BLAMKA_ROUND_0(AX, 64, X8, X9, X10, X11)
|
||||
BLAMKA_ROUND_0(AX, 80, X8, X9, X10, X11)
|
||||
BLAMKA_ROUND_0(AX, 96, X8, X9, X10, X11)
|
||||
BLAMKA_ROUND_0(AX, 112, X8, X9, X10, X11)
|
||||
|
||||
BLAMKA_ROUND_1(AX, 0, X8, X9, X10, X11)
|
||||
BLAMKA_ROUND_1(AX, 2, X8, X9, X10, X11)
|
||||
BLAMKA_ROUND_1(AX, 4, X8, X9, X10, X11)
|
||||
BLAMKA_ROUND_1(AX, 6, X8, X9, X10, X11)
|
||||
BLAMKA_ROUND_1(AX, 8, X8, X9, X10, X11)
|
||||
BLAMKA_ROUND_1(AX, 10, X8, X9, X10, X11)
|
||||
BLAMKA_ROUND_1(AX, 12, X8, X9, X10, X11)
|
||||
BLAMKA_ROUND_1(AX, 14, X8, X9, X10, X11)
|
||||
RET
|
||||
|
||||
// func mixBlocksSSE2(out, a, b, c *block)
|
||||
TEXT ·mixBlocksSSE2(SB), 4, $0-32
|
||||
MOVQ out+0(FP), DX
|
||||
MOVQ a+8(FP), AX
|
||||
MOVQ b+16(FP), BX
|
||||
MOVQ a+24(FP), CX
|
||||
MOVQ $128, BP
|
||||
|
||||
loop:
|
||||
MOVOU 0(AX), X0
|
||||
MOVOU 0(BX), X1
|
||||
MOVOU 0(CX), X2
|
||||
PXOR X1, X0
|
||||
PXOR X2, X0
|
||||
MOVOU X0, 0(DX)
|
||||
ADDQ $16, AX
|
||||
ADDQ $16, BX
|
||||
ADDQ $16, CX
|
||||
ADDQ $16, DX
|
||||
SUBQ $2, BP
|
||||
JA loop
|
||||
RET
|
||||
|
||||
// func xorBlocksSSE2(out, a, b, c *block)
|
||||
TEXT ·xorBlocksSSE2(SB), 4, $0-32
|
||||
MOVQ out+0(FP), DX
|
||||
MOVQ a+8(FP), AX
|
||||
MOVQ b+16(FP), BX
|
||||
MOVQ a+24(FP), CX
|
||||
MOVQ $128, BP
|
||||
|
||||
loop:
|
||||
MOVOU 0(AX), X0
|
||||
MOVOU 0(BX), X1
|
||||
MOVOU 0(CX), X2
|
||||
MOVOU 0(DX), X3
|
||||
PXOR X1, X0
|
||||
PXOR X2, X0
|
||||
PXOR X3, X0
|
||||
MOVOU X0, 0(DX)
|
||||
ADDQ $16, AX
|
||||
ADDQ $16, BX
|
||||
ADDQ $16, CX
|
||||
ADDQ $16, DX
|
||||
SUBQ $2, BP
|
||||
JA loop
|
||||
RET
|
||||
|
||||
// func supportsSSE4() bool
|
||||
TEXT ·supportsSSE4(SB), 4, $0-1
|
||||
MOVL $1, AX
|
||||
CPUID
|
||||
SHRL $19, CX // Bit 19 indicates SSE4 support
|
||||
ANDL $1, CX // CX != 0 if support SSE4
|
||||
MOVB CX, ret+0(FP)
|
||||
RET
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
// 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 argon2
|
||||
|
||||
var useSSE4 bool
|
||||
|
||||
func processBlockGeneric(out, in1, in2 *block, xor bool) {
|
||||
var t block
|
||||
for i := range t {
|
||||
t[i] = in1[i] ^ in2[i]
|
||||
}
|
||||
for i := 0; i < blockLength; i += 16 {
|
||||
blamkaGeneric(
|
||||
&t[i+0], &t[i+1], &t[i+2], &t[i+3],
|
||||
&t[i+4], &t[i+5], &t[i+6], &t[i+7],
|
||||
&t[i+8], &t[i+9], &t[i+10], &t[i+11],
|
||||
&t[i+12], &t[i+13], &t[i+14], &t[i+15],
|
||||
)
|
||||
}
|
||||
for i := 0; i < blockLength/8; i += 2 {
|
||||
blamkaGeneric(
|
||||
&t[i], &t[i+1], &t[16+i], &t[16+i+1],
|
||||
&t[32+i], &t[32+i+1], &t[48+i], &t[48+i+1],
|
||||
&t[64+i], &t[64+i+1], &t[80+i], &t[80+i+1],
|
||||
&t[96+i], &t[96+i+1], &t[112+i], &t[112+i+1],
|
||||
)
|
||||
}
|
||||
if xor {
|
||||
for i := range t {
|
||||
out[i] ^= in1[i] ^ in2[i] ^ t[i]
|
||||
}
|
||||
} else {
|
||||
for i := range t {
|
||||
out[i] = in1[i] ^ in2[i] ^ t[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func blamkaGeneric(t00, t01, t02, t03, t04, t05, t06, t07, t08, t09, t10, t11, t12, t13, t14, t15 *uint64) {
|
||||
v00, v01, v02, v03 := *t00, *t01, *t02, *t03
|
||||
v04, v05, v06, v07 := *t04, *t05, *t06, *t07
|
||||
v08, v09, v10, v11 := *t08, *t09, *t10, *t11
|
||||
v12, v13, v14, v15 := *t12, *t13, *t14, *t15
|
||||
|
||||
v00 += v04 + 2*uint64(uint32(v00))*uint64(uint32(v04))
|
||||
v12 ^= v00
|
||||
v12 = v12>>32 | v12<<32
|
||||
v08 += v12 + 2*uint64(uint32(v08))*uint64(uint32(v12))
|
||||
v04 ^= v08
|
||||
v04 = v04>>24 | v04<<40
|
||||
|
||||
v00 += v04 + 2*uint64(uint32(v00))*uint64(uint32(v04))
|
||||
v12 ^= v00
|
||||
v12 = v12>>16 | v12<<48
|
||||
v08 += v12 + 2*uint64(uint32(v08))*uint64(uint32(v12))
|
||||
v04 ^= v08
|
||||
v04 = v04>>63 | v04<<1
|
||||
|
||||
v01 += v05 + 2*uint64(uint32(v01))*uint64(uint32(v05))
|
||||
v13 ^= v01
|
||||
v13 = v13>>32 | v13<<32
|
||||
v09 += v13 + 2*uint64(uint32(v09))*uint64(uint32(v13))
|
||||
v05 ^= v09
|
||||
v05 = v05>>24 | v05<<40
|
||||
|
||||
v01 += v05 + 2*uint64(uint32(v01))*uint64(uint32(v05))
|
||||
v13 ^= v01
|
||||
v13 = v13>>16 | v13<<48
|
||||
v09 += v13 + 2*uint64(uint32(v09))*uint64(uint32(v13))
|
||||
v05 ^= v09
|
||||
v05 = v05>>63 | v05<<1
|
||||
|
||||
v02 += v06 + 2*uint64(uint32(v02))*uint64(uint32(v06))
|
||||
v14 ^= v02
|
||||
v14 = v14>>32 | v14<<32
|
||||
v10 += v14 + 2*uint64(uint32(v10))*uint64(uint32(v14))
|
||||
v06 ^= v10
|
||||
v06 = v06>>24 | v06<<40
|
||||
|
||||
v02 += v06 + 2*uint64(uint32(v02))*uint64(uint32(v06))
|
||||
v14 ^= v02
|
||||
v14 = v14>>16 | v14<<48
|
||||
v10 += v14 + 2*uint64(uint32(v10))*uint64(uint32(v14))
|
||||
v06 ^= v10
|
||||
v06 = v06>>63 | v06<<1
|
||||
|
||||
v03 += v07 + 2*uint64(uint32(v03))*uint64(uint32(v07))
|
||||
v15 ^= v03
|
||||
v15 = v15>>32 | v15<<32
|
||||
v11 += v15 + 2*uint64(uint32(v11))*uint64(uint32(v15))
|
||||
v07 ^= v11
|
||||
v07 = v07>>24 | v07<<40
|
||||
|
||||
v03 += v07 + 2*uint64(uint32(v03))*uint64(uint32(v07))
|
||||
v15 ^= v03
|
||||
v15 = v15>>16 | v15<<48
|
||||
v11 += v15 + 2*uint64(uint32(v11))*uint64(uint32(v15))
|
||||
v07 ^= v11
|
||||
v07 = v07>>63 | v07<<1
|
||||
|
||||
v00 += v05 + 2*uint64(uint32(v00))*uint64(uint32(v05))
|
||||
v15 ^= v00
|
||||
v15 = v15>>32 | v15<<32
|
||||
v10 += v15 + 2*uint64(uint32(v10))*uint64(uint32(v15))
|
||||
v05 ^= v10
|
||||
v05 = v05>>24 | v05<<40
|
||||
|
||||
v00 += v05 + 2*uint64(uint32(v00))*uint64(uint32(v05))
|
||||
v15 ^= v00
|
||||
v15 = v15>>16 | v15<<48
|
||||
v10 += v15 + 2*uint64(uint32(v10))*uint64(uint32(v15))
|
||||
v05 ^= v10
|
||||
v05 = v05>>63 | v05<<1
|
||||
|
||||
v01 += v06 + 2*uint64(uint32(v01))*uint64(uint32(v06))
|
||||
v12 ^= v01
|
||||
v12 = v12>>32 | v12<<32
|
||||
v11 += v12 + 2*uint64(uint32(v11))*uint64(uint32(v12))
|
||||
v06 ^= v11
|
||||
v06 = v06>>24 | v06<<40
|
||||
|
||||
v01 += v06 + 2*uint64(uint32(v01))*uint64(uint32(v06))
|
||||
v12 ^= v01
|
||||
v12 = v12>>16 | v12<<48
|
||||
v11 += v12 + 2*uint64(uint32(v11))*uint64(uint32(v12))
|
||||
v06 ^= v11
|
||||
v06 = v06>>63 | v06<<1
|
||||
|
||||
v02 += v07 + 2*uint64(uint32(v02))*uint64(uint32(v07))
|
||||
v13 ^= v02
|
||||
v13 = v13>>32 | v13<<32
|
||||
v08 += v13 + 2*uint64(uint32(v08))*uint64(uint32(v13))
|
||||
v07 ^= v08
|
||||
v07 = v07>>24 | v07<<40
|
||||
|
||||
v02 += v07 + 2*uint64(uint32(v02))*uint64(uint32(v07))
|
||||
v13 ^= v02
|
||||
v13 = v13>>16 | v13<<48
|
||||
v08 += v13 + 2*uint64(uint32(v08))*uint64(uint32(v13))
|
||||
v07 ^= v08
|
||||
v07 = v07>>63 | v07<<1
|
||||
|
||||
v03 += v04 + 2*uint64(uint32(v03))*uint64(uint32(v04))
|
||||
v14 ^= v03
|
||||
v14 = v14>>32 | v14<<32
|
||||
v09 += v14 + 2*uint64(uint32(v09))*uint64(uint32(v14))
|
||||
v04 ^= v09
|
||||
v04 = v04>>24 | v04<<40
|
||||
|
||||
v03 += v04 + 2*uint64(uint32(v03))*uint64(uint32(v04))
|
||||
v14 ^= v03
|
||||
v14 = v14>>16 | v14<<48
|
||||
v09 += v14 + 2*uint64(uint32(v09))*uint64(uint32(v14))
|
||||
v04 ^= v09
|
||||
v04 = v04>>63 | v04<<1
|
||||
|
||||
*t00, *t01, *t02, *t03 = v00, v01, v02, v03
|
||||
*t04, *t05, *t06, *t07 = v04, v05, v06, v07
|
||||
*t08, *t09, *t10, *t11 = v08, v09, v10, v11
|
||||
*t12, *t13, *t14, *t15 = v12, v13, v14, v15
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// 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.
|
||||
|
||||
// +build !amd64 appengine gccgo
|
||||
|
||||
package argon2
|
||||
|
||||
func processBlock(out, in1, in2 *block) {
|
||||
processBlockGeneric(out, in1, in2, false)
|
||||
}
|
||||
|
||||
func processBlockXOR(out, in1, in2 *block) {
|
||||
processBlockGeneric(out, in1, in2, true)
|
||||
}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
// Copyright 2016 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 ChaCha20 implements the core ChaCha20 function as specified in https://tools.ietf.org/html/rfc7539#section-2.3.
|
||||
package chacha20
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
const rounds = 20
|
||||
|
||||
// core applies the ChaCha20 core function to 16-byte input in, 32-byte key k,
|
||||
// and 16-byte constant c, and puts the result into 64-byte array out.
|
||||
func core(out *[64]byte, in *[16]byte, k *[32]byte) {
|
||||
j0 := uint32(0x61707865)
|
||||
j1 := uint32(0x3320646e)
|
||||
j2 := uint32(0x79622d32)
|
||||
j3 := uint32(0x6b206574)
|
||||
j4 := binary.LittleEndian.Uint32(k[0:4])
|
||||
j5 := binary.LittleEndian.Uint32(k[4:8])
|
||||
j6 := binary.LittleEndian.Uint32(k[8:12])
|
||||
j7 := binary.LittleEndian.Uint32(k[12:16])
|
||||
j8 := binary.LittleEndian.Uint32(k[16:20])
|
||||
j9 := binary.LittleEndian.Uint32(k[20:24])
|
||||
j10 := binary.LittleEndian.Uint32(k[24:28])
|
||||
j11 := binary.LittleEndian.Uint32(k[28:32])
|
||||
j12 := binary.LittleEndian.Uint32(in[0:4])
|
||||
j13 := binary.LittleEndian.Uint32(in[4:8])
|
||||
j14 := binary.LittleEndian.Uint32(in[8:12])
|
||||
j15 := binary.LittleEndian.Uint32(in[12:16])
|
||||
|
||||
x0, x1, x2, x3, x4, x5, x6, x7 := j0, j1, j2, j3, j4, j5, j6, j7
|
||||
x8, x9, x10, x11, x12, x13, x14, x15 := j8, j9, j10, j11, j12, j13, j14, j15
|
||||
|
||||
for i := 0; i < rounds; i += 2 {
|
||||
x0 += x4
|
||||
x12 ^= x0
|
||||
x12 = (x12 << 16) | (x12 >> (16))
|
||||
x8 += x12
|
||||
x4 ^= x8
|
||||
x4 = (x4 << 12) | (x4 >> (20))
|
||||
x0 += x4
|
||||
x12 ^= x0
|
||||
x12 = (x12 << 8) | (x12 >> (24))
|
||||
x8 += x12
|
||||
x4 ^= x8
|
||||
x4 = (x4 << 7) | (x4 >> (25))
|
||||
x1 += x5
|
||||
x13 ^= x1
|
||||
x13 = (x13 << 16) | (x13 >> 16)
|
||||
x9 += x13
|
||||
x5 ^= x9
|
||||
x5 = (x5 << 12) | (x5 >> 20)
|
||||
x1 += x5
|
||||
x13 ^= x1
|
||||
x13 = (x13 << 8) | (x13 >> 24)
|
||||
x9 += x13
|
||||
x5 ^= x9
|
||||
x5 = (x5 << 7) | (x5 >> 25)
|
||||
x2 += x6
|
||||
x14 ^= x2
|
||||
x14 = (x14 << 16) | (x14 >> 16)
|
||||
x10 += x14
|
||||
x6 ^= x10
|
||||
x6 = (x6 << 12) | (x6 >> 20)
|
||||
x2 += x6
|
||||
x14 ^= x2
|
||||
x14 = (x14 << 8) | (x14 >> 24)
|
||||
x10 += x14
|
||||
x6 ^= x10
|
||||
x6 = (x6 << 7) | (x6 >> 25)
|
||||
x3 += x7
|
||||
x15 ^= x3
|
||||
x15 = (x15 << 16) | (x15 >> 16)
|
||||
x11 += x15
|
||||
x7 ^= x11
|
||||
x7 = (x7 << 12) | (x7 >> 20)
|
||||
x3 += x7
|
||||
x15 ^= x3
|
||||
x15 = (x15 << 8) | (x15 >> 24)
|
||||
x11 += x15
|
||||
x7 ^= x11
|
||||
x7 = (x7 << 7) | (x7 >> 25)
|
||||
x0 += x5
|
||||
x15 ^= x0
|
||||
x15 = (x15 << 16) | (x15 >> 16)
|
||||
x10 += x15
|
||||
x5 ^= x10
|
||||
x5 = (x5 << 12) | (x5 >> 20)
|
||||
x0 += x5
|
||||
x15 ^= x0
|
||||
x15 = (x15 << 8) | (x15 >> 24)
|
||||
x10 += x15
|
||||
x5 ^= x10
|
||||
x5 = (x5 << 7) | (x5 >> 25)
|
||||
x1 += x6
|
||||
x12 ^= x1
|
||||
x12 = (x12 << 16) | (x12 >> 16)
|
||||
x11 += x12
|
||||
x6 ^= x11
|
||||
x6 = (x6 << 12) | (x6 >> 20)
|
||||
x1 += x6
|
||||
x12 ^= x1
|
||||
x12 = (x12 << 8) | (x12 >> 24)
|
||||
x11 += x12
|
||||
x6 ^= x11
|
||||
x6 = (x6 << 7) | (x6 >> 25)
|
||||
x2 += x7
|
||||
x13 ^= x2
|
||||
x13 = (x13 << 16) | (x13 >> 16)
|
||||
x8 += x13
|
||||
x7 ^= x8
|
||||
x7 = (x7 << 12) | (x7 >> 20)
|
||||
x2 += x7
|
||||
x13 ^= x2
|
||||
x13 = (x13 << 8) | (x13 >> 24)
|
||||
x8 += x13
|
||||
x7 ^= x8
|
||||
x7 = (x7 << 7) | (x7 >> 25)
|
||||
x3 += x4
|
||||
x14 ^= x3
|
||||
x14 = (x14 << 16) | (x14 >> 16)
|
||||
x9 += x14
|
||||
x4 ^= x9
|
||||
x4 = (x4 << 12) | (x4 >> 20)
|
||||
x3 += x4
|
||||
x14 ^= x3
|
||||
x14 = (x14 << 8) | (x14 >> 24)
|
||||
x9 += x14
|
||||
x4 ^= x9
|
||||
x4 = (x4 << 7) | (x4 >> 25)
|
||||
}
|
||||
|
||||
x0 += j0
|
||||
x1 += j1
|
||||
x2 += j2
|
||||
x3 += j3
|
||||
x4 += j4
|
||||
x5 += j5
|
||||
x6 += j6
|
||||
x7 += j7
|
||||
x8 += j8
|
||||
x9 += j9
|
||||
x10 += j10
|
||||
x11 += j11
|
||||
x12 += j12
|
||||
x13 += j13
|
||||
x14 += j14
|
||||
x15 += j15
|
||||
|
||||
binary.LittleEndian.PutUint32(out[0:4], x0)
|
||||
binary.LittleEndian.PutUint32(out[4:8], x1)
|
||||
binary.LittleEndian.PutUint32(out[8:12], x2)
|
||||
binary.LittleEndian.PutUint32(out[12:16], x3)
|
||||
binary.LittleEndian.PutUint32(out[16:20], x4)
|
||||
binary.LittleEndian.PutUint32(out[20:24], x5)
|
||||
binary.LittleEndian.PutUint32(out[24:28], x6)
|
||||
binary.LittleEndian.PutUint32(out[28:32], x7)
|
||||
binary.LittleEndian.PutUint32(out[32:36], x8)
|
||||
binary.LittleEndian.PutUint32(out[36:40], x9)
|
||||
binary.LittleEndian.PutUint32(out[40:44], x10)
|
||||
binary.LittleEndian.PutUint32(out[44:48], x11)
|
||||
binary.LittleEndian.PutUint32(out[48:52], x12)
|
||||
binary.LittleEndian.PutUint32(out[52:56], x13)
|
||||
binary.LittleEndian.PutUint32(out[56:60], x14)
|
||||
binary.LittleEndian.PutUint32(out[60:64], x15)
|
||||
}
|
||||
|
||||
// XORKeyStream crypts bytes from in to out using the given key and counters.
|
||||
// In and out must overlap entirely or not at all. Counter contains the raw
|
||||
// ChaCha20 counter bytes (i.e. block counter followed by nonce).
|
||||
func XORKeyStream(out, in []byte, counter *[16]byte, key *[32]byte) {
|
||||
var block [64]byte
|
||||
var counterCopy [16]byte
|
||||
copy(counterCopy[:], counter[:])
|
||||
|
||||
for len(in) >= 64 {
|
||||
core(&block, &counterCopy, key)
|
||||
for i, x := range block {
|
||||
out[i] = in[i] ^ x
|
||||
}
|
||||
u := uint32(1)
|
||||
for i := 0; i < 4; i++ {
|
||||
u += uint32(counterCopy[i])
|
||||
counterCopy[i] = byte(u)
|
||||
u >>= 8
|
||||
}
|
||||
in = in[64:]
|
||||
out = out[64:]
|
||||
}
|
||||
|
||||
if len(in) > 0 {
|
||||
core(&block, &counterCopy, key)
|
||||
for i, v := range in {
|
||||
out[i] = v ^ block[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2016 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 chacha20
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCore(t *testing.T) {
|
||||
// This is just a smoke test that checks the example from
|
||||
// https://tools.ietf.org/html/rfc7539#section-2.3.2. The
|
||||
// chacha20poly1305 package contains much more extensive tests of this
|
||||
// code.
|
||||
var key [32]byte
|
||||
for i := range key {
|
||||
key[i] = byte(i)
|
||||
}
|
||||
|
||||
var input [16]byte
|
||||
input[0] = 1
|
||||
input[7] = 9
|
||||
input[11] = 0x4a
|
||||
|
||||
var out [64]byte
|
||||
XORKeyStream(out[:], out[:], &input, &key)
|
||||
const expected = "10f1e7e4d13b5915500fdd1fa32071c4c7d1f4c733c068030422aa9ac3d46c4ed2826446079faa0914c2d705d98b02a2b5129cd1de164eb9cbd083e8a2503c4e"
|
||||
if result := hex.EncodeToString(out[:]); result != expected {
|
||||
t.Errorf("wanted %x but got %x", expected, result)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2014 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.
|
||||
|
||||
// +build darwin dragonfly freebsd linux netbsd openbsd
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBannerCallbackAgainstOpenSSH(t *testing.T) {
|
||||
server := newServer(t)
|
||||
defer server.Shutdown()
|
||||
|
||||
clientConf := clientConfig()
|
||||
|
||||
var receivedBanner string
|
||||
clientConf.BannerCallback = func(message string) error {
|
||||
receivedBanner = message
|
||||
return nil
|
||||
}
|
||||
|
||||
conn := server.Dial(clientConf)
|
||||
defer conn.Close()
|
||||
|
||||
expected := "Server Banner"
|
||||
if receivedBanner != expected {
|
||||
t.Fatalf("got %v; want %v", receivedBanner, expected)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
// 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.
|
||||
|
||||
// Tests for ssh client multi-auth
|
||||
//
|
||||
// These tests run a simple go ssh client against OpenSSH server
|
||||
// over unix domain sockets. The tests use multiple combinations
|
||||
// of password, keyboard-interactive and publickey authentication
|
||||
// methods.
|
||||
//
|
||||
// A wrapper library for making sshd PAM authentication use test
|
||||
// passwords is required in ./sshd_test_pw.so. If the library does
|
||||
// not exist these tests will be skipped. See compile instructions
|
||||
// (for linux) in file ./sshd_test_pw.c.
|
||||
|
||||
// +build linux
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// test cases
|
||||
type multiAuthTestCase struct {
|
||||
authMethods []string
|
||||
expectedPasswordCbs int
|
||||
expectedKbdIntCbs int
|
||||
}
|
||||
|
||||
// test context
|
||||
type multiAuthTestCtx struct {
|
||||
password string
|
||||
numPasswordCbs int
|
||||
numKbdIntCbs int
|
||||
}
|
||||
|
||||
// create test context
|
||||
func newMultiAuthTestCtx(t *testing.T) *multiAuthTestCtx {
|
||||
password, err := randomPassword()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate random test password: %s", err.Error())
|
||||
}
|
||||
|
||||
return &multiAuthTestCtx{
|
||||
password: password,
|
||||
}
|
||||
}
|
||||
|
||||
// password callback
|
||||
func (ctx *multiAuthTestCtx) passwordCb() (secret string, err error) {
|
||||
ctx.numPasswordCbs++
|
||||
return ctx.password, nil
|
||||
}
|
||||
|
||||
// keyboard-interactive callback
|
||||
func (ctx *multiAuthTestCtx) kbdIntCb(user, instruction string, questions []string, echos []bool) (answers []string, err error) {
|
||||
if len(questions) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ctx.numKbdIntCbs++
|
||||
if len(questions) == 1 {
|
||||
return []string{ctx.password}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported keyboard-interactive flow")
|
||||
}
|
||||
|
||||
// TestMultiAuth runs several subtests for different combinations of password, keyboard-interactive and publickey authentication methods
|
||||
func TestMultiAuth(t *testing.T) {
|
||||
testCases := []multiAuthTestCase{
|
||||
// Test password,publickey authentication, assert that password callback is called 1 time
|
||||
multiAuthTestCase{
|
||||
authMethods: []string{"password", "publickey"},
|
||||
expectedPasswordCbs: 1,
|
||||
},
|
||||
// Test keyboard-interactive,publickey authentication, assert that keyboard-interactive callback is called 1 time
|
||||
multiAuthTestCase{
|
||||
authMethods: []string{"keyboard-interactive", "publickey"},
|
||||
expectedKbdIntCbs: 1,
|
||||
},
|
||||
// Test publickey,password authentication, assert that password callback is called 1 time
|
||||
multiAuthTestCase{
|
||||
authMethods: []string{"publickey", "password"},
|
||||
expectedPasswordCbs: 1,
|
||||
},
|
||||
// Test publickey,keyboard-interactive authentication, assert that keyboard-interactive callback is called 1 time
|
||||
multiAuthTestCase{
|
||||
authMethods: []string{"publickey", "keyboard-interactive"},
|
||||
expectedKbdIntCbs: 1,
|
||||
},
|
||||
// Test password,password authentication, assert that password callback is called 2 times
|
||||
multiAuthTestCase{
|
||||
authMethods: []string{"password", "password"},
|
||||
expectedPasswordCbs: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(strings.Join(testCase.authMethods, ","), func(t *testing.T) {
|
||||
ctx := newMultiAuthTestCtx(t)
|
||||
|
||||
server := newServerForConfig(t, "MultiAuth", map[string]string{"AuthMethods": strings.Join(testCase.authMethods, ",")})
|
||||
defer server.Shutdown()
|
||||
|
||||
clientConfig := clientConfig()
|
||||
server.setTestPassword(clientConfig.User, ctx.password)
|
||||
|
||||
publicKeyAuthMethod := clientConfig.Auth[0]
|
||||
clientConfig.Auth = nil
|
||||
for _, authMethod := range testCase.authMethods {
|
||||
switch authMethod {
|
||||
case "publickey":
|
||||
clientConfig.Auth = append(clientConfig.Auth, publicKeyAuthMethod)
|
||||
case "password":
|
||||
clientConfig.Auth = append(clientConfig.Auth,
|
||||
ssh.RetryableAuthMethod(ssh.PasswordCallback(ctx.passwordCb), 5))
|
||||
case "keyboard-interactive":
|
||||
clientConfig.Auth = append(clientConfig.Auth,
|
||||
ssh.RetryableAuthMethod(ssh.KeyboardInteractive(ctx.kbdIntCb), 5))
|
||||
default:
|
||||
t.Fatalf("Unknown authentication method %s", authMethod)
|
||||
}
|
||||
}
|
||||
|
||||
conn := server.Dial(clientConfig)
|
||||
defer conn.Close()
|
||||
|
||||
if ctx.numPasswordCbs != testCase.expectedPasswordCbs {
|
||||
t.Fatalf("passwordCallback was called %d times, expected %d times", ctx.numPasswordCbs, testCase.expectedPasswordCbs)
|
||||
}
|
||||
|
||||
if ctx.numKbdIntCbs != testCase.expectedKbdIntCbs {
|
||||
t.Fatalf("keyboardInteractiveCallback was called %d times, expected %d times", ctx.numKbdIntCbs, testCase.expectedKbdIntCbs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
// 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.
|
||||
|
||||
// sshd_test_pw.c
|
||||
// Wrapper to inject test password data for sshd PAM authentication
|
||||
//
|
||||
// This wrapper implements custom versions of getpwnam, getpwnam_r,
|
||||
// getspnam and getspnam_r. These functions first call their real
|
||||
// libc versions, then check if the requested user matches test user
|
||||
// specified in env variable TEST_USER and if so replace the password
|
||||
// with crypted() value of TEST_PASSWD env variable.
|
||||
//
|
||||
// Compile:
|
||||
// gcc -Wall -shared -o sshd_test_pw.so -fPIC sshd_test_pw.c
|
||||
//
|
||||
// Compile with debug:
|
||||
// gcc -DVERBOSE -Wall -shared -o sshd_test_pw.so -fPIC sshd_test_pw.c
|
||||
//
|
||||
// Run sshd:
|
||||
// LD_PRELOAD="sshd_test_pw.so" TEST_USER="..." TEST_PASSWD="..." sshd ...
|
||||
|
||||
// +build ignore
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <string.h>
|
||||
#include <pwd.h>
|
||||
#include <shadow.h>
|
||||
#include <dlfcn.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#ifdef VERBOSE
|
||||
#define DEBUG(X...) fprintf(stderr, X)
|
||||
#else
|
||||
#define DEBUG(X...) while (0) { }
|
||||
#endif
|
||||
|
||||
/* crypt() password */
|
||||
static char *
|
||||
pwhash(char *passwd) {
|
||||
return strdup(crypt(passwd, "$6$"));
|
||||
}
|
||||
|
||||
/* Pointers to real functions in libc */
|
||||
static struct passwd * (*real_getpwnam)(const char *) = NULL;
|
||||
static int (*real_getpwnam_r)(const char *, struct passwd *, char *, size_t, struct passwd **) = NULL;
|
||||
static struct spwd * (*real_getspnam)(const char *) = NULL;
|
||||
static int (*real_getspnam_r)(const char *, struct spwd *, char *, size_t, struct spwd **) = NULL;
|
||||
|
||||
/* Cached test user and test password */
|
||||
static char *test_user = NULL;
|
||||
static char *test_passwd_hash = NULL;
|
||||
|
||||
static void
|
||||
init(void) {
|
||||
/* Fetch real libc function pointers */
|
||||
real_getpwnam = dlsym(RTLD_NEXT, "getpwnam");
|
||||
real_getpwnam_r = dlsym(RTLD_NEXT, "getpwnam_r");
|
||||
real_getspnam = dlsym(RTLD_NEXT, "getspnam");
|
||||
real_getspnam_r = dlsym(RTLD_NEXT, "getspnam_r");
|
||||
|
||||
/* abort if env variables are not defined */
|
||||
if (getenv("TEST_USER") == NULL || getenv("TEST_PASSWD") == NULL) {
|
||||
fprintf(stderr, "env variables TEST_USER and TEST_PASSWD are missing\n");
|
||||
abort();
|
||||
}
|
||||
|
||||
/* Fetch test user and test password from env */
|
||||
test_user = strdup(getenv("TEST_USER"));
|
||||
test_passwd_hash = pwhash(getenv("TEST_PASSWD"));
|
||||
|
||||
DEBUG("sshd_test_pw init():\n");
|
||||
DEBUG("\treal_getpwnam: %p\n", real_getpwnam);
|
||||
DEBUG("\treal_getpwnam_r: %p\n", real_getpwnam_r);
|
||||
DEBUG("\treal_getspnam: %p\n", real_getspnam);
|
||||
DEBUG("\treal_getspnam_r: %p\n", real_getspnam_r);
|
||||
DEBUG("\tTEST_USER: '%s'\n", test_user);
|
||||
DEBUG("\tTEST_PASSWD: '%s'\n", getenv("TEST_PASSWD"));
|
||||
DEBUG("\tTEST_PASSWD_HASH: '%s'\n", test_passwd_hash);
|
||||
}
|
||||
|
||||
static int
|
||||
is_test_user(const char *name) {
|
||||
if (test_user != NULL && strcmp(test_user, name) == 0)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* getpwnam */
|
||||
|
||||
struct passwd *
|
||||
getpwnam(const char *name) {
|
||||
struct passwd *pw;
|
||||
|
||||
DEBUG("sshd_test_pw getpwnam(%s)\n", name);
|
||||
|
||||
if (real_getpwnam == NULL)
|
||||
init();
|
||||
if ((pw = real_getpwnam(name)) == NULL)
|
||||
return NULL;
|
||||
|
||||
if (is_test_user(name))
|
||||
pw->pw_passwd = strdup(test_passwd_hash);
|
||||
|
||||
return pw;
|
||||
}
|
||||
|
||||
/* getpwnam_r */
|
||||
|
||||
int
|
||||
getpwnam_r(const char *name,
|
||||
struct passwd *pwd,
|
||||
char *buf,
|
||||
size_t buflen,
|
||||
struct passwd **result) {
|
||||
int r;
|
||||
|
||||
DEBUG("sshd_test_pw getpwnam_r(%s)\n", name);
|
||||
|
||||
if (real_getpwnam_r == NULL)
|
||||
init();
|
||||
if ((r = real_getpwnam_r(name, pwd, buf, buflen, result)) != 0 || *result == NULL)
|
||||
return r;
|
||||
|
||||
if (is_test_user(name))
|
||||
pwd->pw_passwd = strdup(test_passwd_hash);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* getspnam */
|
||||
|
||||
struct spwd *
|
||||
getspnam(const char *name) {
|
||||
struct spwd *sp;
|
||||
|
||||
DEBUG("sshd_test_pw getspnam(%s)\n", name);
|
||||
|
||||
if (real_getspnam == NULL)
|
||||
init();
|
||||
if ((sp = real_getspnam(name)) == NULL)
|
||||
return NULL;
|
||||
|
||||
if (is_test_user(name))
|
||||
sp->sp_pwdp = strdup(test_passwd_hash);
|
||||
|
||||
return sp;
|
||||
}
|
||||
|
||||
/* getspnam_r */
|
||||
|
||||
int
|
||||
getspnam_r(const char *name,
|
||||
struct spwd *spbuf,
|
||||
char *buf,
|
||||
size_t buflen,
|
||||
struct spwd **spbufp) {
|
||||
int r;
|
||||
|
||||
DEBUG("sshd_test_pw getspnam_r(%s)\n", name);
|
||||
|
||||
if (real_getspnam_r == NULL)
|
||||
init();
|
||||
if ((r = real_getspnam_r(name, spbuf, buf, buflen, spbufp)) != 0)
|
||||
return r;
|
||||
|
||||
if (is_test_user(name))
|
||||
spbuf->sp_pwdp = strdup(test_passwd_hash);
|
||||
|
||||
return r;
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright 2009 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.
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
// System call support for plan9 on arm
|
||||
|
||||
// Just jump to package syscall's implementation for all these functions.
|
||||
// The runtime may know about them.
|
||||
|
||||
TEXT ·Syscall(SB),NOSPLIT,$0-32
|
||||
JMP syscall·Syscall(SB)
|
||||
|
||||
TEXT ·Syscall6(SB),NOSPLIT,$0-44
|
||||
JMP syscall·Syscall6(SB)
|
||||
|
||||
TEXT ·RawSyscall(SB),NOSPLIT,$0-28
|
||||
JMP syscall·RawSyscall(SB)
|
||||
|
||||
TEXT ·RawSyscall6(SB),NOSPLIT,$0-40
|
||||
JMP syscall·RawSyscall6(SB)
|
||||
|
||||
TEXT ·seek(SB),NOSPLIT,$0-36
|
||||
JMP syscall·exit(SB)
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
// mksyscall.pl -l32 -plan9 -tags plan9,arm syscall_plan9.go
|
||||
// MACHINE GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
|
||||
|
||||
// +build plan9,arm
|
||||
|
||||
package plan9
|
||||
|
||||
import "unsafe"
|
||||
|
||||
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||
|
||||
func fd2path(fd int, buf []byte) (err error) {
|
||||
var _p0 unsafe.Pointer
|
||||
if len(buf) > 0 {
|
||||
_p0 = unsafe.Pointer(&buf[0])
|
||||
} else {
|
||||
_p0 = unsafe.Pointer(&_zero)
|
||||
}
|
||||
r0, _, e1 := Syscall(SYS_FD2PATH, uintptr(fd), uintptr(_p0), uintptr(len(buf)))
|
||||
if int32(r0) == -1 {
|
||||
err = e1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||
|
||||
func pipe(p *[2]int32) (err error) {
|
||||
r0, _, e1 := Syscall(SYS_PIPE, uintptr(unsafe.Pointer(p)), 0, 0)
|
||||
if int32(r0) == -1 {
|
||||
err = e1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||
|
||||
func await(s []byte) (n int, err error) {
|
||||
var _p0 unsafe.Pointer
|
||||
if len(s) > 0 {
|
||||
_p0 = unsafe.Pointer(&s[0])
|
||||
} else {
|
||||
_p0 = unsafe.Pointer(&_zero)
|
||||
}
|
||||
r0, _, e1 := Syscall(SYS_AWAIT, uintptr(_p0), uintptr(len(s)), 0)
|
||||
n = int(r0)
|
||||
if int32(r0) == -1 {
|
||||
err = e1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||
|
||||
func open(path string, mode int) (fd int, err error) {
|
||||
var _p0 *byte
|
||||
_p0, err = BytePtrFromString(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
r0, _, e1 := Syscall(SYS_OPEN, uintptr(unsafe.Pointer(_p0)), uintptr(mode), 0)
|
||||
fd = int(r0)
|
||||
if int32(r0) == -1 {
|
||||
err = e1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||
|
||||
func create(path string, mode int, perm uint32) (fd int, err error) {
|
||||
var _p0 *byte
|
||||
_p0, err = BytePtrFromString(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
r0, _, e1 := Syscall(SYS_CREATE, uintptr(unsafe.Pointer(_p0)), uintptr(mode), uintptr(perm))
|
||||
fd = int(r0)
|
||||
if int32(r0) == -1 {
|
||||
err = e1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||
|
||||
func remove(path string) (err error) {
|
||||
var _p0 *byte
|
||||
_p0, err = BytePtrFromString(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
r0, _, e1 := Syscall(SYS_REMOVE, uintptr(unsafe.Pointer(_p0)), 0, 0)
|
||||
if int32(r0) == -1 {
|
||||
err = e1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||
|
||||
func stat(path string, edir []byte) (n int, err error) {
|
||||
var _p0 *byte
|
||||
_p0, err = BytePtrFromString(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var _p1 unsafe.Pointer
|
||||
if len(edir) > 0 {
|
||||
_p1 = unsafe.Pointer(&edir[0])
|
||||
} else {
|
||||
_p1 = unsafe.Pointer(&_zero)
|
||||
}
|
||||
r0, _, e1 := Syscall(SYS_STAT, uintptr(unsafe.Pointer(_p0)), uintptr(_p1), uintptr(len(edir)))
|
||||
n = int(r0)
|
||||
if int32(r0) == -1 {
|
||||
err = e1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||
|
||||
func bind(name string, old string, flag int) (err error) {
|
||||
var _p0 *byte
|
||||
_p0, err = BytePtrFromString(name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var _p1 *byte
|
||||
_p1, err = BytePtrFromString(old)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
r0, _, e1 := Syscall(SYS_BIND, uintptr(unsafe.Pointer(_p0)), uintptr(unsafe.Pointer(_p1)), uintptr(flag))
|
||||
if int32(r0) == -1 {
|
||||
err = e1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||
|
||||
func mount(fd int, afd int, old string, flag int, aname string) (err error) {
|
||||
var _p0 *byte
|
||||
_p0, err = BytePtrFromString(old)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var _p1 *byte
|
||||
_p1, err = BytePtrFromString(aname)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
r0, _, e1 := Syscall6(SYS_MOUNT, uintptr(fd), uintptr(afd), uintptr(unsafe.Pointer(_p0)), uintptr(flag), uintptr(unsafe.Pointer(_p1)), 0)
|
||||
if int32(r0) == -1 {
|
||||
err = e1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||
|
||||
func wstat(path string, edir []byte) (err error) {
|
||||
var _p0 *byte
|
||||
_p0, err = BytePtrFromString(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var _p1 unsafe.Pointer
|
||||
if len(edir) > 0 {
|
||||
_p1 = unsafe.Pointer(&edir[0])
|
||||
} else {
|
||||
_p1 = unsafe.Pointer(&_zero)
|
||||
}
|
||||
r0, _, e1 := Syscall(SYS_WSTAT, uintptr(unsafe.Pointer(_p0)), uintptr(_p1), uintptr(len(edir)))
|
||||
if int32(r0) == -1 {
|
||||
err = e1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||
|
||||
func chdir(path string) (err error) {
|
||||
var _p0 *byte
|
||||
_p0, err = BytePtrFromString(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
r0, _, e1 := Syscall(SYS_CHDIR, uintptr(unsafe.Pointer(_p0)), 0, 0)
|
||||
if int32(r0) == -1 {
|
||||
err = e1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||
|
||||
func Dup(oldfd int, newfd int) (fd int, err error) {
|
||||
r0, _, e1 := Syscall(SYS_DUP, uintptr(oldfd), uintptr(newfd), 0)
|
||||
fd = int(r0)
|
||||
if int32(r0) == -1 {
|
||||
err = e1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||
|
||||
func Pread(fd int, p []byte, offset int64) (n int, err error) {
|
||||
var _p0 unsafe.Pointer
|
||||
if len(p) > 0 {
|
||||
_p0 = unsafe.Pointer(&p[0])
|
||||
} else {
|
||||
_p0 = unsafe.Pointer(&_zero)
|
||||
}
|
||||
r0, _, e1 := Syscall6(SYS_PREAD, uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(offset), uintptr(offset>>32), 0)
|
||||
n = int(r0)
|
||||
if int32(r0) == -1 {
|
||||
err = e1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||
|
||||
func Pwrite(fd int, p []byte, offset int64) (n int, err error) {
|
||||
var _p0 unsafe.Pointer
|
||||
if len(p) > 0 {
|
||||
_p0 = unsafe.Pointer(&p[0])
|
||||
} else {
|
||||
_p0 = unsafe.Pointer(&_zero)
|
||||
}
|
||||
r0, _, e1 := Syscall6(SYS_PWRITE, uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(offset), uintptr(offset>>32), 0)
|
||||
n = int(r0)
|
||||
if int32(r0) == -1 {
|
||||
err = e1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||
|
||||
func Close(fd int) (err error) {
|
||||
r0, _, e1 := Syscall(SYS_CLOSE, uintptr(fd), 0, 0)
|
||||
if int32(r0) == -1 {
|
||||
err = e1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||
|
||||
func Fstat(fd int, edir []byte) (n int, err error) {
|
||||
var _p0 unsafe.Pointer
|
||||
if len(edir) > 0 {
|
||||
_p0 = unsafe.Pointer(&edir[0])
|
||||
} else {
|
||||
_p0 = unsafe.Pointer(&_zero)
|
||||
}
|
||||
r0, _, e1 := Syscall(SYS_FSTAT, uintptr(fd), uintptr(_p0), uintptr(len(edir)))
|
||||
n = int(r0)
|
||||
if int32(r0) == -1 {
|
||||
err = e1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
|
||||
|
||||
func Fwstat(fd int, edir []byte) (err error) {
|
||||
var _p0 unsafe.Pointer
|
||||
if len(edir) > 0 {
|
||||
_p0 = unsafe.Pointer(&edir[0])
|
||||
} else {
|
||||
_p0 = unsafe.Pointer(&_zero)
|
||||
}
|
||||
r0, _, e1 := Syscall(SYS_FWSTAT, uintptr(fd), uintptr(_p0), uintptr(len(edir)))
|
||||
if int32(r0) == -1 {
|
||||
err = e1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
// Copyright 2018 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.
|
||||
|
||||
// CPU affinity functions
|
||||
|
||||
package unix
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const cpuSetSize = _CPU_SETSIZE / _NCPUBITS
|
||||
|
||||
// CPUSet represents a CPU affinity mask.
|
||||
type CPUSet [cpuSetSize]cpuMask
|
||||
|
||||
func schedAffinity(trap uintptr, pid int, set *CPUSet) error {
|
||||
_, _, e := RawSyscall(trap, uintptr(pid), uintptr(unsafe.Sizeof(*set)), uintptr(unsafe.Pointer(set)))
|
||||
if e != 0 {
|
||||
return errnoErr(e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SchedGetaffinity gets the CPU affinity mask of the thread specified by pid.
|
||||
// If pid is 0 the calling thread is used.
|
||||
func SchedGetaffinity(pid int, set *CPUSet) error {
|
||||
return schedAffinity(SYS_SCHED_GETAFFINITY, pid, set)
|
||||
}
|
||||
|
||||
// SchedSetaffinity sets the CPU affinity mask of the thread specified by pid.
|
||||
// If pid is 0 the calling thread is used.
|
||||
func SchedSetaffinity(pid int, set *CPUSet) error {
|
||||
return schedAffinity(SYS_SCHED_SETAFFINITY, pid, set)
|
||||
}
|
||||
|
||||
// Zero clears the set s, so that it contains no CPUs.
|
||||
func (s *CPUSet) Zero() {
|
||||
for i := range s {
|
||||
s[i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
func cpuBitsIndex(cpu int) int {
|
||||
return cpu / _NCPUBITS
|
||||
}
|
||||
|
||||
func cpuBitsMask(cpu int) cpuMask {
|
||||
return cpuMask(1 << (uint(cpu) % _NCPUBITS))
|
||||
}
|
||||
|
||||
// Set adds cpu to the set s.
|
||||
func (s *CPUSet) Set(cpu int) {
|
||||
i := cpuBitsIndex(cpu)
|
||||
if i < len(s) {
|
||||
s[i] |= cpuBitsMask(cpu)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear removes cpu from the set s.
|
||||
func (s *CPUSet) Clear(cpu int) {
|
||||
i := cpuBitsIndex(cpu)
|
||||
if i < len(s) {
|
||||
s[i] &^= cpuBitsMask(cpu)
|
||||
}
|
||||
}
|
||||
|
||||
// IsSet reports whether cpu is in the set s.
|
||||
func (s *CPUSet) IsSet(cpu int) bool {
|
||||
i := cpuBitsIndex(cpu)
|
||||
if i < len(s) {
|
||||
return s[i]&cpuBitsMask(cpu) != 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Count returns the number of CPUs in the set s.
|
||||
func (s *CPUSet) Count() int {
|
||||
c := 0
|
||||
for _, b := range s {
|
||||
c += onesCount64(uint64(b))
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// onesCount64 is a copy of Go 1.9's math/bits.OnesCount64.
|
||||
// Once this package can require Go 1.9, we can delete this
|
||||
// and update the caller to use bits.OnesCount64.
|
||||
func onesCount64(x uint64) int {
|
||||
const m0 = 0x5555555555555555 // 01010101 ...
|
||||
const m1 = 0x3333333333333333 // 00110011 ...
|
||||
const m2 = 0x0f0f0f0f0f0f0f0f // 00001111 ...
|
||||
const m3 = 0x00ff00ff00ff00ff // etc.
|
||||
const m4 = 0x0000ffff0000ffff
|
||||
|
||||
// Implementation: Parallel summing of adjacent bits.
|
||||
// See "Hacker's Delight", Chap. 5: Counting Bits.
|
||||
// The following pattern shows the general approach:
|
||||
//
|
||||
// x = x>>1&(m0&m) + x&(m0&m)
|
||||
// x = x>>2&(m1&m) + x&(m1&m)
|
||||
// x = x>>4&(m2&m) + x&(m2&m)
|
||||
// x = x>>8&(m3&m) + x&(m3&m)
|
||||
// x = x>>16&(m4&m) + x&(m4&m)
|
||||
// x = x>>32&(m5&m) + x&(m5&m)
|
||||
// return int(x)
|
||||
//
|
||||
// Masking (& operations) can be left away when there's no
|
||||
// danger that a field's sum will carry over into the next
|
||||
// field: Since the result cannot be > 64, 8 bits is enough
|
||||
// and we can ignore the masks for the shifts by 8 and up.
|
||||
// Per "Hacker's Delight", the first line can be simplified
|
||||
// more, but it saves at best one instruction, so we leave
|
||||
// it alone for clarity.
|
||||
const m = 1<<64 - 1
|
||||
x = x>>1&(m0&m) + x&(m0&m)
|
||||
x = x>>2&(m1&m) + x&(m1&m)
|
||||
x = (x>>4 + x) & (m2 & m)
|
||||
x += x >> 8
|
||||
x += x >> 16
|
||||
x += x >> 32
|
||||
return int(x) & (1<<7 - 1)
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright 2018 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.
|
||||
|
||||
// +build linux,!gccgo
|
||||
|
||||
package unix
|
||||
|
||||
// SyscallNoError may be used instead of Syscall for syscalls that don't fail.
|
||||
func SyscallNoError(trap, a1, a2, a3 uintptr) (r1, r2 uintptr)
|
||||
|
||||
// RawSyscallNoError may be used instead of RawSyscall for syscalls that don't
|
||||
// fail.
|
||||
func RawSyscallNoError(trap, a1, a2, a3 uintptr) (r1, r2 uintptr)
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright 2017 The Go Authors. All right reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||
|
||||
package unix_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestTimeToTimespec(t *testing.T) {
|
||||
timeTests := []struct {
|
||||
time time.Time
|
||||
valid bool
|
||||
}{
|
||||
{time.Unix(0, 0), true},
|
||||
{time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC), true},
|
||||
{time.Date(2262, time.December, 31, 23, 0, 0, 0, time.UTC), false},
|
||||
{time.Unix(0x7FFFFFFF, 0), true},
|
||||
{time.Unix(0x80000000, 0), false},
|
||||
{time.Unix(0x7FFFFFFF, 1000000000), false},
|
||||
{time.Unix(0x7FFFFFFF, 999999999), true},
|
||||
{time.Unix(-0x80000000, 0), true},
|
||||
{time.Unix(-0x80000001, 0), false},
|
||||
{time.Date(2038, time.January, 19, 3, 14, 7, 0, time.UTC), true},
|
||||
{time.Date(2038, time.January, 19, 3, 14, 8, 0, time.UTC), false},
|
||||
{time.Date(1901, time.December, 13, 20, 45, 52, 0, time.UTC), true},
|
||||
{time.Date(1901, time.December, 13, 20, 45, 51, 0, time.UTC), false},
|
||||
}
|
||||
|
||||
// Currently all targets have either int32 or int64 for Timespec.Sec.
|
||||
// If there were a new target with unsigned or floating point type for
|
||||
// it, this test must be adjusted.
|
||||
have64BitTime := (unsafe.Sizeof(unix.Timespec{}.Sec) == 8)
|
||||
for _, tt := range timeTests {
|
||||
ts, err := unix.TimeToTimespec(tt.time)
|
||||
tt.valid = tt.valid || have64BitTime
|
||||
if tt.valid && err != nil {
|
||||
t.Errorf("TimeToTimespec(%v): %v", tt.time, err)
|
||||
}
|
||||
if err == nil {
|
||||
tstime := time.Unix(int64(ts.Sec), int64(ts.Nsec))
|
||||
if !tstime.Equal(tt.time) {
|
||||
t.Errorf("TimeToTimespec(%v) is the time %v", tt.time, tstime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,49 @@
|
|||
// 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 main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/build"
|
||||
"go/parser"
|
||||
|
||||
"golang.org/x/tools/go/loader"
|
||||
)
|
||||
|
||||
const (
|
||||
extractFile = "extracted.gotext.json"
|
||||
outFile = "out.gotext.json"
|
||||
gotextSuffix = ".gotext.json"
|
||||
)
|
||||
|
||||
// NOTE: The command line tool already prefixes with "gotext:".
|
||||
var (
|
||||
wrap = func(err error, msg string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s: %v", msg, err)
|
||||
}
|
||||
errorf = fmt.Errorf
|
||||
)
|
||||
|
||||
// TODO: still used. Remove when possible.
|
||||
func loadPackages(conf *loader.Config, args []string) (*loader.Program, error) {
|
||||
if len(args) == 0 {
|
||||
args = []string{"."}
|
||||
}
|
||||
|
||||
conf.Build = &build.Default
|
||||
conf.ParserMode = parser.ParseComments
|
||||
|
||||
// Use the initial packages from the command line.
|
||||
args, err := conf.FromArgs(args, false)
|
||||
if err != nil {
|
||||
return nil, wrap(err, "loading packages failed")
|
||||
}
|
||||
|
||||
// Load, parse and type-check the whole program.
|
||||
return conf.Load()
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
"golang.org/x/text/message/catalog"
|
||||
)
|
||||
|
||||
type dictionary struct {
|
||||
index []uint32
|
||||
data string
|
||||
}
|
||||
|
||||
func (d *dictionary) Lookup(key string) (data string, ok bool) {
|
||||
p := messageKeyToIndex[key]
|
||||
start, end := d.index[p], d.index[p+1]
|
||||
if start == end {
|
||||
return "", false
|
||||
}
|
||||
return d.data[start:end], true
|
||||
}
|
||||
|
||||
func init() {
|
||||
dict := map[string]catalog.Dictionary{
|
||||
"de": &dictionary{index: deIndex, data: deData},
|
||||
"en_US": &dictionary{index: en_USIndex, data: en_USData},
|
||||
"zh": &dictionary{index: zhIndex, data: zhData},
|
||||
}
|
||||
fallback := language.MustParse("en-US")
|
||||
cat, err := catalog.NewFromMap(dict, catalog.Fallback(fallback))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
message.DefaultCatalog = cat
|
||||
}
|
||||
|
||||
var messageKeyToIndex = map[string]int{
|
||||
"%.2[1]f miles traveled (%[1]f)": 8,
|
||||
"%[1]s is visiting %[3]s!\n": 3,
|
||||
"%d files remaining!": 5,
|
||||
"%d more files remaining!": 4,
|
||||
"%s is out of order!": 7,
|
||||
"%s is visiting %s!\n": 2,
|
||||
"Hello %s!\n": 1,
|
||||
"Hello world!\n": 0,
|
||||
"Use the following code for your discount: %d\n": 6,
|
||||
}
|
||||
|
||||
var deIndex = []uint32{ // 10 elements
|
||||
0x00000000, 0x00000011, 0x00000023, 0x0000003d,
|
||||
0x00000057, 0x00000076, 0x00000076, 0x00000076,
|
||||
0x00000076, 0x00000076,
|
||||
} // Size: 64 bytes
|
||||
|
||||
const deData string = "" + // Size: 118 bytes
|
||||
"\x04\x00\x01\x0a\x0c\x02Hallo Welt!\x04\x00\x01\x0a\x0d\x02Hallo %[1]s!" +
|
||||
"\x04\x00\x01\x0a\x15\x02%[1]s besucht %[2]s!\x04\x00\x01\x0a\x15\x02%[1]" +
|
||||
"s besucht %[3]s!\x02Noch %[1]d Bestände zu gehen!"
|
||||
|
||||
var en_USIndex = []uint32{ // 10 elements
|
||||
0x00000000, 0x00000012, 0x00000024, 0x00000042,
|
||||
0x00000060, 0x000000a3, 0x000000ba, 0x000000ef,
|
||||
0x00000106, 0x00000125,
|
||||
} // Size: 64 bytes
|
||||
|
||||
const en_USData string = "" + // Size: 293 bytes
|
||||
"\x04\x00\x01\x0a\x0d\x02Hello world!\x04\x00\x01\x0a\x0d\x02Hello %[1]sn" +
|
||||
"\x04\x00\x01\x0a\x19\x02%[1]s is visiting %[2]s!\x04\x00\x01\x0a\x19\x02" +
|
||||
"%[1]s is visiting %[3]s!\x14\x01\x81\x01\x00\x02\x14\x02One file remaini" +
|
||||
"ng!\x00&\x02There are %[1]d more files remaining!\x02%[1]d files remaini" +
|
||||
"ng!\x04\x00\x01\x0a0\x02Use the following code for your discount: %[1]d" +
|
||||
"\x02%[1]s is out of order!\x02%.2[1]f miles traveled (%[1]f)"
|
||||
|
||||
var zhIndex = []uint32{ // 10 elements
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000,
|
||||
} // Size: 64 bytes
|
||||
|
||||
const zhData string = ""
|
||||
|
||||
// Total table size 603 bytes (0KiB); checksum: 1D2754EE
|
||||
186
vendor/golang.org/x/text/cmd/gotext/examples/extract/locales/de/messages.gotext.json
generated
vendored
Executable file
186
vendor/golang.org/x/text/cmd/gotext/examples/extract/locales/de/messages.gotext.json
generated
vendored
Executable file
|
|
@ -0,0 +1,186 @@
|
|||
{
|
||||
"language": "de",
|
||||
"messages": [
|
||||
{
|
||||
"id": "Hello world!",
|
||||
"key": "Hello world!\n",
|
||||
"message": "Hello world!",
|
||||
"translation": "Hallo Welt!",
|
||||
"position": "golang.org/x/text/cmd/gotext/examples/extract/main.go:27:10"
|
||||
},
|
||||
{
|
||||
"id": "Hello {City}!",
|
||||
"key": "Hello %s!\n",
|
||||
"message": "Hello {City}!",
|
||||
"translation": "Hallo {City}!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "City",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "city"
|
||||
}
|
||||
],
|
||||
"position": "golang.org/x/text/cmd/gotext/examples/extract/main.go:31:10"
|
||||
},
|
||||
{
|
||||
"id": "Hello {Town}!",
|
||||
"key": "Hello %s!\n",
|
||||
"message": "Hello {Town}!",
|
||||
"translation": "Hallo {Town}!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Town",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "town",
|
||||
"comment": "Town"
|
||||
}
|
||||
],
|
||||
"position": "golang.org/x/text/cmd/gotext/examples/extract/main.go:35:10"
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"key": "%s is visiting %s!\n",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "{Person} besucht {Place}!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Person",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "person",
|
||||
"comment": "The person of matter."
|
||||
},
|
||||
{
|
||||
"id": "Place",
|
||||
"string": "%[2]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 2,
|
||||
"expr": "place",
|
||||
"comment": "Place the person is visiting."
|
||||
}
|
||||
],
|
||||
"position": "golang.org/x/text/cmd/gotext/examples/extract/main.go:40:10"
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"key": "%[1]s is visiting %[3]s!\n",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "{Person} besucht {Place}!",
|
||||
"comment": "Person visiting a place.",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Person",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "pp.Person"
|
||||
},
|
||||
{
|
||||
"id": "Place",
|
||||
"string": "%[3]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 3,
|
||||
"expr": "pp.Place",
|
||||
"comment": "Place the person is visiting."
|
||||
},
|
||||
{
|
||||
"id": "Extra",
|
||||
"string": "%[2]v",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 2,
|
||||
"expr": "pp.extra"
|
||||
}
|
||||
],
|
||||
"position": "golang.org/x/text/cmd/gotext/examples/extract/main.go:55:10"
|
||||
},
|
||||
{
|
||||
"id": "{N} more files remaining!",
|
||||
"key": "%d more files remaining!",
|
||||
"message": "{N} more files remaining!",
|
||||
"translation": "Noch {N} Bestände zu gehen!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "N",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "n"
|
||||
}
|
||||
],
|
||||
"position": "golang.org/x/text/cmd/gotext/examples/extract/main.go:67:10"
|
||||
},
|
||||
{
|
||||
"id": "Use the following code for your discount: {ReferralCode}",
|
||||
"key": "Use the following code for your discount: %d\n",
|
||||
"message": "Use the following code for your discount: {ReferralCode}",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "ReferralCode",
|
||||
"string": "%[1]d",
|
||||
"type": "golang.org/x/text/cmd/gotext/examples/extract.referralCode",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "c"
|
||||
}
|
||||
],
|
||||
"position": "golang.org/x/text/cmd/gotext/examples/extract/main.go:73:10"
|
||||
},
|
||||
{
|
||||
"id": [ "msgOutOfOrder", "{Device} is out of order!" ],
|
||||
"key": "%s is out of order!",
|
||||
"message": "{Device} is out of order!",
|
||||
"translation": "",
|
||||
"comment": "FOO\n",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Device",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "device"
|
||||
}
|
||||
],
|
||||
"position": "golang.org/x/text/cmd/gotext/examples/extract/main.go:81:10"
|
||||
},
|
||||
{
|
||||
"id": "{Miles} miles traveled ({Miles_1})",
|
||||
"key": "%.2[1]f miles traveled (%[1]f)",
|
||||
"message": "{Miles} miles traveled ({Miles_1})",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Miles",
|
||||
"string": "%.2[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
},
|
||||
{
|
||||
"id": "Miles_1",
|
||||
"string": "%[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
}
|
||||
],
|
||||
"position": "golang.org/x/text/cmd/gotext/examples/extract/main.go:85:10"
|
||||
}
|
||||
]
|
||||
}
|
||||
137
vendor/golang.org/x/text/cmd/gotext/examples/extract/locales/de/out.gotext.json
generated
vendored
Executable file
137
vendor/golang.org/x/text/cmd/gotext/examples/extract/locales/de/out.gotext.json
generated
vendored
Executable file
|
|
@ -0,0 +1,137 @@
|
|||
{
|
||||
"language": "de",
|
||||
"messages": [
|
||||
{
|
||||
"id": "Hello world!",
|
||||
"message": "Hello world!",
|
||||
"translation": "Hallo Welt!"
|
||||
},
|
||||
{
|
||||
"id": "Hello {City}!",
|
||||
"message": "Hello {City}!",
|
||||
"translation": "Hallo {City}!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "City",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "city"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "{Person} besucht {Place}!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Person",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "person",
|
||||
"comment": "The person of matter."
|
||||
},
|
||||
{
|
||||
"id": "Place",
|
||||
"string": "%[2]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 2,
|
||||
"expr": "place",
|
||||
"comment": "Place the person is visiting."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{2} files remaining!",
|
||||
"message": "{2} files remaining!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "2",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{N} more files remaining!",
|
||||
"message": "{N} more files remaining!",
|
||||
"translation": "Noch {N} Bestände zu gehen!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "N",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "n"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Use the following code for your discount: {ReferralCode}",
|
||||
"message": "Use the following code for your discount: {ReferralCode}",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "ReferralCode",
|
||||
"string": "%[1]d",
|
||||
"type": "golang.org/x/text/cmd/gotext/examples/extract.referralCode",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "c"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": [
|
||||
"msgOutOfOrder",
|
||||
"{Device} is out of order!"
|
||||
],
|
||||
"message": "{Device} is out of order!",
|
||||
"translation": "",
|
||||
"comment": "FOO\n",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Device",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "device"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Miles} miles traveled ({Miles_1})",
|
||||
"message": "{Miles} miles traveled ({Miles_1})",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Miles",
|
||||
"string": "%.2[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
},
|
||||
{
|
||||
"id": "Miles_1",
|
||||
"string": "%[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
82
vendor/golang.org/x/text/cmd/gotext/examples/extract/locales/en-US/messages.gotext.json
generated
vendored
Executable file
82
vendor/golang.org/x/text/cmd/gotext/examples/extract/locales/en-US/messages.gotext.json
generated
vendored
Executable file
|
|
@ -0,0 +1,82 @@
|
|||
{
|
||||
"language": "en-US",
|
||||
"messages": [
|
||||
{
|
||||
"id": "Hello world!",
|
||||
"key": "Hello world!\n",
|
||||
"message": "Hello world!",
|
||||
"translation": "Hello world!",
|
||||
"position": "golang.org/x/text/cmd/gotext/examples/extract/main.go:27:10"
|
||||
},
|
||||
{
|
||||
"id": "Hello {City}!",
|
||||
"key": "Hello %s!\n",
|
||||
"message": "Hello {City}!",
|
||||
"translation": "Hello {City}n"
|
||||
},
|
||||
{
|
||||
"id": "Hello {Town}!",
|
||||
"key": "Hello %s!\n",
|
||||
"message": "Hello {Town}!",
|
||||
"translation": "Hello {Town}!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Town",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "town",
|
||||
"comment": "Town"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"key": "%s is visiting %s!\n",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "{Person} is visiting {Place}!\n"
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"key": "%[1]s is visiting %[3]s!\n",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "{Person} is visiting {Place}!",
|
||||
"comment": "Person visiting a place."
|
||||
},
|
||||
{
|
||||
"id": "{N} more files remaining!",
|
||||
"key": "%d more files remaining!",
|
||||
"message": "{N} more files remaining!",
|
||||
"translation": {
|
||||
"select": {
|
||||
"feature": "plural",
|
||||
"arg": "N",
|
||||
"cases": {
|
||||
"one": "One file remaining!",
|
||||
"other": "There are {N} more files remaining!"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Use the following code for your discount: {ReferralCode}",
|
||||
"key": "Use the following code for your discount: %d\n",
|
||||
"message": "Use the following code for your discount: {ReferralCode}",
|
||||
"translation": ""
|
||||
},
|
||||
{
|
||||
"id": [ "msgOutOfOrder", "{Device} is out of order!" ],
|
||||
"key": "%s is out of order!",
|
||||
"message": "{Device} is out of order!",
|
||||
"translation": "{Device} is out of order!",
|
||||
"comment": "FOO\n"
|
||||
},
|
||||
{
|
||||
"id": "{Miles} miles traveled ({Miles_1})",
|
||||
"key": "%.2[1]f miles traveled (%[1]f)",
|
||||
"message": "{Miles} miles traveled ({Miles_1})",
|
||||
"translation": "{Miles} miles traveled ({Miles_1})"
|
||||
}
|
||||
]
|
||||
}
|
||||
154
vendor/golang.org/x/text/cmd/gotext/examples/extract/locales/en-US/out.gotext.json
generated
vendored
Executable file
154
vendor/golang.org/x/text/cmd/gotext/examples/extract/locales/en-US/out.gotext.json
generated
vendored
Executable file
|
|
@ -0,0 +1,154 @@
|
|||
{
|
||||
"language": "en-US",
|
||||
"messages": [
|
||||
{
|
||||
"id": "Hello world!",
|
||||
"message": "Hello world!",
|
||||
"translation": "Hello world!"
|
||||
},
|
||||
{
|
||||
"id": "Hello {City}!",
|
||||
"message": "Hello {City}!",
|
||||
"translation": "Hello {City}n",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "City",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "city"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "{Person} is visiting {Place}!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Person",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "person",
|
||||
"comment": "The person of matter."
|
||||
},
|
||||
{
|
||||
"id": "Place",
|
||||
"string": "%[2]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 2,
|
||||
"expr": "place",
|
||||
"comment": "Place the person is visiting."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{2} files remaining!",
|
||||
"message": "{2} files remaining!",
|
||||
"translation": "{2} files remaining!",
|
||||
"translatorComment": "Copied from source.",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "2",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "2"
|
||||
}
|
||||
],
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "{N} more files remaining!",
|
||||
"message": "{N} more files remaining!",
|
||||
"translation": {
|
||||
"select": {
|
||||
"feature": "plural",
|
||||
"arg": "N",
|
||||
"cases": {
|
||||
"one": {
|
||||
"msg": "One file remaining!"
|
||||
},
|
||||
"other": {
|
||||
"msg": "There are {N} more files remaining!"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "N",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "n"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Use the following code for your discount: {ReferralCode}",
|
||||
"message": "Use the following code for your discount: {ReferralCode}",
|
||||
"translation": "Use the following code for your discount: {ReferralCode}",
|
||||
"translatorComment": "Copied from source.",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "ReferralCode",
|
||||
"string": "%[1]d",
|
||||
"type": "golang.org/x/text/cmd/gotext/examples/extract.referralCode",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "c"
|
||||
}
|
||||
],
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": [
|
||||
"msgOutOfOrder",
|
||||
"{Device} is out of order!"
|
||||
],
|
||||
"message": "{Device} is out of order!",
|
||||
"translation": "{Device} is out of order!",
|
||||
"comment": "FOO\n",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Device",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "device"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Miles} miles traveled ({Miles_1})",
|
||||
"message": "{Miles} miles traveled ({Miles_1})",
|
||||
"translation": "{Miles} miles traveled ({Miles_1})",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Miles",
|
||||
"string": "%.2[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
},
|
||||
{
|
||||
"id": "Miles_1",
|
||||
"string": "%[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
203
vendor/golang.org/x/text/cmd/gotext/examples/extract/locales/zh/messages.gotext.json
generated
vendored
Executable file
203
vendor/golang.org/x/text/cmd/gotext/examples/extract/locales/zh/messages.gotext.json
generated
vendored
Executable file
|
|
@ -0,0 +1,203 @@
|
|||
{
|
||||
"language": "zh",
|
||||
"messages": [
|
||||
{
|
||||
"id": "Hello world!",
|
||||
"key": "Hello world!\n",
|
||||
"message": "Hello world!",
|
||||
"translation": "",
|
||||
"position": "golang.org/x/text/cmd/gotext/examples/extract/main.go:27:10"
|
||||
},
|
||||
{
|
||||
"id": "Hello {City}!",
|
||||
"key": "Hello %s!\n",
|
||||
"message": "Hello {City}!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "City",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "city"
|
||||
}
|
||||
],
|
||||
"position": "golang.org/x/text/cmd/gotext/examples/extract/main.go:31:10"
|
||||
},
|
||||
{
|
||||
"id": "Hello {Town}!",
|
||||
"key": "Hello %s!\n",
|
||||
"message": "Hello {Town}!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Town",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "town",
|
||||
"comment": "Town"
|
||||
}
|
||||
],
|
||||
"position": "golang.org/x/text/cmd/gotext/examples/extract/main.go:35:10"
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"key": "%s is visiting %s!\n",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Person",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "person",
|
||||
"comment": "The person of matter."
|
||||
},
|
||||
{
|
||||
"id": "Place",
|
||||
"string": "%[2]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 2,
|
||||
"expr": "place",
|
||||
"comment": "Place the person is visiting."
|
||||
}
|
||||
],
|
||||
"position": "golang.org/x/text/cmd/gotext/examples/extract/main.go:40:10"
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"key": "%[1]s is visiting %[3]s!\n",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "",
|
||||
"comment": "Person visiting a place.",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Person",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "pp.Person"
|
||||
},
|
||||
{
|
||||
"id": "Place",
|
||||
"string": "%[3]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 3,
|
||||
"expr": "pp.Place",
|
||||
"comment": "Place the person is visiting."
|
||||
},
|
||||
{
|
||||
"id": "Extra",
|
||||
"string": "%[2]v",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 2,
|
||||
"expr": "pp.extra"
|
||||
}
|
||||
],
|
||||
"position": "golang.org/x/text/cmd/gotext/examples/extract/main.go:55:10"
|
||||
},
|
||||
{
|
||||
"id": "{} files remaining!",
|
||||
"key": "%d files remaining!",
|
||||
"message": "{} files remaining!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "2"
|
||||
}
|
||||
],
|
||||
"position": "golang.org/x/text/cmd/gotext/examples/extract/main.go:62:10"
|
||||
},
|
||||
{
|
||||
"id": "{N} more files remaining!",
|
||||
"key": "%d more files remaining!",
|
||||
"message": "{N} more files remaining!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "N",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "n"
|
||||
}
|
||||
],
|
||||
"position": "golang.org/x/text/cmd/gotext/examples/extract/main.go:67:10"
|
||||
},
|
||||
{
|
||||
"id": "Use the following code for your discount: {ReferralCode}\n",
|
||||
"key": "Use the following code for your discount: %d\n",
|
||||
"message": "Use the following code for your discount: {ReferralCode}\n",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "ReferralCode",
|
||||
"string": "%[1]d",
|
||||
"type": "golang.org/x/text/cmd/gotext/examples/extract.referralCode",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "c"
|
||||
}
|
||||
],
|
||||
"position": "golang.org/x/text/cmd/gotext/examples/extract/main.go:73:10"
|
||||
},
|
||||
{
|
||||
"id": [ "{Device} is out of order!", "msgOutOfOrder" ],
|
||||
"key": "%s is out of order!",
|
||||
"message": "{Device} is out of order!",
|
||||
"translation": "",
|
||||
"comment": "FOO\n",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Device",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "device"
|
||||
}
|
||||
],
|
||||
"position": "golang.org/x/text/cmd/gotext/examples/extract/main.go:81:10"
|
||||
},
|
||||
{
|
||||
"id": "{Miles} miles traveled ({Miles_1})",
|
||||
"key": "%.2[1]f miles traveled (%[1]f)",
|
||||
"message": "{Miles} miles traveled ({Miles_1})",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Miles",
|
||||
"string": "%.2[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
},
|
||||
{
|
||||
"id": "Miles_1",
|
||||
"string": "%[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
}
|
||||
],
|
||||
"position": "golang.org/x/text/cmd/gotext/examples/extract/main.go:85:10"
|
||||
}
|
||||
]
|
||||
}
|
||||
137
vendor/golang.org/x/text/cmd/gotext/examples/extract/locales/zh/out.gotext.json
generated
vendored
Executable file
137
vendor/golang.org/x/text/cmd/gotext/examples/extract/locales/zh/out.gotext.json
generated
vendored
Executable file
|
|
@ -0,0 +1,137 @@
|
|||
{
|
||||
"language": "zh",
|
||||
"messages": [
|
||||
{
|
||||
"id": "Hello world!",
|
||||
"message": "Hello world!",
|
||||
"translation": ""
|
||||
},
|
||||
{
|
||||
"id": "Hello {City}!",
|
||||
"message": "Hello {City}!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "City",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "city"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Person",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "person",
|
||||
"comment": "The person of matter."
|
||||
},
|
||||
{
|
||||
"id": "Place",
|
||||
"string": "%[2]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 2,
|
||||
"expr": "place",
|
||||
"comment": "Place the person is visiting."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{2} files remaining!",
|
||||
"message": "{2} files remaining!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "2",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{N} more files remaining!",
|
||||
"message": "{N} more files remaining!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "N",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "n"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Use the following code for your discount: {ReferralCode}",
|
||||
"message": "Use the following code for your discount: {ReferralCode}",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "ReferralCode",
|
||||
"string": "%[1]d",
|
||||
"type": "golang.org/x/text/cmd/gotext/examples/extract.referralCode",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "c"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": [
|
||||
"msgOutOfOrder",
|
||||
"{Device} is out of order!"
|
||||
],
|
||||
"message": "{Device} is out of order!",
|
||||
"translation": "",
|
||||
"comment": "FOO\n",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Device",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "device"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Miles} miles traveled ({Miles_1})",
|
||||
"message": "{Miles} miles traveled ({Miles_1})",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Miles",
|
||||
"string": "%.2[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
},
|
||||
{
|
||||
"id": "Miles_1",
|
||||
"string": "%[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
// 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 main
|
||||
|
||||
//go:generate gotext update -out catalog.go
|
||||
|
||||
import (
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
)
|
||||
|
||||
func main() {
|
||||
p := message.NewPrinter(language.English)
|
||||
|
||||
p.Print("Hello world!\n")
|
||||
|
||||
p.Println("Hello", "world!")
|
||||
|
||||
person := "Sheila"
|
||||
place := "Zürich"
|
||||
|
||||
p.Print("Hello ", person, " in ", place, "!\n")
|
||||
|
||||
// Greet everyone.
|
||||
p.Printf("Hello world!\n")
|
||||
|
||||
city := "Amsterdam"
|
||||
// Greet a city.
|
||||
p.Printf("Hello %s!\n", city)
|
||||
|
||||
town := "Amsterdam"
|
||||
// Greet a town.
|
||||
p.Printf("Hello %s!\n",
|
||||
town, // Town
|
||||
)
|
||||
|
||||
// Person visiting a place.
|
||||
p.Printf("%s is visiting %s!\n",
|
||||
person, // The person of matter.
|
||||
place, // Place the person is visiting.
|
||||
)
|
||||
|
||||
pp := struct {
|
||||
Person string // The person of matter. // TODO: get this comment.
|
||||
Place string
|
||||
extra int
|
||||
}{
|
||||
person, place, 4,
|
||||
}
|
||||
|
||||
// extract will drop this comment in favor of the one below.
|
||||
// argument is added as a placeholder.
|
||||
p.Printf("%[1]s is visiting %[3]s!\n", // Person visiting a place.
|
||||
pp.Person,
|
||||
pp.extra,
|
||||
pp.Place, // Place the person is visiting.
|
||||
)
|
||||
|
||||
// Numeric literal
|
||||
p.Printf("%d files remaining!", 2)
|
||||
|
||||
const n = 2
|
||||
|
||||
// Numeric var
|
||||
p.Printf("%d more files remaining!", n)
|
||||
|
||||
// Infer better names from type names.
|
||||
type referralCode int
|
||||
|
||||
const c = referralCode(5)
|
||||
p.Printf("Use the following code for your discount: %d\n", c)
|
||||
|
||||
// Using a constant for a message will cause the constant name to be
|
||||
// added as an identifier, allowing for stable message identifiers.
|
||||
|
||||
// Explain that a device is out of order.
|
||||
const msgOutOfOrder = "%s is out of order!" // FOO
|
||||
const device = "Soda machine"
|
||||
p.Printf(msgOutOfOrder, device)
|
||||
|
||||
// Double arguments.
|
||||
miles := 1.2345
|
||||
p.Printf("%.2[1]f miles traveled (%[1]f)", miles)
|
||||
}
|
||||
57
vendor/golang.org/x/text/cmd/gotext/examples/extract_http/catalog_gen.go
generated
vendored
Normal file
57
vendor/golang.org/x/text/cmd/gotext/examples/extract_http/catalog_gen.go
generated
vendored
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
"golang.org/x/text/message/catalog"
|
||||
)
|
||||
|
||||
type dictionary struct {
|
||||
index []uint32
|
||||
data string
|
||||
}
|
||||
|
||||
func (d *dictionary) Lookup(key string) (data string, ok bool) {
|
||||
p := messageKeyToIndex[key]
|
||||
start, end := d.index[p], d.index[p+1]
|
||||
if start == end {
|
||||
return "", false
|
||||
}
|
||||
return d.data[start:end], true
|
||||
}
|
||||
|
||||
func init() {
|
||||
dict := map[string]catalog.Dictionary{
|
||||
"en": &dictionary{index: enIndex, data: enData},
|
||||
"zh": &dictionary{index: zhIndex, data: zhData},
|
||||
}
|
||||
fallback := language.MustParse("en")
|
||||
cat, err := catalog.NewFromMap(dict, catalog.Fallback(fallback))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
message.DefaultCatalog = cat
|
||||
}
|
||||
|
||||
var messageKeyToIndex = map[string]int{
|
||||
"Do you like your browser (%s)?\n": 1,
|
||||
"Hello %s!\n": 0,
|
||||
}
|
||||
|
||||
var enIndex = []uint32{ // 3 elements
|
||||
0x00000000, 0x00000012, 0x00000039,
|
||||
} // Size: 36 bytes
|
||||
|
||||
const enData string = "" + // Size: 57 bytes
|
||||
"\x04\x00\x01\x0a\x0d\x02Hello %[1]s!\x04\x00\x01\x0a\x22\x02Do you like " +
|
||||
"your browser (%[1]s)?"
|
||||
|
||||
var zhIndex = []uint32{ // 3 elements
|
||||
0x00000000, 0x00000000, 0x00000000,
|
||||
} // Size: 36 bytes
|
||||
|
||||
const zhData string = ""
|
||||
|
||||
// Total table size 129 bytes (0KiB); checksum: 9C146C82
|
||||
39
vendor/golang.org/x/text/cmd/gotext/examples/extract_http/locales/de/out.gotext.json
generated
vendored
Executable file
39
vendor/golang.org/x/text/cmd/gotext/examples/extract_http/locales/de/out.gotext.json
generated
vendored
Executable file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"language": "de",
|
||||
"messages": [
|
||||
{
|
||||
"id": "Hello {From}!",
|
||||
"key": "Hello %s!\n",
|
||||
"message": "Hello {From}!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "From",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "r.Header.Get(\"From\")"
|
||||
}
|
||||
],
|
||||
"position": "golang.org/x/text/cmd/gotext/examples/extract_http/pkg/pkg.go:22:11"
|
||||
},
|
||||
{
|
||||
"id": "Do you like your browser ({User_Agent})?",
|
||||
"key": "Do you like your browser (%s)?\n",
|
||||
"message": "Do you like your browser ({User_Agent})?",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "User_Agent",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "r.Header.Get(\"User-Agent\")"
|
||||
}
|
||||
],
|
||||
"position": "golang.org/x/text/cmd/gotext/examples/extract_http/pkg/pkg.go:24:11"
|
||||
}
|
||||
]
|
||||
}
|
||||
39
vendor/golang.org/x/text/cmd/gotext/examples/extract_http/locales/en-US/out.gotext.json
generated
vendored
Executable file
39
vendor/golang.org/x/text/cmd/gotext/examples/extract_http/locales/en-US/out.gotext.json
generated
vendored
Executable file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"language": "en-US",
|
||||
"messages": [
|
||||
{
|
||||
"id": "Hello {From}!",
|
||||
"key": "Hello %s!\n",
|
||||
"message": "Hello {From}!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "From",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "r.Header.Get(\"From\")"
|
||||
}
|
||||
],
|
||||
"position": "golang.org/x/text/cmd/gotext/examples/extract_http/pkg/pkg.go:22:11"
|
||||
},
|
||||
{
|
||||
"id": "Do you like your browser ({User_Agent})?",
|
||||
"key": "Do you like your browser (%s)?\n",
|
||||
"message": "Do you like your browser ({User_Agent})?",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "User_Agent",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "r.Header.Get(\"User-Agent\")"
|
||||
}
|
||||
],
|
||||
"position": "golang.org/x/text/cmd/gotext/examples/extract_http/pkg/pkg.go:24:11"
|
||||
}
|
||||
]
|
||||
}
|
||||
39
vendor/golang.org/x/text/cmd/gotext/examples/extract_http/locales/en/out.gotext.json
generated
vendored
Normal file
39
vendor/golang.org/x/text/cmd/gotext/examples/extract_http/locales/en/out.gotext.json
generated
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"language": "en",
|
||||
"messages": [
|
||||
{
|
||||
"id": "Hello {From}!",
|
||||
"message": "Hello {From}!",
|
||||
"translation": "Hello {From}!",
|
||||
"translatorComment": "Copied from source.",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "From",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "r.Header.Get(\"From\")"
|
||||
}
|
||||
],
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Do you like your browser ({User_Agent})?",
|
||||
"message": "Do you like your browser ({User_Agent})?",
|
||||
"translation": "Do you like your browser ({User_Agent})?",
|
||||
"translatorComment": "Copied from source.",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "User_Agent",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "r.Header.Get(\"User-Agent\")"
|
||||
}
|
||||
],
|
||||
"fuzzy": true
|
||||
}
|
||||
]
|
||||
}
|
||||
35
vendor/golang.org/x/text/cmd/gotext/examples/extract_http/locales/zh/out.gotext.json
generated
vendored
Executable file
35
vendor/golang.org/x/text/cmd/gotext/examples/extract_http/locales/zh/out.gotext.json
generated
vendored
Executable file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"language": "zh",
|
||||
"messages": [
|
||||
{
|
||||
"id": "Hello {From}!",
|
||||
"message": "Hello {From}!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "From",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "r.Header.Get(\"From\")"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Do you like your browser ({User_Agent})?",
|
||||
"message": "Do you like your browser ({User_Agent})?",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "User_Agent",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "r.Header.Get(\"User-Agent\")"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
// 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 main
|
||||
|
||||
//go:generate gotext -srclang=en update -out=catalog_gen.go -lang=en,zh
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/text/cmd/gotext/examples/extract_http/pkg"
|
||||
)
|
||||
|
||||
func main() {
|
||||
http.Handle("/generize", http.HandlerFunc(pkg.Generize))
|
||||
}
|
||||
25
vendor/golang.org/x/text/cmd/gotext/examples/extract_http/pkg/pkg.go
generated
vendored
Normal file
25
vendor/golang.org/x/text/cmd/gotext/examples/extract_http/pkg/pkg.go
generated
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// 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 pkg
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
)
|
||||
|
||||
var matcher = language.NewMatcher(message.DefaultCatalog.Languages())
|
||||
|
||||
func Generize(w http.ResponseWriter, r *http.Request) {
|
||||
lang, _ := r.Cookie("lang")
|
||||
accept := r.Header.Get("Accept-Language")
|
||||
tag := message.MatchLanguage(lang.String(), accept)
|
||||
p := message.NewPrinter(tag)
|
||||
|
||||
p.Fprintf(w, "Hello %s!\n", r.Header.Get("From"))
|
||||
|
||||
p.Fprintf(w, "Do you like your browser (%s)?\n", r.Header.Get("User-Agent"))
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// 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 main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var nPizzas = 4
|
||||
// The following call gets replaced by a call to the globally
|
||||
// defined printer.
|
||||
fmt.Println("We ate", nPizzas, "pizzas.")
|
||||
|
||||
p := message.NewPrinter(language.English)
|
||||
|
||||
// Prevent build failure, although it is okay for gotext.
|
||||
p.Println(1024)
|
||||
|
||||
// Replaced by a call to p.
|
||||
fmt.Println("Example punctuation:", "$%^&!")
|
||||
|
||||
{
|
||||
q := message.NewPrinter(language.French)
|
||||
|
||||
const leaveAnIdentBe = "Don't expand me."
|
||||
fmt.Print(leaveAnIdentBe)
|
||||
q.Println() // Prevent build failure, although it is okay for gotext.
|
||||
}
|
||||
|
||||
fmt.Printf("Hello %s\n", "City")
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// 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.
|
||||
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
)
|
||||
|
||||
// The printer defined here will be picked up by the first print statement
|
||||
// in main.go.
|
||||
var printer = message.NewPrinter(language.English)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// 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 main
|
||||
|
||||
import (
|
||||
"golang.org/x/text/message/pipeline"
|
||||
)
|
||||
|
||||
func init() {
|
||||
out = cmdGenerate.Flag.String("out", "", "output file to write to")
|
||||
}
|
||||
|
||||
var cmdGenerate = &Command{
|
||||
Run: runGenerate,
|
||||
UsageLine: "generate <package>",
|
||||
Short: "generates code to insert translated messages",
|
||||
}
|
||||
|
||||
func runGenerate(cmd *Command, config *pipeline.Config, args []string) error {
|
||||
config.Packages = args
|
||||
s, err := pipeline.Extract(config)
|
||||
if err != nil {
|
||||
return wrap(err, "extraction failed")
|
||||
}
|
||||
if err := s.Import(); err != nil {
|
||||
return wrap(err, "import failed")
|
||||
}
|
||||
return wrap(s.Generate(), "generation failed")
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
// 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 main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"golang.org/x/text/message/pipeline"
|
||||
)
|
||||
|
||||
const printerType = "golang.org/x/text/message.Printer"
|
||||
|
||||
// TODO:
|
||||
// - merge information into existing files
|
||||
// - handle different file formats (PO, XLIFF)
|
||||
// - handle features (gender, plural)
|
||||
// - message rewriting
|
||||
|
||||
func init() {
|
||||
overwrite = cmdRewrite.Flag.Bool("w", false, "write files in place")
|
||||
}
|
||||
|
||||
var (
|
||||
overwrite *bool
|
||||
)
|
||||
|
||||
var cmdRewrite = &Command{
|
||||
Run: runRewrite,
|
||||
UsageLine: "rewrite <package>",
|
||||
Short: "rewrites fmt functions to use a message Printer",
|
||||
Long: `
|
||||
rewrite is typically done once for a project. It rewrites all usages of
|
||||
fmt to use x/text's message package whenever a message.Printer is in scope.
|
||||
It rewrites Print and Println calls with constant strings to the equivalent
|
||||
using Printf to allow translators to reorder arguments.
|
||||
`,
|
||||
}
|
||||
|
||||
func runRewrite(cmd *Command, _ *pipeline.Config, args []string) error {
|
||||
w := os.Stdout
|
||||
if *overwrite {
|
||||
w = nil
|
||||
}
|
||||
pkg := "."
|
||||
switch len(args) {
|
||||
case 0:
|
||||
case 1:
|
||||
pkg = args[0]
|
||||
default:
|
||||
return errorf("can only specify at most one package")
|
||||
}
|
||||
return pipeline.Rewrite(w, pkg)
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
// Copyright 2016 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 main
|
||||
|
||||
import (
|
||||
"golang.org/x/text/message/pipeline"
|
||||
)
|
||||
|
||||
// TODO:
|
||||
// - merge information into existing files
|
||||
// - handle different file formats (PO, XLIFF)
|
||||
// - handle features (gender, plural)
|
||||
// - message rewriting
|
||||
|
||||
var (
|
||||
lang *string
|
||||
out *string
|
||||
)
|
||||
|
||||
func init() {
|
||||
lang = cmdUpdate.Flag.String("lang", "en-US", "comma-separated list of languages to process")
|
||||
out = cmdUpdate.Flag.String("out", "", "output file to write to")
|
||||
}
|
||||
|
||||
var cmdUpdate = &Command{
|
||||
Run: runUpdate,
|
||||
UsageLine: "update <package>* [-out <gofile>]",
|
||||
Short: "merge translations and generate catalog",
|
||||
}
|
||||
|
||||
func runUpdate(cmd *Command, config *pipeline.Config, args []string) error {
|
||||
config.Packages = args
|
||||
state, err := pipeline.Extract(config)
|
||||
if err != nil {
|
||||
return wrap(err, "extract failed")
|
||||
}
|
||||
if err := state.Import(); err != nil {
|
||||
return wrap(err, "import failed")
|
||||
}
|
||||
if err := state.Merge(); err != nil {
|
||||
return wrap(err, "merge failed")
|
||||
}
|
||||
if err := state.Export(); err != nil {
|
||||
return wrap(err, "export failed")
|
||||
}
|
||||
if *out != "" {
|
||||
return wrap(state.Generate(), "generation failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
// Copyright 2016 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.
|
||||
|
||||
// +build go1.10
|
||||
|
||||
package idna
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/text/internal/gen"
|
||||
"golang.org/x/text/internal/testtext"
|
||||
"golang.org/x/text/internal/ucd"
|
||||
)
|
||||
|
||||
func TestTables(t *testing.T) {
|
||||
testtext.SkipIfNotLong(t)
|
||||
|
||||
lookup := func(r rune) info {
|
||||
v, _ := trie.lookupString(string(r))
|
||||
return info(v)
|
||||
}
|
||||
|
||||
ucd.Parse(gen.OpenUnicodeFile("idna", "", "IdnaMappingTable.txt"), func(p *ucd.Parser) {
|
||||
r := p.Rune(0)
|
||||
x := lookup(r)
|
||||
if got, want := x.category(), catFromEntry(p); got != want {
|
||||
t.Errorf("%U:category: got %x; want %x", r, got, want)
|
||||
}
|
||||
|
||||
mapped := false
|
||||
switch p.String(1) {
|
||||
case "mapped", "disallowed_STD3_mapped", "deviation":
|
||||
mapped = true
|
||||
}
|
||||
if x.isMapped() != mapped {
|
||||
t.Errorf("%U:isMapped: got %v; want %v", r, x.isMapped(), mapped)
|
||||
}
|
||||
if !mapped {
|
||||
return
|
||||
}
|
||||
want := string(p.Runes(2))
|
||||
got := string(x.appendMapping(nil, string(r)))
|
||||
if got != want {
|
||||
t.Errorf("%U:mapping: got %+q; want %+q", r, got, want)
|
||||
}
|
||||
|
||||
if x.isMapped() {
|
||||
return
|
||||
}
|
||||
wantMark := unicode.In(r, unicode.Mark)
|
||||
gotMark := x.isModifier()
|
||||
if gotMark != wantMark {
|
||||
t.Errorf("IsMark(%U) = %v; want %v", r, gotMark, wantMark)
|
||||
}
|
||||
})
|
||||
|
||||
ucd.Parse(gen.OpenUCDFile("UnicodeData.txt"), func(p *ucd.Parser) {
|
||||
r := p.Rune(0)
|
||||
x := lookup(r)
|
||||
got := x.isViramaModifier()
|
||||
|
||||
const cccVirama = 9
|
||||
want := p.Int(ucd.CanonicalCombiningClass) == cccVirama
|
||||
if got != want {
|
||||
t.Errorf("IsVirama(%U) = %v; want %v", r, got, want)
|
||||
}
|
||||
|
||||
rtl := false
|
||||
switch p.String(ucd.BidiClass) {
|
||||
case "R", "AL", "AN":
|
||||
rtl = true
|
||||
}
|
||||
if got := x.isBidi("A"); got != rtl && !x.isMapped() {
|
||||
t.Errorf("IsBidi(%U) = %v; want %v", r, got, rtl)
|
||||
}
|
||||
})
|
||||
|
||||
ucd.Parse(gen.OpenUCDFile("extracted/DerivedJoiningType.txt"), func(p *ucd.Parser) {
|
||||
r := p.Rune(0)
|
||||
x := lookup(r)
|
||||
if x.isMapped() {
|
||||
return
|
||||
}
|
||||
got := x.joinType()
|
||||
want := joinType[p.String(1)]
|
||||
if got != want {
|
||||
t.Errorf("JoinType(%U) = %x; want %x", r, got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
// Copyright 2016 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.
|
||||
|
||||
// +build !go1.10
|
||||
|
||||
package idna
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/text/internal/gen"
|
||||
"golang.org/x/text/internal/testtext"
|
||||
"golang.org/x/text/internal/ucd"
|
||||
)
|
||||
|
||||
func TestTables(t *testing.T) {
|
||||
testtext.SkipIfNotLong(t)
|
||||
|
||||
lookup := func(r rune) info {
|
||||
v, _ := trie.lookupString(string(r))
|
||||
return info(v)
|
||||
}
|
||||
|
||||
ucd.Parse(gen.OpenUnicodeFile("idna", "", "IdnaMappingTable.txt"), func(p *ucd.Parser) {
|
||||
r := p.Rune(0)
|
||||
x := lookup(r)
|
||||
if got, want := x.category(), catFromEntry(p); got != want {
|
||||
t.Errorf("%U:category: got %x; want %x", r, got, want)
|
||||
}
|
||||
|
||||
mapped := false
|
||||
switch p.String(1) {
|
||||
case "mapped", "disallowed_STD3_mapped", "deviation":
|
||||
mapped = true
|
||||
}
|
||||
if x.isMapped() != mapped {
|
||||
t.Errorf("%U:isMapped: got %v; want %v", r, x.isMapped(), mapped)
|
||||
}
|
||||
if !mapped {
|
||||
return
|
||||
}
|
||||
want := string(p.Runes(2))
|
||||
got := string(x.appendMapping(nil, string(r)))
|
||||
if got != want {
|
||||
t.Errorf("%U:mapping: got %+q; want %+q", r, got, want)
|
||||
}
|
||||
|
||||
if x.isMapped() {
|
||||
return
|
||||
}
|
||||
wantMark := unicode.In(r, unicode.Mark)
|
||||
gotMark := x.isModifier()
|
||||
if gotMark != wantMark {
|
||||
t.Errorf("IsMark(%U) = %v; want %v", r, gotMark, wantMark)
|
||||
}
|
||||
})
|
||||
|
||||
ucd.Parse(gen.OpenUCDFile("UnicodeData.txt"), func(p *ucd.Parser) {
|
||||
r := p.Rune(0)
|
||||
x := lookup(r)
|
||||
got := x.isViramaModifier()
|
||||
|
||||
const cccVirama = 9
|
||||
want := p.Int(ucd.CanonicalCombiningClass) == cccVirama
|
||||
if got != want {
|
||||
t.Errorf("IsVirama(%U) = %v; want %v", r, got, want)
|
||||
}
|
||||
})
|
||||
|
||||
ucd.Parse(gen.OpenUCDFile("extracted/DerivedJoiningType.txt"), func(p *ucd.Parser) {
|
||||
r := p.Rune(0)
|
||||
x := lookup(r)
|
||||
if x.isMapped() {
|
||||
return
|
||||
}
|
||||
got := x.joinType()
|
||||
want := joinType[p.String(1)]
|
||||
if got != want {
|
||||
t.Errorf("JoinType(%U) = %x; want %x", r, got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,733 @@
|
|||
// Copyright 2016 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.
|
||||
|
||||
// +build go1.10
|
||||
//go:generate go run gen.go gen_trieval.go gen_common.go
|
||||
|
||||
// Package idna implements IDNA2008 using the compatibility processing
|
||||
// defined by UTS (Unicode Technical Standard) #46, which defines a standard to
|
||||
// deal with the transition from IDNA2003.
|
||||
//
|
||||
// IDNA2008 (Internationalized Domain Names for Applications), is defined in RFC
|
||||
// 5890, RFC 5891, RFC 5892, RFC 5893 and RFC 5894.
|
||||
// UTS #46 is defined in http://www.unicode.org/reports/tr46.
|
||||
// See http://unicode.org/cldr/utility/idna.jsp for a visualization of the
|
||||
// differences between these two standards.
|
||||
package idna // import "golang.org/x/text/internal/export/idna"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/text/secure/bidirule"
|
||||
"golang.org/x/text/unicode/bidi"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
// NOTE: Unlike common practice in Go APIs, the functions will return a
|
||||
// sanitized domain name in case of errors. Browsers sometimes use a partially
|
||||
// evaluated string as lookup.
|
||||
// TODO: the current error handling is, in my opinion, the least opinionated.
|
||||
// Other strategies are also viable, though:
|
||||
// Option 1) Return an empty string in case of error, but allow the user to
|
||||
// specify explicitly which errors to ignore.
|
||||
// Option 2) Return the partially evaluated string if it is itself a valid
|
||||
// string, otherwise return the empty string in case of error.
|
||||
// Option 3) Option 1 and 2.
|
||||
// Option 4) Always return an empty string for now and implement Option 1 as
|
||||
// needed, and document that the return string may not be empty in case of
|
||||
// error in the future.
|
||||
// I think Option 1 is best, but it is quite opinionated.
|
||||
|
||||
// ToASCII is a wrapper for Punycode.ToASCII.
|
||||
func ToASCII(s string) (string, error) {
|
||||
return Punycode.process(s, true)
|
||||
}
|
||||
|
||||
// ToUnicode is a wrapper for Punycode.ToUnicode.
|
||||
func ToUnicode(s string) (string, error) {
|
||||
return Punycode.process(s, false)
|
||||
}
|
||||
|
||||
// An Option configures a Profile at creation time.
|
||||
type Option func(*options)
|
||||
|
||||
// Transitional sets a Profile to use the Transitional mapping as defined in UTS
|
||||
// #46. This will cause, for example, "ß" to be mapped to "ss". Using the
|
||||
// transitional mapping provides a compromise between IDNA2003 and IDNA2008
|
||||
// compatibility. It is used by most browsers when resolving domain names. This
|
||||
// option is only meaningful if combined with MapForLookup.
|
||||
func Transitional(transitional bool) Option {
|
||||
return func(o *options) { o.transitional = true }
|
||||
}
|
||||
|
||||
// VerifyDNSLength sets whether a Profile should fail if any of the IDN parts
|
||||
// are longer than allowed by the RFC.
|
||||
func VerifyDNSLength(verify bool) Option {
|
||||
return func(o *options) { o.verifyDNSLength = verify }
|
||||
}
|
||||
|
||||
// RemoveLeadingDots removes leading label separators. Leading runes that map to
|
||||
// dots, such as U+3002 IDEOGRAPHIC FULL STOP, are removed as well.
|
||||
//
|
||||
// This is the behavior suggested by the UTS #46 and is adopted by some
|
||||
// browsers.
|
||||
func RemoveLeadingDots(remove bool) Option {
|
||||
return func(o *options) { o.removeLeadingDots = remove }
|
||||
}
|
||||
|
||||
// ValidateLabels sets whether to check the mandatory label validation criteria
|
||||
// as defined in Section 5.4 of RFC 5891. This includes testing for correct use
|
||||
// of hyphens ('-'), normalization, validity of runes, and the context rules.
|
||||
func ValidateLabels(enable bool) Option {
|
||||
return func(o *options) {
|
||||
// Don't override existing mappings, but set one that at least checks
|
||||
// normalization if it is not set.
|
||||
if o.mapping == nil && enable {
|
||||
o.mapping = normalize
|
||||
}
|
||||
o.trie = trie
|
||||
o.validateLabels = enable
|
||||
o.fromPuny = validateFromPunycode
|
||||
}
|
||||
}
|
||||
|
||||
// StrictDomainName limits the set of permissible ASCII characters to those
|
||||
// allowed in domain names as defined in RFC 1034 (A-Z, a-z, 0-9 and the
|
||||
// hyphen). This is set by default for MapForLookup and ValidateForRegistration.
|
||||
//
|
||||
// This option is useful, for instance, for browsers that allow characters
|
||||
// outside this range, for example a '_' (U+005F LOW LINE). See
|
||||
// http://www.rfc-editor.org/std/std3.txt for more details This option
|
||||
// corresponds to the UseSTD3ASCIIRules option in UTS #46.
|
||||
func StrictDomainName(use bool) Option {
|
||||
return func(o *options) {
|
||||
o.trie = trie
|
||||
o.useSTD3Rules = use
|
||||
o.fromPuny = validateFromPunycode
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: the following options pull in tables. The tables should not be linked
|
||||
// in as long as the options are not used.
|
||||
|
||||
// BidiRule enables the Bidi rule as defined in RFC 5893. Any application
|
||||
// that relies on proper validation of labels should include this rule.
|
||||
func BidiRule() Option {
|
||||
return func(o *options) { o.bidirule = bidirule.ValidString }
|
||||
}
|
||||
|
||||
// ValidateForRegistration sets validation options to verify that a given IDN is
|
||||
// properly formatted for registration as defined by Section 4 of RFC 5891.
|
||||
func ValidateForRegistration() Option {
|
||||
return func(o *options) {
|
||||
o.mapping = validateRegistration
|
||||
StrictDomainName(true)(o)
|
||||
ValidateLabels(true)(o)
|
||||
VerifyDNSLength(true)(o)
|
||||
BidiRule()(o)
|
||||
}
|
||||
}
|
||||
|
||||
// MapForLookup sets validation and mapping options such that a given IDN is
|
||||
// transformed for domain name lookup according to the requirements set out in
|
||||
// Section 5 of RFC 5891. The mappings follow the recommendations of RFC 5894,
|
||||
// RFC 5895 and UTS 46. It does not add the Bidi Rule. Use the BidiRule option
|
||||
// to add this check.
|
||||
//
|
||||
// The mappings include normalization and mapping case, width and other
|
||||
// compatibility mappings.
|
||||
func MapForLookup() Option {
|
||||
return func(o *options) {
|
||||
o.mapping = validateAndMap
|
||||
StrictDomainName(true)(o)
|
||||
ValidateLabels(true)(o)
|
||||
}
|
||||
}
|
||||
|
||||
type options struct {
|
||||
transitional bool
|
||||
useSTD3Rules bool
|
||||
validateLabels bool
|
||||
verifyDNSLength bool
|
||||
removeLeadingDots bool
|
||||
|
||||
trie *idnaTrie
|
||||
|
||||
// fromPuny calls validation rules when converting A-labels to U-labels.
|
||||
fromPuny func(p *Profile, s string) error
|
||||
|
||||
// mapping implements a validation and mapping step as defined in RFC 5895
|
||||
// or UTS 46, tailored to, for example, domain registration or lookup.
|
||||
mapping func(p *Profile, s string) (mapped string, isBidi bool, err error)
|
||||
|
||||
// bidirule, if specified, checks whether s conforms to the Bidi Rule
|
||||
// defined in RFC 5893.
|
||||
bidirule func(s string) bool
|
||||
}
|
||||
|
||||
// A Profile defines the configuration of an IDNA mapper.
|
||||
type Profile struct {
|
||||
options
|
||||
}
|
||||
|
||||
func apply(o *options, opts []Option) {
|
||||
for _, f := range opts {
|
||||
f(o)
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new Profile.
|
||||
//
|
||||
// With no options, the returned Profile is the most permissive and equals the
|
||||
// Punycode Profile. Options can be passed to further restrict the Profile. The
|
||||
// MapForLookup and ValidateForRegistration options set a collection of options,
|
||||
// for lookup and registration purposes respectively, which can be tailored by
|
||||
// adding more fine-grained options, where later options override earlier
|
||||
// options.
|
||||
func New(o ...Option) *Profile {
|
||||
p := &Profile{}
|
||||
apply(&p.options, o)
|
||||
return p
|
||||
}
|
||||
|
||||
// ToASCII converts a domain or domain label to its ASCII form. For example,
|
||||
// ToASCII("bücher.example.com") is "xn--bcher-kva.example.com", and
|
||||
// ToASCII("golang") is "golang". If an error is encountered it will return
|
||||
// an error and a (partially) processed result.
|
||||
func (p *Profile) ToASCII(s string) (string, error) {
|
||||
return p.process(s, true)
|
||||
}
|
||||
|
||||
// ToUnicode converts a domain or domain label to its Unicode form. For example,
|
||||
// ToUnicode("xn--bcher-kva.example.com") is "bücher.example.com", and
|
||||
// ToUnicode("golang") is "golang". If an error is encountered it will return
|
||||
// an error and a (partially) processed result.
|
||||
func (p *Profile) ToUnicode(s string) (string, error) {
|
||||
pp := *p
|
||||
pp.transitional = false
|
||||
return pp.process(s, false)
|
||||
}
|
||||
|
||||
// String reports a string with a description of the profile for debugging
|
||||
// purposes. The string format may change with different versions.
|
||||
func (p *Profile) String() string {
|
||||
s := ""
|
||||
if p.transitional {
|
||||
s = "Transitional"
|
||||
} else {
|
||||
s = "NonTransitional"
|
||||
}
|
||||
if p.useSTD3Rules {
|
||||
s += ":UseSTD3Rules"
|
||||
}
|
||||
if p.validateLabels {
|
||||
s += ":ValidateLabels"
|
||||
}
|
||||
if p.verifyDNSLength {
|
||||
s += ":VerifyDNSLength"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
var (
|
||||
// Punycode is a Profile that does raw punycode processing with a minimum
|
||||
// of validation.
|
||||
Punycode *Profile = punycode
|
||||
|
||||
// Lookup is the recommended profile for looking up domain names, according
|
||||
// to Section 5 of RFC 5891. The exact configuration of this profile may
|
||||
// change over time.
|
||||
Lookup *Profile = lookup
|
||||
|
||||
// Display is the recommended profile for displaying domain names.
|
||||
// The configuration of this profile may change over time.
|
||||
Display *Profile = display
|
||||
|
||||
// Registration is the recommended profile for checking whether a given
|
||||
// IDN is valid for registration, according to Section 4 of RFC 5891.
|
||||
Registration *Profile = registration
|
||||
|
||||
punycode = &Profile{}
|
||||
lookup = &Profile{options{
|
||||
transitional: true,
|
||||
useSTD3Rules: true,
|
||||
validateLabels: true,
|
||||
trie: trie,
|
||||
fromPuny: validateFromPunycode,
|
||||
mapping: validateAndMap,
|
||||
bidirule: bidirule.ValidString,
|
||||
}}
|
||||
display = &Profile{options{
|
||||
useSTD3Rules: true,
|
||||
validateLabels: true,
|
||||
trie: trie,
|
||||
fromPuny: validateFromPunycode,
|
||||
mapping: validateAndMap,
|
||||
bidirule: bidirule.ValidString,
|
||||
}}
|
||||
registration = &Profile{options{
|
||||
useSTD3Rules: true,
|
||||
validateLabels: true,
|
||||
verifyDNSLength: true,
|
||||
trie: trie,
|
||||
fromPuny: validateFromPunycode,
|
||||
mapping: validateRegistration,
|
||||
bidirule: bidirule.ValidString,
|
||||
}}
|
||||
|
||||
// TODO: profiles
|
||||
// Register: recommended for approving domain names: don't do any mappings
|
||||
// but rather reject on invalid input. Bundle or block deviation characters.
|
||||
)
|
||||
|
||||
type labelError struct{ label, code_ string }
|
||||
|
||||
func (e labelError) code() string { return e.code_ }
|
||||
func (e labelError) Error() string {
|
||||
return fmt.Sprintf("idna: invalid label %q", e.label)
|
||||
}
|
||||
|
||||
type runeError rune
|
||||
|
||||
func (e runeError) code() string { return "P1" }
|
||||
func (e runeError) Error() string {
|
||||
return fmt.Sprintf("idna: disallowed rune %U", e)
|
||||
}
|
||||
|
||||
// process implements the algorithm described in section 4 of UTS #46,
|
||||
// see http://www.unicode.org/reports/tr46.
|
||||
func (p *Profile) process(s string, toASCII bool) (string, error) {
|
||||
var err error
|
||||
var isBidi bool
|
||||
if p.mapping != nil {
|
||||
s, isBidi, err = p.mapping(p, s)
|
||||
}
|
||||
// Remove leading empty labels.
|
||||
if p.removeLeadingDots {
|
||||
for ; len(s) > 0 && s[0] == '.'; s = s[1:] {
|
||||
}
|
||||
}
|
||||
// TODO: allow for a quick check of the tables data.
|
||||
// It seems like we should only create this error on ToASCII, but the
|
||||
// UTS 46 conformance tests suggests we should always check this.
|
||||
if err == nil && p.verifyDNSLength && s == "" {
|
||||
err = &labelError{s, "A4"}
|
||||
}
|
||||
labels := labelIter{orig: s}
|
||||
for ; !labels.done(); labels.next() {
|
||||
label := labels.label()
|
||||
if label == "" {
|
||||
// Empty labels are not okay. The label iterator skips the last
|
||||
// label if it is empty.
|
||||
if err == nil && p.verifyDNSLength {
|
||||
err = &labelError{s, "A4"}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(label, acePrefix) {
|
||||
u, err2 := decode(label[len(acePrefix):])
|
||||
if err2 != nil {
|
||||
if err == nil {
|
||||
err = err2
|
||||
}
|
||||
// Spec says keep the old label.
|
||||
continue
|
||||
}
|
||||
isBidi = isBidi || bidirule.DirectionString(u) != bidi.LeftToRight
|
||||
labels.set(u)
|
||||
if err == nil && p.validateLabels {
|
||||
err = p.fromPuny(p, u)
|
||||
}
|
||||
if err == nil {
|
||||
// This should be called on NonTransitional, according to the
|
||||
// spec, but that currently does not have any effect. Use the
|
||||
// original profile to preserve options.
|
||||
err = p.validateLabel(u)
|
||||
}
|
||||
} else if err == nil {
|
||||
err = p.validateLabel(label)
|
||||
}
|
||||
}
|
||||
if isBidi && p.bidirule != nil && err == nil {
|
||||
for labels.reset(); !labels.done(); labels.next() {
|
||||
if !p.bidirule(labels.label()) {
|
||||
err = &labelError{s, "B"}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if toASCII {
|
||||
for labels.reset(); !labels.done(); labels.next() {
|
||||
label := labels.label()
|
||||
if !ascii(label) {
|
||||
a, err2 := encode(acePrefix, label)
|
||||
if err == nil {
|
||||
err = err2
|
||||
}
|
||||
label = a
|
||||
labels.set(a)
|
||||
}
|
||||
n := len(label)
|
||||
if p.verifyDNSLength && err == nil && (n == 0 || n > 63) {
|
||||
err = &labelError{label, "A4"}
|
||||
}
|
||||
}
|
||||
}
|
||||
s = labels.result()
|
||||
if toASCII && p.verifyDNSLength && err == nil {
|
||||
// Compute the length of the domain name minus the root label and its dot.
|
||||
n := len(s)
|
||||
if n > 0 && s[n-1] == '.' {
|
||||
n--
|
||||
}
|
||||
if len(s) < 1 || n > 253 {
|
||||
err = &labelError{s, "A4"}
|
||||
}
|
||||
}
|
||||
return s, err
|
||||
}
|
||||
|
||||
func normalize(p *Profile, s string) (mapped string, isBidi bool, err error) {
|
||||
// TODO: consider first doing a quick check to see if any of these checks
|
||||
// need to be done. This will make it slower in the general case, but
|
||||
// faster in the common case.
|
||||
mapped = norm.NFC.String(s)
|
||||
isBidi = bidirule.DirectionString(mapped) == bidi.RightToLeft
|
||||
return mapped, isBidi, nil
|
||||
}
|
||||
|
||||
func validateRegistration(p *Profile, s string) (idem string, bidi bool, err error) {
|
||||
// TODO: filter need for normalization in loop below.
|
||||
if !norm.NFC.IsNormalString(s) {
|
||||
return s, false, &labelError{s, "V1"}
|
||||
}
|
||||
for i := 0; i < len(s); {
|
||||
v, sz := trie.lookupString(s[i:])
|
||||
if sz == 0 {
|
||||
return s, bidi, runeError(utf8.RuneError)
|
||||
}
|
||||
bidi = bidi || info(v).isBidi(s[i:])
|
||||
// Copy bytes not copied so far.
|
||||
switch p.simplify(info(v).category()) {
|
||||
// TODO: handle the NV8 defined in the Unicode idna data set to allow
|
||||
// for strict conformance to IDNA2008.
|
||||
case valid, deviation:
|
||||
case disallowed, mapped, unknown, ignored:
|
||||
r, _ := utf8.DecodeRuneInString(s[i:])
|
||||
return s, bidi, runeError(r)
|
||||
}
|
||||
i += sz
|
||||
}
|
||||
return s, bidi, nil
|
||||
}
|
||||
|
||||
func (c info) isBidi(s string) bool {
|
||||
if !c.isMapped() {
|
||||
return c&attributesMask == rtl
|
||||
}
|
||||
// TODO: also store bidi info for mapped data. This is possible, but a bit
|
||||
// cumbersome and not for the common case.
|
||||
p, _ := bidi.LookupString(s)
|
||||
switch p.Class() {
|
||||
case bidi.R, bidi.AL, bidi.AN:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func validateAndMap(p *Profile, s string) (vm string, bidi bool, err error) {
|
||||
var (
|
||||
b []byte
|
||||
k int
|
||||
)
|
||||
// combinedInfoBits contains the or-ed bits of all runes. We use this
|
||||
// to derive the mayNeedNorm bit later. This may trigger normalization
|
||||
// overeagerly, but it will not do so in the common case. The end result
|
||||
// is another 10% saving on BenchmarkProfile for the common case.
|
||||
var combinedInfoBits info
|
||||
for i := 0; i < len(s); {
|
||||
v, sz := trie.lookupString(s[i:])
|
||||
if sz == 0 {
|
||||
b = append(b, s[k:i]...)
|
||||
b = append(b, "\ufffd"...)
|
||||
k = len(s)
|
||||
if err == nil {
|
||||
err = runeError(utf8.RuneError)
|
||||
}
|
||||
break
|
||||
}
|
||||
combinedInfoBits |= info(v)
|
||||
bidi = bidi || info(v).isBidi(s[i:])
|
||||
start := i
|
||||
i += sz
|
||||
// Copy bytes not copied so far.
|
||||
switch p.simplify(info(v).category()) {
|
||||
case valid:
|
||||
continue
|
||||
case disallowed:
|
||||
if err == nil {
|
||||
r, _ := utf8.DecodeRuneInString(s[start:])
|
||||
err = runeError(r)
|
||||
}
|
||||
continue
|
||||
case mapped, deviation:
|
||||
b = append(b, s[k:start]...)
|
||||
b = info(v).appendMapping(b, s[start:i])
|
||||
case ignored:
|
||||
b = append(b, s[k:start]...)
|
||||
// drop the rune
|
||||
case unknown:
|
||||
b = append(b, s[k:start]...)
|
||||
b = append(b, "\ufffd"...)
|
||||
}
|
||||
k = i
|
||||
}
|
||||
if k == 0 {
|
||||
// No changes so far.
|
||||
if combinedInfoBits&mayNeedNorm != 0 {
|
||||
s = norm.NFC.String(s)
|
||||
}
|
||||
} else {
|
||||
b = append(b, s[k:]...)
|
||||
if norm.NFC.QuickSpan(b) != len(b) {
|
||||
b = norm.NFC.Bytes(b)
|
||||
}
|
||||
// TODO: the punycode converters require strings as input.
|
||||
s = string(b)
|
||||
}
|
||||
return s, bidi, err
|
||||
}
|
||||
|
||||
// A labelIter allows iterating over domain name labels.
|
||||
type labelIter struct {
|
||||
orig string
|
||||
slice []string
|
||||
curStart int
|
||||
curEnd int
|
||||
i int
|
||||
}
|
||||
|
||||
func (l *labelIter) reset() {
|
||||
l.curStart = 0
|
||||
l.curEnd = 0
|
||||
l.i = 0
|
||||
}
|
||||
|
||||
func (l *labelIter) done() bool {
|
||||
return l.curStart >= len(l.orig)
|
||||
}
|
||||
|
||||
func (l *labelIter) result() string {
|
||||
if l.slice != nil {
|
||||
return strings.Join(l.slice, ".")
|
||||
}
|
||||
return l.orig
|
||||
}
|
||||
|
||||
func (l *labelIter) label() string {
|
||||
if l.slice != nil {
|
||||
return l.slice[l.i]
|
||||
}
|
||||
p := strings.IndexByte(l.orig[l.curStart:], '.')
|
||||
l.curEnd = l.curStart + p
|
||||
if p == -1 {
|
||||
l.curEnd = len(l.orig)
|
||||
}
|
||||
return l.orig[l.curStart:l.curEnd]
|
||||
}
|
||||
|
||||
// next sets the value to the next label. It skips the last label if it is empty.
|
||||
func (l *labelIter) next() {
|
||||
l.i++
|
||||
if l.slice != nil {
|
||||
if l.i >= len(l.slice) || l.i == len(l.slice)-1 && l.slice[l.i] == "" {
|
||||
l.curStart = len(l.orig)
|
||||
}
|
||||
} else {
|
||||
l.curStart = l.curEnd + 1
|
||||
if l.curStart == len(l.orig)-1 && l.orig[l.curStart] == '.' {
|
||||
l.curStart = len(l.orig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *labelIter) set(s string) {
|
||||
if l.slice == nil {
|
||||
l.slice = strings.Split(l.orig, ".")
|
||||
}
|
||||
l.slice[l.i] = s
|
||||
}
|
||||
|
||||
// acePrefix is the ASCII Compatible Encoding prefix.
|
||||
const acePrefix = "xn--"
|
||||
|
||||
func (p *Profile) simplify(cat category) category {
|
||||
switch cat {
|
||||
case disallowedSTD3Mapped:
|
||||
if p.useSTD3Rules {
|
||||
cat = disallowed
|
||||
} else {
|
||||
cat = mapped
|
||||
}
|
||||
case disallowedSTD3Valid:
|
||||
if p.useSTD3Rules {
|
||||
cat = disallowed
|
||||
} else {
|
||||
cat = valid
|
||||
}
|
||||
case deviation:
|
||||
if !p.transitional {
|
||||
cat = valid
|
||||
}
|
||||
case validNV8, validXV8:
|
||||
// TODO: handle V2008
|
||||
cat = valid
|
||||
}
|
||||
return cat
|
||||
}
|
||||
|
||||
func validateFromPunycode(p *Profile, s string) error {
|
||||
if !norm.NFC.IsNormalString(s) {
|
||||
return &labelError{s, "V1"}
|
||||
}
|
||||
// TODO: detect whether string may have to be normalized in the following
|
||||
// loop.
|
||||
for i := 0; i < len(s); {
|
||||
v, sz := trie.lookupString(s[i:])
|
||||
if sz == 0 {
|
||||
return runeError(utf8.RuneError)
|
||||
}
|
||||
if c := p.simplify(info(v).category()); c != valid && c != deviation {
|
||||
return &labelError{s, "V6"}
|
||||
}
|
||||
i += sz
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
zwnj = "\u200c"
|
||||
zwj = "\u200d"
|
||||
)
|
||||
|
||||
type joinState int8
|
||||
|
||||
const (
|
||||
stateStart joinState = iota
|
||||
stateVirama
|
||||
stateBefore
|
||||
stateBeforeVirama
|
||||
stateAfter
|
||||
stateFAIL
|
||||
)
|
||||
|
||||
var joinStates = [][numJoinTypes]joinState{
|
||||
stateStart: {
|
||||
joiningL: stateBefore,
|
||||
joiningD: stateBefore,
|
||||
joinZWNJ: stateFAIL,
|
||||
joinZWJ: stateFAIL,
|
||||
joinVirama: stateVirama,
|
||||
},
|
||||
stateVirama: {
|
||||
joiningL: stateBefore,
|
||||
joiningD: stateBefore,
|
||||
},
|
||||
stateBefore: {
|
||||
joiningL: stateBefore,
|
||||
joiningD: stateBefore,
|
||||
joiningT: stateBefore,
|
||||
joinZWNJ: stateAfter,
|
||||
joinZWJ: stateFAIL,
|
||||
joinVirama: stateBeforeVirama,
|
||||
},
|
||||
stateBeforeVirama: {
|
||||
joiningL: stateBefore,
|
||||
joiningD: stateBefore,
|
||||
joiningT: stateBefore,
|
||||
},
|
||||
stateAfter: {
|
||||
joiningL: stateFAIL,
|
||||
joiningD: stateBefore,
|
||||
joiningT: stateAfter,
|
||||
joiningR: stateStart,
|
||||
joinZWNJ: stateFAIL,
|
||||
joinZWJ: stateFAIL,
|
||||
joinVirama: stateAfter, // no-op as we can't accept joiners here
|
||||
},
|
||||
stateFAIL: {
|
||||
0: stateFAIL,
|
||||
joiningL: stateFAIL,
|
||||
joiningD: stateFAIL,
|
||||
joiningT: stateFAIL,
|
||||
joiningR: stateFAIL,
|
||||
joinZWNJ: stateFAIL,
|
||||
joinZWJ: stateFAIL,
|
||||
joinVirama: stateFAIL,
|
||||
},
|
||||
}
|
||||
|
||||
// validateLabel validates the criteria from Section 4.1. Item 1, 4, and 6 are
|
||||
// already implicitly satisfied by the overall implementation.
|
||||
func (p *Profile) validateLabel(s string) (err error) {
|
||||
if s == "" {
|
||||
if p.verifyDNSLength {
|
||||
return &labelError{s, "A4"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !p.validateLabels {
|
||||
return nil
|
||||
}
|
||||
trie := p.trie // p.validateLabels is only set if trie is set.
|
||||
if len(s) > 4 && s[2] == '-' && s[3] == '-' {
|
||||
return &labelError{s, "V2"}
|
||||
}
|
||||
if s[0] == '-' || s[len(s)-1] == '-' {
|
||||
return &labelError{s, "V3"}
|
||||
}
|
||||
// TODO: merge the use of this in the trie.
|
||||
v, sz := trie.lookupString(s)
|
||||
x := info(v)
|
||||
if x.isModifier() {
|
||||
return &labelError{s, "V5"}
|
||||
}
|
||||
// Quickly return in the absence of zero-width (non) joiners.
|
||||
if strings.Index(s, zwj) == -1 && strings.Index(s, zwnj) == -1 {
|
||||
return nil
|
||||
}
|
||||
st := stateStart
|
||||
for i := 0; ; {
|
||||
jt := x.joinType()
|
||||
if s[i:i+sz] == zwj {
|
||||
jt = joinZWJ
|
||||
} else if s[i:i+sz] == zwnj {
|
||||
jt = joinZWNJ
|
||||
}
|
||||
st = joinStates[st][jt]
|
||||
if x.isViramaModifier() {
|
||||
st = joinStates[st][joinVirama]
|
||||
}
|
||||
if i += sz; i == len(s) {
|
||||
break
|
||||
}
|
||||
v, sz = trie.lookupString(s[i:])
|
||||
x = info(v)
|
||||
}
|
||||
if st == stateFAIL || st == stateAfter {
|
||||
return &labelError{s, "C"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ascii(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] >= utf8.RuneSelf {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
// Copyright 2016 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.
|
||||
|
||||
// +build go1.10
|
||||
|
||||
package idna
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestLabelErrors tests strings returned in case of error. All results should
|
||||
// be identical to the reference implementation and can be verified at
|
||||
// http://unicode.org/cldr/utility/idna.jsp. The reference implementation,
|
||||
// however, seems to not display Bidi and ContextJ errors.
|
||||
//
|
||||
// In some cases the behavior of browsers is added as a comment. In all cases,
|
||||
// whenever a resolve search returns an error here, Chrome will treat the input
|
||||
// string as a search string (including those for Bidi and Context J errors),
|
||||
// unless noted otherwise.
|
||||
func TestLabelErrors(t *testing.T) {
|
||||
encode := func(s string) string { s, _ = encode(acePrefix, s); return s }
|
||||
type kind struct {
|
||||
name string
|
||||
f func(string) (string, error)
|
||||
}
|
||||
punyA := kind{"PunycodeA", punycode.ToASCII}
|
||||
resolve := kind{"ResolveA", Lookup.ToASCII}
|
||||
display := kind{"ToUnicode", Display.ToUnicode}
|
||||
p := New(VerifyDNSLength(true), MapForLookup(), BidiRule())
|
||||
lengthU := kind{"CheckLengthU", p.ToUnicode}
|
||||
lengthA := kind{"CheckLengthA", p.ToASCII}
|
||||
p = New(MapForLookup(), StrictDomainName(false))
|
||||
std3 := kind{"STD3", p.ToASCII}
|
||||
|
||||
testCases := []struct {
|
||||
kind
|
||||
input string
|
||||
want string
|
||||
wantErr string
|
||||
}{
|
||||
{lengthU, "", "", "A4"}, // From UTS 46 conformance test.
|
||||
{lengthA, "", "", "A4"},
|
||||
|
||||
{lengthU, "xn--", "", "A4"},
|
||||
{lengthU, "foo.xn--", "foo.", "A4"}, // TODO: is dropping xn-- correct?
|
||||
{lengthU, "xn--.foo", ".foo", "A4"},
|
||||
{lengthU, "foo.xn--.bar", "foo..bar", "A4"},
|
||||
|
||||
{display, "xn--", "", ""},
|
||||
{display, "foo.xn--", "foo.", ""}, // TODO: is dropping xn-- correct?
|
||||
{display, "xn--.foo", ".foo", ""},
|
||||
{display, "foo.xn--.bar", "foo..bar", ""},
|
||||
|
||||
{lengthA, "a..b", "a..b", "A4"},
|
||||
{punyA, ".b", ".b", ""},
|
||||
// For backwards compatibility, the Punycode profile does not map runes.
|
||||
{punyA, "\u3002b", "xn--b-83t", ""},
|
||||
{punyA, "..b", "..b", ""},
|
||||
|
||||
{lengthA, ".b", ".b", "A4"},
|
||||
{lengthA, "\u3002b", ".b", "A4"},
|
||||
{lengthA, "..b", "..b", "A4"},
|
||||
{lengthA, "b..", "b..", ""},
|
||||
|
||||
// Sharpened Bidi rules for Unicode 10.0.0. Apply for ALL labels in ANY
|
||||
// of the labels is RTL.
|
||||
{lengthA, "\ufe05\u3002\u3002\U0002603e\u1ce0", "..xn--t6f5138v", "A4"},
|
||||
{lengthA, "FAX\u2a77\U0001d186\u3002\U0001e942\U000e0181\u180c", "", "B6"},
|
||||
|
||||
{resolve, "a..b", "a..b", ""},
|
||||
// Note that leading dots are not stripped. This is to be consistent
|
||||
// with the Punycode profile as well as the conformance test.
|
||||
{resolve, ".b", ".b", ""},
|
||||
{resolve, "\u3002b", ".b", ""},
|
||||
{resolve, "..b", "..b", ""},
|
||||
{resolve, "b..", "b..", ""},
|
||||
{resolve, "\xed", "", "P1"},
|
||||
|
||||
// Raw punycode
|
||||
{punyA, "", "", ""},
|
||||
{punyA, "*.foo.com", "*.foo.com", ""},
|
||||
{punyA, "Foo.com", "Foo.com", ""},
|
||||
|
||||
// STD3 rules
|
||||
{display, "*.foo.com", "*.foo.com", "P1"},
|
||||
{std3, "*.foo.com", "*.foo.com", ""},
|
||||
|
||||
// Don't map U+2490 (DIGIT NINE FULL STOP). This is the behavior of
|
||||
// Chrome, Safari, and IE. Firefox will first map ⒐ to 9. and return
|
||||
// lab9.be.
|
||||
{resolve, "lab⒐be", "xn--labbe-zh9b", "P1"}, // encode("lab⒐be")
|
||||
{display, "lab⒐be", "lab⒐be", "P1"},
|
||||
|
||||
{resolve, "plan⒐faß.de", "xn--planfass-c31e.de", "P1"}, // encode("plan⒐fass") + ".de"
|
||||
{display, "Plan⒐faß.de", "plan⒐faß.de", "P1"},
|
||||
|
||||
// Chrome 54.0 recognizes the error and treats this input verbatim as a
|
||||
// search string.
|
||||
// Safari 10.0 (non-conform spec) decomposes "⒈" and computes the
|
||||
// punycode on the result using transitional mapping.
|
||||
// Firefox 49.0.1 goes haywire on this string and prints a bunch of what
|
||||
// seems to be nested punycode encodings.
|
||||
{resolve, "日本⒈co.ßßß.de", "xn--co-wuw5954azlb.ssssss.de", "P1"},
|
||||
{display, "日本⒈co.ßßß.de", "日本⒈co.ßßß.de", "P1"},
|
||||
|
||||
{resolve, "a\u200Cb", "ab", ""},
|
||||
{display, "a\u200Cb", "a\u200Cb", "C"},
|
||||
|
||||
{resolve, encode("a\u200Cb"), encode("a\u200Cb"), "C"},
|
||||
{display, "a\u200Cb", "a\u200Cb", "C"},
|
||||
|
||||
{resolve, "grﻋﺮﺑﻲ.de", "xn--gr-gtd9a1b0g.de", "B"},
|
||||
{
|
||||
// Notice how the string gets transformed, even with an error.
|
||||
// Chrome will use the original string if it finds an error, so not
|
||||
// the transformed one.
|
||||
display,
|
||||
"gr\ufecb\ufeae\ufe91\ufef2.de",
|
||||
"gr\u0639\u0631\u0628\u064a.de",
|
||||
"B",
|
||||
},
|
||||
|
||||
{resolve, "\u0671.\u03c3\u07dc", "xn--qib.xn--4xa21s", "B"}, // ٱ.σߜ
|
||||
{display, "\u0671.\u03c3\u07dc", "\u0671.\u03c3\u07dc", "B"},
|
||||
|
||||
// normalize input
|
||||
{resolve, "a\u0323\u0322", "xn--jta191l", ""}, // ạ̢
|
||||
{display, "a\u0323\u0322", "\u1ea1\u0322", ""},
|
||||
|
||||
// Non-normalized strings are not normalized when they originate from
|
||||
// punycode. Despite the error, Chrome, Safari and Firefox will attempt
|
||||
// to look up the input punycode.
|
||||
{resolve, encode("a\u0323\u0322") + ".com", "xn--a-tdbc.com", "V1"},
|
||||
{display, encode("a\u0323\u0322") + ".com", "a\u0323\u0322.com", "V1"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
doTest(t, tc.f, tc.name, tc.input, tc.want, tc.wantErr)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,681 @@
|
|||
// Copyright 2016 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.
|
||||
|
||||
// +build !go1.10
|
||||
//go:generate go run gen.go gen_trieval.go gen_common.go
|
||||
|
||||
// Package idna implements IDNA2008 using the compatibility processing
|
||||
// defined by UTS (Unicode Technical Standard) #46, which defines a standard to
|
||||
// deal with the transition from IDNA2003.
|
||||
//
|
||||
// IDNA2008 (Internationalized Domain Names for Applications), is defined in RFC
|
||||
// 5890, RFC 5891, RFC 5892, RFC 5893 and RFC 5894.
|
||||
// UTS #46 is defined in http://www.unicode.org/reports/tr46.
|
||||
// See http://unicode.org/cldr/utility/idna.jsp for a visualization of the
|
||||
// differences between these two standards.
|
||||
package idna // import "golang.org/x/text/internal/export/idna"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/text/secure/bidirule"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
// NOTE: Unlike common practice in Go APIs, the functions will return a
|
||||
// sanitized domain name in case of errors. Browsers sometimes use a partially
|
||||
// evaluated string as lookup.
|
||||
// TODO: the current error handling is, in my opinion, the least opinionated.
|
||||
// Other strategies are also viable, though:
|
||||
// Option 1) Return an empty string in case of error, but allow the user to
|
||||
// specify explicitly which errors to ignore.
|
||||
// Option 2) Return the partially evaluated string if it is itself a valid
|
||||
// string, otherwise return the empty string in case of error.
|
||||
// Option 3) Option 1 and 2.
|
||||
// Option 4) Always return an empty string for now and implement Option 1 as
|
||||
// needed, and document that the return string may not be empty in case of
|
||||
// error in the future.
|
||||
// I think Option 1 is best, but it is quite opinionated.
|
||||
|
||||
// ToASCII is a wrapper for Punycode.ToASCII.
|
||||
func ToASCII(s string) (string, error) {
|
||||
return Punycode.process(s, true)
|
||||
}
|
||||
|
||||
// ToUnicode is a wrapper for Punycode.ToUnicode.
|
||||
func ToUnicode(s string) (string, error) {
|
||||
return Punycode.process(s, false)
|
||||
}
|
||||
|
||||
// An Option configures a Profile at creation time.
|
||||
type Option func(*options)
|
||||
|
||||
// Transitional sets a Profile to use the Transitional mapping as defined in UTS
|
||||
// #46. This will cause, for example, "ß" to be mapped to "ss". Using the
|
||||
// transitional mapping provides a compromise between IDNA2003 and IDNA2008
|
||||
// compatibility. It is used by most browsers when resolving domain names. This
|
||||
// option is only meaningful if combined with MapForLookup.
|
||||
func Transitional(transitional bool) Option {
|
||||
return func(o *options) { o.transitional = true }
|
||||
}
|
||||
|
||||
// VerifyDNSLength sets whether a Profile should fail if any of the IDN parts
|
||||
// are longer than allowed by the RFC.
|
||||
func VerifyDNSLength(verify bool) Option {
|
||||
return func(o *options) { o.verifyDNSLength = verify }
|
||||
}
|
||||
|
||||
// RemoveLeadingDots removes leading label separators. Leading runes that map to
|
||||
// dots, such as U+3002 IDEOGRAPHIC FULL STOP, are removed as well.
|
||||
//
|
||||
// This is the behavior suggested by the UTS #46 and is adopted by some
|
||||
// browsers.
|
||||
func RemoveLeadingDots(remove bool) Option {
|
||||
return func(o *options) { o.removeLeadingDots = remove }
|
||||
}
|
||||
|
||||
// ValidateLabels sets whether to check the mandatory label validation criteria
|
||||
// as defined in Section 5.4 of RFC 5891. This includes testing for correct use
|
||||
// of hyphens ('-'), normalization, validity of runes, and the context rules.
|
||||
func ValidateLabels(enable bool) Option {
|
||||
return func(o *options) {
|
||||
// Don't override existing mappings, but set one that at least checks
|
||||
// normalization if it is not set.
|
||||
if o.mapping == nil && enable {
|
||||
o.mapping = normalize
|
||||
}
|
||||
o.trie = trie
|
||||
o.validateLabels = enable
|
||||
o.fromPuny = validateFromPunycode
|
||||
}
|
||||
}
|
||||
|
||||
// StrictDomainName limits the set of permissable ASCII characters to those
|
||||
// allowed in domain names as defined in RFC 1034 (A-Z, a-z, 0-9 and the
|
||||
// hyphen). This is set by default for MapForLookup and ValidateForRegistration.
|
||||
//
|
||||
// This option is useful, for instance, for browsers that allow characters
|
||||
// outside this range, for example a '_' (U+005F LOW LINE). See
|
||||
// http://www.rfc-editor.org/std/std3.txt for more details This option
|
||||
// corresponds to the UseSTD3ASCIIRules option in UTS #46.
|
||||
func StrictDomainName(use bool) Option {
|
||||
return func(o *options) {
|
||||
o.trie = trie
|
||||
o.useSTD3Rules = use
|
||||
o.fromPuny = validateFromPunycode
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: the following options pull in tables. The tables should not be linked
|
||||
// in as long as the options are not used.
|
||||
|
||||
// BidiRule enables the Bidi rule as defined in RFC 5893. Any application
|
||||
// that relies on proper validation of labels should include this rule.
|
||||
func BidiRule() Option {
|
||||
return func(o *options) { o.bidirule = bidirule.ValidString }
|
||||
}
|
||||
|
||||
// ValidateForRegistration sets validation options to verify that a given IDN is
|
||||
// properly formatted for registration as defined by Section 4 of RFC 5891.
|
||||
func ValidateForRegistration() Option {
|
||||
return func(o *options) {
|
||||
o.mapping = validateRegistration
|
||||
StrictDomainName(true)(o)
|
||||
ValidateLabels(true)(o)
|
||||
VerifyDNSLength(true)(o)
|
||||
BidiRule()(o)
|
||||
}
|
||||
}
|
||||
|
||||
// MapForLookup sets validation and mapping options such that a given IDN is
|
||||
// transformed for domain name lookup according to the requirements set out in
|
||||
// Section 5 of RFC 5891. The mappings follow the recommendations of RFC 5894,
|
||||
// RFC 5895 and UTS 46. It does not add the Bidi Rule. Use the BidiRule option
|
||||
// to add this check.
|
||||
//
|
||||
// The mappings include normalization and mapping case, width and other
|
||||
// compatibility mappings.
|
||||
func MapForLookup() Option {
|
||||
return func(o *options) {
|
||||
o.mapping = validateAndMap
|
||||
StrictDomainName(true)(o)
|
||||
ValidateLabels(true)(o)
|
||||
RemoveLeadingDots(true)(o)
|
||||
}
|
||||
}
|
||||
|
||||
type options struct {
|
||||
transitional bool
|
||||
useSTD3Rules bool
|
||||
validateLabels bool
|
||||
verifyDNSLength bool
|
||||
removeLeadingDots bool
|
||||
|
||||
trie *idnaTrie
|
||||
|
||||
// fromPuny calls validation rules when converting A-labels to U-labels.
|
||||
fromPuny func(p *Profile, s string) error
|
||||
|
||||
// mapping implements a validation and mapping step as defined in RFC 5895
|
||||
// or UTS 46, tailored to, for example, domain registration or lookup.
|
||||
mapping func(p *Profile, s string) (string, error)
|
||||
|
||||
// bidirule, if specified, checks whether s conforms to the Bidi Rule
|
||||
// defined in RFC 5893.
|
||||
bidirule func(s string) bool
|
||||
}
|
||||
|
||||
// A Profile defines the configuration of a IDNA mapper.
|
||||
type Profile struct {
|
||||
options
|
||||
}
|
||||
|
||||
func apply(o *options, opts []Option) {
|
||||
for _, f := range opts {
|
||||
f(o)
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new Profile.
|
||||
//
|
||||
// With no options, the returned Profile is the most permissive and equals the
|
||||
// Punycode Profile. Options can be passed to further restrict the Profile. The
|
||||
// MapForLookup and ValidateForRegistration options set a collection of options,
|
||||
// for lookup and registration purposes respectively, which can be tailored by
|
||||
// adding more fine-grained options, where later options override earlier
|
||||
// options.
|
||||
func New(o ...Option) *Profile {
|
||||
p := &Profile{}
|
||||
apply(&p.options, o)
|
||||
return p
|
||||
}
|
||||
|
||||
// ToASCII converts a domain or domain label to its ASCII form. For example,
|
||||
// ToASCII("bücher.example.com") is "xn--bcher-kva.example.com", and
|
||||
// ToASCII("golang") is "golang". If an error is encountered it will return
|
||||
// an error and a (partially) processed result.
|
||||
func (p *Profile) ToASCII(s string) (string, error) {
|
||||
return p.process(s, true)
|
||||
}
|
||||
|
||||
// ToUnicode converts a domain or domain label to its Unicode form. For example,
|
||||
// ToUnicode("xn--bcher-kva.example.com") is "bücher.example.com", and
|
||||
// ToUnicode("golang") is "golang". If an error is encountered it will return
|
||||
// an error and a (partially) processed result.
|
||||
func (p *Profile) ToUnicode(s string) (string, error) {
|
||||
pp := *p
|
||||
pp.transitional = false
|
||||
return pp.process(s, false)
|
||||
}
|
||||
|
||||
// String reports a string with a description of the profile for debugging
|
||||
// purposes. The string format may change with different versions.
|
||||
func (p *Profile) String() string {
|
||||
s := ""
|
||||
if p.transitional {
|
||||
s = "Transitional"
|
||||
} else {
|
||||
s = "NonTransitional"
|
||||
}
|
||||
if p.useSTD3Rules {
|
||||
s += ":UseSTD3Rules"
|
||||
}
|
||||
if p.validateLabels {
|
||||
s += ":ValidateLabels"
|
||||
}
|
||||
if p.verifyDNSLength {
|
||||
s += ":VerifyDNSLength"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
var (
|
||||
// Punycode is a Profile that does raw punycode processing with a minimum
|
||||
// of validation.
|
||||
Punycode *Profile = punycode
|
||||
|
||||
// Lookup is the recommended profile for looking up domain names, according
|
||||
// to Section 5 of RFC 5891. The exact configuration of this profile may
|
||||
// change over time.
|
||||
Lookup *Profile = lookup
|
||||
|
||||
// Display is the recommended profile for displaying domain names.
|
||||
// The configuration of this profile may change over time.
|
||||
Display *Profile = display
|
||||
|
||||
// Registration is the recommended profile for checking whether a given
|
||||
// IDN is valid for registration, according to Section 4 of RFC 5891.
|
||||
Registration *Profile = registration
|
||||
|
||||
punycode = &Profile{}
|
||||
lookup = &Profile{options{
|
||||
transitional: true,
|
||||
useSTD3Rules: true,
|
||||
validateLabels: true,
|
||||
removeLeadingDots: true,
|
||||
trie: trie,
|
||||
fromPuny: validateFromPunycode,
|
||||
mapping: validateAndMap,
|
||||
bidirule: bidirule.ValidString,
|
||||
}}
|
||||
display = &Profile{options{
|
||||
useSTD3Rules: true,
|
||||
validateLabels: true,
|
||||
removeLeadingDots: true,
|
||||
trie: trie,
|
||||
fromPuny: validateFromPunycode,
|
||||
mapping: validateAndMap,
|
||||
bidirule: bidirule.ValidString,
|
||||
}}
|
||||
registration = &Profile{options{
|
||||
useSTD3Rules: true,
|
||||
validateLabels: true,
|
||||
verifyDNSLength: true,
|
||||
trie: trie,
|
||||
fromPuny: validateFromPunycode,
|
||||
mapping: validateRegistration,
|
||||
bidirule: bidirule.ValidString,
|
||||
}}
|
||||
|
||||
// TODO: profiles
|
||||
// Register: recommended for approving domain names: don't do any mappings
|
||||
// but rather reject on invalid input. Bundle or block deviation characters.
|
||||
)
|
||||
|
||||
type labelError struct{ label, code_ string }
|
||||
|
||||
func (e labelError) code() string { return e.code_ }
|
||||
func (e labelError) Error() string {
|
||||
return fmt.Sprintf("idna: invalid label %q", e.label)
|
||||
}
|
||||
|
||||
type runeError rune
|
||||
|
||||
func (e runeError) code() string { return "P1" }
|
||||
func (e runeError) Error() string {
|
||||
return fmt.Sprintf("idna: disallowed rune %U", e)
|
||||
}
|
||||
|
||||
// process implements the algorithm described in section 4 of UTS #46,
|
||||
// see http://www.unicode.org/reports/tr46.
|
||||
func (p *Profile) process(s string, toASCII bool) (string, error) {
|
||||
var err error
|
||||
if p.mapping != nil {
|
||||
s, err = p.mapping(p, s)
|
||||
}
|
||||
// Remove leading empty labels.
|
||||
if p.removeLeadingDots {
|
||||
for ; len(s) > 0 && s[0] == '.'; s = s[1:] {
|
||||
}
|
||||
}
|
||||
// It seems like we should only create this error on ToASCII, but the
|
||||
// UTS 46 conformance tests suggests we should always check this.
|
||||
if err == nil && p.verifyDNSLength && s == "" {
|
||||
err = &labelError{s, "A4"}
|
||||
}
|
||||
labels := labelIter{orig: s}
|
||||
for ; !labels.done(); labels.next() {
|
||||
label := labels.label()
|
||||
if label == "" {
|
||||
// Empty labels are not okay. The label iterator skips the last
|
||||
// label if it is empty.
|
||||
if err == nil && p.verifyDNSLength {
|
||||
err = &labelError{s, "A4"}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(label, acePrefix) {
|
||||
u, err2 := decode(label[len(acePrefix):])
|
||||
if err2 != nil {
|
||||
if err == nil {
|
||||
err = err2
|
||||
}
|
||||
// Spec says keep the old label.
|
||||
continue
|
||||
}
|
||||
labels.set(u)
|
||||
if err == nil && p.validateLabels {
|
||||
err = p.fromPuny(p, u)
|
||||
}
|
||||
if err == nil {
|
||||
// This should be called on NonTransitional, according to the
|
||||
// spec, but that currently does not have any effect. Use the
|
||||
// original profile to preserve options.
|
||||
err = p.validateLabel(u)
|
||||
}
|
||||
} else if err == nil {
|
||||
err = p.validateLabel(label)
|
||||
}
|
||||
}
|
||||
if toASCII {
|
||||
for labels.reset(); !labels.done(); labels.next() {
|
||||
label := labels.label()
|
||||
if !ascii(label) {
|
||||
a, err2 := encode(acePrefix, label)
|
||||
if err == nil {
|
||||
err = err2
|
||||
}
|
||||
label = a
|
||||
labels.set(a)
|
||||
}
|
||||
n := len(label)
|
||||
if p.verifyDNSLength && err == nil && (n == 0 || n > 63) {
|
||||
err = &labelError{label, "A4"}
|
||||
}
|
||||
}
|
||||
}
|
||||
s = labels.result()
|
||||
if toASCII && p.verifyDNSLength && err == nil {
|
||||
// Compute the length of the domain name minus the root label and its dot.
|
||||
n := len(s)
|
||||
if n > 0 && s[n-1] == '.' {
|
||||
n--
|
||||
}
|
||||
if len(s) < 1 || n > 253 {
|
||||
err = &labelError{s, "A4"}
|
||||
}
|
||||
}
|
||||
return s, err
|
||||
}
|
||||
|
||||
func normalize(p *Profile, s string) (string, error) {
|
||||
return norm.NFC.String(s), nil
|
||||
}
|
||||
|
||||
func validateRegistration(p *Profile, s string) (string, error) {
|
||||
if !norm.NFC.IsNormalString(s) {
|
||||
return s, &labelError{s, "V1"}
|
||||
}
|
||||
for i := 0; i < len(s); {
|
||||
v, sz := trie.lookupString(s[i:])
|
||||
// Copy bytes not copied so far.
|
||||
switch p.simplify(info(v).category()) {
|
||||
// TODO: handle the NV8 defined in the Unicode idna data set to allow
|
||||
// for strict conformance to IDNA2008.
|
||||
case valid, deviation:
|
||||
case disallowed, mapped, unknown, ignored:
|
||||
r, _ := utf8.DecodeRuneInString(s[i:])
|
||||
return s, runeError(r)
|
||||
}
|
||||
i += sz
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func validateAndMap(p *Profile, s string) (string, error) {
|
||||
var (
|
||||
err error
|
||||
b []byte
|
||||
k int
|
||||
)
|
||||
for i := 0; i < len(s); {
|
||||
v, sz := trie.lookupString(s[i:])
|
||||
start := i
|
||||
i += sz
|
||||
// Copy bytes not copied so far.
|
||||
switch p.simplify(info(v).category()) {
|
||||
case valid:
|
||||
continue
|
||||
case disallowed:
|
||||
if err == nil {
|
||||
r, _ := utf8.DecodeRuneInString(s[start:])
|
||||
err = runeError(r)
|
||||
}
|
||||
continue
|
||||
case mapped, deviation:
|
||||
b = append(b, s[k:start]...)
|
||||
b = info(v).appendMapping(b, s[start:i])
|
||||
case ignored:
|
||||
b = append(b, s[k:start]...)
|
||||
// drop the rune
|
||||
case unknown:
|
||||
b = append(b, s[k:start]...)
|
||||
b = append(b, "\ufffd"...)
|
||||
}
|
||||
k = i
|
||||
}
|
||||
if k == 0 {
|
||||
// No changes so far.
|
||||
s = norm.NFC.String(s)
|
||||
} else {
|
||||
b = append(b, s[k:]...)
|
||||
if norm.NFC.QuickSpan(b) != len(b) {
|
||||
b = norm.NFC.Bytes(b)
|
||||
}
|
||||
// TODO: the punycode converters require strings as input.
|
||||
s = string(b)
|
||||
}
|
||||
return s, err
|
||||
}
|
||||
|
||||
// A labelIter allows iterating over domain name labels.
|
||||
type labelIter struct {
|
||||
orig string
|
||||
slice []string
|
||||
curStart int
|
||||
curEnd int
|
||||
i int
|
||||
}
|
||||
|
||||
func (l *labelIter) reset() {
|
||||
l.curStart = 0
|
||||
l.curEnd = 0
|
||||
l.i = 0
|
||||
}
|
||||
|
||||
func (l *labelIter) done() bool {
|
||||
return l.curStart >= len(l.orig)
|
||||
}
|
||||
|
||||
func (l *labelIter) result() string {
|
||||
if l.slice != nil {
|
||||
return strings.Join(l.slice, ".")
|
||||
}
|
||||
return l.orig
|
||||
}
|
||||
|
||||
func (l *labelIter) label() string {
|
||||
if l.slice != nil {
|
||||
return l.slice[l.i]
|
||||
}
|
||||
p := strings.IndexByte(l.orig[l.curStart:], '.')
|
||||
l.curEnd = l.curStart + p
|
||||
if p == -1 {
|
||||
l.curEnd = len(l.orig)
|
||||
}
|
||||
return l.orig[l.curStart:l.curEnd]
|
||||
}
|
||||
|
||||
// next sets the value to the next label. It skips the last label if it is empty.
|
||||
func (l *labelIter) next() {
|
||||
l.i++
|
||||
if l.slice != nil {
|
||||
if l.i >= len(l.slice) || l.i == len(l.slice)-1 && l.slice[l.i] == "" {
|
||||
l.curStart = len(l.orig)
|
||||
}
|
||||
} else {
|
||||
l.curStart = l.curEnd + 1
|
||||
if l.curStart == len(l.orig)-1 && l.orig[l.curStart] == '.' {
|
||||
l.curStart = len(l.orig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *labelIter) set(s string) {
|
||||
if l.slice == nil {
|
||||
l.slice = strings.Split(l.orig, ".")
|
||||
}
|
||||
l.slice[l.i] = s
|
||||
}
|
||||
|
||||
// acePrefix is the ASCII Compatible Encoding prefix.
|
||||
const acePrefix = "xn--"
|
||||
|
||||
func (p *Profile) simplify(cat category) category {
|
||||
switch cat {
|
||||
case disallowedSTD3Mapped:
|
||||
if p.useSTD3Rules {
|
||||
cat = disallowed
|
||||
} else {
|
||||
cat = mapped
|
||||
}
|
||||
case disallowedSTD3Valid:
|
||||
if p.useSTD3Rules {
|
||||
cat = disallowed
|
||||
} else {
|
||||
cat = valid
|
||||
}
|
||||
case deviation:
|
||||
if !p.transitional {
|
||||
cat = valid
|
||||
}
|
||||
case validNV8, validXV8:
|
||||
// TODO: handle V2008
|
||||
cat = valid
|
||||
}
|
||||
return cat
|
||||
}
|
||||
|
||||
func validateFromPunycode(p *Profile, s string) error {
|
||||
if !norm.NFC.IsNormalString(s) {
|
||||
return &labelError{s, "V1"}
|
||||
}
|
||||
for i := 0; i < len(s); {
|
||||
v, sz := trie.lookupString(s[i:])
|
||||
if c := p.simplify(info(v).category()); c != valid && c != deviation {
|
||||
return &labelError{s, "V6"}
|
||||
}
|
||||
i += sz
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
zwnj = "\u200c"
|
||||
zwj = "\u200d"
|
||||
)
|
||||
|
||||
type joinState int8
|
||||
|
||||
const (
|
||||
stateStart joinState = iota
|
||||
stateVirama
|
||||
stateBefore
|
||||
stateBeforeVirama
|
||||
stateAfter
|
||||
stateFAIL
|
||||
)
|
||||
|
||||
var joinStates = [][numJoinTypes]joinState{
|
||||
stateStart: {
|
||||
joiningL: stateBefore,
|
||||
joiningD: stateBefore,
|
||||
joinZWNJ: stateFAIL,
|
||||
joinZWJ: stateFAIL,
|
||||
joinVirama: stateVirama,
|
||||
},
|
||||
stateVirama: {
|
||||
joiningL: stateBefore,
|
||||
joiningD: stateBefore,
|
||||
},
|
||||
stateBefore: {
|
||||
joiningL: stateBefore,
|
||||
joiningD: stateBefore,
|
||||
joiningT: stateBefore,
|
||||
joinZWNJ: stateAfter,
|
||||
joinZWJ: stateFAIL,
|
||||
joinVirama: stateBeforeVirama,
|
||||
},
|
||||
stateBeforeVirama: {
|
||||
joiningL: stateBefore,
|
||||
joiningD: stateBefore,
|
||||
joiningT: stateBefore,
|
||||
},
|
||||
stateAfter: {
|
||||
joiningL: stateFAIL,
|
||||
joiningD: stateBefore,
|
||||
joiningT: stateAfter,
|
||||
joiningR: stateStart,
|
||||
joinZWNJ: stateFAIL,
|
||||
joinZWJ: stateFAIL,
|
||||
joinVirama: stateAfter, // no-op as we can't accept joiners here
|
||||
},
|
||||
stateFAIL: {
|
||||
0: stateFAIL,
|
||||
joiningL: stateFAIL,
|
||||
joiningD: stateFAIL,
|
||||
joiningT: stateFAIL,
|
||||
joiningR: stateFAIL,
|
||||
joinZWNJ: stateFAIL,
|
||||
joinZWJ: stateFAIL,
|
||||
joinVirama: stateFAIL,
|
||||
},
|
||||
}
|
||||
|
||||
// validateLabel validates the criteria from Section 4.1. Item 1, 4, and 6 are
|
||||
// already implicitly satisfied by the overall implementation.
|
||||
func (p *Profile) validateLabel(s string) error {
|
||||
if s == "" {
|
||||
if p.verifyDNSLength {
|
||||
return &labelError{s, "A4"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if p.bidirule != nil && !p.bidirule(s) {
|
||||
return &labelError{s, "B"}
|
||||
}
|
||||
if !p.validateLabels {
|
||||
return nil
|
||||
}
|
||||
trie := p.trie // p.validateLabels is only set if trie is set.
|
||||
if len(s) > 4 && s[2] == '-' && s[3] == '-' {
|
||||
return &labelError{s, "V2"}
|
||||
}
|
||||
if s[0] == '-' || s[len(s)-1] == '-' {
|
||||
return &labelError{s, "V3"}
|
||||
}
|
||||
// TODO: merge the use of this in the trie.
|
||||
v, sz := trie.lookupString(s)
|
||||
x := info(v)
|
||||
if x.isModifier() {
|
||||
return &labelError{s, "V5"}
|
||||
}
|
||||
// Quickly return in the absence of zero-width (non) joiners.
|
||||
if strings.Index(s, zwj) == -1 && strings.Index(s, zwnj) == -1 {
|
||||
return nil
|
||||
}
|
||||
st := stateStart
|
||||
for i := 0; ; {
|
||||
jt := x.joinType()
|
||||
if s[i:i+sz] == zwj {
|
||||
jt = joinZWJ
|
||||
} else if s[i:i+sz] == zwnj {
|
||||
jt = joinZWNJ
|
||||
}
|
||||
st = joinStates[st][jt]
|
||||
if x.isViramaModifier() {
|
||||
st = joinStates[st][joinVirama]
|
||||
}
|
||||
if i += sz; i == len(s) {
|
||||
break
|
||||
}
|
||||
v, sz = trie.lookupString(s[i:])
|
||||
x = info(v)
|
||||
}
|
||||
if st == stateFAIL || st == stateAfter {
|
||||
return &labelError{s, "C"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ascii(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] >= utf8.RuneSelf {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
// Copyright 2016 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.
|
||||
|
||||
// +build !go1.10
|
||||
|
||||
package idna
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestLabelErrors tests strings returned in case of error. All results should
|
||||
// be identical to the reference implementation and can be verified at
|
||||
// http://unicode.org/cldr/utility/idna.jsp. The reference implementation,
|
||||
// however, seems to not display Bidi and ContextJ errors.
|
||||
//
|
||||
// In some cases the behavior of browsers is added as a comment. In all cases,
|
||||
// whenever a resolve search returns an error here, Chrome will treat the input
|
||||
// string as a search string (including those for Bidi and Context J errors),
|
||||
// unless noted otherwise.
|
||||
func TestLabelErrors(t *testing.T) {
|
||||
encode := func(s string) string { s, _ = encode(acePrefix, s); return s }
|
||||
type kind struct {
|
||||
name string
|
||||
f func(string) (string, error)
|
||||
}
|
||||
punyA := kind{"PunycodeA", punycode.ToASCII}
|
||||
resolve := kind{"ResolveA", Lookup.ToASCII}
|
||||
display := kind{"ToUnicode", Display.ToUnicode}
|
||||
p := New(VerifyDNSLength(true), MapForLookup(), BidiRule())
|
||||
lengthU := kind{"CheckLengthU", p.ToUnicode}
|
||||
lengthA := kind{"CheckLengthA", p.ToASCII}
|
||||
p = New(MapForLookup(), StrictDomainName(false))
|
||||
std3 := kind{"STD3", p.ToASCII}
|
||||
|
||||
testCases := []struct {
|
||||
kind
|
||||
input string
|
||||
want string
|
||||
wantErr string
|
||||
}{
|
||||
{lengthU, "", "", "A4"}, // From UTS 46 conformance test.
|
||||
{lengthA, "", "", "A4"},
|
||||
|
||||
{lengthU, "xn--", "", "A4"},
|
||||
{lengthU, "foo.xn--", "foo.", "A4"}, // TODO: is dropping xn-- correct?
|
||||
{lengthU, "xn--.foo", ".foo", "A4"},
|
||||
{lengthU, "foo.xn--.bar", "foo..bar", "A4"},
|
||||
|
||||
{display, "xn--", "", ""},
|
||||
{display, "foo.xn--", "foo.", ""}, // TODO: is dropping xn-- correct?
|
||||
{display, "xn--.foo", ".foo", ""},
|
||||
{display, "foo.xn--.bar", "foo..bar", ""},
|
||||
|
||||
{lengthA, "a..b", "a..b", "A4"},
|
||||
{punyA, ".b", ".b", ""},
|
||||
// For backwards compatibility, the Punycode profile does not map runes.
|
||||
{punyA, "\u3002b", "xn--b-83t", ""},
|
||||
{punyA, "..b", "..b", ""},
|
||||
// Only strip leading empty labels for certain profiles. Stripping
|
||||
// leading empty labels here but not for "empty" punycode above seems
|
||||
// inconsistent, but seems to be applied by both the conformance test
|
||||
// and Chrome. So we turn it off by default, support it as an option,
|
||||
// and enable it in profiles where it seems commonplace.
|
||||
{lengthA, ".b", "b", ""},
|
||||
{lengthA, "\u3002b", "b", ""},
|
||||
{lengthA, "..b", "b", ""},
|
||||
{lengthA, "b..", "b..", ""},
|
||||
|
||||
{resolve, "a..b", "a..b", ""},
|
||||
{resolve, ".b", "b", ""},
|
||||
{resolve, "\u3002b", "b", ""},
|
||||
{resolve, "..b", "b", ""},
|
||||
{resolve, "b..", "b..", ""},
|
||||
|
||||
// Raw punycode
|
||||
{punyA, "", "", ""},
|
||||
{punyA, "*.foo.com", "*.foo.com", ""},
|
||||
{punyA, "Foo.com", "Foo.com", ""},
|
||||
|
||||
// STD3 rules
|
||||
{display, "*.foo.com", "*.foo.com", "P1"},
|
||||
{std3, "*.foo.com", "*.foo.com", ""},
|
||||
|
||||
// Don't map U+2490 (DIGIT NINE FULL STOP). This is the behavior of
|
||||
// Chrome, Safari, and IE. Firefox will first map ⒐ to 9. and return
|
||||
// lab9.be.
|
||||
{resolve, "lab⒐be", "xn--labbe-zh9b", "P1"}, // encode("lab⒐be")
|
||||
{display, "lab⒐be", "lab⒐be", "P1"},
|
||||
|
||||
{resolve, "plan⒐faß.de", "xn--planfass-c31e.de", "P1"}, // encode("plan⒐fass") + ".de"
|
||||
{display, "Plan⒐faß.de", "plan⒐faß.de", "P1"},
|
||||
|
||||
// Chrome 54.0 recognizes the error and treats this input verbatim as a
|
||||
// search string.
|
||||
// Safari 10.0 (non-conform spec) decomposes "⒈" and computes the
|
||||
// punycode on the result using transitional mapping.
|
||||
// Firefox 49.0.1 goes haywire on this string and prints a bunch of what
|
||||
// seems to be nested punycode encodings.
|
||||
{resolve, "日本⒈co.ßßß.de", "xn--co-wuw5954azlb.ssssss.de", "P1"},
|
||||
{display, "日本⒈co.ßßß.de", "日本⒈co.ßßß.de", "P1"},
|
||||
|
||||
{resolve, "a\u200Cb", "ab", ""},
|
||||
{display, "a\u200Cb", "a\u200Cb", "C"},
|
||||
|
||||
{resolve, encode("a\u200Cb"), encode("a\u200Cb"), "C"},
|
||||
{display, "a\u200Cb", "a\u200Cb", "C"},
|
||||
|
||||
{resolve, "grﻋﺮﺑﻲ.de", "xn--gr-gtd9a1b0g.de", "B"},
|
||||
{
|
||||
// Notice how the string gets transformed, even with an error.
|
||||
// Chrome will use the original string if it finds an error, so not
|
||||
// the transformed one.
|
||||
display,
|
||||
"gr\ufecb\ufeae\ufe91\ufef2.de",
|
||||
"gr\u0639\u0631\u0628\u064a.de",
|
||||
"B",
|
||||
},
|
||||
|
||||
{resolve, "\u0671.\u03c3\u07dc", "xn--qib.xn--4xa21s", "B"}, // ٱ.σߜ
|
||||
{display, "\u0671.\u03c3\u07dc", "\u0671.\u03c3\u07dc", "B"},
|
||||
|
||||
// normalize input
|
||||
{resolve, "a\u0323\u0322", "xn--jta191l", ""}, // ạ̢
|
||||
{display, "a\u0323\u0322", "\u1ea1\u0322", ""},
|
||||
|
||||
// Non-normalized strings are not normalized when they originate from
|
||||
// punycode. Despite the error, Chrome, Safari and Firefox will attempt
|
||||
// to look up the input punycode.
|
||||
{resolve, encode("a\u0323\u0322") + ".com", "xn--a-tdbc.com", "V1"},
|
||||
{display, encode("a\u0323\u0322") + ".com", "a\u0323\u0322.com", "V1"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
doTest(t, tc.f, tc.name, tc.input, tc.want, tc.wantErr)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,357 @@
|
|||
// 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 format
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// A Parser parses a format string. The result from the parse are set in the
|
||||
// struct fields.
|
||||
type Parser struct {
|
||||
Verb rune
|
||||
|
||||
WidthPresent bool
|
||||
PrecPresent bool
|
||||
Minus bool
|
||||
Plus bool
|
||||
Sharp bool
|
||||
Space bool
|
||||
Zero bool
|
||||
|
||||
// For the formats %+v %#v, we set the plusV/sharpV flags
|
||||
// and clear the plus/sharp flags since %+v and %#v are in effect
|
||||
// different, flagless formats set at the top level.
|
||||
PlusV bool
|
||||
SharpV bool
|
||||
|
||||
HasIndex bool
|
||||
|
||||
Width int
|
||||
Prec int // precision
|
||||
|
||||
// retain arguments across calls.
|
||||
Args []interface{}
|
||||
// retain current argument number across calls
|
||||
ArgNum int
|
||||
|
||||
// reordered records whether the format string used argument reordering.
|
||||
Reordered bool
|
||||
// goodArgNum records whether the most recent reordering directive was valid.
|
||||
goodArgNum bool
|
||||
|
||||
// position info
|
||||
format string
|
||||
startPos int
|
||||
endPos int
|
||||
Status Status
|
||||
}
|
||||
|
||||
// Reset initializes a parser to scan format strings for the given args.
|
||||
func (p *Parser) Reset(args []interface{}) {
|
||||
p.Args = args
|
||||
p.ArgNum = 0
|
||||
p.startPos = 0
|
||||
p.Reordered = false
|
||||
}
|
||||
|
||||
// Text returns the part of the format string that was parsed by the last call
|
||||
// to Scan. It returns the original substitution clause if the current scan
|
||||
// parsed a substitution.
|
||||
func (p *Parser) Text() string { return p.format[p.startPos:p.endPos] }
|
||||
|
||||
// SetFormat sets a new format string to parse. It does not reset the argument
|
||||
// count.
|
||||
func (p *Parser) SetFormat(format string) {
|
||||
p.format = format
|
||||
p.startPos = 0
|
||||
p.endPos = 0
|
||||
}
|
||||
|
||||
// Status indicates the result type of a call to Scan.
|
||||
type Status int
|
||||
|
||||
const (
|
||||
StatusText Status = iota
|
||||
StatusSubstitution
|
||||
StatusBadWidthSubstitution
|
||||
StatusBadPrecSubstitution
|
||||
StatusNoVerb
|
||||
StatusBadArgNum
|
||||
StatusMissingArg
|
||||
)
|
||||
|
||||
// ClearFlags reset the parser to default behavior.
|
||||
func (p *Parser) ClearFlags() {
|
||||
p.WidthPresent = false
|
||||
p.PrecPresent = false
|
||||
p.Minus = false
|
||||
p.Plus = false
|
||||
p.Sharp = false
|
||||
p.Space = false
|
||||
p.Zero = false
|
||||
|
||||
p.PlusV = false
|
||||
p.SharpV = false
|
||||
|
||||
p.HasIndex = false
|
||||
}
|
||||
|
||||
// Scan scans the next part of the format string and sets the status to
|
||||
// indicate whether it scanned a string literal, substitution or error.
|
||||
func (p *Parser) Scan() bool {
|
||||
p.Status = StatusText
|
||||
format := p.format
|
||||
end := len(format)
|
||||
if p.endPos >= end {
|
||||
return false
|
||||
}
|
||||
afterIndex := false // previous item in format was an index like [3].
|
||||
|
||||
p.startPos = p.endPos
|
||||
p.goodArgNum = true
|
||||
i := p.startPos
|
||||
for i < end && format[i] != '%' {
|
||||
i++
|
||||
}
|
||||
if i > p.startPos {
|
||||
p.endPos = i
|
||||
return true
|
||||
}
|
||||
// Process one verb
|
||||
i++
|
||||
|
||||
p.Status = StatusSubstitution
|
||||
|
||||
// Do we have flags?
|
||||
p.ClearFlags()
|
||||
|
||||
simpleFormat:
|
||||
for ; i < end; i++ {
|
||||
c := p.format[i]
|
||||
switch c {
|
||||
case '#':
|
||||
p.Sharp = true
|
||||
case '0':
|
||||
p.Zero = !p.Minus // Only allow zero padding to the left.
|
||||
case '+':
|
||||
p.Plus = true
|
||||
case '-':
|
||||
p.Minus = true
|
||||
p.Zero = false // Do not pad with zeros to the right.
|
||||
case ' ':
|
||||
p.Space = true
|
||||
default:
|
||||
// Fast path for common case of ascii lower case simple verbs
|
||||
// without precision or width or argument indices.
|
||||
if 'a' <= c && c <= 'z' && p.ArgNum < len(p.Args) {
|
||||
if c == 'v' {
|
||||
// Go syntax
|
||||
p.SharpV = p.Sharp
|
||||
p.Sharp = false
|
||||
// Struct-field syntax
|
||||
p.PlusV = p.Plus
|
||||
p.Plus = false
|
||||
}
|
||||
p.Verb = rune(c)
|
||||
p.ArgNum++
|
||||
p.endPos = i + 1
|
||||
return true
|
||||
}
|
||||
// Format is more complex than simple flags and a verb or is malformed.
|
||||
break simpleFormat
|
||||
}
|
||||
}
|
||||
|
||||
// Do we have an explicit argument index?
|
||||
i, afterIndex = p.updateArgNumber(format, i)
|
||||
|
||||
// Do we have width?
|
||||
if i < end && format[i] == '*' {
|
||||
i++
|
||||
p.Width, p.WidthPresent = p.intFromArg()
|
||||
|
||||
if !p.WidthPresent {
|
||||
p.Status = StatusBadWidthSubstitution
|
||||
}
|
||||
|
||||
// We have a negative width, so take its value and ensure
|
||||
// that the minus flag is set
|
||||
if p.Width < 0 {
|
||||
p.Width = -p.Width
|
||||
p.Minus = true
|
||||
p.Zero = false // Do not pad with zeros to the right.
|
||||
}
|
||||
afterIndex = false
|
||||
} else {
|
||||
p.Width, p.WidthPresent, i = parsenum(format, i, end)
|
||||
if afterIndex && p.WidthPresent { // "%[3]2d"
|
||||
p.goodArgNum = false
|
||||
}
|
||||
}
|
||||
|
||||
// Do we have precision?
|
||||
if i+1 < end && format[i] == '.' {
|
||||
i++
|
||||
if afterIndex { // "%[3].2d"
|
||||
p.goodArgNum = false
|
||||
}
|
||||
i, afterIndex = p.updateArgNumber(format, i)
|
||||
if i < end && format[i] == '*' {
|
||||
i++
|
||||
p.Prec, p.PrecPresent = p.intFromArg()
|
||||
// Negative precision arguments don't make sense
|
||||
if p.Prec < 0 {
|
||||
p.Prec = 0
|
||||
p.PrecPresent = false
|
||||
}
|
||||
if !p.PrecPresent {
|
||||
p.Status = StatusBadPrecSubstitution
|
||||
}
|
||||
afterIndex = false
|
||||
} else {
|
||||
p.Prec, p.PrecPresent, i = parsenum(format, i, end)
|
||||
if !p.PrecPresent {
|
||||
p.Prec = 0
|
||||
p.PrecPresent = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !afterIndex {
|
||||
i, afterIndex = p.updateArgNumber(format, i)
|
||||
}
|
||||
p.HasIndex = afterIndex
|
||||
|
||||
if i >= end {
|
||||
p.endPos = i
|
||||
p.Status = StatusNoVerb
|
||||
return true
|
||||
}
|
||||
|
||||
verb, w := utf8.DecodeRuneInString(format[i:])
|
||||
p.endPos = i + w
|
||||
p.Verb = verb
|
||||
|
||||
switch {
|
||||
case verb == '%': // Percent does not absorb operands and ignores f.wid and f.prec.
|
||||
p.startPos = p.endPos - 1
|
||||
p.Status = StatusText
|
||||
case !p.goodArgNum:
|
||||
p.Status = StatusBadArgNum
|
||||
case p.ArgNum >= len(p.Args): // No argument left over to print for the current verb.
|
||||
p.Status = StatusMissingArg
|
||||
case verb == 'v':
|
||||
// Go syntax
|
||||
p.SharpV = p.Sharp
|
||||
p.Sharp = false
|
||||
// Struct-field syntax
|
||||
p.PlusV = p.Plus
|
||||
p.Plus = false
|
||||
fallthrough
|
||||
default:
|
||||
p.ArgNum++
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// intFromArg gets the ArgNumth element of Args. On return, isInt reports
|
||||
// whether the argument has integer type.
|
||||
func (p *Parser) intFromArg() (num int, isInt bool) {
|
||||
if p.ArgNum < len(p.Args) {
|
||||
arg := p.Args[p.ArgNum]
|
||||
num, isInt = arg.(int) // Almost always OK.
|
||||
if !isInt {
|
||||
// Work harder.
|
||||
switch v := reflect.ValueOf(arg); v.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
n := v.Int()
|
||||
if int64(int(n)) == n {
|
||||
num = int(n)
|
||||
isInt = true
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
n := v.Uint()
|
||||
if int64(n) >= 0 && uint64(int(n)) == n {
|
||||
num = int(n)
|
||||
isInt = true
|
||||
}
|
||||
default:
|
||||
// Already 0, false.
|
||||
}
|
||||
}
|
||||
p.ArgNum++
|
||||
if tooLarge(num) {
|
||||
num = 0
|
||||
isInt = false
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// parseArgNumber returns the value of the bracketed number, minus 1
|
||||
// (explicit argument numbers are one-indexed but we want zero-indexed).
|
||||
// The opening bracket is known to be present at format[0].
|
||||
// The returned values are the index, the number of bytes to consume
|
||||
// up to the closing paren, if present, and whether the number parsed
|
||||
// ok. The bytes to consume will be 1 if no closing paren is present.
|
||||
func parseArgNumber(format string) (index int, wid int, ok bool) {
|
||||
// There must be at least 3 bytes: [n].
|
||||
if len(format) < 3 {
|
||||
return 0, 1, false
|
||||
}
|
||||
|
||||
// Find closing bracket.
|
||||
for i := 1; i < len(format); i++ {
|
||||
if format[i] == ']' {
|
||||
width, ok, newi := parsenum(format, 1, i)
|
||||
if !ok || newi != i {
|
||||
return 0, i + 1, false
|
||||
}
|
||||
return width - 1, i + 1, true // arg numbers are one-indexed and skip paren.
|
||||
}
|
||||
}
|
||||
return 0, 1, false
|
||||
}
|
||||
|
||||
// updateArgNumber returns the next argument to evaluate, which is either the value of the passed-in
|
||||
// argNum or the value of the bracketed integer that begins format[i:]. It also returns
|
||||
// the new value of i, that is, the index of the next byte of the format to process.
|
||||
func (p *Parser) updateArgNumber(format string, i int) (newi int, found bool) {
|
||||
if len(format) <= i || format[i] != '[' {
|
||||
return i, false
|
||||
}
|
||||
p.Reordered = true
|
||||
index, wid, ok := parseArgNumber(format[i:])
|
||||
if ok && 0 <= index && index < len(p.Args) {
|
||||
p.ArgNum = index
|
||||
return i + wid, true
|
||||
}
|
||||
p.goodArgNum = false
|
||||
return i + wid, ok
|
||||
}
|
||||
|
||||
// tooLarge reports whether the magnitude of the integer is
|
||||
// too large to be used as a formatting width or precision.
|
||||
func tooLarge(x int) bool {
|
||||
const max int = 1e6
|
||||
return x > max || x < -max
|
||||
}
|
||||
|
||||
// parsenum converts ASCII to integer. num is 0 (and isnum is false) if no number present.
|
||||
func parsenum(s string, start, end int) (num int, isnum bool, newi int) {
|
||||
if start >= end {
|
||||
return 0, false, end
|
||||
}
|
||||
for newi = start; newi < end && '0' <= s[newi] && s[newi] <= '9'; newi++ {
|
||||
if tooLarge(num) {
|
||||
return 0, false, end // Overflow; crazy long number most likely.
|
||||
}
|
||||
num = num*10 + int(s[newi]-'0')
|
||||
isnum = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// 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 format
|
||||
|
||||
import "testing"
|
||||
|
||||
// TODO: most of Parser is tested in x/message. Move some tests here.
|
||||
|
||||
func TestParsenum(t *testing.T) {
|
||||
testCases := []struct {
|
||||
s string
|
||||
start, end int
|
||||
num int
|
||||
isnum bool
|
||||
newi int
|
||||
}{
|
||||
{"a123", 0, 4, 0, false, 0},
|
||||
{"1234", 1, 1, 0, false, 1},
|
||||
{"123a", 0, 4, 123, true, 3},
|
||||
{"12a3", 0, 4, 12, true, 2},
|
||||
{"1234", 0, 4, 1234, true, 4},
|
||||
{"1a234", 1, 3, 0, false, 1},
|
||||
}
|
||||
for _, tt := range testCases {
|
||||
num, isnum, newi := parsenum(tt.s, tt.start, tt.end)
|
||||
if num != tt.num || isnum != tt.isnum || newi != tt.newi {
|
||||
t.Errorf("parsenum(%q, %d, %d) = %d, %v, %d, want %d, %v, %d", tt.s, tt.start, tt.end, num, isnum, newi, tt.num, tt.isnum, tt.newi)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// 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.
|
||||
|
||||
// +build go1.9
|
||||
|
||||
package catalog
|
||||
|
||||
import "golang.org/x/text/internal/catmsg"
|
||||
|
||||
// A Message holds a collection of translations for the same phrase that may
|
||||
// vary based on the values of substitution arguments.
|
||||
type Message = catmsg.Message
|
||||
|
||||
type firstInSequence = catmsg.FirstOf
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
// 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.
|
||||
|
||||
// +build !go1.9
|
||||
|
||||
package catalog
|
||||
|
||||
import "golang.org/x/text/internal/catmsg"
|
||||
|
||||
// A Message holds a collection of translations for the same phrase that may
|
||||
// vary based on the values of substitution arguments.
|
||||
type Message interface {
|
||||
catmsg.Message
|
||||
}
|
||||
|
||||
func firstInSequence(m []Message) catmsg.Message {
|
||||
a := []catmsg.Message{}
|
||||
for _, m := range m {
|
||||
a = append(a, m)
|
||||
}
|
||||
return catmsg.FirstOf(a)
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
// 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 message
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message/catalog"
|
||||
)
|
||||
|
||||
func TestMatchLanguage(t *testing.T) {
|
||||
c := catalog.NewBuilder(catalog.Fallback(language.English))
|
||||
c.SetString(language.Bengali, "", "")
|
||||
c.SetString(language.English, "", "")
|
||||
c.SetString(language.German, "", "")
|
||||
|
||||
testCases := []struct {
|
||||
args string // '|'-separated list
|
||||
want string
|
||||
}{{
|
||||
args: "de-CH",
|
||||
want: "de",
|
||||
}, {
|
||||
args: "bn-u-nu-latn|en-US,en;q=0.9,de;q=0.8,nl;q=0.7",
|
||||
want: "bn-u-nu-latn",
|
||||
}, {
|
||||
args: "gr",
|
||||
want: "en",
|
||||
}}
|
||||
for _, tc := range testCases {
|
||||
DefaultCatalog = c
|
||||
t.Run(tc.args, func(t *testing.T) {
|
||||
got := MatchLanguage(strings.Split(tc.args, "|")...)
|
||||
if got != language.Make(tc.want) {
|
||||
t.Errorf("got %q; want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,314 @@
|
|||
// Copyright 2016 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 pipeline
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/constant"
|
||||
"go/format"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
fmtparser "golang.org/x/text/internal/format"
|
||||
"golang.org/x/tools/go/loader"
|
||||
)
|
||||
|
||||
// TODO:
|
||||
// - merge information into existing files
|
||||
// - handle different file formats (PO, XLIFF)
|
||||
// - handle features (gender, plural)
|
||||
// - message rewriting
|
||||
|
||||
// - %m substitutions
|
||||
// - `msg:"etc"` tags
|
||||
// - msg/Msg top-level vars and strings.
|
||||
|
||||
// Extract extracts all strings form the package defined in Config.
|
||||
func Extract(c *Config) (*State, error) {
|
||||
conf := loader.Config{}
|
||||
prog, err := loadPackages(&conf, c.Packages)
|
||||
if err != nil {
|
||||
return nil, wrap(err, "")
|
||||
}
|
||||
|
||||
// print returns Go syntax for the specified node.
|
||||
print := func(n ast.Node) string {
|
||||
var buf bytes.Buffer
|
||||
format.Node(&buf, conf.Fset, n)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
var messages []Message
|
||||
|
||||
for _, info := range prog.AllPackages {
|
||||
for _, f := range info.Files {
|
||||
// Associate comments with nodes.
|
||||
cmap := ast.NewCommentMap(prog.Fset, f, f.Comments)
|
||||
getComment := func(n ast.Node) string {
|
||||
cs := cmap.Filter(n).Comments()
|
||||
if len(cs) > 0 {
|
||||
return strings.TrimSpace(cs[0].Text())
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Find function calls.
|
||||
ast.Inspect(f, func(n ast.Node) bool {
|
||||
call, ok := n.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// Skip calls of functions other than
|
||||
// (*message.Printer).{Sp,Fp,P}rintf.
|
||||
sel, ok := call.Fun.(*ast.SelectorExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
meth := info.Selections[sel]
|
||||
if meth == nil || meth.Kind() != types.MethodVal {
|
||||
return true
|
||||
}
|
||||
// TODO: remove cheap hack and check if the type either
|
||||
// implements some interface or is specifically of type
|
||||
// "golang.org/x/text/message".Printer.
|
||||
m, ok := extractFuncs[path.Base(meth.Recv().String())]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
fmtType, ok := m[meth.Obj().Name()]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
// argn is the index of the format string.
|
||||
argn := fmtType.arg
|
||||
if argn >= len(call.Args) {
|
||||
return true
|
||||
}
|
||||
|
||||
args := call.Args[fmtType.arg:]
|
||||
|
||||
fmtMsg, ok := msgStr(info, args[0])
|
||||
if !ok {
|
||||
// TODO: identify the type of the format argument. If it
|
||||
// is not a string, multiple keys may be defined.
|
||||
return true
|
||||
}
|
||||
comment := ""
|
||||
key := []string{}
|
||||
if ident, ok := args[0].(*ast.Ident); ok {
|
||||
key = append(key, ident.Name)
|
||||
if v, ok := ident.Obj.Decl.(*ast.ValueSpec); ok && v.Comment != nil {
|
||||
// TODO: get comment above ValueSpec as well
|
||||
comment = v.Comment.Text()
|
||||
}
|
||||
}
|
||||
|
||||
arguments := []argument{}
|
||||
args = args[1:]
|
||||
simArgs := make([]interface{}, len(args))
|
||||
for i, arg := range args {
|
||||
expr := print(arg)
|
||||
val := ""
|
||||
if v := info.Types[arg].Value; v != nil {
|
||||
val = v.ExactString()
|
||||
simArgs[i] = val
|
||||
switch arg.(type) {
|
||||
case *ast.BinaryExpr, *ast.UnaryExpr:
|
||||
expr = val
|
||||
}
|
||||
}
|
||||
arguments = append(arguments, argument{
|
||||
ArgNum: i + 1,
|
||||
Type: info.Types[arg].Type.String(),
|
||||
UnderlyingType: info.Types[arg].Type.Underlying().String(),
|
||||
Expr: expr,
|
||||
Value: val,
|
||||
Comment: getComment(arg),
|
||||
Position: posString(conf, info, arg.Pos()),
|
||||
// TODO report whether it implements
|
||||
// interfaces plural.Interface,
|
||||
// gender.Interface.
|
||||
})
|
||||
}
|
||||
msg := ""
|
||||
|
||||
ph := placeholders{index: map[string]string{}}
|
||||
|
||||
trimmed, _, _ := trimWS(fmtMsg)
|
||||
|
||||
p := fmtparser.Parser{}
|
||||
p.Reset(simArgs)
|
||||
for p.SetFormat(trimmed); p.Scan(); {
|
||||
switch p.Status {
|
||||
case fmtparser.StatusText:
|
||||
msg += p.Text()
|
||||
case fmtparser.StatusSubstitution,
|
||||
fmtparser.StatusBadWidthSubstitution,
|
||||
fmtparser.StatusBadPrecSubstitution:
|
||||
arguments[p.ArgNum-1].used = true
|
||||
arg := arguments[p.ArgNum-1]
|
||||
sub := p.Text()
|
||||
if !p.HasIndex {
|
||||
r, sz := utf8.DecodeLastRuneInString(sub)
|
||||
sub = fmt.Sprintf("%s[%d]%c", sub[:len(sub)-sz], p.ArgNum, r)
|
||||
}
|
||||
msg += fmt.Sprintf("{%s}", ph.addArg(&arg, sub))
|
||||
}
|
||||
}
|
||||
key = append(key, msg)
|
||||
|
||||
// Add additional Placeholders that can be used in translations
|
||||
// that are not present in the string.
|
||||
for _, arg := range arguments {
|
||||
if arg.used {
|
||||
continue
|
||||
}
|
||||
ph.addArg(&arg, fmt.Sprintf("%%[%d]v", arg.ArgNum))
|
||||
}
|
||||
|
||||
if c := getComment(call.Args[0]); c != "" {
|
||||
comment = c
|
||||
}
|
||||
|
||||
messages = append(messages, Message{
|
||||
ID: key,
|
||||
Key: fmtMsg,
|
||||
Message: Text{Msg: msg},
|
||||
// TODO(fix): this doesn't get the before comment.
|
||||
Comment: comment,
|
||||
Placeholders: ph.slice,
|
||||
Position: posString(conf, info, call.Lparen),
|
||||
})
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &State{
|
||||
Config: *c,
|
||||
program: prog,
|
||||
Extracted: Messages{
|
||||
Language: c.SourceLanguage,
|
||||
Messages: messages,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func posString(conf loader.Config, info *loader.PackageInfo, pos token.Pos) string {
|
||||
p := conf.Fset.Position(pos)
|
||||
file := fmt.Sprintf("%s:%d:%d", filepath.Base(p.Filename), p.Line, p.Column)
|
||||
return filepath.Join(info.Pkg.Path(), file)
|
||||
}
|
||||
|
||||
// extractFuncs indicates the types and methods for which to extract strings,
|
||||
// and which argument to extract.
|
||||
// TODO: use the types in conf.Import("golang.org/x/text/message") to extract
|
||||
// the correct instances.
|
||||
var extractFuncs = map[string]map[string]extractType{
|
||||
// TODO: Printer -> *golang.org/x/text/message.Printer
|
||||
"message.Printer": {
|
||||
"Printf": extractType{arg: 0, format: true},
|
||||
"Sprintf": extractType{arg: 0, format: true},
|
||||
"Fprintf": extractType{arg: 1, format: true},
|
||||
|
||||
"Lookup": extractType{arg: 0},
|
||||
},
|
||||
}
|
||||
|
||||
type extractType struct {
|
||||
// format indicates if the next arg is a formatted string or whether to
|
||||
// concatenate all arguments
|
||||
format bool
|
||||
// arg indicates the position of the argument to extract.
|
||||
arg int
|
||||
}
|
||||
|
||||
func getID(arg *argument) string {
|
||||
s := getLastComponent(arg.Expr)
|
||||
s = strip(s)
|
||||
s = strings.Replace(s, " ", "", -1)
|
||||
// For small variable names, use user-defined types for more info.
|
||||
if len(s) <= 2 && arg.UnderlyingType != arg.Type {
|
||||
s = getLastComponent(arg.Type)
|
||||
}
|
||||
return strings.Title(s)
|
||||
}
|
||||
|
||||
// strip is a dirty hack to convert function calls to placeholder IDs.
|
||||
func strip(s string) string {
|
||||
s = strings.Map(func(r rune) rune {
|
||||
if unicode.IsSpace(r) || r == '-' {
|
||||
return '_'
|
||||
}
|
||||
if !unicode.In(r, unicode.Letter, unicode.Mark, unicode.Number) {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, s)
|
||||
// Strip "Get" from getter functions.
|
||||
if strings.HasPrefix(s, "Get") || strings.HasPrefix(s, "get") {
|
||||
if len(s) > len("get") {
|
||||
r, _ := utf8.DecodeRuneInString(s)
|
||||
if !unicode.In(r, unicode.Ll, unicode.M) { // not lower or mark
|
||||
s = s[len("get"):]
|
||||
}
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type placeholders struct {
|
||||
index map[string]string
|
||||
slice []Placeholder
|
||||
}
|
||||
|
||||
func (p *placeholders) addArg(arg *argument, sub string) (id string) {
|
||||
id = getID(arg)
|
||||
id1 := id
|
||||
alt, ok := p.index[id1]
|
||||
for i := 1; ok && alt != sub; i++ {
|
||||
id1 = fmt.Sprintf("%s_%d", id, i)
|
||||
alt, ok = p.index[id1]
|
||||
}
|
||||
p.index[id1] = sub
|
||||
p.slice = append(p.slice, Placeholder{
|
||||
ID: id1,
|
||||
String: sub,
|
||||
Type: arg.Type,
|
||||
UnderlyingType: arg.UnderlyingType,
|
||||
ArgNum: arg.ArgNum,
|
||||
Expr: arg.Expr,
|
||||
Comment: arg.Comment,
|
||||
})
|
||||
return id1
|
||||
}
|
||||
|
||||
func getLastComponent(s string) string {
|
||||
return s[1+strings.LastIndexByte(s, '.'):]
|
||||
}
|
||||
|
||||
func msgStr(info *loader.PackageInfo, e ast.Expr) (s string, ok bool) {
|
||||
v := info.Types[e].Value
|
||||
if v == nil || v.Kind() != constant.String {
|
||||
return "", false
|
||||
}
|
||||
s = constant.StringVal(v)
|
||||
// Only record strings with letters.
|
||||
for _, r := range s {
|
||||
if unicode.In(r, unicode.L) {
|
||||
return s, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
|
@ -0,0 +1,314 @@
|
|||
// 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 pipeline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/build"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"golang.org/x/text/collate"
|
||||
"golang.org/x/text/feature/plural"
|
||||
"golang.org/x/text/internal"
|
||||
"golang.org/x/text/internal/catmsg"
|
||||
"golang.org/x/text/internal/gen"
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/tools/go/loader"
|
||||
)
|
||||
|
||||
var transRe = regexp.MustCompile(`messages\.(.*)\.json`)
|
||||
|
||||
// Generate writes a Go file that defines a Catalog with translated messages.
|
||||
// Translations are retrieved from s.Messages, not s.Translations, so it
|
||||
// is assumed Merge has been called.
|
||||
func (s *State) Generate() error {
|
||||
path := s.Config.GenPackage
|
||||
if path == "" {
|
||||
path = "."
|
||||
}
|
||||
isDir := path[0] == '.'
|
||||
prog, err := loadPackages(&loader.Config{}, []string{path})
|
||||
if err != nil {
|
||||
return wrap(err, "could not load package")
|
||||
}
|
||||
pkgs := prog.InitialPackages()
|
||||
if len(pkgs) != 1 {
|
||||
return errorf("more than one package selected: %v", pkgs)
|
||||
}
|
||||
pkg := pkgs[0].Pkg.Name()
|
||||
|
||||
cw, err := s.generate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isDir {
|
||||
gopath := build.Default.GOPATH
|
||||
path = filepath.Join(gopath, filepath.FromSlash(pkgs[0].Pkg.Path()))
|
||||
}
|
||||
path = filepath.Join(path, s.Config.GenFile)
|
||||
cw.WriteGoFile(path, pkg) // TODO: WriteGoFile should return error.
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteGen writes a Go file with the given package name to w that defines a
|
||||
// Catalog with translated messages. Translations are retrieved from s.Messages,
|
||||
// not s.Translations, so it is assumed Merge has been called.
|
||||
func (s *State) WriteGen(w io.Writer, pkg string) error {
|
||||
cw, err := s.generate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = cw.WriteGo(w, pkg, "")
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate is deprecated; use (*State).Generate().
|
||||
func Generate(w io.Writer, pkg string, extracted *Messages, trans ...Messages) (n int, err error) {
|
||||
s := State{
|
||||
Extracted: *extracted,
|
||||
Translations: trans,
|
||||
}
|
||||
cw, err := s.generate()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return cw.WriteGo(w, pkg, "")
|
||||
}
|
||||
|
||||
func (s *State) generate() (*gen.CodeWriter, error) {
|
||||
// Build up index of translations and original messages.
|
||||
translations := map[language.Tag]map[string]Message{}
|
||||
languages := []language.Tag{}
|
||||
usedKeys := map[string]int{}
|
||||
|
||||
for _, loc := range s.Messages {
|
||||
tag := loc.Language
|
||||
if _, ok := translations[tag]; !ok {
|
||||
translations[tag] = map[string]Message{}
|
||||
languages = append(languages, tag)
|
||||
}
|
||||
for _, m := range loc.Messages {
|
||||
if !m.Translation.IsEmpty() {
|
||||
for _, id := range m.ID {
|
||||
if _, ok := translations[tag][id]; ok {
|
||||
warnf("Duplicate translation in locale %q for message %q", tag, id)
|
||||
}
|
||||
translations[tag][id] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify completeness and register keys.
|
||||
internal.SortTags(languages)
|
||||
|
||||
langVars := []string{}
|
||||
for _, tag := range languages {
|
||||
langVars = append(langVars, strings.Replace(tag.String(), "-", "_", -1))
|
||||
dict := translations[tag]
|
||||
for _, msg := range s.Extracted.Messages {
|
||||
for _, id := range msg.ID {
|
||||
if trans, ok := dict[id]; ok && !trans.Translation.IsEmpty() {
|
||||
if _, ok := usedKeys[msg.Key]; !ok {
|
||||
usedKeys[msg.Key] = len(usedKeys)
|
||||
}
|
||||
break
|
||||
}
|
||||
// TODO: log missing entry.
|
||||
warnf("%s: Missing entry for %q.", tag, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cw := gen.NewCodeWriter()
|
||||
|
||||
x := &struct {
|
||||
Fallback language.Tag
|
||||
Languages []string
|
||||
}{
|
||||
Fallback: s.Extracted.Language,
|
||||
Languages: langVars,
|
||||
}
|
||||
|
||||
if err := lookup.Execute(cw, x); err != nil {
|
||||
return nil, wrap(err, "error")
|
||||
}
|
||||
|
||||
keyToIndex := []string{}
|
||||
for k := range usedKeys {
|
||||
keyToIndex = append(keyToIndex, k)
|
||||
}
|
||||
sort.Strings(keyToIndex)
|
||||
fmt.Fprint(cw, "var messageKeyToIndex = map[string]int{\n")
|
||||
for _, k := range keyToIndex {
|
||||
fmt.Fprintf(cw, "%q: %d,\n", k, usedKeys[k])
|
||||
}
|
||||
fmt.Fprint(cw, "}\n\n")
|
||||
|
||||
for i, tag := range languages {
|
||||
dict := translations[tag]
|
||||
a := make([]string, len(usedKeys))
|
||||
for _, msg := range s.Extracted.Messages {
|
||||
for _, id := range msg.ID {
|
||||
if trans, ok := dict[id]; ok && !trans.Translation.IsEmpty() {
|
||||
m, err := assemble(&msg, &trans.Translation)
|
||||
if err != nil {
|
||||
return nil, wrap(err, "error")
|
||||
}
|
||||
_, leadWS, trailWS := trimWS(msg.Key)
|
||||
if leadWS != "" || trailWS != "" {
|
||||
m = catmsg.Affix{
|
||||
Message: m,
|
||||
Prefix: leadWS,
|
||||
Suffix: trailWS,
|
||||
}
|
||||
}
|
||||
// TODO: support macros.
|
||||
data, err := catmsg.Compile(tag, nil, m)
|
||||
if err != nil {
|
||||
return nil, wrap(err, "error")
|
||||
}
|
||||
key := usedKeys[msg.Key]
|
||||
if d := a[key]; d != "" && d != data {
|
||||
warnf("Duplicate non-consistent translation for key %q, picking the one for message %q", msg.Key, id)
|
||||
}
|
||||
a[key] = string(data)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
index := []uint32{0}
|
||||
p := 0
|
||||
for _, s := range a {
|
||||
p += len(s)
|
||||
index = append(index, uint32(p))
|
||||
}
|
||||
|
||||
cw.WriteVar(langVars[i]+"Index", index)
|
||||
cw.WriteConst(langVars[i]+"Data", strings.Join(a, ""))
|
||||
}
|
||||
return cw, nil
|
||||
}
|
||||
|
||||
func assemble(m *Message, t *Text) (msg catmsg.Message, err error) {
|
||||
keys := []string{}
|
||||
for k := range t.Var {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var a []catmsg.Message
|
||||
for _, k := range keys {
|
||||
t := t.Var[k]
|
||||
m, err := assemble(m, &t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a = append(a, &catmsg.Var{Name: k, Message: m})
|
||||
}
|
||||
if t.Select != nil {
|
||||
s, err := assembleSelect(m, t.Select)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a = append(a, s)
|
||||
}
|
||||
if t.Msg != "" {
|
||||
sub, err := m.Substitute(t.Msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a = append(a, catmsg.String(sub))
|
||||
}
|
||||
switch len(a) {
|
||||
case 0:
|
||||
return nil, errorf("generate: empty message")
|
||||
case 1:
|
||||
return a[0], nil
|
||||
default:
|
||||
return catmsg.FirstOf(a), nil
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func assembleSelect(m *Message, s *Select) (msg catmsg.Message, err error) {
|
||||
cases := []string{}
|
||||
for c := range s.Cases {
|
||||
cases = append(cases, c)
|
||||
}
|
||||
sortCases(cases)
|
||||
|
||||
caseMsg := []interface{}{}
|
||||
for _, c := range cases {
|
||||
cm := s.Cases[c]
|
||||
m, err := assemble(m, &cm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
caseMsg = append(caseMsg, c, m)
|
||||
}
|
||||
|
||||
ph := m.Placeholder(s.Arg)
|
||||
|
||||
switch s.Feature {
|
||||
case "plural":
|
||||
// TODO: only printf-style selects are supported as of yet.
|
||||
return plural.Selectf(ph.ArgNum, ph.String, caseMsg...), nil
|
||||
}
|
||||
return nil, errorf("unknown feature type %q", s.Feature)
|
||||
}
|
||||
|
||||
func sortCases(cases []string) {
|
||||
// TODO: implement full interface.
|
||||
sort.Slice(cases, func(i, j int) bool {
|
||||
if cases[j] == "other" && cases[i] != "other" {
|
||||
return true
|
||||
}
|
||||
// the following code relies on '<' < '=' < any letter.
|
||||
return cmpNumeric(cases[i], cases[j]) == -1
|
||||
})
|
||||
}
|
||||
|
||||
var cmpNumeric = collate.New(language.Und, collate.Numeric).CompareString
|
||||
|
||||
var lookup = template.Must(template.New("gen").Parse(`
|
||||
import (
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
"golang.org/x/text/message/catalog"
|
||||
)
|
||||
|
||||
type dictionary struct {
|
||||
index []uint32
|
||||
data string
|
||||
}
|
||||
|
||||
func (d *dictionary) Lookup(key string) (data string, ok bool) {
|
||||
p := messageKeyToIndex[key]
|
||||
start, end := d.index[p], d.index[p+1]
|
||||
if start == end {
|
||||
return "", false
|
||||
}
|
||||
return d.data[start:end], true
|
||||
}
|
||||
|
||||
func init() {
|
||||
dict := map[string]catalog.Dictionary{
|
||||
{{range .Languages}}"{{.}}": &dictionary{index: {{.}}Index, data: {{.}}Data },
|
||||
{{end}}
|
||||
}
|
||||
fallback := language.MustParse("{{.Fallback}}")
|
||||
cat, err := catalog.NewFromMap(dict, catalog.Fallback(fallback))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
message.DefaultCatalog = cat
|
||||
}
|
||||
|
||||
`))
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
// 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.
|
||||
|
||||
// +build go1.9
|
||||
|
||||
package pipeline
|
||||
|
||||
import "testing"
|
||||
|
||||
func init() {
|
||||
setHelper = (*testing.T).Helper
|
||||
}
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
// 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 pipeline
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// TODO: these definitions should be moved to a package so that the can be used
|
||||
// by other tools.
|
||||
|
||||
// The file contains the structures used to define translations of a certain
|
||||
// messages.
|
||||
//
|
||||
// A translation may have multiple translations strings, or messages, depending
|
||||
// on the feature values of the various arguments. For instance, consider
|
||||
// a hypothetical translation from English to English, where the source defines
|
||||
// the format string "%d file(s) remaining".
|
||||
// See the examples directory for examples of extracted messages.
|
||||
|
||||
// Messages is used to store translations for a single language.
|
||||
type Messages struct {
|
||||
Language language.Tag `json:"language"`
|
||||
Messages []Message `json:"messages"`
|
||||
Macros map[string]Text `json:"macros,omitempty"`
|
||||
}
|
||||
|
||||
// A Message describes a message to be translated.
|
||||
type Message struct {
|
||||
// ID contains a list of identifiers for the message.
|
||||
ID IDList `json:"id"`
|
||||
// Key is the string that is used to look up the message at runtime.
|
||||
Key string `json:"key,omitempty"`
|
||||
Meaning string `json:"meaning,omitempty"`
|
||||
Message Text `json:"message"`
|
||||
Translation Text `json:"translation"`
|
||||
|
||||
Comment string `json:"comment,omitempty"`
|
||||
TranslatorComment string `json:"translatorComment,omitempty"`
|
||||
|
||||
Placeholders []Placeholder `json:"placeholders,omitempty"`
|
||||
|
||||
// Fuzzy indicates that the provide translation needs review by a
|
||||
// translator, for instance because it was derived from automated
|
||||
// translation.
|
||||
Fuzzy bool `json:"fuzzy,omitempty"`
|
||||
|
||||
// TODO: default placeholder syntax is {foo}. Allow alternative escaping
|
||||
// like `foo`.
|
||||
|
||||
// Extraction information.
|
||||
Position string `json:"position,omitempty"` // filePosition:line
|
||||
}
|
||||
|
||||
// Placeholder reports the placeholder for the given ID if it is defined or nil
|
||||
// otherwise.
|
||||
func (m *Message) Placeholder(id string) *Placeholder {
|
||||
for _, p := range m.Placeholders {
|
||||
if p.ID == id {
|
||||
return &p
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Substitute replaces placeholders in msg with their original value.
|
||||
func (m *Message) Substitute(msg string) (sub string, err error) {
|
||||
last := 0
|
||||
for i := 0; i < len(msg); {
|
||||
pLeft := strings.IndexByte(msg[i:], '{')
|
||||
if pLeft == -1 {
|
||||
break
|
||||
}
|
||||
pLeft += i
|
||||
pRight := strings.IndexByte(msg[pLeft:], '}')
|
||||
if pRight == -1 {
|
||||
return "", errorf("unmatched '}'")
|
||||
}
|
||||
pRight += pLeft
|
||||
id := strings.TrimSpace(msg[pLeft+1 : pRight])
|
||||
i = pRight + 1
|
||||
if id != "" && id[0] == '$' {
|
||||
continue
|
||||
}
|
||||
sub += msg[last:pLeft]
|
||||
last = i
|
||||
ph := m.Placeholder(id)
|
||||
if ph == nil {
|
||||
return "", errorf("unknown placeholder %q in message %q", id, msg)
|
||||
}
|
||||
sub += ph.String
|
||||
}
|
||||
sub += msg[last:]
|
||||
return sub, err
|
||||
}
|
||||
|
||||
var errIncompatibleMessage = errors.New("messages incompatible")
|
||||
|
||||
func checkEquivalence(a, b *Message) error {
|
||||
for _, v := range a.ID {
|
||||
for _, w := range b.ID {
|
||||
if v == w {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: canonicalize placeholders and check for type equivalence.
|
||||
return errIncompatibleMessage
|
||||
}
|
||||
|
||||
// A Placeholder is a part of the message that should not be changed by a
|
||||
// translator. It can be used to hide or prettify format strings (e.g. %d or
|
||||
// {{.Count}}), hide HTML, or mark common names that should not be translated.
|
||||
type Placeholder struct {
|
||||
// ID is the placeholder identifier without the curly braces.
|
||||
ID string `json:"id"`
|
||||
|
||||
// String is the string with which to replace the placeholder. This may be a
|
||||
// formatting string (for instance "%d" or "{{.Count}}") or a literal string
|
||||
// (<div>).
|
||||
String string `json:"string"`
|
||||
|
||||
Type string `json:"type"`
|
||||
UnderlyingType string `json:"underlyingType"`
|
||||
// ArgNum and Expr are set if the placeholder is a substitution of an
|
||||
// argument.
|
||||
ArgNum int `json:"argNum,omitempty"`
|
||||
Expr string `json:"expr,omitempty"`
|
||||
|
||||
Comment string `json:"comment,omitempty"`
|
||||
Example string `json:"example,omitempty"`
|
||||
|
||||
// Features contains the features that are available for the implementation
|
||||
// of this argument.
|
||||
Features []Feature `json:"features,omitempty"`
|
||||
}
|
||||
|
||||
// An argument contains information about the arguments passed to a message.
|
||||
type argument struct {
|
||||
// ArgNum corresponds to the number that should be used for explicit argument indexes (e.g.
|
||||
// "%[1]d").
|
||||
ArgNum int `json:"argNum,omitempty"`
|
||||
|
||||
used bool // Used by Placeholder
|
||||
Type string `json:"type"`
|
||||
UnderlyingType string `json:"underlyingType"`
|
||||
Expr string `json:"expr"`
|
||||
Value string `json:"value,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
Position string `json:"position,omitempty"`
|
||||
}
|
||||
|
||||
// Feature holds information about a feature that can be implemented by
|
||||
// an Argument.
|
||||
type Feature struct {
|
||||
Type string `json:"type"` // Right now this is only gender and plural.
|
||||
|
||||
// TODO: possible values and examples for the language under consideration.
|
||||
|
||||
}
|
||||
|
||||
// Text defines a message to be displayed.
|
||||
type Text struct {
|
||||
// Msg and Select contains the message to be displayed. Msg may be used as
|
||||
// a fallback value if none of the select cases match.
|
||||
Msg string `json:"msg,omitempty"`
|
||||
Select *Select `json:"select,omitempty"`
|
||||
|
||||
// Var defines a map of variables that may be substituted in the selected
|
||||
// message.
|
||||
Var map[string]Text `json:"var,omitempty"`
|
||||
|
||||
// Example contains an example message formatted with default values.
|
||||
Example string `json:"example,omitempty"`
|
||||
}
|
||||
|
||||
// IsEmpty reports whether this Text can generate anything.
|
||||
func (t *Text) IsEmpty() bool {
|
||||
return t.Msg == "" && t.Select == nil && t.Var == nil
|
||||
}
|
||||
|
||||
// rawText erases the UnmarshalJSON method.
|
||||
type rawText Text
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (t *Text) UnmarshalJSON(b []byte) error {
|
||||
if b[0] == '"' {
|
||||
return json.Unmarshal(b, &t.Msg)
|
||||
}
|
||||
return json.Unmarshal(b, (*rawText)(t))
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (t *Text) MarshalJSON() ([]byte, error) {
|
||||
if t.Select == nil && t.Var == nil && t.Example == "" {
|
||||
return json.Marshal(t.Msg)
|
||||
}
|
||||
return json.Marshal((*rawText)(t))
|
||||
}
|
||||
|
||||
// IDList is a set identifiers that each may refer to possibly different
|
||||
// versions of the same message. When looking up a messages, the first
|
||||
// identifier in the list takes precedence.
|
||||
type IDList []string
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (id *IDList) UnmarshalJSON(b []byte) error {
|
||||
if b[0] == '"' {
|
||||
*id = []string{""}
|
||||
return json.Unmarshal(b, &((*id)[0]))
|
||||
}
|
||||
return json.Unmarshal(b, (*[]string)(id))
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (id *IDList) MarshalJSON() ([]byte, error) {
|
||||
if len(*id) == 1 {
|
||||
return json.Marshal((*id)[0])
|
||||
}
|
||||
return json.Marshal((*[]string)(id))
|
||||
}
|
||||
|
||||
// Select selects a Text based on the feature value associated with a feature of
|
||||
// a certain argument.
|
||||
type Select struct {
|
||||
Feature string `json:"feature"` // Name of Feature type (e.g plural)
|
||||
Arg string `json:"arg"` // The placeholder ID
|
||||
Cases map[string]Text `json:"cases"`
|
||||
}
|
||||
|
||||
// TODO: order matters, but can we derive the ordering from the case keys?
|
||||
// type Case struct {
|
||||
// Key string `json:"key"`
|
||||
// Value Text `json:"value"`
|
||||
// }
|
||||
|
|
@ -0,0 +1,422 @@
|
|||
// 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 pipeline provides tools for creating translation pipelines.
|
||||
//
|
||||
// NOTE: UNDER DEVELOPMENT. API MAY CHANGE.
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"go/parser"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/text/internal"
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/runes"
|
||||
"golang.org/x/tools/go/loader"
|
||||
)
|
||||
|
||||
const (
|
||||
extractFile = "extracted.gotext.json"
|
||||
outFile = "out.gotext.json"
|
||||
gotextSuffix = "gotext.json"
|
||||
)
|
||||
|
||||
// Config contains configuration for the translation pipeline.
|
||||
type Config struct {
|
||||
// Supported indicates the languages for which data should be generated.
|
||||
// The default is to support all locales for which there are matching
|
||||
// translation files.
|
||||
Supported []language.Tag
|
||||
|
||||
// --- Extraction
|
||||
|
||||
SourceLanguage language.Tag
|
||||
|
||||
Packages []string
|
||||
|
||||
// --- File structure
|
||||
|
||||
// Dir is the root dir for all operations.
|
||||
Dir string
|
||||
|
||||
// TranslationsPattern is a regular expression to match incoming translation
|
||||
// files. These files may appear in any directory rooted at Dir.
|
||||
// language for the translation files is determined as follows:
|
||||
// 1. From the Language field in the file.
|
||||
// 2. If not present, from a valid language tag in the filename, separated
|
||||
// by dots (e.g. "en-US.json" or "incoming.pt_PT.xmb").
|
||||
// 3. If not present, from a the closest subdirectory in which the file
|
||||
// is contained that parses as a valid language tag.
|
||||
TranslationsPattern string
|
||||
|
||||
// OutPattern defines the location for translation files for a certain
|
||||
// language. The default is "{{.Dir}}/{{.Language}}/out.{{.Ext}}"
|
||||
OutPattern string
|
||||
|
||||
// Format defines the file format for generated translation files.
|
||||
// The default is XMB. Alternatives are GetText, XLIFF, L20n, GoText.
|
||||
Format string
|
||||
|
||||
Ext string
|
||||
|
||||
// TODO:
|
||||
// Actions are additional actions to be performed after the initial extract
|
||||
// and merge.
|
||||
// Actions []struct {
|
||||
// Name string
|
||||
// Options map[string]string
|
||||
// }
|
||||
|
||||
// --- Generation
|
||||
|
||||
// GenFile may be in a different package. It is not defined, it will
|
||||
// be written to stdout.
|
||||
GenFile string
|
||||
|
||||
// GenPackage is the package or relative path into which to generate the
|
||||
// file. If not specified it is relative to the current directory.
|
||||
GenPackage string
|
||||
|
||||
// DeclareVar defines a variable to which to assing the generated Catalog.
|
||||
DeclareVar string
|
||||
|
||||
// SetDefault determines whether to assign the generated Catalog to
|
||||
// message.DefaultCatalog. The default for this is true if DeclareVar is
|
||||
// not defined, false otherwise.
|
||||
SetDefault bool
|
||||
|
||||
// TODO:
|
||||
// - Printf-style configuration
|
||||
// - Template-style configuration
|
||||
// - Extraction options
|
||||
// - Rewrite options
|
||||
// - Generation options
|
||||
}
|
||||
|
||||
// Operations:
|
||||
// - extract: get the strings
|
||||
// - disambiguate: find messages with the same key, but possible different meaning.
|
||||
// - create out: create a list of messages that need translations
|
||||
// - load trans: load the list of current translations
|
||||
// - merge: assign list of translations as done
|
||||
// - (action)expand: analyze features and create example sentences for each version.
|
||||
// - (action)googletrans: pre-populate messages with automatic translations.
|
||||
// - (action)export: send out messages somewhere non-standard
|
||||
// - (action)import: load messages from somewhere non-standard
|
||||
// - vet program: don't pass "foo" + var + "bar" strings. Not using funcs for translated strings.
|
||||
// - vet trans: coverage: all translations/ all features.
|
||||
// - generate: generate Go code
|
||||
|
||||
// State holds all accumulated information on translations during processing.
|
||||
type State struct {
|
||||
Config Config
|
||||
|
||||
Package string
|
||||
program *loader.Program
|
||||
|
||||
Extracted Messages `json:"messages"`
|
||||
|
||||
// Messages includes all messages for which there need to be translations.
|
||||
// Duplicates may be eliminated. Generation will be done from these messages
|
||||
// (usually after merging).
|
||||
Messages []Messages
|
||||
|
||||
// Translations are incoming translations for the application messages.
|
||||
Translations []Messages
|
||||
}
|
||||
|
||||
func (s *State) dir() string {
|
||||
if d := s.Config.Dir; d != "" {
|
||||
return d
|
||||
}
|
||||
return "./locales"
|
||||
}
|
||||
|
||||
func outPattern(s *State) (string, error) {
|
||||
c := s.Config
|
||||
pat := c.OutPattern
|
||||
if pat == "" {
|
||||
pat = "{{.Dir}}/{{.Language}}/out.{{.Ext}}"
|
||||
}
|
||||
|
||||
ext := c.Ext
|
||||
if ext == "" {
|
||||
ext = c.Format
|
||||
}
|
||||
if ext == "" {
|
||||
ext = gotextSuffix
|
||||
}
|
||||
t, err := template.New("").Parse(pat)
|
||||
if err != nil {
|
||||
return "", wrap(err, "error parsing template")
|
||||
}
|
||||
buf := bytes.Buffer{}
|
||||
err = t.Execute(&buf, map[string]string{
|
||||
"Dir": s.dir(),
|
||||
"Language": "%s",
|
||||
"Ext": ext,
|
||||
})
|
||||
return filepath.FromSlash(buf.String()), wrap(err, "incorrect OutPattern")
|
||||
}
|
||||
|
||||
var transRE = regexp.MustCompile(`.*\.` + gotextSuffix)
|
||||
|
||||
// Import loads existing translation files.
|
||||
func (s *State) Import() error {
|
||||
outPattern, err := outPattern(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
re := transRE
|
||||
if pat := s.Config.TranslationsPattern; pat != "" {
|
||||
if re, err = regexp.Compile(pat); err != nil {
|
||||
return wrapf(err, "error parsing regexp %q", s.Config.TranslationsPattern)
|
||||
}
|
||||
}
|
||||
x := importer{s, outPattern, re}
|
||||
return x.walkImport(s.dir(), s.Config.SourceLanguage)
|
||||
}
|
||||
|
||||
type importer struct {
|
||||
state *State
|
||||
outPattern string
|
||||
transFile *regexp.Regexp
|
||||
}
|
||||
|
||||
func (i *importer) walkImport(path string, tag language.Tag) error {
|
||||
files, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
for _, f := range files {
|
||||
name := f.Name()
|
||||
tag := tag
|
||||
if f.IsDir() {
|
||||
if t, err := language.Parse(name); err == nil {
|
||||
tag = t
|
||||
}
|
||||
// We ignore errors
|
||||
if err := i.walkImport(filepath.Join(path, name), tag); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
for _, l := range strings.Split(name, ".") {
|
||||
if t, err := language.Parse(l); err == nil {
|
||||
tag = t
|
||||
}
|
||||
}
|
||||
file := filepath.Join(path, name)
|
||||
// TODO: Should we skip files that match output files?
|
||||
if fmt.Sprintf(i.outPattern, tag) == file {
|
||||
continue
|
||||
}
|
||||
// TODO: handle different file formats.
|
||||
if !i.transFile.MatchString(name) {
|
||||
continue
|
||||
}
|
||||
b, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return wrap(err, "read file failed")
|
||||
}
|
||||
var translations Messages
|
||||
if err := json.Unmarshal(b, &translations); err != nil {
|
||||
return wrap(err, "parsing translation file failed")
|
||||
}
|
||||
i.state.Translations = append(i.state.Translations, translations)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Merge merges the extracted messages with the existing translations.
|
||||
func (s *State) Merge() error {
|
||||
if s.Messages != nil {
|
||||
panic("already merged")
|
||||
}
|
||||
// Create an index for each unique message.
|
||||
// Duplicates are okay as long as the substitution arguments are okay as
|
||||
// well.
|
||||
// Top-level messages are okay to appear in multiple substitution points.
|
||||
|
||||
// Collect key equivalence.
|
||||
msgs := []*Message{}
|
||||
keyToIDs := map[string]*Message{}
|
||||
for _, m := range s.Extracted.Messages {
|
||||
m := m
|
||||
if prev, ok := keyToIDs[m.Key]; ok {
|
||||
if err := checkEquivalence(&m, prev); err != nil {
|
||||
warnf("Key %q matches conflicting messages: %v and %v", m.Key, prev.ID, m.ID)
|
||||
// TODO: track enough information so that the rewriter can
|
||||
// suggest/disambiguate messages.
|
||||
}
|
||||
// TODO: add position to message.
|
||||
continue
|
||||
}
|
||||
i := len(msgs)
|
||||
msgs = append(msgs, &m)
|
||||
keyToIDs[m.Key] = msgs[i]
|
||||
}
|
||||
|
||||
// Messages with different keys may still refer to the same translated
|
||||
// message (e.g. different whitespace). Filter these.
|
||||
idMap := map[string]bool{}
|
||||
filtered := []*Message{}
|
||||
for _, m := range msgs {
|
||||
found := false
|
||||
for _, id := range m.ID {
|
||||
found = found || idMap[id]
|
||||
}
|
||||
if !found {
|
||||
filtered = append(filtered, m)
|
||||
}
|
||||
for _, id := range m.ID {
|
||||
idMap[id] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Build index of translations.
|
||||
translations := map[language.Tag]map[string]Message{}
|
||||
languages := append([]language.Tag{}, s.Config.Supported...)
|
||||
|
||||
for _, t := range s.Translations {
|
||||
tag := t.Language
|
||||
if _, ok := translations[tag]; !ok {
|
||||
translations[tag] = map[string]Message{}
|
||||
languages = append(languages, tag)
|
||||
}
|
||||
for _, m := range t.Messages {
|
||||
if !m.Translation.IsEmpty() {
|
||||
for _, id := range m.ID {
|
||||
if _, ok := translations[tag][id]; ok {
|
||||
warnf("Duplicate translation in locale %q for message %q", tag, id)
|
||||
}
|
||||
translations[tag][id] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
languages = internal.UniqueTags(languages)
|
||||
|
||||
for _, tag := range languages {
|
||||
ms := Messages{Language: tag}
|
||||
for _, orig := range filtered {
|
||||
m := *orig
|
||||
m.Key = ""
|
||||
m.Position = ""
|
||||
|
||||
for _, id := range m.ID {
|
||||
if t, ok := translations[tag][id]; ok {
|
||||
m.Translation = t.Translation
|
||||
if t.TranslatorComment != "" {
|
||||
m.TranslatorComment = t.TranslatorComment
|
||||
m.Fuzzy = t.Fuzzy
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if tag == s.Config.SourceLanguage && m.Translation.IsEmpty() {
|
||||
m.Translation = m.Message
|
||||
if m.TranslatorComment == "" {
|
||||
m.TranslatorComment = "Copied from source."
|
||||
m.Fuzzy = true
|
||||
}
|
||||
}
|
||||
// TODO: if translation is empty: pre-expand based on available
|
||||
// linguistic features. This may also be done as a plugin.
|
||||
ms.Messages = append(ms.Messages, m)
|
||||
}
|
||||
s.Messages = append(s.Messages, ms)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Export writes out the messages to translation out files.
|
||||
func (s *State) Export() error {
|
||||
path, err := outPattern(s)
|
||||
if err != nil {
|
||||
return wrap(err, "export failed")
|
||||
}
|
||||
for _, out := range s.Messages {
|
||||
// TODO: inject translations from existing files to avoid retranslation.
|
||||
data, err := json.MarshalIndent(out, "", " ")
|
||||
if err != nil {
|
||||
return wrap(err, "JSON marshal failed")
|
||||
}
|
||||
file := fmt.Sprintf(path, out.Language)
|
||||
if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil {
|
||||
return wrap(err, "dir create failed")
|
||||
}
|
||||
if err := ioutil.WriteFile(file, data, 0644); err != nil {
|
||||
return wrap(err, "write failed")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
ws = runes.In(unicode.White_Space).Contains
|
||||
notWS = runes.NotIn(unicode.White_Space).Contains
|
||||
)
|
||||
|
||||
func trimWS(s string) (trimmed, leadWS, trailWS string) {
|
||||
trimmed = strings.TrimRightFunc(s, ws)
|
||||
trailWS = s[len(trimmed):]
|
||||
if i := strings.IndexFunc(trimmed, notWS); i > 0 {
|
||||
leadWS = trimmed[:i]
|
||||
trimmed = trimmed[i:]
|
||||
}
|
||||
return trimmed, leadWS, trailWS
|
||||
}
|
||||
|
||||
// NOTE: The command line tool already prefixes with "gotext:".
|
||||
var (
|
||||
wrap = func(err error, msg string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s: %v", msg, err)
|
||||
}
|
||||
wrapf = func(err error, msg string, args ...interface{}) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return wrap(err, fmt.Sprintf(msg, args...))
|
||||
}
|
||||
errorf = fmt.Errorf
|
||||
)
|
||||
|
||||
func warnf(format string, args ...interface{}) {
|
||||
// TODO: don't log.
|
||||
log.Printf(format, args...)
|
||||
}
|
||||
|
||||
func loadPackages(conf *loader.Config, args []string) (*loader.Program, error) {
|
||||
if len(args) == 0 {
|
||||
args = []string{"."}
|
||||
}
|
||||
|
||||
conf.Build = &build.Default
|
||||
conf.ParserMode = parser.ParseComments
|
||||
|
||||
// Use the initial packages from the command line.
|
||||
args, err := conf.FromArgs(args, false)
|
||||
if err != nil {
|
||||
return nil, wrap(err, "loading packages failed")
|
||||
}
|
||||
|
||||
// Load, parse and type-check the whole program.
|
||||
return conf.Load()
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
// 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 pipeline
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
var genFiles = flag.Bool("gen", false, "generate output files instead of comparing")
|
||||
|
||||
// setHelper is testing.T.Helper on Go 1.9+, overridden by go19_test.go.
|
||||
var setHelper = func(t *testing.T) {}
|
||||
|
||||
func TestFullCycle(t *testing.T) {
|
||||
const path = "./testdata"
|
||||
dirs, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, f := range dirs {
|
||||
t.Run(f.Name(), func(t *testing.T) {
|
||||
chk := func(t *testing.T, err error) {
|
||||
setHelper(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
dir := filepath.Join(path, f.Name())
|
||||
pkgPath := fmt.Sprintf("%s/%s", path, f.Name())
|
||||
config := Config{
|
||||
SourceLanguage: language.AmericanEnglish,
|
||||
Packages: []string{pkgPath},
|
||||
Dir: filepath.Join(dir, "locales"),
|
||||
GenFile: "catalog_gen.go",
|
||||
GenPackage: pkgPath,
|
||||
}
|
||||
// TODO: load config if available.
|
||||
s, err := Extract(&config)
|
||||
chk(t, err)
|
||||
chk(t, s.Import())
|
||||
chk(t, s.Merge())
|
||||
// TODO:
|
||||
// for range s.Config.Actions {
|
||||
// // TODO: do the actions.
|
||||
// }
|
||||
chk(t, s.Export())
|
||||
chk(t, s.Generate())
|
||||
|
||||
writeJSON(t, filepath.Join(dir, "extracted.gotext.json"), s.Extracted)
|
||||
checkOutput(t, dir)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func checkOutput(t *testing.T, p string) {
|
||||
filepath.Walk(p, func(p string, f os.FileInfo, err error) error {
|
||||
if f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if filepath.Ext(p) != ".want" {
|
||||
return nil
|
||||
}
|
||||
gotFile := p[:len(p)-len(".want")]
|
||||
got, err := ioutil.ReadFile(gotFile)
|
||||
if err != nil {
|
||||
t.Errorf("failed to read %q", p)
|
||||
return nil
|
||||
}
|
||||
if *genFiles {
|
||||
if err := ioutil.WriteFile(p, got, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
want, err := ioutil.ReadFile(p)
|
||||
if err != nil {
|
||||
t.Errorf("failed to read %q", p)
|
||||
} else {
|
||||
scanGot := bufio.NewScanner(bytes.NewReader(got))
|
||||
scanWant := bufio.NewScanner(bytes.NewReader(want))
|
||||
line := 0
|
||||
clean := func(s string) string {
|
||||
if i := strings.LastIndex(s, "//"); i != -1 {
|
||||
s = s[:i]
|
||||
}
|
||||
return path.Clean(filepath.ToSlash(s))
|
||||
}
|
||||
for scanGot.Scan() && scanWant.Scan() {
|
||||
got := clean(scanGot.Text())
|
||||
want := clean(scanWant.Text())
|
||||
if got != want {
|
||||
t.Errorf("file %q differs from .want file at line %d:\n\t%s\n\t%s", gotFile, line, got, want)
|
||||
break
|
||||
}
|
||||
line++
|
||||
}
|
||||
if scanGot.Scan() || scanWant.Scan() {
|
||||
t.Errorf("file %q differs from .want file at line %d.", gotFile, line)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func writeJSON(t *testing.T, path string, x interface{}) {
|
||||
data, err := json.MarshalIndent(x, "", " ")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ioutil.WriteFile(path, data, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
// 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 pipeline
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/constant"
|
||||
"go/format"
|
||||
"go/token"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/go/loader"
|
||||
)
|
||||
|
||||
const printerType = "golang.org/x/text/message.Printer"
|
||||
|
||||
// Rewrite rewrites the Go files in a single package to use the localization
|
||||
// machinery and rewrites strings to adopt best practices when possible.
|
||||
// If w is not nil the generated files are written to it, each files with a
|
||||
// "--- <filename>" header. Otherwise the files are overwritten.
|
||||
func Rewrite(w io.Writer, args ...string) error {
|
||||
conf := &loader.Config{
|
||||
AllowErrors: true, // Allow unused instances of message.Printer.
|
||||
}
|
||||
prog, err := loadPackages(conf, args)
|
||||
if err != nil {
|
||||
return wrap(err, "")
|
||||
}
|
||||
|
||||
for _, info := range prog.InitialPackages() {
|
||||
for _, f := range info.Files {
|
||||
// Associate comments with nodes.
|
||||
|
||||
// Pick up initialized Printers at the package level.
|
||||
r := rewriter{info: info, conf: conf}
|
||||
for _, n := range info.InitOrder {
|
||||
if t := r.info.Types[n.Rhs].Type.String(); strings.HasSuffix(t, printerType) {
|
||||
r.printerVar = n.Lhs[0].Name()
|
||||
}
|
||||
}
|
||||
|
||||
ast.Walk(&r, f)
|
||||
|
||||
w := w
|
||||
if w == nil {
|
||||
var err error
|
||||
if w, err = os.Create(conf.Fset.File(f.Pos()).Name()); err != nil {
|
||||
return wrap(err, "open failed")
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintln(w, "---", conf.Fset.File(f.Pos()).Name())
|
||||
}
|
||||
|
||||
if err := format.Node(w, conf.Fset, f); err != nil {
|
||||
return wrap(err, "go format failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type rewriter struct {
|
||||
info *loader.PackageInfo
|
||||
conf *loader.Config
|
||||
printerVar string
|
||||
}
|
||||
|
||||
// print returns Go syntax for the specified node.
|
||||
func (r *rewriter) print(n ast.Node) string {
|
||||
var buf bytes.Buffer
|
||||
format.Node(&buf, r.conf.Fset, n)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (r *rewriter) Visit(n ast.Node) ast.Visitor {
|
||||
// Save the state by scope.
|
||||
if _, ok := n.(*ast.BlockStmt); ok {
|
||||
r := *r
|
||||
return &r
|
||||
}
|
||||
// Find Printers created by assignment.
|
||||
stmt, ok := n.(*ast.AssignStmt)
|
||||
if ok {
|
||||
for _, v := range stmt.Lhs {
|
||||
if r.printerVar == r.print(v) {
|
||||
r.printerVar = ""
|
||||
}
|
||||
}
|
||||
for i, v := range stmt.Rhs {
|
||||
if t := r.info.Types[v].Type.String(); strings.HasSuffix(t, printerType) {
|
||||
r.printerVar = r.print(stmt.Lhs[i])
|
||||
return r
|
||||
}
|
||||
}
|
||||
}
|
||||
// Find Printers created by variable declaration.
|
||||
spec, ok := n.(*ast.ValueSpec)
|
||||
if ok {
|
||||
for _, v := range spec.Names {
|
||||
if r.printerVar == r.print(v) {
|
||||
r.printerVar = ""
|
||||
}
|
||||
}
|
||||
for i, v := range spec.Values {
|
||||
if t := r.info.Types[v].Type.String(); strings.HasSuffix(t, printerType) {
|
||||
r.printerVar = r.print(spec.Names[i])
|
||||
return r
|
||||
}
|
||||
}
|
||||
}
|
||||
if r.printerVar == "" {
|
||||
return r
|
||||
}
|
||||
call, ok := n.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return r
|
||||
}
|
||||
|
||||
// TODO: Handle literal values?
|
||||
sel, ok := call.Fun.(*ast.SelectorExpr)
|
||||
if !ok {
|
||||
return r
|
||||
}
|
||||
meth := r.info.Selections[sel]
|
||||
|
||||
source := r.print(sel.X)
|
||||
fun := r.print(sel.Sel)
|
||||
if meth != nil {
|
||||
source = meth.Recv().String()
|
||||
fun = meth.Obj().Name()
|
||||
}
|
||||
|
||||
// TODO: remove cheap hack and check if the type either
|
||||
// implements some interface or is specifically of type
|
||||
// "golang.org/x/text/message".Printer.
|
||||
m, ok := rewriteFuncs[source]
|
||||
if !ok {
|
||||
return r
|
||||
}
|
||||
|
||||
rewriteType, ok := m[fun]
|
||||
if !ok {
|
||||
return r
|
||||
}
|
||||
ident := ast.NewIdent(r.printerVar)
|
||||
ident.NamePos = sel.X.Pos()
|
||||
sel.X = ident
|
||||
if rewriteType.method != "" {
|
||||
sel.Sel.Name = rewriteType.method
|
||||
}
|
||||
|
||||
// Analyze arguments.
|
||||
argn := rewriteType.arg
|
||||
if rewriteType.format || argn >= len(call.Args) {
|
||||
return r
|
||||
}
|
||||
hasConst := false
|
||||
for _, a := range call.Args[argn:] {
|
||||
if v := r.info.Types[a].Value; v != nil && v.Kind() == constant.String {
|
||||
hasConst = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasConst {
|
||||
return r
|
||||
}
|
||||
sel.Sel.Name = rewriteType.methodf
|
||||
|
||||
// We are done if there is only a single string that does not need to be
|
||||
// escaped.
|
||||
if len(call.Args) == 1 {
|
||||
s, ok := constStr(r.info, call.Args[0])
|
||||
if ok && !strings.Contains(s, "%") && !rewriteType.newLine {
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
// Rewrite arguments as format string.
|
||||
expr := &ast.BasicLit{
|
||||
ValuePos: call.Lparen,
|
||||
Kind: token.STRING,
|
||||
}
|
||||
newArgs := append(call.Args[:argn:argn], expr)
|
||||
newStr := []string{}
|
||||
for i, a := range call.Args[argn:] {
|
||||
if s, ok := constStr(r.info, a); ok {
|
||||
newStr = append(newStr, strings.Replace(s, "%", "%%", -1))
|
||||
} else {
|
||||
newStr = append(newStr, "%v")
|
||||
newArgs = append(newArgs, call.Args[argn+i])
|
||||
}
|
||||
}
|
||||
s := strings.Join(newStr, rewriteType.sep)
|
||||
if rewriteType.newLine {
|
||||
s += "\n"
|
||||
}
|
||||
expr.Value = fmt.Sprintf("%q", s)
|
||||
|
||||
call.Args = newArgs
|
||||
|
||||
// TODO: consider creating an expression instead of a constant string and
|
||||
// then wrapping it in an escape function or so:
|
||||
// call.Args[argn+i] = &ast.CallExpr{
|
||||
// Fun: &ast.SelectorExpr{
|
||||
// X: ast.NewIdent("message"),
|
||||
// Sel: ast.NewIdent("Lookup"),
|
||||
// },
|
||||
// Args: []ast.Expr{a},
|
||||
// }
|
||||
// }
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
type rewriteType struct {
|
||||
// method is the name of the equivalent method on a printer, or "" if it is
|
||||
// the same.
|
||||
method string
|
||||
|
||||
// methodf is the method to use if the arguments can be rewritten as a
|
||||
// arguments to a printf-style call.
|
||||
methodf string
|
||||
|
||||
// format is true if the method takes a formatting string followed by
|
||||
// substitution arguments.
|
||||
format bool
|
||||
|
||||
// arg indicates the position of the argument to extract. If all is
|
||||
// positive, all arguments from this argument onwards needs to be extracted.
|
||||
arg int
|
||||
|
||||
sep string
|
||||
newLine bool
|
||||
}
|
||||
|
||||
// rewriteFuncs list functions that can be directly mapped to the printer
|
||||
// functions of the message package.
|
||||
var rewriteFuncs = map[string]map[string]rewriteType{
|
||||
// TODO: Printer -> *golang.org/x/text/message.Printer
|
||||
"fmt": {
|
||||
"Print": rewriteType{methodf: "Printf"},
|
||||
"Sprint": rewriteType{methodf: "Sprintf"},
|
||||
"Fprint": rewriteType{methodf: "Fprintf"},
|
||||
|
||||
"Println": rewriteType{methodf: "Printf", sep: " ", newLine: true},
|
||||
"Sprintln": rewriteType{methodf: "Sprintf", sep: " ", newLine: true},
|
||||
"Fprintln": rewriteType{methodf: "Fprintf", sep: " ", newLine: true},
|
||||
|
||||
"Printf": rewriteType{method: "Printf", format: true},
|
||||
"Sprintf": rewriteType{method: "Sprintf", format: true},
|
||||
"Fprintf": rewriteType{method: "Fprintf", format: true},
|
||||
},
|
||||
}
|
||||
|
||||
func constStr(info *loader.PackageInfo, e ast.Expr) (s string, ok bool) {
|
||||
v := info.Types[e].Value
|
||||
if v == nil || v.Kind() != constant.String {
|
||||
return "", false
|
||||
}
|
||||
return constant.StringVal(v), true
|
||||
}
|
||||
85
vendor/golang.org/x/text/message/pipeline/testdata/test1/catalog_gen.go
generated
vendored
Normal file
85
vendor/golang.org/x/text/message/pipeline/testdata/test1/catalog_gen.go
generated
vendored
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
"golang.org/x/text/message/catalog"
|
||||
)
|
||||
|
||||
type dictionary struct {
|
||||
index []uint32
|
||||
data string
|
||||
}
|
||||
|
||||
func (d *dictionary) Lookup(key string) (data string, ok bool) {
|
||||
p := messageKeyToIndex[key]
|
||||
start, end := d.index[p], d.index[p+1]
|
||||
if start == end {
|
||||
return "", false
|
||||
}
|
||||
return d.data[start:end], true
|
||||
}
|
||||
|
||||
func init() {
|
||||
dict := map[string]catalog.Dictionary{
|
||||
"de": &dictionary{index: deIndex, data: deData},
|
||||
"en_US": &dictionary{index: en_USIndex, data: en_USData},
|
||||
"zh": &dictionary{index: zhIndex, data: zhData},
|
||||
}
|
||||
fallback := language.MustParse("en-US")
|
||||
cat, err := catalog.NewFromMap(dict, catalog.Fallback(fallback))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
message.DefaultCatalog = cat
|
||||
}
|
||||
|
||||
var messageKeyToIndex = map[string]int{
|
||||
"%.2[1]f miles traveled (%[1]f)": 8,
|
||||
"%[1]s is visiting %[3]s!\n": 3,
|
||||
"%d files remaining!": 4,
|
||||
"%d more files remaining!": 5,
|
||||
"%s is out of order!": 7,
|
||||
"%s is visiting %s!\n": 2,
|
||||
"Hello %s!\n": 1,
|
||||
"Hello world!\n": 0,
|
||||
"Use the following code for your discount: %d\n": 6,
|
||||
}
|
||||
|
||||
var deIndex = []uint32{ // 10 elements
|
||||
0x00000000, 0x00000011, 0x00000023, 0x0000003d,
|
||||
0x00000057, 0x00000075, 0x00000094, 0x00000094,
|
||||
0x00000094, 0x00000094,
|
||||
} // Size: 64 bytes
|
||||
|
||||
const deData string = "" + // Size: 148 bytes
|
||||
"\x04\x00\x01\x0a\x0c\x02Hallo Welt!\x04\x00\x01\x0a\x0d\x02Hallo %[1]s!" +
|
||||
"\x04\x00\x01\x0a\x15\x02%[1]s besucht %[2]s!\x04\x00\x01\x0a\x15\x02%[1]" +
|
||||
"s besucht %[3]s!\x02Noch zwei Bestände zu gehen!\x02Noch %[1]d Bestände " +
|
||||
"zu gehen!"
|
||||
|
||||
var en_USIndex = []uint32{ // 10 elements
|
||||
0x00000000, 0x00000012, 0x00000024, 0x00000042,
|
||||
0x00000060, 0x00000077, 0x000000ba, 0x000000ef,
|
||||
0x00000106, 0x00000125,
|
||||
} // Size: 64 bytes
|
||||
|
||||
const en_USData string = "" + // Size: 293 bytes
|
||||
"\x04\x00\x01\x0a\x0d\x02Hello world!\x04\x00\x01\x0a\x0d\x02Hello %[1]s!" +
|
||||
"\x04\x00\x01\x0a\x19\x02%[1]s is visiting %[2]s!\x04\x00\x01\x0a\x19\x02" +
|
||||
"%[1]s is visiting %[3]s!\x02%[1]d files remaining!\x14\x01\x81\x01\x00" +
|
||||
"\x02\x14\x02One file remaining!\x00&\x02There are %[1]d more files remai" +
|
||||
"ning!\x04\x00\x01\x0a0\x02Use the following code for your discount: %[1]" +
|
||||
"d\x02%[1]s is out of order!\x02%.2[1]f miles traveled (%[1]f)"
|
||||
|
||||
var zhIndex = []uint32{ // 10 elements
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000,
|
||||
} // Size: 64 bytes
|
||||
|
||||
const zhData string = ""
|
||||
|
||||
// Total table size 633 bytes (0KiB); checksum: 74B32E70
|
||||
85
vendor/golang.org/x/text/message/pipeline/testdata/test1/catalog_gen.go.want
generated
vendored
Normal file
85
vendor/golang.org/x/text/message/pipeline/testdata/test1/catalog_gen.go.want
generated
vendored
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
"golang.org/x/text/message/catalog"
|
||||
)
|
||||
|
||||
type dictionary struct {
|
||||
index []uint32
|
||||
data string
|
||||
}
|
||||
|
||||
func (d *dictionary) Lookup(key string) (data string, ok bool) {
|
||||
p := messageKeyToIndex[key]
|
||||
start, end := d.index[p], d.index[p+1]
|
||||
if start == end {
|
||||
return "", false
|
||||
}
|
||||
return d.data[start:end], true
|
||||
}
|
||||
|
||||
func init() {
|
||||
dict := map[string]catalog.Dictionary{
|
||||
"de": &dictionary{index: deIndex, data: deData},
|
||||
"en_US": &dictionary{index: en_USIndex, data: en_USData},
|
||||
"zh": &dictionary{index: zhIndex, data: zhData},
|
||||
}
|
||||
fallback := language.MustParse("en-US")
|
||||
cat, err := catalog.NewFromMap(dict, catalog.Fallback(fallback))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
message.DefaultCatalog = cat
|
||||
}
|
||||
|
||||
var messageKeyToIndex = map[string]int{
|
||||
"%.2[1]f miles traveled (%[1]f)": 8,
|
||||
"%[1]s is visiting %[3]s!\n": 3,
|
||||
"%d files remaining!": 4,
|
||||
"%d more files remaining!": 5,
|
||||
"%s is out of order!": 7,
|
||||
"%s is visiting %s!\n": 2,
|
||||
"Hello %s!\n": 1,
|
||||
"Hello world!\n": 0,
|
||||
"Use the following code for your discount: %d\n": 6,
|
||||
}
|
||||
|
||||
var deIndex = []uint32{ // 10 elements
|
||||
0x00000000, 0x00000011, 0x00000023, 0x0000003d,
|
||||
0x00000057, 0x00000075, 0x00000094, 0x00000094,
|
||||
0x00000094, 0x00000094,
|
||||
} // Size: 64 bytes
|
||||
|
||||
const deData string = "" + // Size: 148 bytes
|
||||
"\x04\x00\x01\x0a\x0c\x02Hallo Welt!\x04\x00\x01\x0a\x0d\x02Hallo %[1]s!" +
|
||||
"\x04\x00\x01\x0a\x15\x02%[1]s besucht %[2]s!\x04\x00\x01\x0a\x15\x02%[1]" +
|
||||
"s besucht %[3]s!\x02Noch zwei Bestände zu gehen!\x02Noch %[1]d Bestände " +
|
||||
"zu gehen!"
|
||||
|
||||
var en_USIndex = []uint32{ // 10 elements
|
||||
0x00000000, 0x00000012, 0x00000024, 0x00000042,
|
||||
0x00000060, 0x00000077, 0x000000ba, 0x000000ef,
|
||||
0x00000106, 0x00000125,
|
||||
} // Size: 64 bytes
|
||||
|
||||
const en_USData string = "" + // Size: 293 bytes
|
||||
"\x04\x00\x01\x0a\x0d\x02Hello world!\x04\x00\x01\x0a\x0d\x02Hello %[1]s!" +
|
||||
"\x04\x00\x01\x0a\x19\x02%[1]s is visiting %[2]s!\x04\x00\x01\x0a\x19\x02" +
|
||||
"%[1]s is visiting %[3]s!\x02%[1]d files remaining!\x14\x01\x81\x01\x00" +
|
||||
"\x02\x14\x02One file remaining!\x00&\x02There are %[1]d more files remai" +
|
||||
"ning!\x04\x00\x01\x0a0\x02Use the following code for your discount: %[1]" +
|
||||
"d\x02%[1]s is out of order!\x02%.2[1]f miles traveled (%[1]f)"
|
||||
|
||||
var zhIndex = []uint32{ // 10 elements
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000,
|
||||
} // Size: 64 bytes
|
||||
|
||||
const zhData string = ""
|
||||
|
||||
// Total table size 633 bytes (0KiB); checksum: 74B32E70
|
||||
49
vendor/golang.org/x/text/message/pipeline/testdata/test1/catalog_test.go
generated
vendored
Normal file
49
vendor/golang.org/x/text/message/pipeline/testdata/test1/catalog_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// 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 main
|
||||
|
||||
import (
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/text/message"
|
||||
)
|
||||
|
||||
func TestCatalog(t *testing.T) {
|
||||
args := func(a ...interface{}) []interface{} { return a }
|
||||
testCases := []struct {
|
||||
lang string
|
||||
key string
|
||||
args []interface{}
|
||||
want string
|
||||
}{{
|
||||
lang: "en",
|
||||
key: "Hello world!\n",
|
||||
want: "Hello world!\n",
|
||||
}, {
|
||||
lang: "de",
|
||||
key: "Hello world!\n",
|
||||
want: "Hallo Welt!\n",
|
||||
}, {
|
||||
lang: "en",
|
||||
key: "%d more files remaining!",
|
||||
args: args(1),
|
||||
want: "One file remaining!",
|
||||
}, {
|
||||
lang: "en-u-nu-fullwide",
|
||||
key: "%d more files remaining!",
|
||||
args: args(5),
|
||||
want: "There are 5 more files remaining!",
|
||||
}}
|
||||
for _, tc := range testCases {
|
||||
t.Run(path.Join(tc.lang, tc.key), func(t *testing.T) {
|
||||
p := message.NewPrinter(message.MatchLanguage(tc.lang))
|
||||
got := p.Sprintf(tc.key, tc.args...)
|
||||
if got != tc.want {
|
||||
t.Errorf("got %q; want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
188
vendor/golang.org/x/text/message/pipeline/testdata/test1/extracted.gotext.json
generated
vendored
Normal file
188
vendor/golang.org/x/text/message/pipeline/testdata/test1/extracted.gotext.json
generated
vendored
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
{
|
||||
"language": "en-US",
|
||||
"messages": [
|
||||
{
|
||||
"id": "Hello world!",
|
||||
"key": "Hello world!\n",
|
||||
"message": "Hello world!",
|
||||
"translation": "",
|
||||
"position": "testdata/test1/test1.go:19:10"
|
||||
},
|
||||
{
|
||||
"id": "Hello {City}!",
|
||||
"key": "Hello %s!\n",
|
||||
"message": "Hello {City}!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "City",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "city"
|
||||
}
|
||||
],
|
||||
"position": "testdata/test1/test1.go:24:10"
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"key": "%s is visiting %s!\n",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Person",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "person",
|
||||
"comment": "The person of matter."
|
||||
},
|
||||
{
|
||||
"id": "Place",
|
||||
"string": "%[2]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 2,
|
||||
"expr": "place",
|
||||
"comment": "Place the person is visiting."
|
||||
}
|
||||
],
|
||||
"position": "testdata/test1/test1.go:30:10"
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"key": "%[1]s is visiting %[3]s!\n",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "",
|
||||
"comment": "Field names are placeholders.",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Person",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "pp.Person"
|
||||
},
|
||||
{
|
||||
"id": "Place",
|
||||
"string": "%[3]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 3,
|
||||
"expr": "pp.Place",
|
||||
"comment": "Place the person is visiting."
|
||||
},
|
||||
{
|
||||
"id": "Extra",
|
||||
"string": "%[2]v",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 2,
|
||||
"expr": "pp.extra"
|
||||
}
|
||||
],
|
||||
"position": "testdata/test1/test1.go:44:10"
|
||||
},
|
||||
{
|
||||
"id": "{2} files remaining!",
|
||||
"key": "%d files remaining!",
|
||||
"message": "{2} files remaining!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "2",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "2"
|
||||
}
|
||||
],
|
||||
"position": "testdata/test1/test1.go:51:10"
|
||||
},
|
||||
{
|
||||
"id": "{N} more files remaining!",
|
||||
"key": "%d more files remaining!",
|
||||
"message": "{N} more files remaining!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "N",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "n"
|
||||
}
|
||||
],
|
||||
"position": "testdata/test1/test1.go:56:10"
|
||||
},
|
||||
{
|
||||
"id": "Use the following code for your discount: {ReferralCode}",
|
||||
"key": "Use the following code for your discount: %d\n",
|
||||
"message": "Use the following code for your discount: {ReferralCode}",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "ReferralCode",
|
||||
"string": "%[1]d",
|
||||
"type": "./testdata/test1.referralCode",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "c"
|
||||
}
|
||||
],
|
||||
"position": "testdata/test1/test1.go:64:10"
|
||||
},
|
||||
{
|
||||
"id": [
|
||||
"msgOutOfOrder",
|
||||
"{Device} is out of order!"
|
||||
],
|
||||
"key": "%s is out of order!",
|
||||
"message": "{Device} is out of order!",
|
||||
"translation": "",
|
||||
"comment": "This comment wins.\n",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Device",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "device"
|
||||
}
|
||||
],
|
||||
"position": "testdata/test1/test1.go:70:10"
|
||||
},
|
||||
{
|
||||
"id": "{Miles} miles traveled ({Miles_1})",
|
||||
"key": "%.2[1]f miles traveled (%[1]f)",
|
||||
"message": "{Miles} miles traveled ({Miles_1})",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Miles",
|
||||
"string": "%.2[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
},
|
||||
{
|
||||
"id": "Miles_1",
|
||||
"string": "%[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
}
|
||||
],
|
||||
"position": "testdata/test1/test1.go:74:10"
|
||||
}
|
||||
]
|
||||
}
|
||||
188
vendor/golang.org/x/text/message/pipeline/testdata/test1/extracted.gotext.json.want
generated
vendored
Normal file
188
vendor/golang.org/x/text/message/pipeline/testdata/test1/extracted.gotext.json.want
generated
vendored
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
{
|
||||
"language": "en-US",
|
||||
"messages": [
|
||||
{
|
||||
"id": "Hello world!",
|
||||
"key": "Hello world!\n",
|
||||
"message": "Hello world!",
|
||||
"translation": "",
|
||||
"position": "testdata/test1/test1.go:19:10"
|
||||
},
|
||||
{
|
||||
"id": "Hello {City}!",
|
||||
"key": "Hello %s!\n",
|
||||
"message": "Hello {City}!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "City",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "city"
|
||||
}
|
||||
],
|
||||
"position": "testdata/test1/test1.go:24:10"
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"key": "%s is visiting %s!\n",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Person",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "person",
|
||||
"comment": "The person of matter."
|
||||
},
|
||||
{
|
||||
"id": "Place",
|
||||
"string": "%[2]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 2,
|
||||
"expr": "place",
|
||||
"comment": "Place the person is visiting."
|
||||
}
|
||||
],
|
||||
"position": "testdata/test1/test1.go:30:10"
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"key": "%[1]s is visiting %[3]s!\n",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "",
|
||||
"comment": "Field names are placeholders.",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Person",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "pp.Person"
|
||||
},
|
||||
{
|
||||
"id": "Place",
|
||||
"string": "%[3]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 3,
|
||||
"expr": "pp.Place",
|
||||
"comment": "Place the person is visiting."
|
||||
},
|
||||
{
|
||||
"id": "Extra",
|
||||
"string": "%[2]v",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 2,
|
||||
"expr": "pp.extra"
|
||||
}
|
||||
],
|
||||
"position": "testdata/test1/test1.go:44:10"
|
||||
},
|
||||
{
|
||||
"id": "{2} files remaining!",
|
||||
"key": "%d files remaining!",
|
||||
"message": "{2} files remaining!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "2",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "2"
|
||||
}
|
||||
],
|
||||
"position": "testdata/test1/test1.go:51:10"
|
||||
},
|
||||
{
|
||||
"id": "{N} more files remaining!",
|
||||
"key": "%d more files remaining!",
|
||||
"message": "{N} more files remaining!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "N",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "n"
|
||||
}
|
||||
],
|
||||
"position": "testdata/test1/test1.go:56:10"
|
||||
},
|
||||
{
|
||||
"id": "Use the following code for your discount: {ReferralCode}",
|
||||
"key": "Use the following code for your discount: %d\n",
|
||||
"message": "Use the following code for your discount: {ReferralCode}",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "ReferralCode",
|
||||
"string": "%[1]d",
|
||||
"type": "./testdata/test1.referralCode",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "c"
|
||||
}
|
||||
],
|
||||
"position": "testdata/test1/test1.go:64:10"
|
||||
},
|
||||
{
|
||||
"id": [
|
||||
"msgOutOfOrder",
|
||||
"{Device} is out of order!"
|
||||
],
|
||||
"key": "%s is out of order!",
|
||||
"message": "{Device} is out of order!",
|
||||
"translation": "",
|
||||
"comment": "This comment wins.\n",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Device",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "device"
|
||||
}
|
||||
],
|
||||
"position": "testdata/test1/test1.go:70:10"
|
||||
},
|
||||
{
|
||||
"id": "{Miles} miles traveled ({Miles_1})",
|
||||
"key": "%.2[1]f miles traveled (%[1]f)",
|
||||
"message": "{Miles} miles traveled ({Miles_1})",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Miles",
|
||||
"string": "%.2[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
},
|
||||
{
|
||||
"id": "Miles_1",
|
||||
"string": "%[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
}
|
||||
],
|
||||
"position": "testdata/test1/test1.go:74:10"
|
||||
}
|
||||
]
|
||||
}
|
||||
123
vendor/golang.org/x/text/message/pipeline/testdata/test1/locales/de/messages.gotext.json
generated
vendored
Executable file
123
vendor/golang.org/x/text/message/pipeline/testdata/test1/locales/de/messages.gotext.json
generated
vendored
Executable file
|
|
@ -0,0 +1,123 @@
|
|||
{
|
||||
"language": "de",
|
||||
"messages": [
|
||||
{
|
||||
"id": "Hello world!",
|
||||
"key": "Hello world!\n",
|
||||
"message": "Hello world!",
|
||||
"translation": "Hallo Welt!"
|
||||
},
|
||||
{
|
||||
"id": "Hello {City}!",
|
||||
"key": "Hello %s!\n",
|
||||
"message": "Hello {City}!",
|
||||
"translation": "Hallo {City}!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "City",
|
||||
"string": "%[1]s"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"key": "%s is visiting %s!\n",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "{Person} besucht {Place}!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Person",
|
||||
"string": "%[1]s"
|
||||
},
|
||||
{
|
||||
"id": "Place",
|
||||
"string": "%[2]s"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"key": "%[1]s is visiting %[3]s!\n",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "{Person} besucht {Place}!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Person",
|
||||
"string": "%[1]s"
|
||||
},
|
||||
{
|
||||
"id": "Place",
|
||||
"string": "%[3]s"
|
||||
},
|
||||
{
|
||||
"id": "Extra",
|
||||
"string": "%[2]v"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{2} files remaining!",
|
||||
"key": "%d files remaining!",
|
||||
"message": "{N} files remaining!",
|
||||
"translation": "Noch zwei Bestände zu gehen!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "2",
|
||||
"string": "%[1]d"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{N} more files remaining!",
|
||||
"key": "%d more files remaining!",
|
||||
"message": "{N} more files remaining!",
|
||||
"translation": "Noch {N} Bestände zu gehen!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "N",
|
||||
"string": "%[1]d"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Use the following code for your discount: {ReferralCode}",
|
||||
"key": "Use the following code for your discount: %d\n",
|
||||
"message": "Use the following code for your discount: {ReferralCode}",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "ReferralCode",
|
||||
"string": "%[1]d"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": [ "msgOutOfOrder", "{Device} is out of order!" ],
|
||||
"key": "%s is out of order!",
|
||||
"message": "{Device} is out of order!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Device",
|
||||
"string": "%[1]s"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Miles} miles traveled ({Miles_1})",
|
||||
"key": "%.2[1]f miles traveled (%[1]f)",
|
||||
"message": "{Miles} miles traveled ({Miles_1})",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Miles",
|
||||
"string": "%.2[1]f"
|
||||
},
|
||||
{
|
||||
"id": "Miles_1",
|
||||
"string": "%[1]f"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
137
vendor/golang.org/x/text/message/pipeline/testdata/test1/locales/de/out.gotext.json
generated
vendored
Executable file
137
vendor/golang.org/x/text/message/pipeline/testdata/test1/locales/de/out.gotext.json
generated
vendored
Executable file
|
|
@ -0,0 +1,137 @@
|
|||
{
|
||||
"language": "de",
|
||||
"messages": [
|
||||
{
|
||||
"id": "Hello world!",
|
||||
"message": "Hello world!",
|
||||
"translation": "Hallo Welt!"
|
||||
},
|
||||
{
|
||||
"id": "Hello {City}!",
|
||||
"message": "Hello {City}!",
|
||||
"translation": "Hallo {City}!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "City",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "city"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "{Person} besucht {Place}!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Person",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "person",
|
||||
"comment": "The person of matter."
|
||||
},
|
||||
{
|
||||
"id": "Place",
|
||||
"string": "%[2]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 2,
|
||||
"expr": "place",
|
||||
"comment": "Place the person is visiting."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{2} files remaining!",
|
||||
"message": "{2} files remaining!",
|
||||
"translation": "Noch zwei Bestände zu gehen!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "2",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{N} more files remaining!",
|
||||
"message": "{N} more files remaining!",
|
||||
"translation": "Noch {N} Bestände zu gehen!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "N",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "n"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Use the following code for your discount: {ReferralCode}",
|
||||
"message": "Use the following code for your discount: {ReferralCode}",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "ReferralCode",
|
||||
"string": "%[1]d",
|
||||
"type": "./testdata/test1.referralCode",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "c"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": [
|
||||
"msgOutOfOrder",
|
||||
"{Device} is out of order!"
|
||||
],
|
||||
"message": "{Device} is out of order!",
|
||||
"translation": "",
|
||||
"comment": "This comment wins.\n",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Device",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "device"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Miles} miles traveled ({Miles_1})",
|
||||
"message": "{Miles} miles traveled ({Miles_1})",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Miles",
|
||||
"string": "%.2[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
},
|
||||
{
|
||||
"id": "Miles_1",
|
||||
"string": "%[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
137
vendor/golang.org/x/text/message/pipeline/testdata/test1/locales/de/out.gotext.json.want
generated
vendored
Executable file
137
vendor/golang.org/x/text/message/pipeline/testdata/test1/locales/de/out.gotext.json.want
generated
vendored
Executable file
|
|
@ -0,0 +1,137 @@
|
|||
{
|
||||
"language": "de",
|
||||
"messages": [
|
||||
{
|
||||
"id": "Hello world!",
|
||||
"message": "Hello world!",
|
||||
"translation": "Hallo Welt!"
|
||||
},
|
||||
{
|
||||
"id": "Hello {City}!",
|
||||
"message": "Hello {City}!",
|
||||
"translation": "Hallo {City}!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "City",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "city"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "{Person} besucht {Place}!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Person",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "person",
|
||||
"comment": "The person of matter."
|
||||
},
|
||||
{
|
||||
"id": "Place",
|
||||
"string": "%[2]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 2,
|
||||
"expr": "place",
|
||||
"comment": "Place the person is visiting."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{2} files remaining!",
|
||||
"message": "{2} files remaining!",
|
||||
"translation": "Noch zwei Bestände zu gehen!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "2",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{N} more files remaining!",
|
||||
"message": "{N} more files remaining!",
|
||||
"translation": "Noch {N} Bestände zu gehen!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "N",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "n"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Use the following code for your discount: {ReferralCode}",
|
||||
"message": "Use the following code for your discount: {ReferralCode}",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "ReferralCode",
|
||||
"string": "%[1]d",
|
||||
"type": "./testdata/test1.referralCode",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "c"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": [
|
||||
"msgOutOfOrder",
|
||||
"{Device} is out of order!"
|
||||
],
|
||||
"message": "{Device} is out of order!",
|
||||
"translation": "",
|
||||
"comment": "This comment wins.\n",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Device",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "device"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Miles} miles traveled ({Miles_1})",
|
||||
"message": "{Miles} miles traveled ({Miles_1})",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Miles",
|
||||
"string": "%.2[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
},
|
||||
{
|
||||
"id": "Miles_1",
|
||||
"string": "%[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
91
vendor/golang.org/x/text/message/pipeline/testdata/test1/locales/en-US/messages.gotext.json
generated
vendored
Executable file
91
vendor/golang.org/x/text/message/pipeline/testdata/test1/locales/en-US/messages.gotext.json
generated
vendored
Executable file
|
|
@ -0,0 +1,91 @@
|
|||
{
|
||||
"language": "en-US",
|
||||
"messages": [
|
||||
{
|
||||
"id": "Hello world!",
|
||||
"key": "Hello world!\n",
|
||||
"message": "Hello world!",
|
||||
"translation": "Hello world!"
|
||||
},
|
||||
{
|
||||
"id": "Hello {City}!",
|
||||
"key": "Hello %s!\n",
|
||||
"message": "Hello {City}!",
|
||||
"translation": "Hello {City}!"
|
||||
},
|
||||
{
|
||||
"id": "Hello {Town}!",
|
||||
"key": "Hello %s!\n",
|
||||
"message": "Hello {Town}!",
|
||||
"translation": "Hello {Town}!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Town",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "town",
|
||||
"comment": "Town"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"key": "%s is visiting %s!\n",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "{Person} is visiting {Place}!"
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"key": "%[1]s is visiting %[3]s!\n",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "{Person} is visiting {Place}!"
|
||||
},
|
||||
{
|
||||
"id": "{2} files remaining!",
|
||||
"key": "%d files remaining!",
|
||||
"message": "{N} files remaining!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "2",
|
||||
"string": "%[1]d"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{N} more files remaining!",
|
||||
"key": "%d more files remaining!",
|
||||
"message": "{N} more files remaining!",
|
||||
"translation": {
|
||||
"select": {
|
||||
"feature": "plural",
|
||||
"arg": "N",
|
||||
"cases": {
|
||||
"one": "One file remaining!",
|
||||
"other": "There are {N} more files remaining!"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Use the following code for your discount: {ReferralCode}",
|
||||
"key": "Use the following code for your discount: %d\n",
|
||||
"message": "Use the following code for your discount: {ReferralCode}",
|
||||
"translation": ""
|
||||
},
|
||||
{
|
||||
"id": [ "msgOutOfOrder", "{Device} is out of order!" ],
|
||||
"key": "%s is out of order!",
|
||||
"message": "{Device} is out of order!",
|
||||
"translation": "{Device} is out of order!"
|
||||
},
|
||||
{
|
||||
"id": "{Miles} miles traveled ({Miles_1})",
|
||||
"key": "%.2[1]f miles traveled (%[1]f)",
|
||||
"message": "{Miles} miles traveled ({Miles_1})",
|
||||
"translation": "{Miles} miles traveled ({Miles_1})"
|
||||
}
|
||||
]
|
||||
}
|
||||
154
vendor/golang.org/x/text/message/pipeline/testdata/test1/locales/en-US/out.gotext.json
generated
vendored
Executable file
154
vendor/golang.org/x/text/message/pipeline/testdata/test1/locales/en-US/out.gotext.json
generated
vendored
Executable file
|
|
@ -0,0 +1,154 @@
|
|||
{
|
||||
"language": "en-US",
|
||||
"messages": [
|
||||
{
|
||||
"id": "Hello world!",
|
||||
"message": "Hello world!",
|
||||
"translation": "Hello world!"
|
||||
},
|
||||
{
|
||||
"id": "Hello {City}!",
|
||||
"message": "Hello {City}!",
|
||||
"translation": "Hello {City}!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "City",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "city"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "{Person} is visiting {Place}!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Person",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "person",
|
||||
"comment": "The person of matter."
|
||||
},
|
||||
{
|
||||
"id": "Place",
|
||||
"string": "%[2]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 2,
|
||||
"expr": "place",
|
||||
"comment": "Place the person is visiting."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{2} files remaining!",
|
||||
"message": "{2} files remaining!",
|
||||
"translation": "{2} files remaining!",
|
||||
"translatorComment": "Copied from source.",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "2",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "2"
|
||||
}
|
||||
],
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "{N} more files remaining!",
|
||||
"message": "{N} more files remaining!",
|
||||
"translation": {
|
||||
"select": {
|
||||
"feature": "plural",
|
||||
"arg": "N",
|
||||
"cases": {
|
||||
"one": {
|
||||
"msg": "One file remaining!"
|
||||
},
|
||||
"other": {
|
||||
"msg": "There are {N} more files remaining!"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "N",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "n"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Use the following code for your discount: {ReferralCode}",
|
||||
"message": "Use the following code for your discount: {ReferralCode}",
|
||||
"translation": "Use the following code for your discount: {ReferralCode}",
|
||||
"translatorComment": "Copied from source.",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "ReferralCode",
|
||||
"string": "%[1]d",
|
||||
"type": "./testdata/test1.referralCode",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "c"
|
||||
}
|
||||
],
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": [
|
||||
"msgOutOfOrder",
|
||||
"{Device} is out of order!"
|
||||
],
|
||||
"message": "{Device} is out of order!",
|
||||
"translation": "{Device} is out of order!",
|
||||
"comment": "This comment wins.\n",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Device",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "device"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Miles} miles traveled ({Miles_1})",
|
||||
"message": "{Miles} miles traveled ({Miles_1})",
|
||||
"translation": "{Miles} miles traveled ({Miles_1})",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Miles",
|
||||
"string": "%.2[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
},
|
||||
{
|
||||
"id": "Miles_1",
|
||||
"string": "%[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
154
vendor/golang.org/x/text/message/pipeline/testdata/test1/locales/en-US/out.gotext.json.want
generated
vendored
Executable file
154
vendor/golang.org/x/text/message/pipeline/testdata/test1/locales/en-US/out.gotext.json.want
generated
vendored
Executable file
|
|
@ -0,0 +1,154 @@
|
|||
{
|
||||
"language": "en-US",
|
||||
"messages": [
|
||||
{
|
||||
"id": "Hello world!",
|
||||
"message": "Hello world!",
|
||||
"translation": "Hello world!"
|
||||
},
|
||||
{
|
||||
"id": "Hello {City}!",
|
||||
"message": "Hello {City}!",
|
||||
"translation": "Hello {City}!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "City",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "city"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "{Person} is visiting {Place}!",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Person",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "person",
|
||||
"comment": "The person of matter."
|
||||
},
|
||||
{
|
||||
"id": "Place",
|
||||
"string": "%[2]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 2,
|
||||
"expr": "place",
|
||||
"comment": "Place the person is visiting."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{2} files remaining!",
|
||||
"message": "{2} files remaining!",
|
||||
"translation": "{2} files remaining!",
|
||||
"translatorComment": "Copied from source.",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "2",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "2"
|
||||
}
|
||||
],
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "{N} more files remaining!",
|
||||
"message": "{N} more files remaining!",
|
||||
"translation": {
|
||||
"select": {
|
||||
"feature": "plural",
|
||||
"arg": "N",
|
||||
"cases": {
|
||||
"one": {
|
||||
"msg": "One file remaining!"
|
||||
},
|
||||
"other": {
|
||||
"msg": "There are {N} more files remaining!"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "N",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "n"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Use the following code for your discount: {ReferralCode}",
|
||||
"message": "Use the following code for your discount: {ReferralCode}",
|
||||
"translation": "Use the following code for your discount: {ReferralCode}",
|
||||
"translatorComment": "Copied from source.",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "ReferralCode",
|
||||
"string": "%[1]d",
|
||||
"type": "./testdata/test1.referralCode",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "c"
|
||||
}
|
||||
],
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": [
|
||||
"msgOutOfOrder",
|
||||
"{Device} is out of order!"
|
||||
],
|
||||
"message": "{Device} is out of order!",
|
||||
"translation": "{Device} is out of order!",
|
||||
"comment": "This comment wins.\n",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Device",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "device"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Miles} miles traveled ({Miles_1})",
|
||||
"message": "{Miles} miles traveled ({Miles_1})",
|
||||
"translation": "{Miles} miles traveled ({Miles_1})",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Miles",
|
||||
"string": "%.2[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
},
|
||||
{
|
||||
"id": "Miles_1",
|
||||
"string": "%[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
135
vendor/golang.org/x/text/message/pipeline/testdata/test1/locales/zh/messages.gotext.json
generated
vendored
Executable file
135
vendor/golang.org/x/text/message/pipeline/testdata/test1/locales/zh/messages.gotext.json
generated
vendored
Executable file
|
|
@ -0,0 +1,135 @@
|
|||
{
|
||||
"language": "zh",
|
||||
"messages": [
|
||||
{
|
||||
"id": "Hello world!",
|
||||
"key": "Hello world!\n",
|
||||
"message": "Hello world!",
|
||||
"translation": ""
|
||||
},
|
||||
{
|
||||
"id": "Hello {City}!",
|
||||
"key": "Hello %s!\n",
|
||||
"message": "Hello {City}!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "City",
|
||||
"string": "%[1]s"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Hello {Town}!",
|
||||
"key": "Hello %s!\n",
|
||||
"message": "Hello {Town}!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Town",
|
||||
"string": "%[1]s"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"key": "%s is visiting %s!\n",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Person",
|
||||
"string": "%[1]s"
|
||||
},
|
||||
{
|
||||
"id": "Place",
|
||||
"string": "%[2]s"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"key": "%[1]s is visiting %[3]s!\n",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Person",
|
||||
"string": "%[1]s"
|
||||
},
|
||||
{
|
||||
"id": "Place",
|
||||
"string": "%[3]s"
|
||||
},
|
||||
{
|
||||
"id": "Extra",
|
||||
"string": "%[2]v"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{2} files remaining!",
|
||||
"key": "%d files remaining!",
|
||||
"message": "{2} files remaining!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "",
|
||||
"string": "%[1]d"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{N} more files remaining!",
|
||||
"key": "%d more files remaining!",
|
||||
"message": "{N} more files remaining!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "N",
|
||||
"string": "%[1]d"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Use the following code for your discount: {ReferralCode}",
|
||||
"key": "Use the following code for your discount: %d\n",
|
||||
"message": "Use the following code for your discount: {ReferralCode}",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "ReferralCode",
|
||||
"string": "%[1]d"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": [ "{Device} is out of order!", "msgOutOfOrder" ],
|
||||
"key": "%s is out of order!",
|
||||
"message": "{Device} is out of order!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Device",
|
||||
"string": "%[1]s"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Miles} miles traveled ({Miles_1})",
|
||||
"key": "%.2[1]f miles traveled (%[1]f)",
|
||||
"message": "{Miles} miles traveled ({Miles_1})",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Miles",
|
||||
"string": "%.2[1]f"
|
||||
},
|
||||
{
|
||||
"id": "Miles_1",
|
||||
"string": "%[1]f"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
137
vendor/golang.org/x/text/message/pipeline/testdata/test1/locales/zh/out.gotext.json
generated
vendored
Executable file
137
vendor/golang.org/x/text/message/pipeline/testdata/test1/locales/zh/out.gotext.json
generated
vendored
Executable file
|
|
@ -0,0 +1,137 @@
|
|||
{
|
||||
"language": "zh",
|
||||
"messages": [
|
||||
{
|
||||
"id": "Hello world!",
|
||||
"message": "Hello world!",
|
||||
"translation": ""
|
||||
},
|
||||
{
|
||||
"id": "Hello {City}!",
|
||||
"message": "Hello {City}!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "City",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "city"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Person",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "person",
|
||||
"comment": "The person of matter."
|
||||
},
|
||||
{
|
||||
"id": "Place",
|
||||
"string": "%[2]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 2,
|
||||
"expr": "place",
|
||||
"comment": "Place the person is visiting."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{2} files remaining!",
|
||||
"message": "{2} files remaining!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "2",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{N} more files remaining!",
|
||||
"message": "{N} more files remaining!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "N",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "n"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Use the following code for your discount: {ReferralCode}",
|
||||
"message": "Use the following code for your discount: {ReferralCode}",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "ReferralCode",
|
||||
"string": "%[1]d",
|
||||
"type": "./testdata/test1.referralCode",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "c"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": [
|
||||
"msgOutOfOrder",
|
||||
"{Device} is out of order!"
|
||||
],
|
||||
"message": "{Device} is out of order!",
|
||||
"translation": "",
|
||||
"comment": "This comment wins.\n",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Device",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "device"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Miles} miles traveled ({Miles_1})",
|
||||
"message": "{Miles} miles traveled ({Miles_1})",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Miles",
|
||||
"string": "%.2[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
},
|
||||
{
|
||||
"id": "Miles_1",
|
||||
"string": "%[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
137
vendor/golang.org/x/text/message/pipeline/testdata/test1/locales/zh/out.gotext.json.want
generated
vendored
Executable file
137
vendor/golang.org/x/text/message/pipeline/testdata/test1/locales/zh/out.gotext.json.want
generated
vendored
Executable file
|
|
@ -0,0 +1,137 @@
|
|||
{
|
||||
"language": "zh",
|
||||
"messages": [
|
||||
{
|
||||
"id": "Hello world!",
|
||||
"message": "Hello world!",
|
||||
"translation": ""
|
||||
},
|
||||
{
|
||||
"id": "Hello {City}!",
|
||||
"message": "Hello {City}!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "City",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "city"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Person} is visiting {Place}!",
|
||||
"message": "{Person} is visiting {Place}!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Person",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "person",
|
||||
"comment": "The person of matter."
|
||||
},
|
||||
{
|
||||
"id": "Place",
|
||||
"string": "%[2]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 2,
|
||||
"expr": "place",
|
||||
"comment": "Place the person is visiting."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{2} files remaining!",
|
||||
"message": "{2} files remaining!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "2",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{N} more files remaining!",
|
||||
"message": "{N} more files remaining!",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "N",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "n"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Use the following code for your discount: {ReferralCode}",
|
||||
"message": "Use the following code for your discount: {ReferralCode}",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "ReferralCode",
|
||||
"string": "%[1]d",
|
||||
"type": "./testdata/test1.referralCode",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "c"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": [
|
||||
"msgOutOfOrder",
|
||||
"{Device} is out of order!"
|
||||
],
|
||||
"message": "{Device} is out of order!",
|
||||
"translation": "",
|
||||
"comment": "This comment wins.\n",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Device",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "device"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "{Miles} miles traveled ({Miles_1})",
|
||||
"message": "{Miles} miles traveled ({Miles_1})",
|
||||
"translation": "",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Miles",
|
||||
"string": "%.2[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
},
|
||||
{
|
||||
"id": "Miles_1",
|
||||
"string": "%[1]f",
|
||||
"type": "float64",
|
||||
"underlyingType": "float64",
|
||||
"argNum": 1,
|
||||
"expr": "miles"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
// 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 main
|
||||
|
||||
import "golang.org/x/text/message"
|
||||
|
||||
func main() {
|
||||
p := message.NewPrinter(message.MatchLanguage("en"))
|
||||
|
||||
// NOT EXTRACTED: strings passed to Println are not extracted.
|
||||
p.Println("Hello world!")
|
||||
|
||||
// NOT EXTRACTED: strings passed to Print are not extracted.
|
||||
p.Print("Hello world!\n")
|
||||
|
||||
// Extract and trim whitespace (TODO).
|
||||
p.Printf("Hello world!\n")
|
||||
|
||||
// NOT EXTRACTED: city is not used as a pattern or passed to %m.
|
||||
city := "Amsterdam"
|
||||
// This comment is extracted.
|
||||
p.Printf("Hello %s!\n", city)
|
||||
|
||||
person := "Sheila"
|
||||
place := "Zürich"
|
||||
|
||||
// Substitutions replaced by variable names.
|
||||
p.Printf("%s is visiting %s!\n",
|
||||
person, // The person of matter.
|
||||
place, // Place the person is visiting.
|
||||
)
|
||||
|
||||
pp := struct {
|
||||
Person string // The person of matter. // TODO: get this comment.
|
||||
Place string
|
||||
extra int
|
||||
}{
|
||||
person, place, 4,
|
||||
}
|
||||
|
||||
// extract will drop this comment in favor of the one below.
|
||||
p.Printf("%[1]s is visiting %[3]s!\n", // Field names are placeholders.
|
||||
pp.Person,
|
||||
pp.extra,
|
||||
pp.Place, // Place the person is visiting.
|
||||
)
|
||||
|
||||
// Numeric literal becomes placeholder.
|
||||
p.Printf("%d files remaining!", 2)
|
||||
|
||||
const n = 2
|
||||
|
||||
// Constant identifier becomes placeholder.
|
||||
p.Printf("%d more files remaining!", n)
|
||||
|
||||
// Infer better names from type names.
|
||||
type referralCode int
|
||||
|
||||
const c = referralCode(5)
|
||||
|
||||
// Use type name as placeholder.
|
||||
p.Printf("Use the following code for your discount: %d\n", c)
|
||||
|
||||
// Use constant name as message ID.
|
||||
const msgOutOfOrder = "%s is out of order!" // This comment wins.
|
||||
const device = "Soda machine"
|
||||
// This message has two IDs.
|
||||
p.Printf(msgOutOfOrder, device)
|
||||
|
||||
// Multiple substitutions for same argument.
|
||||
miles := 1.2345
|
||||
p.Printf("%.2[1]f miles traveled (%[1]f)", miles)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
// Copyright 2016 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.
|
||||
|
||||
// +build go1.10
|
||||
|
||||
package bidirule
|
||||
|
||||
func (t *Transformer) isFinal() bool {
|
||||
return t.state == ruleLTRFinal || t.state == ruleRTLFinal || t.state == ruleInitial
|
||||
}
|
||||
|
|
@ -0,0 +1,694 @@
|
|||
// Copyright 2016 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.
|
||||
|
||||
// +build go1.10
|
||||
|
||||
package bidirule
|
||||
|
||||
import (
|
||||
"golang.org/x/text/transform"
|
||||
"golang.org/x/text/unicode/bidi"
|
||||
)
|
||||
|
||||
var testCases = [][]ruleTest{
|
||||
// Go-specific rules.
|
||||
// Invalid UTF-8 is invalid.
|
||||
0: []ruleTest{{
|
||||
in: "",
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: "\x80",
|
||||
dir: bidi.LeftToRight,
|
||||
err: ErrInvalid,
|
||||
n: 0,
|
||||
}, {
|
||||
in: "\xcc",
|
||||
dir: bidi.LeftToRight,
|
||||
err: ErrInvalid,
|
||||
n: 0,
|
||||
}, {
|
||||
in: "abc\x80",
|
||||
dir: bidi.LeftToRight,
|
||||
err: ErrInvalid,
|
||||
n: 3,
|
||||
}, {
|
||||
in: "abc\xcc",
|
||||
dir: bidi.LeftToRight,
|
||||
err: ErrInvalid,
|
||||
n: 3,
|
||||
}, {
|
||||
in: "abc\xccdef",
|
||||
dir: bidi.LeftToRight,
|
||||
err: ErrInvalid,
|
||||
n: 3,
|
||||
}, {
|
||||
in: "\xccdef",
|
||||
dir: bidi.LeftToRight,
|
||||
err: ErrInvalid,
|
||||
n: 0,
|
||||
}, {
|
||||
in: strR + "\x80",
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: len(strR),
|
||||
}, {
|
||||
in: strR + "\xcc",
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: len(strR),
|
||||
}, {
|
||||
in: strAL + "\xcc" + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: len(strAL),
|
||||
}, {
|
||||
in: "\xcc" + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: 0,
|
||||
}},
|
||||
|
||||
// Rule 2.1: The first character must be a character with Bidi property L,
|
||||
// R, or AL. If it has the R or AL property, it is an RTL label; if it has
|
||||
// the L property, it is an LTR label.
|
||||
1: []ruleTest{{
|
||||
in: strL,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAN,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strEN,
|
||||
dir: bidi.LeftToRight,
|
||||
err: ErrInvalid,
|
||||
n: len(strEN),
|
||||
}, {
|
||||
in: strES,
|
||||
dir: bidi.LeftToRight,
|
||||
err: ErrInvalid,
|
||||
n: len(strES),
|
||||
}, {
|
||||
in: strET,
|
||||
dir: bidi.LeftToRight,
|
||||
err: ErrInvalid,
|
||||
n: len(strET),
|
||||
}, {
|
||||
in: strCS,
|
||||
dir: bidi.LeftToRight,
|
||||
err: ErrInvalid,
|
||||
n: len(strCS),
|
||||
}, {
|
||||
in: strNSM,
|
||||
dir: bidi.LeftToRight,
|
||||
err: ErrInvalid,
|
||||
n: len(strNSM),
|
||||
}, {
|
||||
in: strBN,
|
||||
dir: bidi.LeftToRight,
|
||||
err: ErrInvalid,
|
||||
n: len(strBN),
|
||||
}, {
|
||||
in: strB,
|
||||
dir: bidi.LeftToRight,
|
||||
err: ErrInvalid,
|
||||
n: len(strB),
|
||||
}, {
|
||||
in: strS,
|
||||
dir: bidi.LeftToRight,
|
||||
err: ErrInvalid,
|
||||
n: len(strS),
|
||||
}, {
|
||||
in: strWS,
|
||||
dir: bidi.LeftToRight,
|
||||
err: ErrInvalid,
|
||||
n: len(strWS),
|
||||
}, {
|
||||
in: strON,
|
||||
dir: bidi.LeftToRight,
|
||||
err: ErrInvalid,
|
||||
n: len(strON),
|
||||
}, {
|
||||
in: strEN + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: 3,
|
||||
}, {
|
||||
in: strES + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: 2,
|
||||
}, {
|
||||
in: strET + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: 1,
|
||||
}, {
|
||||
in: strCS + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: 1,
|
||||
}, {
|
||||
in: strNSM + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: 2,
|
||||
}, {
|
||||
in: strBN + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: 3,
|
||||
}, {
|
||||
in: strB + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: 3,
|
||||
}, {
|
||||
in: strS + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: 1,
|
||||
}, {
|
||||
in: strWS + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: 1,
|
||||
}, {
|
||||
in: strON + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: 1,
|
||||
}},
|
||||
|
||||
// Rule 2.2: In an RTL label, only characters with the Bidi properties R,
|
||||
// AL, AN, EN, ES, CS, ET, ON, BN, or NSM are allowed.
|
||||
2: []ruleTest{{
|
||||
in: strR + strR + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strAL + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strAN + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strEN + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strES + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strCS + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strET + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strON + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strBN + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strNSM + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strL + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strR + strB + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strR + strS + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strR + strWS + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strR + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strAL + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strAN + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strEN + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strES + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strCS + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strET + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strON + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strBN + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strNSM + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strL + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strB + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strS + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strWS + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL),
|
||||
err: ErrInvalid,
|
||||
}},
|
||||
|
||||
// Rule 2.3: In an RTL label, the end of the label must be a character with
|
||||
// Bidi property R, AL, EN, or AN, followed by zero or more characters with
|
||||
// Bidi property NSM.
|
||||
3: []ruleTest{{
|
||||
in: strR + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strAL + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strEN + strNSM + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strAN,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strES + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR + strES + strNSM),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strR + strCS + strNSM + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR + strCS + strNSM + strNSM),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strR + strET,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR + strET),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strR + strON + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR + strON + strNSM),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strR + strBN + strNSM + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR + strBN + strNSM + strNSM),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strR + strL + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strR + strB + strNSM + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strR + strS,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strR + strWS,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strAL + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strEN + strNSM + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strAN,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strES + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL + strES + strNSM),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strCS + strNSM + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL + strCS + strNSM + strNSM),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strET,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL + strET),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strON + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL + strON + strNSM),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strBN + strNSM + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL + strBN + strNSM + strNSM),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strL + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strB + strNSM + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strS,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strWS,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL),
|
||||
err: ErrInvalid,
|
||||
}},
|
||||
|
||||
// Rule 2.4: In an RTL label, if an EN is present, no AN may be present,
|
||||
// and vice versa.
|
||||
4: []ruleTest{{
|
||||
in: strR + strEN + strAN,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR + strEN),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strR + strAN + strEN + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR + strAN),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strEN + strAN,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL + strEN),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strAN + strEN + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL + strAN),
|
||||
err: ErrInvalid,
|
||||
}},
|
||||
|
||||
// Rule 2.5: In an LTR label, only characters with the Bidi properties L,
|
||||
// EN, ES, CS, ET, ON, BN, or NSM are allowed.
|
||||
5: []ruleTest{{
|
||||
in: strL + strL + strL,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strEN + strL,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strES + strL,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strCS + strL,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strET + strL,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strON + strL,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strBN + strL,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strNSM + strL,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strR + strL,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strAL + strL,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strAN + strL,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strB + strL,
|
||||
dir: bidi.LeftToRight,
|
||||
n: len(strL + strB + strL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strB + strL + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL + strB + strL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strS + strL,
|
||||
dir: bidi.LeftToRight,
|
||||
n: len(strL + strS + strL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strS + strL + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL + strS + strL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strWS + strL,
|
||||
dir: bidi.LeftToRight,
|
||||
n: len(strL + strWS + strL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strWS + strL + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL + strWS + strL),
|
||||
err: ErrInvalid,
|
||||
}},
|
||||
|
||||
// Rule 2.6: In an LTR label, the end of the label must be a character with
|
||||
// Bidi property L or EN, followed by zero or more characters with Bidi
|
||||
// property NSM.
|
||||
6: []ruleTest{{
|
||||
in: strL,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strNSM,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strNSM + strNSM,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strEN,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strEN + strNSM,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strEN + strNSM + strNSM,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strES,
|
||||
dir: bidi.LeftToRight,
|
||||
n: len(strL + strES),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strES + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL + strES),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strCS,
|
||||
dir: bidi.LeftToRight,
|
||||
n: len(strL + strCS),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strCS + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL + strCS),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strET,
|
||||
dir: bidi.LeftToRight,
|
||||
n: len(strL + strET),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strET + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL + strET),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strON,
|
||||
dir: bidi.LeftToRight,
|
||||
n: len(strL + strON),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strON + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL + strON),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strBN,
|
||||
dir: bidi.LeftToRight,
|
||||
n: len(strL + strBN),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strBN + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL + strBN),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strAN,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strB,
|
||||
dir: bidi.LeftToRight,
|
||||
n: len(strL + strB),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strB + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL + strB),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strS,
|
||||
dir: bidi.LeftToRight,
|
||||
n: len(strL + strS),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strS + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL + strS),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strWS,
|
||||
dir: bidi.LeftToRight,
|
||||
n: len(strL + strWS),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strWS + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL + strWS),
|
||||
err: ErrInvalid,
|
||||
}},
|
||||
|
||||
// Incremental processing.
|
||||
9: []ruleTest{{
|
||||
in: "e\u0301", // é
|
||||
dir: bidi.LeftToRight,
|
||||
|
||||
pSrc: 2,
|
||||
nSrc: 1,
|
||||
err0: transform.ErrShortSrc,
|
||||
}, {
|
||||
in: "e\u1000f", // é
|
||||
dir: bidi.LeftToRight,
|
||||
|
||||
pSrc: 3,
|
||||
nSrc: 1,
|
||||
err0: transform.ErrShortSrc,
|
||||
}, {
|
||||
// Remain invalid once invalid.
|
||||
in: strR + "ab",
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR),
|
||||
err: ErrInvalid,
|
||||
|
||||
pSrc: len(strR) + 1,
|
||||
nSrc: len(strR),
|
||||
err0: ErrInvalid,
|
||||
}, {
|
||||
// Short destination
|
||||
in: "abcdefghij",
|
||||
dir: bidi.LeftToRight,
|
||||
|
||||
pSrc: 10,
|
||||
szDst: 5,
|
||||
nSrc: 5,
|
||||
err0: transform.ErrShortDst,
|
||||
}, {
|
||||
in: "\U000102f7",
|
||||
dir: bidi.LeftToRight,
|
||||
n: len("\U000102f7"),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
// Short destination splitting input rune
|
||||
in: "e\u0301",
|
||||
dir: bidi.LeftToRight,
|
||||
|
||||
pSrc: 3,
|
||||
szDst: 2,
|
||||
nSrc: 1,
|
||||
err0: transform.ErrShortDst,
|
||||
}, {
|
||||
// Unicode 10.0.0 IDNA test string.
|
||||
in: "FAX\u2a77\U0001d186",
|
||||
dir: bidi.LeftToRight,
|
||||
n: len("FAX\u2a77\U0001d186"),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: "\x80\u0660",
|
||||
dir: bidi.RightToLeft,
|
||||
n: 0,
|
||||
err: ErrInvalid,
|
||||
}},
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright 2016 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.
|
||||
|
||||
// +build !go1.10
|
||||
|
||||
package bidirule
|
||||
|
||||
func (t *Transformer) isFinal() bool {
|
||||
if !t.isRTL() {
|
||||
return true
|
||||
}
|
||||
return t.state == ruleLTRFinal || t.state == ruleRTLFinal || t.state == ruleInitial
|
||||
}
|
||||
|
|
@ -0,0 +1,668 @@
|
|||
// Copyright 2016 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.
|
||||
|
||||
// +build !go1.10
|
||||
|
||||
package bidirule
|
||||
|
||||
import (
|
||||
"golang.org/x/text/transform"
|
||||
"golang.org/x/text/unicode/bidi"
|
||||
)
|
||||
|
||||
var testCases = [][]ruleTest{
|
||||
// Go-specific rules.
|
||||
// Invalid UTF-8 is invalid.
|
||||
0: []ruleTest{{
|
||||
in: "",
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: "\x80",
|
||||
dir: bidi.LeftToRight,
|
||||
err: ErrInvalid,
|
||||
n: 0,
|
||||
}, {
|
||||
in: "\xcc",
|
||||
dir: bidi.LeftToRight,
|
||||
err: ErrInvalid,
|
||||
n: 0,
|
||||
}, {
|
||||
in: "abc\x80",
|
||||
dir: bidi.LeftToRight,
|
||||
err: ErrInvalid,
|
||||
n: 3,
|
||||
}, {
|
||||
in: "abc\xcc",
|
||||
dir: bidi.LeftToRight,
|
||||
err: ErrInvalid,
|
||||
n: 3,
|
||||
}, {
|
||||
in: "abc\xccdef",
|
||||
dir: bidi.LeftToRight,
|
||||
err: ErrInvalid,
|
||||
n: 3,
|
||||
}, {
|
||||
in: "\xccdef",
|
||||
dir: bidi.LeftToRight,
|
||||
err: ErrInvalid,
|
||||
n: 0,
|
||||
}, {
|
||||
in: strR + "\x80",
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: len(strR),
|
||||
}, {
|
||||
in: strR + "\xcc",
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: len(strR),
|
||||
}, {
|
||||
in: strAL + "\xcc" + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: len(strAL),
|
||||
}, {
|
||||
in: "\xcc" + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: 0,
|
||||
}},
|
||||
|
||||
// Rule 2.1: The first character must be a character with Bidi property L,
|
||||
// R, or AL. If it has the R or AL property, it is an RTL label; if it has
|
||||
// the L property, it is an LTR label.
|
||||
1: []ruleTest{{
|
||||
in: strL,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAN,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strEN,
|
||||
dir: bidi.LeftToRight,
|
||||
err: nil, // not an RTL string
|
||||
}, {
|
||||
in: strES,
|
||||
dir: bidi.LeftToRight,
|
||||
err: nil, // not an RTL string
|
||||
}, {
|
||||
in: strET,
|
||||
dir: bidi.LeftToRight,
|
||||
err: nil, // not an RTL string
|
||||
}, {
|
||||
in: strCS,
|
||||
dir: bidi.LeftToRight,
|
||||
err: nil, // not an RTL string
|
||||
}, {
|
||||
in: strNSM,
|
||||
dir: bidi.LeftToRight,
|
||||
err: nil, // not an RTL string
|
||||
}, {
|
||||
in: strBN,
|
||||
dir: bidi.LeftToRight,
|
||||
err: nil, // not an RTL string
|
||||
}, {
|
||||
in: strB,
|
||||
dir: bidi.LeftToRight,
|
||||
err: nil, // not an RTL string
|
||||
}, {
|
||||
in: strS,
|
||||
dir: bidi.LeftToRight,
|
||||
err: nil, // not an RTL string
|
||||
}, {
|
||||
in: strWS,
|
||||
dir: bidi.LeftToRight,
|
||||
err: nil, // not an RTL string
|
||||
}, {
|
||||
in: strON,
|
||||
dir: bidi.LeftToRight,
|
||||
err: nil, // not an RTL string
|
||||
}, {
|
||||
in: strEN + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: 3,
|
||||
}, {
|
||||
in: strES + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: 2,
|
||||
}, {
|
||||
in: strET + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: 1,
|
||||
}, {
|
||||
in: strCS + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: 1,
|
||||
}, {
|
||||
in: strNSM + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: 2,
|
||||
}, {
|
||||
in: strBN + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: 3,
|
||||
}, {
|
||||
in: strB + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: 3,
|
||||
}, {
|
||||
in: strS + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: 1,
|
||||
}, {
|
||||
in: strWS + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: 1,
|
||||
}, {
|
||||
in: strON + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
err: ErrInvalid,
|
||||
n: 1,
|
||||
}},
|
||||
|
||||
// Rule 2.2: In an RTL label, only characters with the Bidi properties R,
|
||||
// AL, AN, EN, ES, CS, ET, ON, BN, or NSM are allowed.
|
||||
2: []ruleTest{{
|
||||
in: strR + strR + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strAL + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strAN + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strEN + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strES + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strCS + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strET + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strON + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strBN + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strNSM + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strL + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strR + strB + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strR + strS + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strR + strWS + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strR + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strAL + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strAN + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strEN + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strES + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strCS + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strET + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strON + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strBN + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strNSM + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strL + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strB + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strS + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strWS + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL),
|
||||
err: ErrInvalid,
|
||||
}},
|
||||
|
||||
// Rule 2.3: In an RTL label, the end of the label must be a character with
|
||||
// Bidi property R, AL, EN, or AN, followed by zero or more characters with
|
||||
// Bidi property NSM.
|
||||
3: []ruleTest{{
|
||||
in: strR + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strAL + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strEN + strNSM + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strAN,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strR + strES + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR + strES + strNSM),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strR + strCS + strNSM + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR + strCS + strNSM + strNSM),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strR + strET,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR + strET),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strR + strON + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR + strON + strNSM),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strR + strBN + strNSM + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR + strBN + strNSM + strNSM),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strR + strL + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strR + strB + strNSM + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strR + strS,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strR + strWS,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strAL + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strEN + strNSM + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strAN,
|
||||
dir: bidi.RightToLeft,
|
||||
}, {
|
||||
in: strAL + strES + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL + strES + strNSM),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strCS + strNSM + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL + strCS + strNSM + strNSM),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strET,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL + strET),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strON + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL + strON + strNSM),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strBN + strNSM + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL + strBN + strNSM + strNSM),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strL + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strB + strNSM + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strS,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strWS,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL),
|
||||
err: ErrInvalid,
|
||||
}},
|
||||
|
||||
// Rule 2.4: In an RTL label, if an EN is present, no AN may be present,
|
||||
// and vice versa.
|
||||
4: []ruleTest{{
|
||||
in: strR + strEN + strAN,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR + strEN),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strR + strAN + strEN + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR + strAN),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strEN + strAN,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL + strEN),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strAL + strAN + strEN + strNSM,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strAL + strAN),
|
||||
err: ErrInvalid,
|
||||
}},
|
||||
|
||||
// Rule 2.5: In an LTR label, only characters with the Bidi properties L,
|
||||
// EN, ES, CS, ET, ON, BN, or NSM are allowed.
|
||||
5: []ruleTest{{
|
||||
in: strL + strL + strL,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strEN + strL,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strES + strL,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strCS + strL,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strET + strL,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strON + strL,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strBN + strL,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strNSM + strL,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strR + strL,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strAL + strL,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strAN + strL,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strB + strL,
|
||||
dir: bidi.LeftToRight,
|
||||
n: len(strL + strAN + strL),
|
||||
err: nil,
|
||||
}, {
|
||||
in: strL + strB + strL + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL + strB + strL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strS + strL,
|
||||
dir: bidi.LeftToRight,
|
||||
n: len(strL + strS + strL),
|
||||
err: nil,
|
||||
}, {
|
||||
in: strL + strS + strL + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL + strS + strL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strWS + strL,
|
||||
dir: bidi.LeftToRight,
|
||||
n: len(strL + strWS + strL),
|
||||
err: nil,
|
||||
}, {
|
||||
in: strL + strWS + strL + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL + strWS + strL),
|
||||
err: ErrInvalid,
|
||||
}},
|
||||
|
||||
// Rule 2.6: In an LTR label, the end of the label must be a character with
|
||||
// Bidi property L or EN, followed by zero or more characters with Bidi
|
||||
// property NSM.
|
||||
6: []ruleTest{{
|
||||
in: strL,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strNSM,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strNSM + strNSM,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strEN,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strEN + strNSM,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strEN + strNSM + strNSM,
|
||||
dir: bidi.LeftToRight,
|
||||
}, {
|
||||
in: strL + strES,
|
||||
dir: bidi.LeftToRight,
|
||||
n: len(strL + strES),
|
||||
err: nil,
|
||||
}, {
|
||||
in: strL + strES + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL + strES),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strCS,
|
||||
dir: bidi.LeftToRight,
|
||||
n: len(strL + strCS),
|
||||
err: nil,
|
||||
}, {
|
||||
in: strL + strCS + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL + strCS),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strET,
|
||||
dir: bidi.LeftToRight,
|
||||
n: len(strL + strET),
|
||||
err: nil,
|
||||
}, {
|
||||
in: strL + strET + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL + strET),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strON,
|
||||
dir: bidi.LeftToRight,
|
||||
n: len(strL + strON),
|
||||
err: nil,
|
||||
}, {
|
||||
in: strL + strON + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL + strON),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strBN,
|
||||
dir: bidi.LeftToRight,
|
||||
n: len(strL + strBN),
|
||||
err: nil,
|
||||
}, {
|
||||
in: strL + strBN + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL + strBN),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strAL,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strAN,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strB,
|
||||
dir: bidi.LeftToRight,
|
||||
n: len(strL + strB),
|
||||
err: nil,
|
||||
}, {
|
||||
in: strL + strB + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL + strB),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strB,
|
||||
dir: bidi.LeftToRight,
|
||||
n: len(strL + strB),
|
||||
err: nil,
|
||||
}, {
|
||||
in: strL + strB + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL + strB),
|
||||
err: ErrInvalid,
|
||||
}, {
|
||||
in: strL + strB,
|
||||
dir: bidi.LeftToRight,
|
||||
n: len(strL + strB),
|
||||
err: nil,
|
||||
}, {
|
||||
in: strL + strB + strR,
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strL + strB),
|
||||
err: ErrInvalid,
|
||||
}},
|
||||
|
||||
// Incremental processing.
|
||||
9: []ruleTest{{
|
||||
in: "e\u0301", // é
|
||||
dir: bidi.LeftToRight,
|
||||
|
||||
pSrc: 2,
|
||||
nSrc: 1,
|
||||
err0: transform.ErrShortSrc,
|
||||
}, {
|
||||
in: "e\u1000f", // é
|
||||
dir: bidi.LeftToRight,
|
||||
|
||||
pSrc: 3,
|
||||
nSrc: 1,
|
||||
err0: transform.ErrShortSrc,
|
||||
}, {
|
||||
// Remain invalid once invalid.
|
||||
in: strR + "ab",
|
||||
dir: bidi.RightToLeft,
|
||||
n: len(strR),
|
||||
err: ErrInvalid,
|
||||
|
||||
pSrc: len(strR) + 1,
|
||||
nSrc: len(strR),
|
||||
err0: ErrInvalid,
|
||||
}, {
|
||||
// Short destination
|
||||
in: "abcdefghij",
|
||||
dir: bidi.LeftToRight,
|
||||
|
||||
pSrc: 10,
|
||||
szDst: 5,
|
||||
nSrc: 5,
|
||||
err0: transform.ErrShortDst,
|
||||
}, {
|
||||
// Short destination splitting input rune
|
||||
in: "e\u0301",
|
||||
dir: bidi.LeftToRight,
|
||||
|
||||
pSrc: 3,
|
||||
szDst: 2,
|
||||
nSrc: 1,
|
||||
err0: transform.ErrShortDst,
|
||||
}},
|
||||
}
|
||||
|
|
@ -0,0 +1,244 @@
|
|||
// Copyright 2015 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.
|
||||
|
||||
// +build go1.10
|
||||
|
||||
package precis
|
||||
|
||||
import (
|
||||
"golang.org/x/text/secure/bidirule"
|
||||
)
|
||||
|
||||
var enforceTestCases = []struct {
|
||||
name string
|
||||
p *Profile
|
||||
cases []testCase
|
||||
}{
|
||||
{"Basic", NewFreeform(), []testCase{
|
||||
{"e\u0301\u031f", "\u00e9\u031f", nil}, // normalize
|
||||
}},
|
||||
|
||||
{"Context Rule 1", NewFreeform(), []testCase{
|
||||
// Rule 1: zero-width non-joiner (U+200C)
|
||||
// From RFC:
|
||||
// False
|
||||
// If Canonical_Combining_Class(Before(cp)) .eq. Virama Then True;
|
||||
// If RegExpMatch((Joining_Type:{L,D})(Joining_Type:T)*\u200C
|
||||
// (Joining_Type:T)*(Joining_Type:{R,D})) Then True;
|
||||
//
|
||||
// Example runes for different joining types:
|
||||
// Join L: U+A872; PHAGS-PA SUPERFIXED LETTER RA
|
||||
// Join D: U+062C; HAH WITH DOT BELOW
|
||||
// Join T: U+0610; ARABIC SIGN SALLALLAHOU ALAYHE WASSALLAM
|
||||
// Join R: U+0627; ALEF
|
||||
// Virama: U+0A4D; GURMUKHI SIGN VIRAMA
|
||||
// Virama and Join T: U+0ACD; GUJARATI SIGN VIRAMA
|
||||
{"\u200c", "", errContext},
|
||||
{"\u200ca", "", errContext},
|
||||
{"a\u200c", "", errContext},
|
||||
{"\u200c\u0627", "", errContext}, // missing JoinStart
|
||||
{"\u062c\u200c", "", errContext}, // missing JoinEnd
|
||||
{"\u0610\u200c\u0610\u0627", "", errContext}, // missing JoinStart
|
||||
{"\u062c\u0610\u200c\u0610", "", errContext}, // missing JoinEnd
|
||||
|
||||
// Variants of: D T* U+200c T* R
|
||||
{"\u062c\u200c\u0627", "\u062c\u200c\u0627", nil},
|
||||
{"\u062c\u0610\u200c\u0610\u0627", "\u062c\u0610\u200c\u0610\u0627", nil},
|
||||
{"\u062c\u0610\u0610\u200c\u0610\u0610\u0627", "\u062c\u0610\u0610\u200c\u0610\u0610\u0627", nil},
|
||||
{"\u062c\u0610\u200c\u0627", "\u062c\u0610\u200c\u0627", nil},
|
||||
{"\u062c\u200c\u0610\u0627", "\u062c\u200c\u0610\u0627", nil},
|
||||
|
||||
// Variants of: L T* U+200c T* D
|
||||
{"\ua872\u200c\u062c", "\ua872\u200c\u062c", nil},
|
||||
{"\ua872\u0610\u200c\u0610\u062c", "\ua872\u0610\u200c\u0610\u062c", nil},
|
||||
{"\ua872\u0610\u0610\u200c\u0610\u0610\u062c", "\ua872\u0610\u0610\u200c\u0610\u0610\u062c", nil},
|
||||
{"\ua872\u0610\u200c\u062c", "\ua872\u0610\u200c\u062c", nil},
|
||||
{"\ua872\u200c\u0610\u062c", "\ua872\u200c\u0610\u062c", nil},
|
||||
|
||||
// Virama
|
||||
{"\u0a4d\u200c", "\u0a4d\u200c", nil},
|
||||
{"\ua872\u0a4d\u200c", "\ua872\u0a4d\u200c", nil},
|
||||
{"\ua872\u0a4d\u0610\u200c", "", errContext},
|
||||
{"\ua872\u0a4d\u0610\u200c", "", errContext},
|
||||
|
||||
{"\u0acd\u200c", "\u0acd\u200c", nil},
|
||||
{"\ua872\u0acd\u200c", "\ua872\u0acd\u200c", nil},
|
||||
{"\ua872\u0acd\u0610\u200c", "", errContext},
|
||||
{"\ua872\u0acd\u0610\u200c", "", errContext},
|
||||
|
||||
// Using Virama as join T
|
||||
{"\ua872\u0acd\u200c\u062c", "\ua872\u0acd\u200c\u062c", nil},
|
||||
{"\ua872\u200c\u0acd\u062c", "\ua872\u200c\u0acd\u062c", nil},
|
||||
}},
|
||||
|
||||
{"Context Rule 2", NewFreeform(), []testCase{
|
||||
// Rule 2: zero-width joiner (U+200D)
|
||||
{"\u200d", "", errContext},
|
||||
{"\u200da", "", errContext},
|
||||
{"a\u200d", "", errContext},
|
||||
|
||||
{"\u0a4d\u200d", "\u0a4d\u200d", nil},
|
||||
{"\ua872\u0a4d\u200d", "\ua872\u0a4d\u200d", nil},
|
||||
{"\u0a4da\u200d", "", errContext},
|
||||
}},
|
||||
|
||||
{"Context Rule 3", NewFreeform(), []testCase{
|
||||
// Rule 3: middle dot
|
||||
{"·", "", errContext},
|
||||
{"l·", "", errContext},
|
||||
{"·l", "", errContext},
|
||||
{"a·", "", errContext},
|
||||
{"l·a", "", errContext},
|
||||
{"a·a", "", errContext},
|
||||
{"l·l", "l·l", nil},
|
||||
{"al·la", "al·la", nil},
|
||||
}},
|
||||
|
||||
{"Context Rule 4", NewFreeform(), []testCase{
|
||||
// Rule 4: Greek lower numeral U+0375
|
||||
{"͵", "", errContext},
|
||||
{"͵a", "", errContext},
|
||||
{"α͵", "", errContext},
|
||||
{"͵α", "͵α", nil},
|
||||
{"α͵α", "α͵α", nil},
|
||||
{"͵͵α", "͵͵α", nil}, // The numeric sign is itself Greek.
|
||||
{"α͵͵α", "α͵͵α", nil},
|
||||
{"α͵͵", "", errContext},
|
||||
{"α͵͵a", "", errContext},
|
||||
}},
|
||||
|
||||
{"Context Rule 5+6", NewFreeform(), []testCase{
|
||||
// Rule 5+6: Hebrew preceding
|
||||
// U+05f3: Geresh
|
||||
{"׳", "", errContext},
|
||||
{"׳ה", "", errContext},
|
||||
{"a׳b", "", errContext},
|
||||
{"ש׳", "ש׳", nil}, // U+05e9 U+05f3
|
||||
{"ש׳׳׳", "ש׳׳׳", nil}, // U+05e9 U+05f3
|
||||
|
||||
// U+05f4: Gershayim
|
||||
{"״", "", errContext},
|
||||
{"״ה", "", errContext},
|
||||
{"a״b", "", errContext},
|
||||
{"ש״", "ש״", nil}, // U+05e9 U+05f4
|
||||
{"ש״״״", "ש״״״", nil}, // U+05e9 U+05f4
|
||||
{"aש״״״", "aש״״״", nil}, // U+05e9 U+05f4
|
||||
}},
|
||||
|
||||
{"Context Rule 7", NewFreeform(), []testCase{
|
||||
// Rule 7: Katakana middle Dot
|
||||
{"・", "", errContext},
|
||||
{"abc・", "", errContext},
|
||||
{"・def", "", errContext},
|
||||
{"abc・def", "", errContext},
|
||||
{"aヅc・def", "aヅc・def", nil},
|
||||
{"abc・dぶf", "abc・dぶf", nil},
|
||||
{"⺐bc・def", "⺐bc・def", nil},
|
||||
}},
|
||||
|
||||
{"Context Rule 8+9", NewFreeform(), []testCase{
|
||||
// Rule 8+9: Arabic Indic Digit
|
||||
{"١٢٣٤٥۶", "", errContext},
|
||||
{"۱۲۳۴۵٦", "", errContext},
|
||||
{"١٢٣٤٥", "١٢٣٤٥", nil},
|
||||
{"۱۲۳۴۵", "۱۲۳۴۵", nil},
|
||||
}},
|
||||
|
||||
{"Nickname", Nickname, []testCase{
|
||||
{" Swan of Avon ", "Swan of Avon", nil},
|
||||
{"", "", errEmptyString},
|
||||
{" ", "", errEmptyString},
|
||||
{" ", "", errEmptyString},
|
||||
{"a\u00A0a\u1680a\u2000a\u2001a\u2002a\u2003a\u2004a\u2005a\u2006a\u2007a\u2008a\u2009a\u200Aa\u202Fa\u205Fa\u3000a", "a a a a a a a a a a a a a a a a a", nil},
|
||||
{"Foo", "Foo", nil},
|
||||
{"foo", "foo", nil},
|
||||
{"Foo Bar", "Foo Bar", nil},
|
||||
{"foo bar", "foo bar", nil},
|
||||
{"\u03A3", "\u03A3", nil},
|
||||
{"\u03C3", "\u03C3", nil},
|
||||
// Greek final sigma is left as is (do not fold!)
|
||||
{"\u03C2", "\u03C2", nil},
|
||||
{"\u265A", "♚", nil},
|
||||
{"Richard \u2163", "Richard IV", nil},
|
||||
{"\u212B", "Å", nil},
|
||||
{"\uFB00", "ff", nil}, // because of NFKC
|
||||
{"שa", "שa", nil}, // no bidi rule
|
||||
{"동일조건변경허락", "동일조건변경허락", nil},
|
||||
}},
|
||||
{"OpaqueString", OpaqueString, []testCase{
|
||||
{" Swan of Avon ", " Swan of Avon ", nil},
|
||||
{"", "", errEmptyString},
|
||||
{" ", " ", nil},
|
||||
{" ", " ", nil},
|
||||
{"a\u00A0a\u1680a\u2000a\u2001a\u2002a\u2003a\u2004a\u2005a\u2006a\u2007a\u2008a\u2009a\u200Aa\u202Fa\u205Fa\u3000a", "a a a a a a a a a a a a a a a a a", nil},
|
||||
{"Foo", "Foo", nil},
|
||||
{"foo", "foo", nil},
|
||||
{"Foo Bar", "Foo Bar", nil},
|
||||
{"foo bar", "foo bar", nil},
|
||||
{"\u03C3", "\u03C3", nil},
|
||||
{"Richard \u2163", "Richard \u2163", nil},
|
||||
{"\u212B", "Å", nil},
|
||||
{"Jack of \u2666s", "Jack of \u2666s", nil},
|
||||
{"my cat is a \u0009by", "", errDisallowedRune},
|
||||
{"שa", "שa", nil}, // no bidi rule
|
||||
}},
|
||||
{"UsernameCaseMapped", UsernameCaseMapped, []testCase{
|
||||
// TODO: Should this work?
|
||||
// {UsernameCaseMapped, "", "", errDisallowedRune},
|
||||
{"juliet@example.com", "juliet@example.com", nil},
|
||||
{"fussball", "fussball", nil},
|
||||
{"fu\u00DFball", "fu\u00DFball", nil},
|
||||
{"\u03C0", "\u03C0", nil},
|
||||
{"\u03A3", "\u03C3", nil},
|
||||
{"\u03C3", "\u03C3", nil},
|
||||
// Greek final sigma is left as is (do not fold!)
|
||||
{"\u03C2", "\u03C2", nil},
|
||||
{"\u0049", "\u0069", nil},
|
||||
{"\u0049", "\u0069", nil},
|
||||
{"\u03D2", "", errDisallowedRune},
|
||||
{"\u03B0", "\u03B0", nil},
|
||||
{"foo bar", "", errDisallowedRune},
|
||||
{"♚", "", bidirule.ErrInvalid},
|
||||
{"\u007E", "~", nil},
|
||||
{"a", "a", nil},
|
||||
{"!", "!", nil},
|
||||
{"²", "", bidirule.ErrInvalid},
|
||||
{"\t", "", errDisallowedRune},
|
||||
{"\n", "", errDisallowedRune},
|
||||
{"\u26D6", "", bidirule.ErrInvalid},
|
||||
{"\u26FF", "", bidirule.ErrInvalid},
|
||||
{"\uFB00", "", errDisallowedRune},
|
||||
{"\u1680", "", bidirule.ErrInvalid},
|
||||
{" ", "", errDisallowedRune},
|
||||
{" ", "", errDisallowedRune},
|
||||
{"\u01C5", "", errDisallowedRune},
|
||||
{"\u16EE", "", errDisallowedRune}, // Nl RUNIC ARLAUG SYMBOL
|
||||
{"\u0488", "", bidirule.ErrInvalid}, // Me COMBINING CYRILLIC HUNDRED THOUSANDS SIGN
|
||||
{"\u212B", "\u00e5", nil}, // Angstrom sign, NFC -> U+00E5
|
||||
{"A\u030A", "å", nil}, // A + ring
|
||||
{"\u00C5", "å", nil}, // A with ring
|
||||
{"\u00E7", "ç", nil}, // c cedille
|
||||
{"\u0063\u0327", "ç", nil}, // c + cedille
|
||||
{"\u0158", "ř", nil},
|
||||
{"\u0052\u030C", "ř", nil},
|
||||
|
||||
{"\u1E61", "\u1E61", nil}, // LATIN SMALL LETTER S WITH DOT ABOVE
|
||||
|
||||
// Confusable characters ARE allowed and should NOT be mapped.
|
||||
{"\u0410", "\u0430", nil}, // CYRILLIC CAPITAL LETTER A
|
||||
|
||||
// Full width should be mapped to the canonical decomposition.
|
||||
{"AB", "ab", nil},
|
||||
{"שc", "", bidirule.ErrInvalid}, // bidi rule
|
||||
|
||||
}},
|
||||
{"UsernameCasePreserved", UsernameCasePreserved, []testCase{
|
||||
{"ABC", "ABC", nil},
|
||||
{"AB", "AB", nil},
|
||||
{"שc", "", bidirule.ErrInvalid}, // bidi rule
|
||||
{"\uFB00", "", errDisallowedRune},
|
||||
{"\u212B", "\u00c5", nil}, // Angstrom sign, NFC -> U+00E5
|
||||
{"ẛ", "", errDisallowedRune}, // LATIN SMALL LETTER LONG S WITH DOT ABOVE
|
||||
}},
|
||||
}
|
||||
|
|
@ -0,0 +1,244 @@
|
|||
// Copyright 2015 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.
|
||||
|
||||
// +build !go1.10
|
||||
|
||||
package precis
|
||||
|
||||
import (
|
||||
"golang.org/x/text/secure/bidirule"
|
||||
)
|
||||
|
||||
var enforceTestCases = []struct {
|
||||
name string
|
||||
p *Profile
|
||||
cases []testCase
|
||||
}{
|
||||
{"Basic", NewFreeform(), []testCase{
|
||||
{"e\u0301\u031f", "\u00e9\u031f", nil}, // normalize
|
||||
}},
|
||||
|
||||
{"Context Rule 1", NewFreeform(), []testCase{
|
||||
// Rule 1: zero-width non-joiner (U+200C)
|
||||
// From RFC:
|
||||
// False
|
||||
// If Canonical_Combining_Class(Before(cp)) .eq. Virama Then True;
|
||||
// If RegExpMatch((Joining_Type:{L,D})(Joining_Type:T)*\u200C
|
||||
// (Joining_Type:T)*(Joining_Type:{R,D})) Then True;
|
||||
//
|
||||
// Example runes for different joining types:
|
||||
// Join L: U+A872; PHAGS-PA SUPERFIXED LETTER RA
|
||||
// Join D: U+062C; HAH WITH DOT BELOW
|
||||
// Join T: U+0610; ARABIC SIGN SALLALLAHOU ALAYHE WASSALLAM
|
||||
// Join R: U+0627; ALEF
|
||||
// Virama: U+0A4D; GURMUKHI SIGN VIRAMA
|
||||
// Virama and Join T: U+0ACD; GUJARATI SIGN VIRAMA
|
||||
{"\u200c", "", errContext},
|
||||
{"\u200ca", "", errContext},
|
||||
{"a\u200c", "", errContext},
|
||||
{"\u200c\u0627", "", errContext}, // missing JoinStart
|
||||
{"\u062c\u200c", "", errContext}, // missing JoinEnd
|
||||
{"\u0610\u200c\u0610\u0627", "", errContext}, // missing JoinStart
|
||||
{"\u062c\u0610\u200c\u0610", "", errContext}, // missing JoinEnd
|
||||
|
||||
// Variants of: D T* U+200c T* R
|
||||
{"\u062c\u200c\u0627", "\u062c\u200c\u0627", nil},
|
||||
{"\u062c\u0610\u200c\u0610\u0627", "\u062c\u0610\u200c\u0610\u0627", nil},
|
||||
{"\u062c\u0610\u0610\u200c\u0610\u0610\u0627", "\u062c\u0610\u0610\u200c\u0610\u0610\u0627", nil},
|
||||
{"\u062c\u0610\u200c\u0627", "\u062c\u0610\u200c\u0627", nil},
|
||||
{"\u062c\u200c\u0610\u0627", "\u062c\u200c\u0610\u0627", nil},
|
||||
|
||||
// Variants of: L T* U+200c T* D
|
||||
{"\ua872\u200c\u062c", "\ua872\u200c\u062c", nil},
|
||||
{"\ua872\u0610\u200c\u0610\u062c", "\ua872\u0610\u200c\u0610\u062c", nil},
|
||||
{"\ua872\u0610\u0610\u200c\u0610\u0610\u062c", "\ua872\u0610\u0610\u200c\u0610\u0610\u062c", nil},
|
||||
{"\ua872\u0610\u200c\u062c", "\ua872\u0610\u200c\u062c", nil},
|
||||
{"\ua872\u200c\u0610\u062c", "\ua872\u200c\u0610\u062c", nil},
|
||||
|
||||
// Virama
|
||||
{"\u0a4d\u200c", "\u0a4d\u200c", nil},
|
||||
{"\ua872\u0a4d\u200c", "\ua872\u0a4d\u200c", nil},
|
||||
{"\ua872\u0a4d\u0610\u200c", "", errContext},
|
||||
{"\ua872\u0a4d\u0610\u200c", "", errContext},
|
||||
|
||||
{"\u0acd\u200c", "\u0acd\u200c", nil},
|
||||
{"\ua872\u0acd\u200c", "\ua872\u0acd\u200c", nil},
|
||||
{"\ua872\u0acd\u0610\u200c", "", errContext},
|
||||
{"\ua872\u0acd\u0610\u200c", "", errContext},
|
||||
|
||||
// Using Virama as join T
|
||||
{"\ua872\u0acd\u200c\u062c", "\ua872\u0acd\u200c\u062c", nil},
|
||||
{"\ua872\u200c\u0acd\u062c", "\ua872\u200c\u0acd\u062c", nil},
|
||||
}},
|
||||
|
||||
{"Context Rule 2", NewFreeform(), []testCase{
|
||||
// Rule 2: zero-width joiner (U+200D)
|
||||
{"\u200d", "", errContext},
|
||||
{"\u200da", "", errContext},
|
||||
{"a\u200d", "", errContext},
|
||||
|
||||
{"\u0a4d\u200d", "\u0a4d\u200d", nil},
|
||||
{"\ua872\u0a4d\u200d", "\ua872\u0a4d\u200d", nil},
|
||||
{"\u0a4da\u200d", "", errContext},
|
||||
}},
|
||||
|
||||
{"Context Rule 3", NewFreeform(), []testCase{
|
||||
// Rule 3: middle dot
|
||||
{"·", "", errContext},
|
||||
{"l·", "", errContext},
|
||||
{"·l", "", errContext},
|
||||
{"a·", "", errContext},
|
||||
{"l·a", "", errContext},
|
||||
{"a·a", "", errContext},
|
||||
{"l·l", "l·l", nil},
|
||||
{"al·la", "al·la", nil},
|
||||
}},
|
||||
|
||||
{"Context Rule 4", NewFreeform(), []testCase{
|
||||
// Rule 4: Greek lower numeral U+0375
|
||||
{"͵", "", errContext},
|
||||
{"͵a", "", errContext},
|
||||
{"α͵", "", errContext},
|
||||
{"͵α", "͵α", nil},
|
||||
{"α͵α", "α͵α", nil},
|
||||
{"͵͵α", "͵͵α", nil}, // The numeric sign is itself Greek.
|
||||
{"α͵͵α", "α͵͵α", nil},
|
||||
{"α͵͵", "", errContext},
|
||||
{"α͵͵a", "", errContext},
|
||||
}},
|
||||
|
||||
{"Context Rule 5+6", NewFreeform(), []testCase{
|
||||
// Rule 5+6: Hebrew preceding
|
||||
// U+05f3: Geresh
|
||||
{"׳", "", errContext},
|
||||
{"׳ה", "", errContext},
|
||||
{"a׳b", "", errContext},
|
||||
{"ש׳", "ש׳", nil}, // U+05e9 U+05f3
|
||||
{"ש׳׳׳", "ש׳׳׳", nil}, // U+05e9 U+05f3
|
||||
|
||||
// U+05f4: Gershayim
|
||||
{"״", "", errContext},
|
||||
{"״ה", "", errContext},
|
||||
{"a״b", "", errContext},
|
||||
{"ש״", "ש״", nil}, // U+05e9 U+05f4
|
||||
{"ש״״״", "ש״״״", nil}, // U+05e9 U+05f4
|
||||
{"aש״״״", "aש״״״", nil}, // U+05e9 U+05f4
|
||||
}},
|
||||
|
||||
{"Context Rule 7", NewFreeform(), []testCase{
|
||||
// Rule 7: Katakana middle Dot
|
||||
{"・", "", errContext},
|
||||
{"abc・", "", errContext},
|
||||
{"・def", "", errContext},
|
||||
{"abc・def", "", errContext},
|
||||
{"aヅc・def", "aヅc・def", nil},
|
||||
{"abc・dぶf", "abc・dぶf", nil},
|
||||
{"⺐bc・def", "⺐bc・def", nil},
|
||||
}},
|
||||
|
||||
{"Context Rule 8+9", NewFreeform(), []testCase{
|
||||
// Rule 8+9: Arabic Indic Digit
|
||||
{"١٢٣٤٥۶", "", errContext},
|
||||
{"۱۲۳۴۵٦", "", errContext},
|
||||
{"١٢٣٤٥", "١٢٣٤٥", nil},
|
||||
{"۱۲۳۴۵", "۱۲۳۴۵", nil},
|
||||
}},
|
||||
|
||||
{"Nickname", Nickname, []testCase{
|
||||
{" Swan of Avon ", "Swan of Avon", nil},
|
||||
{"", "", errEmptyString},
|
||||
{" ", "", errEmptyString},
|
||||
{" ", "", errEmptyString},
|
||||
{"a\u00A0a\u1680a\u2000a\u2001a\u2002a\u2003a\u2004a\u2005a\u2006a\u2007a\u2008a\u2009a\u200Aa\u202Fa\u205Fa\u3000a", "a a a a a a a a a a a a a a a a a", nil},
|
||||
{"Foo", "Foo", nil},
|
||||
{"foo", "foo", nil},
|
||||
{"Foo Bar", "Foo Bar", nil},
|
||||
{"foo bar", "foo bar", nil},
|
||||
{"\u03A3", "\u03A3", nil},
|
||||
{"\u03C3", "\u03C3", nil},
|
||||
// Greek final sigma is left as is (do not fold!)
|
||||
{"\u03C2", "\u03C2", nil},
|
||||
{"\u265A", "♚", nil},
|
||||
{"Richard \u2163", "Richard IV", nil},
|
||||
{"\u212B", "Å", nil},
|
||||
{"\uFB00", "ff", nil}, // because of NFKC
|
||||
{"שa", "שa", nil}, // no bidi rule
|
||||
{"동일조건변경허락", "동일조건변경허락", nil},
|
||||
}},
|
||||
{"OpaqueString", OpaqueString, []testCase{
|
||||
{" Swan of Avon ", " Swan of Avon ", nil},
|
||||
{"", "", errEmptyString},
|
||||
{" ", " ", nil},
|
||||
{" ", " ", nil},
|
||||
{"a\u00A0a\u1680a\u2000a\u2001a\u2002a\u2003a\u2004a\u2005a\u2006a\u2007a\u2008a\u2009a\u200Aa\u202Fa\u205Fa\u3000a", "a a a a a a a a a a a a a a a a a", nil},
|
||||
{"Foo", "Foo", nil},
|
||||
{"foo", "foo", nil},
|
||||
{"Foo Bar", "Foo Bar", nil},
|
||||
{"foo bar", "foo bar", nil},
|
||||
{"\u03C3", "\u03C3", nil},
|
||||
{"Richard \u2163", "Richard \u2163", nil},
|
||||
{"\u212B", "Å", nil},
|
||||
{"Jack of \u2666s", "Jack of \u2666s", nil},
|
||||
{"my cat is a \u0009by", "", errDisallowedRune},
|
||||
{"שa", "שa", nil}, // no bidi rule
|
||||
}},
|
||||
{"UsernameCaseMapped", UsernameCaseMapped, []testCase{
|
||||
// TODO: Should this work?
|
||||
// {UsernameCaseMapped, "", "", errDisallowedRune},
|
||||
{"juliet@example.com", "juliet@example.com", nil},
|
||||
{"fussball", "fussball", nil},
|
||||
{"fu\u00DFball", "fu\u00DFball", nil},
|
||||
{"\u03C0", "\u03C0", nil},
|
||||
{"\u03A3", "\u03C3", nil},
|
||||
{"\u03C3", "\u03C3", nil},
|
||||
// Greek final sigma is left as is (do not fold!)
|
||||
{"\u03C2", "\u03C2", nil},
|
||||
{"\u0049", "\u0069", nil},
|
||||
{"\u0049", "\u0069", nil},
|
||||
{"\u03D2", "", errDisallowedRune},
|
||||
{"\u03B0", "\u03B0", nil},
|
||||
{"foo bar", "", errDisallowedRune},
|
||||
{"♚", "", errDisallowedRune},
|
||||
{"\u007E", "~", nil},
|
||||
{"a", "a", nil},
|
||||
{"!", "!", nil},
|
||||
{"²", "", errDisallowedRune},
|
||||
{"\t", "", errDisallowedRune},
|
||||
{"\n", "", errDisallowedRune},
|
||||
{"\u26D6", "", errDisallowedRune},
|
||||
{"\u26FF", "", errDisallowedRune},
|
||||
{"\uFB00", "", errDisallowedRune},
|
||||
{"\u1680", "", errDisallowedRune},
|
||||
{" ", "", errDisallowedRune},
|
||||
{" ", "", errDisallowedRune},
|
||||
{"\u01C5", "", errDisallowedRune},
|
||||
{"\u16EE", "", errDisallowedRune}, // Nl RUNIC ARLAUG SYMBOL
|
||||
{"\u0488", "", errDisallowedRune}, // Me COMBINING CYRILLIC HUNDRED THOUSANDS SIGN
|
||||
{"\u212B", "\u00e5", nil}, // Angstrom sign, NFC -> U+00E5
|
||||
{"A\u030A", "å", nil}, // A + ring
|
||||
{"\u00C5", "å", nil}, // A with ring
|
||||
{"\u00E7", "ç", nil}, // c cedille
|
||||
{"\u0063\u0327", "ç", nil}, // c + cedille
|
||||
{"\u0158", "ř", nil},
|
||||
{"\u0052\u030C", "ř", nil},
|
||||
|
||||
{"\u1E61", "\u1E61", nil}, // LATIN SMALL LETTER S WITH DOT ABOVE
|
||||
|
||||
// Confusable characters ARE allowed and should NOT be mapped.
|
||||
{"\u0410", "\u0430", nil}, // CYRILLIC CAPITAL LETTER A
|
||||
|
||||
// Full width should be mapped to the canonical decomposition.
|
||||
{"AB", "ab", nil},
|
||||
{"שc", "", bidirule.ErrInvalid}, // bidi rule
|
||||
|
||||
}},
|
||||
{"UsernameCasePreserved", UsernameCasePreserved, []testCase{
|
||||
{"ABC", "ABC", nil},
|
||||
{"AB", "AB", nil},
|
||||
{"שc", "", bidirule.ErrInvalid}, // bidi rule
|
||||
{"\uFB00", "", errDisallowedRune},
|
||||
{"\u212B", "\u00c5", nil}, // Angstrom sign, NFC -> U+00E5
|
||||
{"ẛ", "", errDisallowedRune}, // LATIN SMALL LETTER LONG S WITH DOT ABOVE
|
||||
}},
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue