@ -0,0 +1,12 @@ | |||
{ | |||
"ImportPath": "git.crumpington.com/public/ttunnel", | |||
"GoVersion": "go1.9", | |||
"GodepVersion": "v79", | |||
"Deps": [ | |||
{ | |||
"ImportPath": "github.com/BurntSushi/toml", | |||
"Comment": "v0.2.0-21-g9906417", | |||
"Rev": "99064174e013895bbd9b025c31100bd1d9b590ca" | |||
} | |||
] | |||
} |
@ -0,0 +1,5 @@ | |||
This directory tree is generated automatically by godep. | |||
Please do not edit. | |||
See https://github.com/tools/godep for more information. |
@ -0,0 +1,7 @@ | |||
package main | |||
import "git.crumpington.com/public/ttunnel" | |||
func main() { | |||
ttunnel.Main() | |||
} |
@ -0,0 +1,88 @@ | |||
package ttunnel | |||
import ( | |||
"crypto/tls" | |||
"crypto/x509" | |||
"io" | |||
"log" | |||
"net" | |||
"strings" | |||
"github.com/BurntSushi/toml" | |||
) | |||
type ClientConfig struct { | |||
Listen string | |||
Server string // Must be IP:port. | |||
Password string | |||
password []byte | |||
} | |||
func (cc ClientConfig) ServerIP() string { | |||
return strings.Split(cc.Server, ":")[0] | |||
} | |||
// RunClient runs a client using the named configuration. | |||
func RunClient(path string) { | |||
conf := ClientConfig{} | |||
if _, err := toml.DecodeFile(path, &conf); err != nil { | |||
log.Printf("Failed to load config file %s: %v", path, err) | |||
return | |||
} | |||
conf.password = []byte(conf.Password) | |||
_, certBuf := genCerts(conf.password, conf.ServerIP()) | |||
// Create a certificate pool for the client. | |||
certPool := x509.NewCertPool() | |||
if !certPool.AppendCertsFromPEM(certBuf) { | |||
log.Printf("Failed to append certificate to pool.") | |||
return | |||
} | |||
// Create the client configuration. | |||
config := tls.Config{RootCAs: certPool} | |||
// Accept connections on the local port. | |||
ln, err := net.Listen("tcp", conf.Listen) | |||
if err != nil { | |||
log.Printf("Failed to listen on %s: %v", conf.Listen, err) | |||
return | |||
} | |||
// Accept connections forever. | |||
for { | |||
lConn, err := ln.Accept() | |||
if err != nil { | |||
log.Printf("Failed to accept connection: %v", err) | |||
continue | |||
} | |||
go clientHandler(conf, lConn, &config) | |||
} | |||
} | |||
func clientHandler(conf ClientConfig, lConn net.Conn, config *tls.Config) { | |||
// Dial remote server. | |||
rConn, err := tls.Dial("tcp", conf.Server, config) | |||
if err != nil { | |||
log.Printf("Error dialing server %s: %v", conf.Server, err) | |||
goto closeConns | |||
} | |||
// Send password. | |||
if _, err = rConn.Write(conf.password); err != nil { | |||
log.Printf("Error sending password: %v", err) | |||
goto closeConns | |||
} | |||
// Forward traffic. | |||
go io.Copy(lConn, rConn) | |||
go io.Copy(rConn, lConn) | |||
return | |||
closeConns: | |||
lConn.Close() | |||
if rConn != nil { | |||
rConn.Close() | |||
} | |||
} |
@ -0,0 +1,72 @@ | |||
package ttunnel | |||
import ( | |||
"bytes" | |||
"crypto/ecdsa" | |||
"crypto/elliptic" | |||
"crypto/x509" | |||
"crypto/x509/pkix" | |||
"encoding/pem" | |||
"math/big" | |||
"net" | |||
"time" | |||
) | |||
func genCerts(password []byte, ip string) ([]byte, []byte) { | |||
reader := NewRandReader(password) | |||
defer reader.Close() | |||
// Generate a private key. | |||
priv, err := ecdsa.GenerateKey(elliptic.P521(), reader) | |||
if err != nil { | |||
panic("Failed to generate ECDSA key") | |||
} | |||
// Parse the IP address. | |||
ipAddr := net.ParseIP(ip) | |||
if ipAddr == nil { | |||
panic("Invalid IP address: " + ip) | |||
} | |||
keyUsage := x509.KeyUsageKeyEncipherment | | |||
x509.KeyUsageDigitalSignature | | |||
x509.KeyUsageCertSign | |||
template := x509.Certificate{ | |||
SerialNumber: big.NewInt(1), | |||
Subject: pkix.Name{Organization: []string{"Acme Co"}}, | |||
NotBefore: time.Unix(0, 0), | |||
NotAfter: time.Unix(0, 0).Add(200 * 365 * 24 * time.Hour), | |||
KeyUsage: keyUsage, | |||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, | |||
IsCA: true, | |||
BasicConstraintsValid: true, | |||
} | |||
template.IPAddresses = append(template.IPAddresses, ipAddr) | |||
// Create the certificate. | |||
derBytes, err := x509.CreateCertificate( | |||
reader, &template, &template, &priv.PublicKey, priv) | |||
if err != nil { | |||
panic("Failed to create certificate: " + err.Error()) | |||
} | |||
certBuf := new(bytes.Buffer) | |||
pem.Encode(certBuf, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) | |||
// Create the private keyl. | |||
keyBuf := new(bytes.Buffer) | |||
privBuf, err := x509.MarshalECPrivateKey(priv) | |||
if err != nil { | |||
panic("Failed to marshal ECDSA key: " + err.Error()) | |||
} | |||
pem.Encode(keyBuf, | |||
&pem.Block{ | |||
Type: "RSA PRIVATE KEY", | |||
Bytes: privBuf, | |||
}) | |||
return keyBuf.Bytes(), certBuf.Bytes() | |||
} |
@ -0,0 +1,22 @@ | |||
package ttunnel | |||
import ( | |||
"bytes" | |||
"testing" | |||
) | |||
func TestGenCertsMatch(t *testing.T) { | |||
password := []byte("FlufHojNacMuabciCaiHijTinleHicAj") | |||
ip := "192.168.0.102" | |||
key1, cert1 := genCerts(password, ip) | |||
key2, cert2 := genCerts(password, ip) | |||
if !bytes.Equal(key1, key2) { | |||
t.Fatal("Keys not equal") | |||
} | |||
if !bytes.Equal(cert1, cert2) { | |||
t.Fatalf("Certs not equal: \n%s\n!=\n%s", string(cert1), string(cert2)) | |||
} | |||
} |
@ -0,0 +1,33 @@ | |||
package ttunnel | |||
import ( | |||
"fmt" | |||
"log" | |||
"os" | |||
) | |||
var mainUsage = fmt.Sprintf(`Usage: %s command config-file | |||
Available commands: | |||
server | |||
client | |||
`, os.Args[0]) | |||
func Main() { | |||
if len(os.Args) != 3 { | |||
log.Println(mainUsage) | |||
os.Exit(1) | |||
} | |||
switch os.Args[1] { | |||
case "server": | |||
RunServer(os.Args[2]) | |||
case "client": | |||
RunClient(os.Args[2]) | |||
default: | |||
log.Println(mainUsage) | |||
os.Exit(1) | |||
} | |||
} |
@ -0,0 +1,66 @@ | |||
package ttunnel | |||
import ( | |||
"crypto/sha512" | |||
"hash" | |||
) | |||
// In order to generate matching certificates on a client and server, we need a | |||
// deterministic reader for random data. The reader is seeded with the | |||
// password and uses multiple hashings to generate additional bytes. | |||
type RandReader struct { | |||
seed []byte | |||
close chan bool | |||
closed bool | |||
stream chan byte | |||
h hash.Hash | |||
} | |||
func NewRandReader(seed []byte) *RandReader { | |||
rr := RandReader{ | |||
seed: seed, | |||
close: make(chan bool), | |||
closed: false, | |||
stream: make(chan byte), | |||
h: sha512.New(), | |||
} | |||
go rr.run() | |||
return &rr | |||
} | |||
func (rr *RandReader) run() { | |||
buf := []byte{} | |||
for { | |||
_, _ = rr.h.Write(rr.seed) // Never returns an error (see docs). | |||
buf = buf[:] | |||
buf = rr.h.Sum(buf) | |||
for _, b := range buf { | |||
select { | |||
case <-rr.close: | |||
rr.closed = true | |||
return | |||
case rr.stream <- b: | |||
// Continue | |||
} | |||
} | |||
} | |||
} | |||
func (rr *RandReader) Read(p []byte) (n int, err error) { | |||
if rr.closed { | |||
panic("Reading from closed RandReader") | |||
} | |||
for i := range p { | |||
p[i] = <-rr.stream | |||
} | |||
return len(p), nil | |||
} | |||
func (rr *RandReader) Close() { | |||
rr.close <- true | |||
} |
@ -0,0 +1,27 @@ | |||
package ttunnel | |||
import ( | |||
"bytes" | |||
"testing" | |||
) | |||
func TestRandReader(t *testing.T) { | |||
seed := []byte("oushBafepGochCotlekcebvallAwJedd") | |||
rr1 := NewRandReader(seed) | |||
rr2 := NewRandReader(seed) | |||
defer rr1.Close() | |||
defer rr2.Close() | |||
// Read some long buffers. | |||
b1 := make([]byte, 32768) | |||
b2 := make([]byte, 32768) | |||
for i := 0; i < 8; i++ { | |||
rr1.Read(b1) | |||
rr2.Read(b2) | |||
if !bytes.Equal(b1, b2) { | |||
t.Fatal("Not equal!") | |||
} | |||
} | |||
} |
@ -0,0 +1,91 @@ | |||
package ttunnel | |||
import ( | |||
"crypto/subtle" | |||
"crypto/tls" | |||
"io" | |||
"log" | |||
"net" | |||
"strings" | |||
"github.com/BurntSushi/toml" | |||
) | |||
type ServerConfig struct { | |||
Listen string // Must be IP:port. | |||
Forward string | |||
Password string | |||
password []byte | |||
} | |||
func (sc ServerConfig) IP() string { | |||
return strings.Split(sc.Listen, ":")[0] | |||
} | |||
func RunServer(path string) { | |||
conf := ServerConfig{} | |||
_, err := toml.DecodeFile(path, &conf) | |||
if err != nil { | |||
log.Printf("Failed to load config file %s: %v", path, err) | |||
return | |||
} | |||
conf.password = []byte(conf.Password) | |||
keyBuf, certBuf := genCerts(conf.password, conf.IP()) | |||
cert, err := tls.X509KeyPair(certBuf, keyBuf) | |||
if err != nil { | |||
log.Printf("Failed to create X509 key pair: %v", err) | |||
return | |||
} | |||
// Create TLS configuration for the server. | |||
config := tls.Config{Certificates: []tls.Certificate{cert}} | |||
// Accept connections on the given address. | |||
ln, err := tls.Listen("tcp", conf.Listen, &config) | |||
if err != nil { | |||
log.Printf("Failed to listen on %s: %v", conf.Listen, err) | |||
return | |||
} | |||
for { | |||
conn, err := ln.Accept() | |||
if err != nil { | |||
log.Printf("Failed to accept connection: %v", err) | |||
continue | |||
} | |||
go serverHandler(conn, conf) | |||
} | |||
} | |||
func serverHandler(conn net.Conn, conf ServerConfig) { | |||
var err error | |||
var cConn net.Conn // Client connection. | |||
buf := make([]byte, len(conf.Password)) | |||
if _, err = conn.Read(buf); err != nil { | |||
log.Printf("Failed to read password: %v", err) | |||
goto closeConn | |||
} | |||
if subtle.ConstantTimeCompare(buf, conf.password) != 1 { | |||
log.Printf("Invalid password provided") | |||
goto closeConn | |||
} | |||
// Connect to the remote address for the client. | |||
cConn, err = net.Dial("tcp", conf.Forward) | |||
if err != nil { | |||
log.Printf("Failed to connect to host %s: %v", conf.Forward, err) | |||
goto closeConn | |||
} | |||
// Forward traffic. | |||
go io.Copy(cConn, conn) | |||
go io.Copy(conn, cConn) | |||
return | |||
closeConn: | |||
conn.Close() | |||
} |
@ -0,0 +1,5 @@ | |||
TAGS | |||
tags | |||
.*.swp | |||
tomlcheck/tomlcheck | |||
toml.test |
@ -0,0 +1,15 @@ | |||
language: go | |||
go: | |||
- 1.1 | |||
- 1.2 | |||
- 1.3 | |||
- 1.4 | |||
- 1.5 | |||
- 1.6 | |||
- tip | |||
install: | |||
- go install ./... | |||
- go get github.com/BurntSushi/toml-test | |||
script: | |||
- export PATH="$PATH:$HOME/gopath/bin" | |||
- make test |
@ -0,0 +1,3 @@ | |||
Compatible with TOML version | |||
[v0.2.0](https://github.com/mojombo/toml/blob/master/versions/toml-v0.2.0.md) | |||
@ -0,0 +1,14 @@ | |||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE | |||
Version 2, December 2004 | |||
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net> | |||
Everyone is permitted to copy and distribute verbatim or modified | |||
copies of this license document, and changing it is allowed as long | |||
as the name is changed. | |||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE | |||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION | |||
0. You just DO WHAT THE FUCK YOU WANT TO. | |||
@ -0,0 +1,19 @@ | |||
install: | |||
go install ./... | |||
test: install | |||
go test -v | |||
toml-test toml-test-decoder | |||
toml-test -encoder toml-test-encoder | |||
fmt: | |||
gofmt -w *.go */*.go | |||
colcheck *.go */*.go | |||
tags: | |||
find ./ -name '*.go' -print0 | xargs -0 gotags > TAGS | |||
push: | |||
git push origin master | |||
git push github master | |||
@ -0,0 +1,220 @@ | |||
## TOML parser and encoder for Go with reflection | |||
TOML stands for Tom's Obvious, Minimal Language. This Go package provides a | |||
reflection interface similar to Go's standard library `json` and `xml` | |||
packages. This package also supports the `encoding.TextUnmarshaler` and | |||
`encoding.TextMarshaler` interfaces so that you can define custom data | |||
representations. (There is an example of this below.) | |||
Spec: https://github.com/mojombo/toml | |||
Compatible with TOML version | |||
[v0.2.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.2.0.md) | |||
Documentation: http://godoc.org/github.com/BurntSushi/toml | |||
Installation: | |||
```bash | |||
go get github.com/BurntSushi/toml | |||
``` | |||
Try the toml validator: | |||
```bash | |||
go get github.com/BurntSushi/toml/cmd/tomlv | |||
tomlv some-toml-file.toml | |||
``` | |||
[](https://travis-ci.org/BurntSushi/toml) | |||
### Testing | |||
This package passes all tests in | |||
[toml-test](https://github.com/BurntSushi/toml-test) for both the decoder | |||
and the encoder. | |||
### Examples | |||
This package works similarly to how the Go standard library handles `XML` | |||
and `JSON`. Namely, data is loaded into Go values via reflection. | |||
For the simplest example, consider some TOML file as just a list of keys | |||
and values: | |||
```toml | |||
Age = 25 | |||
Cats = [ "Cauchy", "Plato" ] | |||
Pi = 3.14 | |||
Perfection = [ 6, 28, 496, 8128 ] | |||
DOB = 1987-07-05T05:45:00Z | |||
``` | |||
Which could be defined in Go as: | |||
```go | |||
type Config struct { | |||
Age int | |||
Cats []string | |||
Pi float64 | |||
Perfection []int | |||
DOB time.Time // requires `import time` | |||
} | |||
``` | |||
And then decoded with: | |||
```go | |||
var conf Config | |||
if _, err := toml.Decode(tomlData, &conf); err != nil { | |||
// handle error | |||
} | |||
``` | |||
You can also use struct tags if your struct field name doesn't map to a TOML | |||
key value directly: | |||
```toml | |||
some_key_NAME = "wat" | |||
``` | |||
```go | |||
type TOML struct { | |||
ObscureKey string `toml:"some_key_NAME"` | |||
} | |||
``` | |||
### Using the `encoding.TextUnmarshaler` interface | |||
Here's an example that automatically parses duration strings into | |||
`time.Duration` values: | |||
```toml | |||
[[song]] | |||
name = "Thunder Road" | |||
duration = "4m49s" | |||
[[song]] | |||
name = "Stairway to Heaven" | |||
duration = "8m03s" | |||
``` | |||
Which can be decoded with: | |||
```go | |||
type song struct { | |||
Name string | |||
Duration duration | |||
} | |||
type songs struct { | |||
Song []song | |||
} | |||
var favorites songs | |||
if _, err := toml.Decode(blob, &favorites); err != nil { | |||
log.Fatal(err) | |||
} | |||
for _, s := range favorites.Song { | |||
fmt.Printf("%s (%s)\n", s.Name, s.Duration) | |||
} | |||
``` | |||
And you'll also need a `duration` type that satisfies the | |||
`encoding.TextUnmarshaler` interface: | |||
```go | |||
type duration struct { | |||
time.Duration | |||
} | |||
func (d *duration) UnmarshalText(text []byte) error { | |||
var err error | |||
d.Duration, err = time.ParseDuration(string(text)) | |||
return err | |||
} | |||
``` | |||
### More complex usage | |||
Here's an example of how to load the example from the official spec page: | |||
```toml | |||
# This is a TOML document. Boom. | |||
title = "TOML Example" | |||
[owner] | |||
name = "Tom Preston-Werner" | |||
organization = "GitHub" | |||
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." | |||
dob = 1979-05-27T07:32:00Z # First class dates? Why not? | |||
[database] | |||
server = "192.168.1.1" | |||
ports = [ 8001, 8001, 8002 ] | |||
connection_max = 5000 | |||
enabled = true | |||
[servers] | |||
# You can indent as you please. Tabs or spaces. TOML don't care. | |||
[servers.alpha] | |||
ip = "10.0.0.1" | |||
dc = "eqdc10" | |||
[servers.beta] | |||
ip = "10.0.0.2" | |||
dc = "eqdc10" | |||
[clients] | |||
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it | |||
# Line breaks are OK when inside arrays | |||
hosts = [ | |||
"alpha", | |||
"omega" | |||
] | |||
``` | |||
And the corresponding Go types are: | |||
```go | |||
type tomlConfig struct { | |||
Title string | |||
Owner ownerInfo | |||
DB database `toml:"database"` | |||
Servers map[string]server | |||
Clients clients | |||
} | |||
type ownerInfo struct { | |||
Name string | |||
Org string `toml:"organization"` | |||
Bio string | |||
DOB time.Time | |||
} | |||
type database struct { | |||
Server string | |||
Ports []int | |||
ConnMax int `toml:"connection_max"` | |||
Enabled bool | |||
} | |||
type server struct { | |||
IP string | |||
DC string | |||
} | |||
type clients struct { | |||
Data [][]interface{} | |||
Hosts []string | |||
} | |||
``` | |||
Note that a case insensitive match will be tried if an exact match can't be | |||
found. | |||
A working example of the above can be found in `_examples/example.{go,toml}`. | |||
@ -0,0 +1,509 @@ | |||
package toml | |||
import ( | |||
"fmt" | |||
"io" | |||
"io/ioutil" | |||
"math" | |||
"reflect" | |||
"strings" | |||
"time" | |||
) | |||
func e(format string, args ...interface{}) error { | |||
return fmt.Errorf("toml: "+format, args...) | |||
} | |||
// Unmarshaler is the interface implemented by objects that can unmarshal a | |||
// TOML description of themselves. | |||
type Unmarshaler interface { | |||
UnmarshalTOML(interface{}) error | |||
} | |||
// Unmarshal decodes the contents of `p` in TOML format into a pointer `v`. | |||
func Unmarshal(p []byte, v interface{}) error { | |||
_, err := Decode(string(p), v) | |||
return err | |||
} | |||
// Primitive is a TOML value that hasn't been decoded into a Go value. | |||
// When using the various `Decode*` functions, the type `Primitive` may | |||
// be given to any value, and its decoding will be delayed. | |||
// | |||
// A `Primitive` value can be decoded using the `PrimitiveDecode` function. | |||
// | |||
// The underlying representation of a `Primitive` value is subject to change. | |||
// Do not rely on it. | |||
// | |||
// N.B. Primitive values are still parsed, so using them will only avoid | |||
// the overhead of reflection. They can be useful when you don't know the | |||
// exact type of TOML data until run time. | |||
type Primitive struct { | |||
undecoded interface{} | |||
context Key | |||
} | |||
// DEPRECATED! | |||
// | |||
// Use MetaData.PrimitiveDecode instead. | |||
func PrimitiveDecode(primValue Primitive, v interface{}) error { | |||
md := MetaData{decoded: make(map[string]bool)} | |||
return md.unify(primValue.undecoded, rvalue(v)) | |||
} | |||
// PrimitiveDecode is just like the other `Decode*` functions, except it | |||
// decodes a TOML value that has already been parsed. Valid primitive values | |||
// can *only* be obtained from values filled by the decoder functions, | |||
// including this method. (i.e., `v` may contain more `Primitive` | |||
// values.) | |||
// | |||
// Meta data for primitive values is included in the meta data returned by | |||
// the `Decode*` functions with one exception: keys returned by the Undecoded | |||
// method will only reflect keys that were decoded. Namely, any keys hidden | |||
// behind a Primitive will be considered undecoded. Executing this method will | |||
// update the undecoded keys in the meta data. (See the example.) | |||
func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error { | |||
md.context = primValue.context | |||
defer func() { md.context = nil }() | |||
return md.unify(primValue.undecoded, rvalue(v)) | |||
} | |||
// Decode will decode the contents of `data` in TOML format into a pointer | |||
// `v`. | |||
// | |||
// TOML hashes correspond to Go structs or maps. (Dealer's choice. They can be | |||
// used interchangeably.) | |||
// | |||
// TOML arrays of tables correspond to either a slice of structs or a slice | |||
// of maps. | |||
// | |||
// TOML datetimes correspond to Go `time.Time` values. | |||
// | |||
// All other TOML types (float, string, int, bool and array) correspond | |||
// to the obvious Go types. | |||
// | |||
// An exception to the above rules is if a type implements the | |||
// encoding.TextUnmarshaler interface. In this case, any primitive TOML value | |||
// (floats, strings, integers, booleans and datetimes) will be converted to | |||
// a byte string and given to the value's UnmarshalText method. See the | |||
// Unmarshaler example for a demonstration with time duration strings. | |||
// | |||
// Key mapping | |||
// | |||
// TOML keys can map to either keys in a Go map or field names in a Go | |||
// struct. The special `toml` struct tag may be used to map TOML keys to | |||
// struct fields that don't match the key name exactly. (See the example.) | |||
// A case insensitive match to struct names will be tried if an exact match | |||
// can't be found. | |||
// | |||
// The mapping between TOML values and Go values is loose. That is, there | |||
// may exist TOML values that cannot be placed into your representation, and | |||
// there may be parts of your representation that do not correspond to | |||
// TOML values. This loose mapping can be made stricter by using the IsDefined | |||
// and/or Undecoded methods on the MetaData returned. | |||
// | |||
// This decoder will not handle cyclic types. If a cyclic type is passed, | |||
// `Decode` will not terminate. | |||
func Decode(data string, v interface{}) (MetaData, error) { | |||
rv := reflect.ValueOf(v) | |||
if rv.Kind() != reflect.Ptr { | |||
return MetaData{}, e("Decode of non-pointer %s", reflect.TypeOf(v)) | |||
} | |||
if rv.IsNil() { | |||
return MetaData{}, e("Decode of nil %s", reflect.TypeOf(v)) | |||
} | |||
p, err := parse(data) | |||
if err != nil { | |||
return MetaData{}, err | |||
} | |||
md := MetaData{ | |||
p.mapping, p.types, p.ordered, | |||
make(map[string]bool, len(p.ordered)), nil, | |||
} | |||
return md, md.unify(p.mapping, indirect(rv)) | |||
} | |||
// DecodeFile is just like Decode, except it will automatically read the | |||
// contents of the file at `fpath` and decode it for you. | |||
func DecodeFile(fpath string, v interface{}) (MetaData, error) { | |||
bs, err := ioutil.ReadFile(fpath) | |||
if err != nil { | |||
return MetaData{}, err | |||
} | |||
return Decode(string(bs), v) | |||
} | |||
// DecodeReader is just like Decode, except it will consume all bytes | |||
// from the reader and decode it for you. | |||
func DecodeReader(r io.Reader, v interface{}) (MetaData, error) { | |||
bs, err := ioutil.ReadAll(r) | |||
if err != nil { | |||
return MetaData{}, err | |||
} | |||
return Decode(string(bs), v) | |||
} | |||
// unify performs a sort of type unification based on the structure of `rv`, | |||
// which is the client representation. | |||
// | |||
// Any type mismatch produces an error. Finding a type that we don't know | |||
// how to handle produces an unsupported type error. | |||
func (md *MetaData) unify(data interface{}, rv reflect.Value) error { | |||
// Special case. Look for a `Primitive` value. | |||
if rv.Type() == reflect.TypeOf((*Primitive)(nil)).Elem() { | |||
// Save the undecoded data and the key context into the primitive | |||
// value. | |||
context := make(Key, len(md.context)) | |||
copy(context, md.context) | |||
rv.Set(reflect.ValueOf(Primitive{ | |||
undecoded: data, | |||
context: context, | |||
})) | |||
return nil | |||
} | |||
// Special case. Unmarshaler Interface support. | |||
if rv.CanAddr() { | |||
if v, ok := rv.Addr().Interface().(Unmarshaler); ok { | |||
return v.UnmarshalTOML(data) | |||
} | |||
} | |||
// Special case. Handle time.Time values specifically. | |||
// TODO: Remove this code when we decide to drop support for Go 1.1. | |||
// This isn't necessary in Go 1.2 because time.Time satisfies the encoding | |||
// interfaces. | |||
if rv.Type().AssignableTo(rvalue(time.Time{}).Type()) { | |||
return md.unifyDatetime(data, rv) | |||
} | |||
// Special case. Look for a value satisfying the TextUnmarshaler interface. | |||
if v, ok := rv.Interface().(TextUnmarshaler); ok { | |||
return md.unifyText(data, v) | |||
} | |||
// BUG(burntsushi) | |||
// The behavior here is incorrect whenever a Go type satisfies the | |||
// encoding.TextUnmarshaler interface but also corresponds to a TOML | |||
// hash or array. In particular, the unmarshaler should only be applied | |||
// to primitive TOML values. But at this point, it will be applied to | |||
// all kinds of values and produce an incorrect error whenever those values | |||
// are hashes or arrays (including arrays of tables). | |||
k := rv.Kind() | |||
// laziness | |||
if k >= reflect.Int && k <= reflect.Uint64 { | |||
return md.unifyInt(data, rv) | |||
} | |||
switch k { | |||
case reflect.Ptr: | |||
elem := reflect.New(rv.Type().Elem()) | |||
err := md.unify(data, reflect.Indirect(elem)) | |||
if err != nil { | |||
return err | |||
} | |||
rv.Set(elem) | |||
return nil | |||
case reflect.Struct: | |||
return md.unifyStruct(data, rv) | |||
case reflect.Map: | |||
return md.unifyMap(data, rv) | |||
case reflect.Array: | |||
return md.unifyArray(data, rv) | |||
case reflect.Slice: | |||
return md.unifySlice(data, rv) | |||
case reflect.String: | |||
return md.unifyString(data, rv) | |||
case reflect.Bool: | |||
return md.unifyBool(data, rv) | |||
case reflect.Interface: | |||
// we only support empty interfaces. | |||
if rv.NumMethod() > 0 { | |||
return e("unsupported type %s", rv.Type()) | |||
} | |||
return md.unifyAnything(data, rv) | |||
case reflect.Float32: | |||
fallthrough | |||
case reflect.Float64: | |||
return md.unifyFloat64(data, rv) | |||
} | |||
return e("unsupported type %s", rv.Kind()) | |||
} | |||
func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error { | |||
tmap, ok := mapping.(map[string]interface{}) | |||
if !ok { | |||
if mapping == nil { | |||
return nil | |||
} | |||
return e("type mismatch for %s: expected table but found %T", | |||
rv.Type().String(), mapping) | |||
} | |||
for key, datum := range tmap { | |||
var f *field | |||
fields := cachedTypeFields(rv.Type()) | |||
for i := range fields { | |||
ff := &fields[i] | |||
if ff.name == key { | |||
f = ff | |||
break | |||
} | |||
if f == nil && strings.EqualFold(ff.name, key) { | |||
f = ff | |||
} | |||
} | |||
if f != nil { | |||
subv := rv | |||
for _, i := range f.index { | |||
subv = indirect(subv.Field(i)) | |||
} | |||
if isUnifiable(subv) { | |||
md.decoded[md.context.add(key).String()] = true | |||
md.context = append(md.context, key) | |||
if err := md.unify(datum, subv); err != nil { | |||
return err | |||
} | |||
md.context = md.context[0 : len(md.context)-1] | |||
} else if f.name != "" { | |||
// Bad user! No soup for you! | |||
return e("cannot write unexported field %s.%s", | |||
rv.Type().String(), f.name) | |||
} | |||
} | |||
} | |||
return nil | |||
} | |||
func (md *MetaData) unifyMap(mapping interface{}, rv reflect.Value) error { | |||
tmap, ok := mapping.(map[string]interface{}) | |||
if !ok { | |||
if tmap == nil { | |||
return nil | |||
} | |||
return badtype("map", mapping) | |||
} | |||
if rv.IsNil() { | |||
rv.Set(reflect.MakeMap(rv.Type())) | |||
} | |||
for k, v := range tmap { | |||
md.decoded[md.context.add(k).String()] = true | |||
md.context = append(md.context, k) | |||
rvkey := indirect(reflect.New(rv.Type().Key())) | |||
rvval := reflect.Indirect(reflect.New(rv.Type().Elem())) | |||
if err := md.unify(v, rvval); err != nil { | |||
return err | |||
} | |||
md.context = md.context[0 : len(md.context)-1] | |||
rvkey.SetString(k) | |||
rv.SetMapIndex(rvkey, rvval) | |||
} | |||
return nil | |||
} | |||
func (md *MetaData) unifyArray(data interface{}, rv reflect.Value) error { | |||
datav := reflect.ValueOf(data) | |||
if datav.Kind() != reflect.Slice { | |||
if !datav.IsValid() { | |||
return nil | |||
} | |||
return badtype("slice", data) | |||
} | |||
sliceLen := datav.Len() | |||
if sliceLen != rv.Len() { | |||
return e("expected array length %d; got TOML array of length %d", | |||
rv.Len(), sliceLen) | |||
} | |||
return md.unifySliceArray(datav, rv) | |||
} | |||
func (md *MetaData) unifySlice(data interface{}, rv reflect.Value) error { | |||
datav := reflect.ValueOf(data) | |||
if datav.Kind() != reflect.Slice { | |||
if !datav.IsValid() { | |||
return nil | |||
} | |||
return badtype("slice", data) | |||
} | |||
n := datav.Len() | |||
if rv.IsNil() || rv.Cap() < n { | |||
rv.Set(reflect.MakeSlice(rv.Type(), n, n)) | |||
} | |||
rv.SetLen(n) | |||
return md.unifySliceArray(datav, rv) | |||
} | |||
func (md *MetaData) unifySliceArray(data, rv reflect.Value) error { | |||
sliceLen := data.Len() | |||
for i := 0; i < sliceLen; i++ { | |||
v := data.Index(i).Interface() | |||
sliceval := indirect(rv.Index(i)) | |||
if err := md.unify(v, sliceval); err != nil { | |||
return err | |||
} | |||
} | |||
return nil | |||
} | |||
func (md *MetaData) unifyDatetime(data interface{}, rv reflect.Value) error { | |||
if _, ok := data.(time.Time); ok { | |||
rv.Set(reflect.ValueOf(data)) | |||
return nil | |||
} | |||
return badtype("time.Time", data) | |||
} | |||
func (md *MetaData) unifyString(data interface{}, rv reflect.Value) error { | |||
if s, ok := data.(string); ok { | |||
rv.SetString(s) | |||
return nil | |||
} | |||
return badtype("string", data) | |||
} | |||
func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error { | |||
if num, ok := data.(float64); ok { | |||
switch rv.Kind() { | |||
case reflect.Float32: | |||
fallthrough | |||
case reflect.Float64: | |||
rv.SetFloat(num) | |||
default: | |||
panic("bug") | |||
} | |||
return nil | |||
} | |||
return badtype("float", data) | |||
} | |||
func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error { | |||
if num, ok := data.(int64); ok { | |||
if rv.Kind() >= reflect.Int && rv.Kind() <= reflect.Int64 { | |||
switch rv.Kind() { | |||
case reflect.Int, reflect.Int64: | |||
// No bounds checking necessary. | |||
case reflect.Int8: | |||
if num < math.MinInt8 || num > math.MaxInt8 { | |||
return e("value %d is out of range for int8", num) | |||
} | |||
case reflect.Int16: | |||
if num < math.MinInt16 || num > math.MaxInt16 { | |||
return e("value %d is out of range for int16", num) | |||
} | |||
case reflect.Int32: | |||
if num < math.MinInt32 || num > math.MaxInt32 { | |||
return e("value %d is out of range for int32", num) | |||
} | |||
} | |||
rv.SetInt(num) | |||
} else if rv.Kind() >= reflect.Uint && rv.Kind() <= reflect.Uint64 { | |||
unum := uint64(num) | |||
switch rv.Kind() { | |||
case reflect.Uint, reflect.Uint64: | |||
// No bounds checking necessary. | |||
case reflect.Uint8: | |||
if num < 0 || unum > math.MaxUint8 { | |||
return e("value %d is out of range for uint8", num) | |||
} | |||
case reflect.Uint16: | |||
if num < 0 || unum > math.MaxUint16 { | |||
return e("value %d is out of range for uint16", num) | |||
} | |||
case reflect.Uint32: | |||
if num < 0 || unum > math.MaxUint32 { | |||
return e("value %d is out of range for uint32", num) | |||
} | |||
} | |||
rv.SetUint(unum) | |||
} else { | |||
panic("unreachable") | |||
} | |||
return nil | |||
} | |||
return badtype("integer", data) | |||
} | |||
func (md *MetaData) unifyBool(data interface{}, rv reflect.Value) error { | |||
if b, ok := data.(bool); ok { | |||
rv.SetBool(b) | |||
return nil | |||
} | |||
return badtype("boolean", data) | |||
} | |||
func (md *MetaData) unifyAnything(data interface{}, rv reflect.Value) error { | |||
rv.Set(reflect.ValueOf(data)) | |||
return nil | |||
} | |||
func (md *MetaData) unifyText(data interface{}, v TextUnmarshaler) error { | |||
var s string | |||
switch sdata := data.(type) { | |||
case TextMarshaler: | |||
text, err := sdata.MarshalText() | |||
if err != nil { | |||
return err | |||
} | |||
s = string(text) | |||
case fmt.Stringer: | |||
s = sdata.String() | |||
case string: | |||
s = sdata | |||
case bool: | |||
s = fmt.Sprintf("%v", sdata) | |||
case int64: | |||
s = fmt.Sprintf("%d", sdata) | |||
case float64: | |||
s = fmt.Sprintf("%f", sdata) | |||
default: | |||
return badtype("primitive (string-like)", data) | |||
} | |||
if err := v.UnmarshalText([]byte(s)); err != nil { | |||
return err | |||
} | |||
return nil | |||
} | |||
// rvalue returns a reflect.Value of `v`. All pointers are resolved. | |||
func rvalue(v interface{}) reflect.Value { | |||
return indirect(reflect.ValueOf(v)) | |||
} | |||
// indirect returns the value pointed to by a pointer. | |||
// Pointers are followed until the value is not a pointer. | |||
// New values are allocated for each nil pointer. | |||
// | |||
// An exception to this rule is if the value satisfies an interface of | |||
// interest to us (like encoding.TextUnmarshaler). | |||
func indirect(v reflect.Value) reflect.Value { | |||
if v.Kind() != reflect.Ptr { | |||
if v.CanSet() { | |||
pv := v.Addr() | |||
if _, ok := pv.Interface().(TextUnmarshaler); ok { | |||
return pv | |||
} | |||
} | |||
return v | |||
} | |||
if v.IsNil() { | |||
v.Set(reflect.New(v.Type().Elem())) | |||
} | |||
return indirect(reflect.Indirect(v)) | |||
} | |||
func isUnifiable(rv reflect.Value) bool { | |||
if rv.CanSet() { | |||
return true | |||
} | |||
if _, ok := rv.Interface().(TextUnmarshaler); ok { | |||
return true | |||
} | |||
return false | |||
} | |||
func badtype(expected string, data interface{}) error { | |||
return e("cannot load TOML value of type %T into a Go %s", data, expected) | |||
} |
@ -0,0 +1,121 @@ | |||
package toml | |||
import "strings" | |||
// MetaData allows access to meta information about TOML data that may not | |||
// be inferrable via reflection. In particular, whether a key has been defined | |||
// and the TOML type of a key. | |||
type MetaData struct { | |||
mapping map[string]interface{} | |||
types map[string]tomlType | |||
keys []Key | |||
decoded map[string]bool | |||
context Key // Used only during decoding. | |||
} | |||
// IsDefined returns true if the key given exists in the TOML data. The key | |||
// should be specified hierarchially. e.g., | |||
// | |||
// // access the TOML key 'a.b.c' | |||
// IsDefined("a", "b", "c") | |||
// | |||
// IsDefined will return false if an empty key given. Keys are case sensitive. | |||
func (md *MetaData) IsDefined(key ...string) bool { | |||
if len(key) == 0 { | |||
return false | |||
} | |||
var hash map[string]interface{} | |||
var ok bool | |||
var hashOrVal interface{} = md.mapping | |||
for _, k := range key { | |||
if hash, ok = hashOrVal.(map[string]interface{}); !ok { | |||
return false | |||
} | |||
if hashOrVal, ok = hash[k]; !ok { | |||
return false | |||
} | |||
} | |||
return true | |||
} | |||
// Type returns a string representation of the type of the key specified. | |||
// | |||
// Type will return the empty string if given an empty key or a key that | |||
// does not exist. Keys are case sensitive. | |||
func (md *MetaData) Type(key ...string) string { | |||
fullkey := strings.Join(key, ".") | |||
if typ, ok := md.types[fullkey]; ok { | |||
return typ.typeString() | |||
} | |||
return "" | |||
} | |||
// Key is the type of any TOML key, including key groups. Use (MetaData).Keys | |||
// to get values of this type. | |||
type Key []string | |||
func (k Key) String() string { | |||
return strings.Join(k, ".") | |||
} | |||
func (k Key) maybeQuotedAll() string { | |||
var ss []string | |||
for i := range k { | |||
ss = append(ss, k.maybeQuoted(i)) | |||
} | |||
return strings.Join(ss, ".") | |||
} | |||
func (k Key) maybeQuoted(i int) string { | |||
quote := false | |||
for _, c := range k[i] { | |||
if !isBareKeyChar(c) { | |||
quote = true | |||
break | |||
} | |||
} | |||
if quote { | |||
return "\"" + strings.Replace(k[i], "\"", "\\\"", -1) + "\"" | |||
} | |||
return k[i] | |||
} | |||
func (k Key) add(piece string) Key { | |||
newKey := make(Key, len(k)+1) | |||
copy(newKey, k) | |||
newKey[len(k)] = piece | |||
return newKey | |||
} | |||
// Keys returns a slice of every key in the TOML data, including key groups. | |||
// Each key is itself a slice, where the first element is the top of the | |||
// hierarchy and the last is the most specific. | |||
// | |||
// The list will have the same order as the keys appeared in the TOML data. | |||
// | |||
// All keys returned are non-empty. | |||
func (md *MetaData) Keys() []Key { | |||
return md.keys | |||
} | |||
// Undecoded returns all keys that have not been decoded in the order in which | |||
// they appear in the original TOML document. | |||
// | |||
// This includes keys that haven't been decoded because of a Primitive value. | |||
// Once the Primitive value is decoded, the keys will be considered decoded. | |||
// | |||
// Also note that decoding into an empty interface will result in no decoding, | |||
// and so no keys will be considered decoded. | |||
// | |||
// In this sense, the Undecoded keys correspond to keys in the TOML document | |||
// that do not have a concrete type in your representation. | |||
func (md *MetaData) Undecoded() []Key { | |||
undecoded := make([]Key, 0, len(md.keys)) | |||
for _, key := range md.keys { | |||
if !md.decoded[key.String()] { | |||
undecoded = append(undecoded, key) | |||
} | |||
} | |||
return undecoded | |||
} |
@ -0,0 +1,27 @@ | |||
/* | |||
Package toml provides facilities for decoding and encoding TOML configuration | |||
files via reflection. There is also support for delaying decoding with | |||
the Primitive type, and querying the set of keys in a TOML document with the | |||
MetaData type. | |||
The specification implemented: https://github.com/mojombo/toml | |||
The sub-command github.com/BurntSushi/toml/cmd/tomlv can be used to verify | |||
whether a file is a valid TOML document. It can also be used to print the | |||
type of each key in a TOML document. | |||
Testing | |||
There are two important types of tests used for this package. The first is | |||
contained inside '*_test.go' files and uses the standard Go unit testing | |||
framework. These tests are primarily devoted to holistically testing the | |||
decoder and encoder. | |||
The second type of testing is used to verify the implementation's adherence | |||
to the TOML specification. These tests have been factored into their own | |||
project: https://github.com/BurntSushi/toml-test | |||
The reason the tests are in a separate project is so that they can be used by | |||
any implementation of TOML. Namely, it is language agnostic. | |||
*/ | |||
package toml |
@ -0,0 +1,568 @@ | |||
package toml | |||
import ( | |||
"bufio" | |||
"errors" | |||
"fmt" | |||
"io" | |||
"reflect" | |||
"sort" | |||
"strconv" | |||
"strings" | |||
"time" | |||
) | |||
type tomlEncodeError struct{ error } | |||
var ( | |||
errArrayMixedElementTypes = errors.New( | |||
"toml: cannot encode array with mixed element types") | |||
errArrayNilElement = errors.New( | |||
"toml: cannot encode array with nil element") | |||
errNonString = errors.New( | |||
"toml: cannot encode a map with non-string key type") | |||
errAnonNonStruct = errors.New( | |||
"toml: cannot encode an anonymous field that is not a struct") | |||
errArrayNoTable = errors.New( | |||
"toml: TOML array element cannot contain a table") | |||
errNoKey = errors.New( | |||
"toml: top-level values must be Go maps or structs") | |||
errAnything = errors.New("") // used in testing | |||
) | |||
var quotedReplacer = strings.NewReplacer( | |||
"\t", "\\t", | |||
"\n", "\\n", | |||
"\r", "\\r", | |||
"\"", "\\\"", | |||
"\\", "\\\\", | |||
) | |||
// Encoder controls the encoding of Go values to a TOML document to some | |||
// io.Writer. | |||
// | |||
// The indentation level can be controlled with the Indent field. | |||
type Encoder struct { | |||
// A single indentation level. By default it is two spaces. | |||
Indent string | |||
// hasWritten is whether we have written any output to w yet. | |||
hasWritten bool | |||
w *bufio.Writer | |||
} | |||
// NewEncoder returns a TOML encoder that encodes Go values to the io.Writer | |||
// given. By default, a single indentation level is 2 spaces. | |||
func NewEncoder(w io.Writer) *Encoder { | |||
return &Encoder{ | |||
w: bufio.NewWriter(w), | |||
Indent: " ", | |||
} | |||
} | |||
// Encode writes a TOML representation of the Go value to the underlying | |||
// io.Writer. If the value given cannot be encoded to a valid TOML document, | |||
// then an error is returned. | |||
// | |||
// The mapping between Go values and TOML values should be precisely the same | |||
// as for the Decode* functions. Similarly, the TextMarshaler interface is | |||
// supported by encoding the resulting bytes as strings. (If you want to write | |||
// arbitrary binary data then you will need to use something like base64 since | |||
// TOML does not have any binary types.) | |||
// | |||
// When encoding TOML hashes (i.e., Go maps or structs), keys without any | |||
// sub-hashes are encoded first. | |||
// | |||
// If a Go map is encoded, then its keys are sorted alphabetically for | |||
// deterministic output. More control over this behavior may be provided if | |||
// there is demand for it. | |||
// | |||
// Encoding Go values without a corresponding TOML representation---like map | |||
// types with non-string keys---will cause an error to be returned. Similarly | |||
// for mixed arrays/slices, arrays/slices with nil elements, embedded | |||
// non-struct types and nested slices containing maps or structs. | |||
// (e.g., [][]map[string]string is not allowed but []map[string]string is OK | |||
// and so is []map[string][]string.) | |||
func (enc *Encoder) Encode(v interface{}) error { | |||
rv := eindirect(reflect.ValueOf(v)) | |||
if err := enc.safeEncode(Key([]string{}), rv); err != nil { | |||
return err | |||
} | |||
return enc.w.Flush() | |||
} | |||
func (enc *Encoder) safeEncode(key Key, rv reflect.Value) (err error) { | |||
defer func() { | |||
if r := recover(); r != nil { | |||
if terr, ok := r.(tomlEncodeError); ok { | |||
err = terr.error | |||
return | |||
} | |||
panic(r) | |||
} | |||
}() | |||
enc.encode(key, rv) | |||
return nil | |||
} | |||
func (enc *Encoder) encode(key Key, rv reflect.Value) { | |||
// Special case. Time needs to be in ISO8601 format. | |||
// Special case. If we can marshal the type to text, then we used that. | |||
// Basically, this prevents the encoder for handling these types as | |||
// generic structs (or whatever the underlying type of a TextMarshaler is). | |||
switch rv.Interface().(type) { | |||
case time.Time, TextMarshaler: | |||
enc.keyEqElement(key, rv) | |||
return | |||
} | |||
k := rv.Kind() | |||
switch k { | |||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, | |||
reflect.Int64, | |||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, | |||
reflect.Uint64, | |||
reflect.Float32, reflect.Float64, reflect.String, reflect.Bool: | |||
enc.keyEqElement(key, rv) | |||
case reflect.Array, reflect.Slice: | |||
if typeEqual(tomlArrayHash, tomlTypeOfGo(rv)) { | |||
enc.eArrayOfTables(key, rv) | |||
} else { | |||
enc.keyEqElement(key, rv) | |||
} | |||
case reflect.Interface: | |||
if rv.IsNil() { | |||
return | |||
} | |||
enc.encode(key, rv.Elem()) | |||
case reflect.Map: | |||
if rv.IsNil() { | |||
return | |||
} | |||
enc.eTable(key, rv) | |||
case reflect.Ptr: | |||
if rv.IsNil() { | |||
return | |||
} | |||
enc.encode(key, rv.Elem()) | |||
case reflect.Struct: | |||
enc.eTable(key, rv) | |||
default: | |||
panic(e("unsupported type for key '%s': %s", key, k)) | |||
} | |||
} | |||
// eElement encodes any value that can be an array element (primitives and | |||
// arrays). | |||
func (enc *Encoder) eElement(rv reflect.Value) { | |||
switch v := rv.Interface().(type) { | |||
case time.Time: | |||
// Special case time.Time as a primitive. Has to come before | |||
// TextMarshaler below because time.Time implements | |||
// encoding.TextMarshaler, but we need to always use UTC. | |||
enc.wf(v.UTC().Format("2006-01-02T15:04:05Z")) | |||
return | |||
case TextMarshaler: | |||
// Special case. Use text marshaler if it's available for this value. | |||
if s, err := v.MarshalText(); err != nil { | |||
encPanic(err) | |||
} else { | |||
enc.writeQuoted(string(s)) | |||
} | |||
return | |||
} | |||
switch rv.Kind() { | |||
case reflect.Bool: | |||
enc.wf(strconv.FormatBool(rv.Bool())) | |||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, | |||
reflect.Int64: | |||
enc.wf(strconv.FormatInt(rv.Int(), 10)) | |||
case reflect.Uint, reflect.Uint8, reflect.Uint16, | |||
reflect.Uint32, reflect.Uint64: | |||
enc.wf(strconv.FormatUint(rv.Uint(), 10)) | |||
case reflect.Float32: | |||
enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 32))) | |||
case reflect.Float64: | |||
enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 64))) | |||
case reflect.Array, reflect.Slice: | |||
enc.eArrayOrSliceElement(rv) | |||
case reflect.Interface: | |||
enc.eElement(rv.Elem()) | |||
case reflect.String: | |||
enc.writeQuoted(rv.String()) | |||
default: | |||
panic(e("unexpected primitive type: %s", rv.Kind())) | |||
} | |||
} | |||
// By the TOML spec, all floats must have a decimal with at least one | |||
// number on either side. | |||
func floatAddDecimal(fstr string) string { | |||
if !strings.Contains(fstr, ".") { | |||
return fstr + ".0" | |||
} | |||
return fstr | |||
} | |||
func (enc *Encoder) writeQuoted(s string) { | |||
enc.wf("\"%s\"", quotedReplacer.Replace(s)) | |||
} | |||
func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) { | |||
length := rv.Len() | |||
enc.wf("[") | |||
for i := 0; i < length; i++ { | |||
elem := rv.Index(i) | |||
enc.eElement(elem) | |||
if i != length-1 { | |||
enc.wf(", ") | |||
} | |||
} | |||
enc.wf("]") | |||
} | |||
func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) { | |||
if len(key) == 0 { | |||
encPanic(errNoKey) | |||
} | |||
for i := 0; i < rv.Len(); i++ { | |||
trv := rv.Index(i) | |||
if isNil(trv) { | |||
continue | |||
} | |||
panicIfInvalidKey(key) | |||
enc.newline() | |||
enc.wf("%s[[%s]]", enc.indentStr(key), key.maybeQuotedAll()) | |||
enc.newline() | |||
enc.eMapOrStruct(key, trv) | |||
} | |||
} | |||
func (enc *Encoder) eTable(key Key, rv reflect.Value) { | |||
panicIfInvalidKey(key) | |||
if len(key) == 1 { | |||
// Output an extra new line between top-level tables. | |||
// (The newline isn't written if nothing else has been written though.) | |||
enc.newline() | |||
} | |||
if len(key) > 0 { | |||
enc.wf("%s[%s]", enc.indentStr(key), key.maybeQuotedAll()) | |||
enc.newline() | |||
} | |||
enc.eMapOrStruct(key, rv) | |||
} | |||
func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value) { | |||
switch rv := eindirect(rv); rv.Kind() { | |||
case reflect.Map: | |||
enc.eMap(key, rv) | |||
case reflect.Struct: | |||
enc.eStruct(key, rv) | |||
default: | |||
panic("eTable: unhandled reflect.Value Kind: " + rv.Kind().String()) | |||
} | |||
} | |||
func (enc *Encoder) eMap(key Key, rv reflect.Value) { | |||
rt := rv.Type() | |||
if rt.Key().Kind() != reflect.String { | |||
encPanic(errNonString) | |||
} | |||
// Sort keys so that we have deterministic output. And write keys directly | |||
// underneath this key first, before writing sub-structs or sub-maps. | |||
var mapKeysDirect, mapKeysSub []string | |||
for _, mapKey := range rv.MapKeys() { | |||
k := mapKey.String() | |||
if typeIsHash(tomlTypeOfGo(rv.MapIndex(mapKey))) { | |||
mapKeysSub = append(mapKeysSub, k) | |||
} else { | |||
mapKeysDirect = append(mapKeysDirect, k) | |||
} | |||
} | |||
var writeMapKeys = func(mapKeys []string) { | |||
sort.Strings(mapKeys) | |||
for _, mapKey := range mapKeys { | |||
mrv := rv.MapIndex(reflect.ValueOf(mapKey)) | |||
if isNil(mrv) { | |||
// Don't write anything for nil fields. | |||
continue | |||
} | |||
enc.encode(key.add(mapKey), mrv) | |||
} | |||
} | |||
writeMapKeys(mapKeysDirect) | |||
writeMapKeys(mapKeysSub) | |||
} | |||
func (enc *Encoder) eStruct(key Key, rv reflect.Value) { | |||
// Write keys for fields directly under this key first, because if we write | |||
// a field that creates a new table, then all keys under it will be in that | |||
// table (not the one we're writing here). | |||
rt := rv.Type() | |||
var fieldsDirect, fieldsSub [][]int | |||
var addFields func(rt reflect.Type, rv reflect.Value, start []int) | |||
addFields = func(rt reflect.Type, rv reflect.Value, start []int) { | |||
for i := 0; i < rt.NumField(); i++ { | |||
f := rt.Field(i) | |||
// skip unexported fields | |||
if f.PkgPath != "" && !f.Anonymous { | |||
continue | |||
} | |||
frv := rv.Field(i) | |||
if f.Anonymous { | |||
t := f.Type | |||
switch t.Kind() { | |||
case reflect.Struct: | |||
// Treat anonymous struct fields with | |||
// tag names as though they are not | |||
// anonymous, like encoding/json does. | |||
if getOptions(f.Tag).name == "" { | |||
addFields(t, frv, f.Index) | |||
continue | |||
} | |||
case reflect.Ptr: | |||
if t.Elem().Kind() == reflect.Struct && | |||
getOptions(f.Tag).name == "" { | |||
if !frv.IsNil() { | |||
addFields(t.Elem(), frv.Elem(), f.Index) | |||
} | |||
continue | |||
} | |||
// Fall through to the normal field encoding logic below | |||
// for non-struct anonymous fields. | |||
} | |||
} | |||
if typeIsHash(tomlTypeOfGo(frv)) { | |||
fieldsSub = append(fieldsSub, append(start, f.Index...)< |