initial commit

This commit is contained in:
Christine Dodrill 2015-10-07 22:42:02 -07:00
commit 833af3b22e
167 changed files with 34320 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
bin
pkg
var

25
Dockerfile Normal file
View File

@ -0,0 +1,25 @@
FROM phusion/baseimage:0.9.17
ENV RUNTIME=DOCKER
ENV DATA_PATH=/home/scream/var
# Expose HTTP port
EXPOSE 5000
# unelevated user
RUN useradd --create-home scream
# Golang compilers
RUN cd /usr/local && wget https://storage.googleapis.com/golang/go1.5.1.linux-amd64.tar.gz && \
tar xf go1.5.1.linux-amd64.tar.gz && rm go1.5.1.linux-amd64.tar.gz
# install gb
RUN mkdir /go && GOPATH=/go go get github.com/constabulary/gb/... \
&& cp /go/bin/gb /usr/bin/gb
# Add application code and build
ADD . /app
RUN cd /app && gb build all
# Run
CMD /sbin/my_init

21
README.md Normal file
View File

@ -0,0 +1,21 @@
scream
======
A new core for Shuo's replacement.
build
-----
```console
$ gb build all
```
test
----
This uses [`gt`](https://godoc.org/rsc.io/gt).
```console
$ go get rsc.io/gt
$ ./test.sh
```

View File

@ -0,0 +1,27 @@
// Copyright (c) 2009 Thomas Jager. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,65 @@
Description
-----------
Event based irc client library.
Features
--------
* Event based. Register Callbacks for the events you need to handle.
* Handles basic irc demands for you
* Standard CTCP
* Reconnections on errors
* Detect stoned servers
Install
-------
$ go get github.com/thoj/go-ircevent
Example
-------
See test/irc_test.go
Events for callbacks
--------------------
* 001 Welcome
* PING
* CTCP Unknown CTCP
* CTCP_VERSION Version request (Handled internaly)
* CTCP_USERINFO
* CTCP_CLIENTINFO
* CTCP_TIME
* CTCP_PING
* CTCP_ACTION (/me)
* PRIVMSG
* MODE
* JOIN
+Many more
AddCallback Example
-------------------
ircobj.AddCallback("PRIVMSG", func(event *irc.Event) {
//event.Message() contains the message
//event.Nick Contains the sender
//event.Arguments[0] Contains the channel
});
Commands
--------
ircobj := irc.IRC("<nick>", "<user>") //Create new ircobj
//Set options
ircobj.UseTLS = true //default is false
//ircobj.TLSOptions //set ssl options
ircobj.Password = "[server password]"
//Commands
ircobj.Connect("irc.someserver.com:6667") //Connect to server
ircobj.SendRaw("<string>") //sends string to server. Adds \r\n
ircobj.SendRawf("<formatstring>", ...) //sends formatted string to server.n
ircobj.Join("<#channel> [password]")
ircobj.Nick("newnick")
ircobj.Privmsg("<nickname | #channel>", "msg")
ircobj.Privmsgf(<nickname | #channel>, "<formatstring>", ...)
ircobj.Notice("<nickname | #channel>", "msg")
ircobj.Noticef("<nickname | #channel>", "<formatstring>", ...)

View File

@ -0,0 +1,469 @@
// Copyright 2009 Thomas Jager <mail@jager.no> All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
This package provides an event based IRC client library. It allows to
register callbacks for the events you need to handle. Its features
include handling standard CTCP, reconnecting on errors and detecting
stones servers.
Details of the IRC protocol can be found in the following RFCs:
https://tools.ietf.org/html/rfc1459
https://tools.ietf.org/html/rfc2810
https://tools.ietf.org/html/rfc2811
https://tools.ietf.org/html/rfc2812
https://tools.ietf.org/html/rfc2813
The details of the client-to-client protocol (CTCP) can be found here: http://www.irchelp.org/irchelp/rfc/ctcpspec.html
*/
package irc
import (
"bufio"
"bytes"
"crypto/tls"
"errors"
"fmt"
"log"
"net"
"os"
"strconv"
"strings"
"time"
)
const (
VERSION = "go-ircevent v2.1"
)
var ErrDisconnected = errors.New("Disconnect Called")
// Read data from a connection. To be used as a goroutine.
func (irc *Connection) readLoop() {
defer irc.Done()
br := bufio.NewReaderSize(irc.socket, 512)
errChan := irc.ErrorChan()
for {
select {
case <-irc.end:
return
default:
// Set a read deadline based on the combined timeout and ping frequency
// We should ALWAYS have received a response from the server within the timeout
// after our own pings
if irc.socket != nil {
irc.socket.SetReadDeadline(time.Now().Add(irc.Timeout + irc.PingFreq))
}
msg, err := br.ReadString('\n')
// We got past our blocking read, so bin timeout
if irc.socket != nil {
var zero time.Time
irc.socket.SetReadDeadline(zero)
}
if err != nil {
errChan <- err
break
}
if irc.Debug {
irc.Log.Printf("<-- %s\n", strings.TrimSpace(msg))
}
irc.lastMessage = time.Now()
event, err := parseToEvent(msg)
event.Connection = irc
if err == nil {
/* XXX: len(args) == 0: args should be empty */
irc.RunCallbacks(event)
}
}
}
return
}
//Parse raw irc messages
func parseToEvent(msg string) (*Event, error) {
msg = strings.TrimSuffix(msg, "\n") //Remove \r\n
msg = strings.TrimSuffix(msg, "\r")
event := &Event{Raw: msg}
if len(msg) < 5 {
return nil, errors.New("Malformed msg from server")
}
if msg[0] == ':' {
if i := strings.Index(msg, " "); i > -1 {
event.Source = msg[1:i]
msg = msg[i+1 : len(msg)]
} else {
return nil, errors.New("Malformed msg from server")
}
if i, j := strings.Index(event.Source, "!"), strings.Index(event.Source, "@"); i > -1 && j > -1 && i < j {
event.Nick = event.Source[0:i]
event.User = event.Source[i+1 : j]
event.Host = event.Source[j+1 : len(event.Source)]
}
}
split := strings.SplitN(msg, " :", 2)
args := strings.Split(split[0], " ")
event.Code = strings.ToUpper(args[0])
event.Arguments = args[1:]
if len(split) > 1 {
event.Arguments = append(event.Arguments, split[1])
}
return event, nil
}
// Loop to write to a connection. To be used as a goroutine.
func (irc *Connection) writeLoop() {
defer irc.Done()
errChan := irc.ErrorChan()
for {
select {
case <-irc.end:
return
default:
b, ok := <-irc.pwrite
if !ok || b == "" || irc.socket == nil {
return
}
if irc.Debug {
irc.Log.Printf("--> %s\n", strings.TrimSpace(b))
}
// Set a write deadline based on the time out
irc.socket.SetWriteDeadline(time.Now().Add(irc.Timeout))
_, err := irc.socket.Write([]byte(b))
// Past blocking write, bin timeout
var zero time.Time
irc.socket.SetWriteDeadline(zero)
if err != nil {
errChan <- err
return
}
}
}
return
}
// Pings the server if we have not received any messages for 5 minutes
// to keep the connection alive. To be used as a goroutine.
func (irc *Connection) pingLoop() {
defer irc.Done()
ticker := time.NewTicker(1 * time.Minute) // Tick every minute for monitoring
ticker2 := time.NewTicker(irc.PingFreq) // Tick at the ping frequency.
for {
select {
case <-ticker.C:
//Ping if we haven't received anything from the server within the keep alive period
if time.Since(irc.lastMessage) >= irc.KeepAlive {
irc.SendRawf("PING %d", time.Now().UnixNano())
}
case <-ticker2.C:
//Ping at the ping frequency
irc.SendRawf("PING %d", time.Now().UnixNano())
//Try to recapture nickname if it's not as configured.
if irc.nick != irc.nickcurrent {
irc.nickcurrent = irc.nick
irc.SendRawf("NICK %s", irc.nick)
}
case <-irc.end:
ticker.Stop()
ticker2.Stop()
return
}
}
}
// Main loop to control the connection.
func (irc *Connection) Loop() {
errChan := irc.ErrorChan()
for !irc.stopped {
err := <-errChan
if irc.stopped {
break
}
irc.Log.Printf("Error, disconnected: %s\n", err)
for !irc.stopped {
if err = irc.Reconnect(); err != nil {
irc.Log.Printf("Error while reconnecting: %s\n", err)
time.Sleep(1 * time.Second)
} else {
break
}
}
}
}
// Quit the current connection and disconnect from the server
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.1.6
func (irc *Connection) Quit() {
irc.SendRaw("QUIT")
irc.stopped = true
}
// Use the connection to join a given channel.
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.2.1
func (irc *Connection) Join(channel string) {
irc.pwrite <- fmt.Sprintf("JOIN %s\r\n", channel)
}
// Leave a given channel.
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.2.2
func (irc *Connection) Part(channel string) {
irc.pwrite <- fmt.Sprintf("PART %s\r\n", channel)
}
// Send a notification to a nickname. This is similar to Privmsg but must not receive replies.
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.4.2
func (irc *Connection) Notice(target, message string) {
irc.pwrite <- fmt.Sprintf("NOTICE %s :%s\r\n", target, message)
}
// Send a formated notification to a nickname.
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.4.2
func (irc *Connection) Noticef(target, format string, a ...interface{}) {
irc.Notice(target, fmt.Sprintf(format, a...))
}
// Send (action) message to a target (channel or nickname).
// No clear RFC on this one...
func (irc *Connection) Action(target, message string) {
irc.pwrite <- fmt.Sprintf("PRIVMSG %s :\001ACTION %s\001\r\n", target, message)
}
// Send formatted (action) message to a target (channel or nickname).
func (irc *Connection) Actionf(target, format string, a ...interface{}) {
irc.Action(target, fmt.Sprintf(format, a...))
}
// Send (private) message to a target (channel or nickname).
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.4.1
func (irc *Connection) Privmsg(target, message string) {
irc.pwrite <- fmt.Sprintf("PRIVMSG %s :%s\r\n", target, message)
}
// Send formated string to specified target (channel or nickname).
func (irc *Connection) Privmsgf(target, format string, a ...interface{}) {
irc.Privmsg(target, fmt.Sprintf(format, a...))
}
// Kick <user> from <channel> with <msg>. For no message, pass empty string ("")
func (irc *Connection) Kick(user, channel, msg string) {
var cmd bytes.Buffer
cmd.WriteString(fmt.Sprintf("KICK %s %s", channel, user))
if msg != "" {
cmd.WriteString(fmt.Sprintf(" :%s", msg))
}
cmd.WriteString("\r\n")
irc.pwrite <- cmd.String()
}
// Kick all <users> from <channel> with <msg>. For no message, pass
// empty string ("")
func (irc *Connection) MultiKick(users []string, channel string, msg string) {
var cmd bytes.Buffer
cmd.WriteString(fmt.Sprintf("KICK %s %s", channel, strings.Join(users, ",")))
if msg != "" {
cmd.WriteString(fmt.Sprintf(" :%s", msg))
}
cmd.WriteString("\r\n")
irc.pwrite <- cmd.String()
}
// Send raw string.
func (irc *Connection) SendRaw(message string) {
irc.pwrite <- message + "\r\n"
}
// Send raw formated string.
func (irc *Connection) SendRawf(format string, a ...interface{}) {
irc.SendRaw(fmt.Sprintf(format, a...))
}
// Set (new) nickname.
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.1.2
func (irc *Connection) Nick(n string) {
irc.nick = n
irc.SendRawf("NICK %s", n)
}
// Determine nick currently used with the connection.
func (irc *Connection) GetNick() string {
return irc.nickcurrent
}
// Query information about a particular nickname.
// RFC 1459: https://tools.ietf.org/html/rfc1459#section-4.5.2
func (irc *Connection) Whois(nick string) {
irc.SendRawf("WHOIS %s", nick)
}
// Query information about a given nickname in the server.
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.5.1
func (irc *Connection) Who(nick string) {
irc.SendRawf("WHO %s", nick)
}
// Set different modes for a target (channel or nickname).
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.2.3
func (irc *Connection) Mode(target string, modestring ...string) {
if len(modestring) > 0 {
mode := strings.Join(modestring, " ")
irc.SendRawf("MODE %s %s", target, mode)
return
}
irc.SendRawf("MODE %s", target)
}
func (irc *Connection) ErrorChan() chan error {
return irc.Error
}
// Returns true if the connection is connected to an IRC server.
func (irc *Connection) Connected() bool {
return !irc.stopped
}
// A disconnect sends all buffered messages (if possible),
// stops all goroutines and then closes the socket.
func (irc *Connection) Disconnect() {
for event := range irc.events {
irc.ClearCallback(event)
}
if irc.end != nil {
close(irc.end)
}
irc.end = nil
if irc.pwrite != nil {
close(irc.pwrite)
}
irc.Wait()
if irc.socket != nil {
irc.socket.Close()
}
irc.socket = nil
irc.ErrorChan() <- ErrDisconnected
}
// Reconnect to a server using the current connection.
func (irc *Connection) Reconnect() error {
if irc.end != nil {
close(irc.end)
}
irc.end = nil
irc.Wait() //make sure that wait group is cleared ensuring that all spawned goroutines have completed
irc.end = make(chan struct{})
return irc.Connect(irc.Server)
}
// Connect to a given server using the current connection configuration.
// This function also takes care of identification if a password is provided.
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.1
func (irc *Connection) Connect(server string) error {
irc.Server = server
// mark Server as stopped since there can be an error during connect
irc.stopped = true
// make sure everything is ready for connection
if len(irc.Server) == 0 {
return errors.New("empty 'server'")
}
if strings.Count(irc.Server, ":") != 1 {
return errors.New("wrong number of ':' in address")
}
if strings.Index(irc.Server, ":") == 0 {
return errors.New("hostname is missing")
}
if strings.Index(irc.Server, ":") == len(irc.Server)-1 {
return errors.New("port missing")
}
// check for valid range
ports := strings.Split(irc.Server, ":")[1]
port, err := strconv.Atoi(ports)
if err != nil {
return errors.New("extracting port failed")
}
if !((port >= 0) && (port <= 65535)) {
return errors.New("port number outside valid range")
}
if irc.Log == nil {
return errors.New("'Log' points to nil")
}
if len(irc.nick) == 0 {
return errors.New("empty 'nick'")
}
if len(irc.user) == 0 {
return errors.New("empty 'user'")
}
if irc.UseTLS {
dialer := &net.Dialer{Timeout: irc.Timeout}
irc.socket, err = tls.DialWithDialer(dialer, "tcp", irc.Server, irc.TLSConfig)
} else {
irc.socket, err = net.DialTimeout("tcp", irc.Server, irc.Timeout)
}
if err != nil {
return err
}
irc.stopped = false
irc.Log.Printf("Connected to %s (%s)\n", irc.Server, irc.socket.RemoteAddr())
irc.pwrite = make(chan string, 10)
irc.Error = make(chan error, 2)
irc.Add(3)
go irc.readLoop()
go irc.writeLoop()
go irc.pingLoop()
if len(irc.Password) > 0 {
irc.pwrite <- fmt.Sprintf("PASS %s\r\n", irc.Password)
}
irc.pwrite <- fmt.Sprintf("NICK %s\r\n", irc.nick)
irc.pwrite <- fmt.Sprintf("USER %s 0.0.0.0 0.0.0.0 :%s\r\n", irc.user, irc.user)
return nil
}
// Create a connection with the (publicly visible) nickname and username.
// The nickname is later used to address the user. Returns nil if nick
// or user are empty.
func IRC(nick, user string) *Connection {
// catch invalid values
if len(nick) == 0 {
return nil
}
if len(user) == 0 {
return nil
}
irc := &Connection{
nick: nick,
nickcurrent: nick,
user: user,
Log: log.New(os.Stdout, "", log.LstdFlags),
end: make(chan struct{}),
Version: VERSION,
KeepAlive: 4 * time.Minute,
Timeout: 1 * time.Minute,
PingFreq: 15 * time.Minute,
}
irc.setupCallbacks()
return irc
}

View File

@ -0,0 +1,229 @@
package irc
import (
"crypto/sha1"
"fmt"
"math/rand"
"reflect"
"strconv"
"strings"
"time"
)
// Register a callback to a connection and event code. A callback is a function
// which takes only an Event pointer as parameter. Valid event codes are all
// IRC/CTCP commands and error/response codes. This function returns the ID of
// the registered callback for later management.
func (irc *Connection) AddCallback(eventcode string, callback func(*Event)) string {
eventcode = strings.ToUpper(eventcode)
if _, ok := irc.events[eventcode]; !ok {
irc.events[eventcode] = make(map[string]func(*Event))
}
h := sha1.New()
rawId := []byte(fmt.Sprintf("%v%d", reflect.ValueOf(callback).Pointer(), rand.Int63()))
h.Write(rawId)
id := fmt.Sprintf("%x", h.Sum(nil))
irc.events[eventcode][id] = callback
return id
}
// Remove callback i (ID) from the given event code. This functions returns
// true upon success, false if any error occurs.
func (irc *Connection) RemoveCallback(eventcode string, i string) bool {
eventcode = strings.ToUpper(eventcode)
if event, ok := irc.events[eventcode]; ok {
if _, ok := event[i]; ok {
delete(irc.events[eventcode], i)
return true
}
irc.Log.Printf("Event found, but no callback found at id %s\n", i)
return false
}
irc.Log.Println("Event not found")
return false
}
// Remove all callbacks from a given event code. It returns true
// if given event code is found and cleared.
func (irc *Connection) ClearCallback(eventcode string) bool {
eventcode = strings.ToUpper(eventcode)
if _, ok := irc.events[eventcode]; ok {
irc.events[eventcode] = make(map[string]func(*Event))
return true
}
irc.Log.Println("Event not found")
return false
}
// Replace callback i (ID) associated with a given event code with a new callback function.
func (irc *Connection) ReplaceCallback(eventcode string, i string, callback func(*Event)) {
eventcode = strings.ToUpper(eventcode)
if event, ok := irc.events[eventcode]; ok {
if _, ok := event[i]; ok {
event[i] = callback
return
}
irc.Log.Printf("Event found, but no callback found at id %s\n", i)
}
irc.Log.Printf("Event not found. Use AddCallBack\n")
}
// Execute all callbacks associated with a given event.
func (irc *Connection) RunCallbacks(event *Event) {
msg := event.Message()
if event.Code == "PRIVMSG" && len(msg) > 2 && msg[0] == '\x01' {
event.Code = "CTCP" //Unknown CTCP
if i := strings.LastIndex(msg, "\x01"); i > 0 {
msg = msg[1:i]
} else {
irc.Log.Printf("Invalid CTCP Message: %s\n", strconv.Quote(msg))
return
}
if msg == "VERSION" {
event.Code = "CTCP_VERSION"
} else if msg == "TIME" {
event.Code = "CTCP_TIME"
} else if strings.HasPrefix(msg, "PING") {
event.Code = "CTCP_PING"
} else if msg == "USERINFO" {
event.Code = "CTCP_USERINFO"
} else if msg == "CLIENTINFO" {
event.Code = "CTCP_CLIENTINFO"
} else if strings.HasPrefix(msg, "ACTION") {
event.Code = "CTCP_ACTION"
if len(msg) > 6 {
msg = msg[7:]
} else {
msg = ""
}
}
event.Arguments[len(event.Arguments)-1] = msg
}
if callbacks, ok := irc.events[event.Code]; ok {
if irc.VerboseCallbackHandler {
irc.Log.Printf("%v (%v) >> %#v\n", event.Code, len(callbacks), event)
}
for _, callback := range callbacks {
go callback(event)
}
} else if irc.VerboseCallbackHandler {
irc.Log.Printf("%v (0) >> %#v\n", event.Code, event)
}
if callbacks, ok := irc.events["*"]; ok {
if irc.VerboseCallbackHandler {
irc.Log.Printf("Wildcard %v (%v) >> %#v\n", event.Code, len(callbacks), event)
}
for _, callback := range callbacks {
go callback(event)
}
}
}
// Set up some initial callbacks to handle the IRC/CTCP protocol.
func (irc *Connection) setupCallbacks() {
irc.events = make(map[string]map[string]func(*Event))
//Handle error events
irc.AddCallback("ERROR", func(e *Event) { irc.Disconnect() })
//Handle ping events
irc.AddCallback("PING", func(e *Event) { irc.SendRaw("PONG :" + e.Message()) })
//Version handler
irc.AddCallback("CTCP_VERSION", func(e *Event) {
irc.SendRawf("NOTICE %s :\x01VERSION %s\x01", e.Nick, irc.Version)
})
irc.AddCallback("CTCP_USERINFO", func(e *Event) {
irc.SendRawf("NOTICE %s :\x01USERINFO %s\x01", e.Nick, irc.user)
})
irc.AddCallback("CTCP_CLIENTINFO", func(e *Event) {
irc.SendRawf("NOTICE %s :\x01CLIENTINFO PING VERSION TIME USERINFO CLIENTINFO\x01", e.Nick)
})
irc.AddCallback("CTCP_TIME", func(e *Event) {
ltime := time.Now()
irc.SendRawf("NOTICE %s :\x01TIME %s\x01", e.Nick, ltime.String())
})
irc.AddCallback("CTCP_PING", func(e *Event) { irc.SendRawf("NOTICE %s :\x01%s\x01", e.Nick, e.Message()) })
// 437: ERR_UNAVAILRESOURCE "<nick/channel> :Nick/channel is temporarily unavailable"
// Add a _ to current nick. If irc.nickcurrent is empty this cannot
// work. It has to be set somewhere first in case the nick is already
// taken or unavailable from the beginning.
irc.AddCallback("437", func(e *Event) {
// If irc.nickcurrent hasn't been set yet, set to irc.nick
if irc.nickcurrent == "" {
irc.nickcurrent = irc.nick
}
if len(irc.nickcurrent) > 8 {
irc.nickcurrent = "_" + irc.nickcurrent
} else {
irc.nickcurrent = irc.nickcurrent + "_"
}
irc.SendRawf("NICK %s", irc.nickcurrent)
})
// 433: ERR_NICKNAMEINUSE "<nick> :Nickname is already in use"
// Add a _ to current nick.
irc.AddCallback("433", func(e *Event) {
// If irc.nickcurrent hasn't been set yet, set to irc.nick
if irc.nickcurrent == "" {
irc.nickcurrent = irc.nick
}
if len(irc.nickcurrent) > 8 {
irc.nickcurrent = "_" + irc.nickcurrent
} else {
irc.nickcurrent = irc.nickcurrent + "_"
}
irc.SendRawf("NICK %s", irc.nickcurrent)
})
irc.AddCallback("PONG", func(e *Event) {
ns, _ := strconv.ParseInt(e.Message(), 10, 64)
delta := time.Duration(time.Now().UnixNano() - ns)
if irc.Debug {
irc.Log.Printf("Lag: %vs\n", delta)
}
})
// NICK Define a nickname.
// Set irc.nickcurrent to the new nick actually used in this connection.
irc.AddCallback("NICK", func(e *Event) {
if e.Nick == irc.nick {
irc.nickcurrent = e.Message()
}
})
// 1: RPL_WELCOME "Welcome to the Internet Relay Network <nick>!<user>@<host>"
// Set irc.nickcurrent to the actually used nick in this connection.
irc.AddCallback("001", func(e *Event) {
irc.nickcurrent = e.Arguments[0]
})
}
func init() {
rand.Seed(time.Now().UnixNano())
}

View File

@ -0,0 +1,66 @@
// Copyright 2009 Thomas Jager <mail@jager.no> All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package irc
import (
"crypto/tls"
"log"
"net"
"sync"
"time"
)
type Connection struct {
sync.WaitGroup
Debug bool
Error chan error
Password string
UseTLS bool
TLSConfig *tls.Config
Version string
Timeout time.Duration
PingFreq time.Duration
KeepAlive time.Duration
Server string
socket net.Conn
pwrite chan string
end chan struct{}
nick string //The nickname we want.
nickcurrent string //The nickname we currently have.
user string
registered bool
events map[string]map[string]func(*Event)
lastMessage time.Time
VerboseCallbackHandler bool
Log *log.Logger
stopped bool
}
// A struct to represent an event.
type Event struct {
Code string
Raw string
Nick string //<nick>
Host string //<nick>!<usr>@<host>
Source string //<host>
User string //<usr>
Arguments []string
Connection *Connection
}
// Retrieve the last message from Event arguments.
// This function leaves the arguments untouched and
// returns an empty string if there are none.
func (e *Event) Message() string {
if len(e.Arguments) == 0 {
return ""
}
return e.Arguments[len(e.Arguments)-1]
}

View File

@ -0,0 +1,258 @@
package irc
import (
"crypto/tls"
"testing"
"time"
)
func TestConnectionEmtpyServer(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
err := irccon.Connect("")
if err == nil {
t.Fatal("emtpy server string not detected")
}
}
func TestConnectionDoubleColon(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
err := irccon.Connect("::")
if err == nil {
t.Fatal("wrong number of ':' not detected")
}
}
func TestConnectionMissingHost(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
err := irccon.Connect(":6667")
if err == nil {
t.Fatal("missing host not detected")
}
}
func TestConnectionMissingPort(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
err := irccon.Connect("chat.freenode.net:")
if err == nil {
t.Fatal("missing port not detected")
}
}
func TestConnectionNegativePort(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
err := irccon.Connect("chat.freenode.net:-1")
if err == nil {
t.Fatal("negative port number not detected")
}
}
func TestConnectionTooLargePort(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
err := irccon.Connect("chat.freenode.net:65536")
if err == nil {
t.Fatal("too large port number not detected")
}
}
func TestConnectionMissingLog(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
irccon.Log = nil
err := irccon.Connect("chat.freenode.net:6667")
if err == nil {
t.Fatal("missing 'Log' not detected")
}
}
func TestConnectionEmptyUser(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
// user may be changed after creation
irccon.user = ""
err := irccon.Connect("chat.freenode.net:6667")
if err == nil {
t.Fatal("empty 'user' not detected")
}
}
func TestConnectionEmptyNick(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
// nick may be changed after creation
irccon.nick = ""
err := irccon.Connect("chat.freenode.net:6667")
if err == nil {
t.Fatal("empty 'nick' not detected")
}
}
func TestRemoveCallback(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
irccon.VerboseCallbackHandler = true
irccon.Debug = true
done := make(chan int, 10)
irccon.AddCallback("TEST", func(e *Event) { done <- 1 })
id := irccon.AddCallback("TEST", func(e *Event) { done <- 2 })
irccon.AddCallback("TEST", func(e *Event) { done <- 3 })
// Should remove callback at index 1
irccon.RemoveCallback("TEST", id)
irccon.RunCallbacks(&Event{
Code: "TEST",
})
var results []int
results = append(results, <-done)
results = append(results, <-done)
if len(results) != 2 || results[0] == 2 || results[1] == 2 {
t.Error("Callback 2 not removed")
}
}
func TestWildcardCallback(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
irccon.VerboseCallbackHandler = true
irccon.Debug = true
done := make(chan int, 10)
irccon.AddCallback("TEST", func(e *Event) { done <- 1 })
irccon.AddCallback("*", func(e *Event) { done <- 2 })
irccon.RunCallbacks(&Event{
Code: "TEST",
})
var results []int
results = append(results, <-done)
results = append(results, <-done)
if len(results) != 2 || !(results[0] == 1 && results[1] == 2) {
t.Error("Wildcard callback not called")
}
}
func TestClearCallback(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
irccon.VerboseCallbackHandler = true
irccon.Debug = true
done := make(chan int, 10)
irccon.AddCallback("TEST", func(e *Event) { done <- 0 })
irccon.AddCallback("TEST", func(e *Event) { done <- 1 })
irccon.ClearCallback("TEST")
irccon.AddCallback("TEST", func(e *Event) { done <- 2 })
irccon.AddCallback("TEST", func(e *Event) { done <- 3 })
irccon.RunCallbacks(&Event{
Code: "TEST",
})
var results []int
results = append(results, <-done)
results = append(results, <-done)
if len(results) != 2 || !(results[0] == 2 && results[1] == 3) {
t.Error("Callbacks not cleared")
}
}
func TestIRCemptyNick(t *testing.T) {
irccon := IRC("", "go-eventirc")
irccon = nil
if irccon != nil {
t.Error("empty nick didn't result in error")
t.Fail()
}
}
func TestIRCemptyUser(t *testing.T) {
irccon := IRC("go-eventirc", "")
if irccon != nil {
t.Error("empty user didn't result in error")
}
}
func TestConnection(t *testing.T) {
irccon1 := IRC("go-eventirc1", "go-eventirc1")
irccon1.VerboseCallbackHandler = true
irccon1.Debug = true
irccon2 := IRC("go-eventirc2", "go-eventirc2")
irccon2.VerboseCallbackHandler = true
irccon2.Debug = true
err := irccon1.Connect("irc.freenode.net:6667")
if err != nil {
t.Log(err.Error())
t.Fatal("Can't connect to freenode.")
}
err = irccon2.Connect("irc.freenode.net:6667")
if err != nil {
t.Log(err.Error())
t.Fatal("Can't connect to freenode.")
}
irccon1.AddCallback("001", func(e *Event) { irccon1.Join("#go-eventirc") })
irccon2.AddCallback("001", func(e *Event) { irccon2.Join("#go-eventirc") })
con2ok := false
irccon1.AddCallback("366", func(e *Event) {
t := time.NewTicker(1 * time.Second)
i := 10
for {
<-t.C
irccon1.Privmsgf("#go-eventirc", "Test Message%d\n", i)
if con2ok {
i -= 1
}
if i == 0 {
t.Stop()
irccon1.Quit()
}
}
})
irccon2.AddCallback("366", func(e *Event) {
irccon2.Privmsg("#go-eventirc", "Test Message\n")
con2ok = true
irccon2.Nick("go-eventnewnick")
})
irccon2.AddCallback("PRIVMSG", func(e *Event) {
t.Log(e.Message())
if e.Message() == "Test Message5" {
irccon2.Quit()
}
})
irccon2.AddCallback("NICK", func(e *Event) {
if irccon2.nickcurrent == "go-eventnewnick" {
t.Fatal("Nick change did not work!")
}
})
go irccon2.Loop()
irccon1.Loop()
}
func TestConnectionSSL(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
irccon.VerboseCallbackHandler = true
irccon.Debug = true
irccon.UseTLS = true
irccon.TLSConfig = &tls.Config{InsecureSkipVerify: true}
err := irccon.Connect("irc.freenode.net:7000")
if err != nil {
t.Log(err.Error())
t.Fatal("Can't connect to freenode.")
}
irccon.AddCallback("001", func(e *Event) { irccon.Join("#go-eventirc") })
irccon.AddCallback("366", func(e *Event) {
irccon.Privmsg("#go-eventirc", "Test Message\n")
time.Sleep(2 * time.Second)
irccon.Quit()
})
irccon.Loop()
}

View File

@ -0,0 +1,14 @@
// +build gofuzz
package irc
func Fuzz(data []byte) int {
b := bytes.NewBuffer(data)
event, err := parseToEvent(b.String())
if err == nil {
irc := IRC("go-eventirc", "go-eventirc")
irc.RunCallbacks(event)
return 1
}
return 0
}

47
vendor/manifest vendored Normal file
View File

@ -0,0 +1,47 @@
{
"version": 0,
"dependencies": [
{
"importpath": "github.com/Xe/uuid",
"repository": "https://github.com/Xe/uuid",
"revision": "62b230097e9c9534ca2074782b25d738c4b68964",
"branch": "master"
},
{
"importpath": "github.com/cjoudrey/gluahttp",
"repository": "https://github.com/cjoudrey/gluahttp",
"revision": "1128ce320b775e8e3fa2b8095b9c2116aa6869db",
"branch": "master"
},
{
"importpath": "github.com/layeh/gopher-json",
"repository": "https://github.com/layeh/gopher-json",
"revision": "bb1ff6467afab1f0ffee68113a256ce7435b578b",
"branch": "master"
},
{
"importpath": "github.com/layeh/gopher-luar",
"repository": "https://github.com/layeh/gopher-luar",
"revision": "ad06026c2b081cb9a2a563d3078f2ce67389d9fa",
"branch": "master"
},
{
"importpath": "github.com/robfig/cron",
"repository": "https://github.com/robfig/cron",
"revision": "67823cd24dece1b04cced3a0a0b3ca2bc84d875e",
"branch": "master"
},
{
"importpath": "github.com/scalingdata/gcfg",
"repository": "https://github.com/scalingdata/gcfg",
"revision": "37aabad69cfd3d20b8390d902a8b10e245c615ff",
"branch": "master"
},
{
"importpath": "github.com/yuin/gopher-lua",
"repository": "https://github.com/yuin/gopher-lua",
"revision": "21b70b48ba3b0da2122b79555094763ea97e3e98",
"branch": "master"
}
]
}

View File

@ -0,0 +1,2 @@
Paul Borman <borman@google.com>
Christine Dodrill <xena@yolo-swag.com>

27
vendor/src/github.com/Xe/uuid/LICENSE vendored Normal file
View File

@ -0,0 +1,27 @@
Copyright (c) 2009 Google Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,5 @@
go-uuid
=======
code.google.com is going away and I use this library a lot. It used to live at
https://code.google.com/p/go-uuid/ but now I take care of it.

84
vendor/src/github.com/Xe/uuid/dce.go vendored Normal file
View File

@ -0,0 +1,84 @@
// Copyright 2011 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"encoding/binary"
"fmt"
"os"
)
// A Domain represents a Version 2 domain
type Domain byte
// Domain constants for DCE Security (Version 2) UUIDs.
const (
Person = Domain(0)
Group = Domain(1)
Org = Domain(2)
)
// NewDCESecurity returns a DCE Security (Version 2) UUID.
//
// The domain should be one of Person, Group or Org.
// On a POSIX system the id should be the users UID for the Person
// domain and the users GID for the Group. The meaning of id for
// the domain Org or on non-POSIX systems is site defined.
//
// For a given domain/id pair the same token may be returned for up to
// 7 minutes and 10 seconds.
func NewDCESecurity(domain Domain, id uint32) UUID {
uuid := NewUUID()
if uuid != nil {
uuid[6] = (uuid[6] & 0x0f) | 0x20 // Version 2
uuid[9] = byte(domain)
binary.BigEndian.PutUint32(uuid[0:], id)
}
return uuid
}
// NewDCEPerson returns a DCE Security (Version 2) UUID in the person
// domain with the id returned by os.Getuid.
//
// NewDCEPerson(Person, uint32(os.Getuid()))
func NewDCEPerson() UUID {
return NewDCESecurity(Person, uint32(os.Getuid()))
}
// NewDCEGroup returns a DCE Security (Version 2) UUID in the group
// domain with the id returned by os.Getgid.
//
// NewDCEGroup(Group, uint32(os.Getgid()))
func NewDCEGroup() UUID {
return NewDCESecurity(Group, uint32(os.Getgid()))
}
// Domain returns the domain for a Version 2 UUID or false.
func (uuid UUID) Domain() (Domain, bool) {
if v, _ := uuid.Version(); v != 2 {
return 0, false
}
return Domain(uuid[9]), true
}
// Id returns the id for a Version 2 UUID or false.
func (uuid UUID) Id() (uint32, bool) {
if v, _ := uuid.Version(); v != 2 {
return 0, false
}
return binary.BigEndian.Uint32(uuid[0:4]), true
}
func (d Domain) String() string {
switch d {
case Person:
return "Person"
case Group:
return "Group"
case Org:
return "Org"
}
return fmt.Sprintf("Domain%d", int(d))
}

8
vendor/src/github.com/Xe/uuid/doc.go vendored Normal file
View File

@ -0,0 +1,8 @@
// Copyright 2011 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The uuid package generates and inspects UUIDs.
//
// UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security Services.
package uuid

53
vendor/src/github.com/Xe/uuid/hash.go vendored Normal file
View File

@ -0,0 +1,53 @@
// Copyright 2011 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"crypto/md5"
"crypto/sha1"
"hash"
)
// Well known Name Space IDs and UUIDs
var (
NameSpace_DNS = Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
NameSpace_URL = Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8")
NameSpace_OID = Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8")
NameSpace_X500 = Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8")
NIL = Parse("00000000-0000-0000-0000-000000000000")
)
// NewHash returns a new UUID dervied from the hash of space concatenated with
// data generated by h. The hash should be at least 16 byte in length. The
// first 16 bytes of the hash are used to form the UUID. The version of the
// UUID will be the lower 4 bits of version. NewHash is used to implement
// NewMD5 and NewSHA1.
func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID {
h.Reset()
h.Write(space)
h.Write([]byte(data))
s := h.Sum(nil)
uuid := make([]byte, 16)
copy(uuid, s)
uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4)
uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant
return uuid
}
// NewMD5 returns a new MD5 (Version 3) UUID based on the
// supplied name space and data.
//
// NewHash(md5.New(), space, data, 3)
func NewMD5(space UUID, data []byte) UUID {
return NewHash(md5.New(), space, data, 3)
}
// NewSHA1 returns a new SHA1 (Version 5) UUID based on the
// supplied name space and data.
//
// NewHash(sha1.New(), space, data, 5)
func NewSHA1(space UUID, data []byte) UUID {
return NewHash(sha1.New(), space, data, 5)
}

101
vendor/src/github.com/Xe/uuid/node.go vendored Normal file
View File

@ -0,0 +1,101 @@
// Copyright 2011 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import "net"
var (
interfaces []net.Interface // cached list of interfaces
ifname string // name of interface being used
nodeID []byte // hardware for version 1 UUIDs
)
// NodeInterface returns the name of the interface from which the NodeID was
// derived. The interface "user" is returned if the NodeID was set by
// SetNodeID.
func NodeInterface() string {
return ifname
}
// SetNodeInterface selects the hardware address to be used for Version 1 UUIDs.
// If name is "" then the first usable interface found will be used or a random
// Node ID will be generated. If a named interface cannot be found then false
// is returned.
//
// SetNodeInterface never fails when name is "".
func SetNodeInterface(name string) bool {
if interfaces == nil {
var err error
interfaces, err = net.Interfaces()
if err != nil && name != "" {
return false
}
}
for _, ifs := range interfaces {
if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) {
if setNodeID(ifs.HardwareAddr) {
ifname = ifs.Name
return true
}
}
}
// We found no interfaces with a valid hardware address. If name
// does not specify a specific interface generate a random Node ID
// (section 4.1.6)
if name == "" {
if nodeID == nil {
nodeID = make([]byte, 6)
}
randomBits(nodeID)
return true
}
return false
}
// NodeID returns a slice of a copy of the current Node ID, setting the Node ID
// if not already set.
func NodeID() []byte {
if nodeID == nil {
SetNodeInterface("")
}
nid := make([]byte, 6)
copy(nid, nodeID)
return nid
}
// SetNodeID sets the Node ID to be used for Version 1 UUIDs. The first 6 bytes
// of id are used. If id is less than 6 bytes then false is returned and the
// Node ID is not set.
func SetNodeID(id []byte) bool {
if setNodeID(id) {
ifname = "user"
return true
}
return false
}
func setNodeID(id []byte) bool {
if len(id) < 6 {
return false
}
if nodeID == nil {
nodeID = make([]byte, 6)
}
copy(nodeID, id)
return true
}
// NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is
// not valid. The NodeID is only well defined for version 1 and 2 UUIDs.
func (uuid UUID) NodeID() []byte {
if len(uuid) != 16 {
return nil
}
node := make([]byte, 6)
copy(node, uuid[10:])
return node
}

132
vendor/src/github.com/Xe/uuid/time.go vendored Normal file
View File

@ -0,0 +1,132 @@
// Copyright 2014 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"encoding/binary"
"sync"
"time"
)
// A Time represents a time as the number of 100's of nanoseconds since 15 Oct
// 1582.
type Time int64
const (
lillian = 2299160 // Julian day of 15 Oct 1582
unix = 2440587 // Julian day of 1 Jan 1970
epoch = unix - lillian // Days between epochs
g1582 = epoch * 86400 // seconds between epochs
g1582ns100 = g1582 * 10000000 // 100s of a nanoseconds between epochs
)
var (
mu sync.Mutex
lasttime uint64 // last time we returned
clock_seq uint16 // clock sequence for this run
timeNow = time.Now // for testing
)
// UnixTime converts t the number of seconds and nanoseconds using the Unix
// epoch of 1 Jan 1970.
func (t Time) UnixTime() (sec, nsec int64) {
sec = int64(t - g1582ns100)
nsec = (sec % 10000000) * 100
sec /= 10000000
return sec, nsec
}
// GetTime returns the current Time (100s of nanoseconds since 15 Oct 1582) and
// adjusts the clock sequence as needed. An error is returned if the current
// time cannot be determined.
func GetTime() (Time, error) {
defer mu.Unlock()
mu.Lock()
return getTime()
}
func getTime() (Time, error) {
t := timeNow()
// If we don't have a clock sequence already, set one.
if clock_seq == 0 {
setClockSequence(-1)
}
now := uint64(t.UnixNano()/100) + g1582ns100
// If time has gone backwards with this clock sequence then we
// increment the clock sequence
if now <= lasttime {
clock_seq = ((clock_seq + 1) & 0x3fff) | 0x8000
}
lasttime = now
return Time(now), nil
}
// ClockSequence returns the current clock sequence, generating one if not
// already set. The clock sequence is only used for Version 1 UUIDs.
//
// The uuid package does not use global static storage for the clock sequence or
// the last time a UUID was generated. Unless SetClockSequence a new random
// clock sequence is generated the first time a clock sequence is requested by
// ClockSequence, GetTime, or NewUUID. (section 4.2.1.1) sequence is generated
// for
func ClockSequence() int {
defer mu.Unlock()
mu.Lock()
return clockSequence()
}
func clockSequence() int {
if clock_seq == 0 {
setClockSequence(-1)
}
return int(clock_seq & 0x3fff)
}
// SetClockSeq sets the clock sequence to the lower 14 bits of seq. Setting to
// -1 causes a new sequence to be generated.
func SetClockSequence(seq int) {
defer mu.Unlock()
mu.Lock()
setClockSequence(seq)
}
func setClockSequence(seq int) {
if seq == -1 {
var b [2]byte
randomBits(b[:]) // clock sequence
seq = int(b[0])<<8 | int(b[1])
}
old_seq := clock_seq
clock_seq = uint16(seq&0x3fff) | 0x8000 // Set our variant
if old_seq != clock_seq {
lasttime = 0
}
}
// Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in
// uuid. It returns false if uuid is not valid. The time is only well defined
// for version 1 and 2 UUIDs.
func (uuid UUID) Time() (Time, bool) {
if len(uuid) != 16 {
return 0, false
}
time := int64(binary.BigEndian.Uint32(uuid[0:4]))
time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32
time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48
return Time(time), true
}
// ClockSequence returns the clock sequence encoded in uuid. It returns false
// if uuid is not valid. The clock sequence is only well defined for version 1
// and 2 UUIDs.
func (uuid UUID) ClockSequence() (int, bool) {
if len(uuid) != 16 {
return 0, false
}
return int(binary.BigEndian.Uint16(uuid[8:10])) & 0x3fff, true
}

43
vendor/src/github.com/Xe/uuid/util.go vendored Normal file
View File

@ -0,0 +1,43 @@
// Copyright 2011 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"io"
)
// randomBits completely fills slice b with random data.
func randomBits(b []byte) {
if _, err := io.ReadFull(rander, b); err != nil {
panic(err.Error()) // rand should never fail
}
}
// xvalues returns the value of a byte as a hexadecimal digit or 255.
var xvalues = []byte{
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255,
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
}
// xtob converts the the first two hex bytes of x into a byte.
func xtob(x string) (byte, bool) {
b1 := xvalues[x[0]]
b2 := xvalues[x[1]]
return (b1 << 4) | b2, b1 != 255 && b2 != 255
}

163
vendor/src/github.com/Xe/uuid/uuid.go vendored Normal file
View File

@ -0,0 +1,163 @@
// Copyright 2011 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"bytes"
"crypto/rand"
"fmt"
"io"
"strings"
)
// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC
// 4122.
type UUID []byte
// A Version represents a UUIDs version.
type Version byte
// A Variant represents a UUIDs variant.
type Variant byte
// Constants returned by Variant.
const (
Invalid = Variant(iota) // Invalid UUID
RFC4122 // The variant specified in RFC4122
Reserved // Reserved, NCS backward compatibility.
Microsoft // Reserved, Microsoft Corporation backward compatibility.
Future // Reserved for future definition.
)
var rander = rand.Reader // random function
// New returns a new random (version 4) UUID as a string. It is a convenience
// function for NewRandom().String().
func New() string {
return NewRandom().String()
}
// Parse decodes s into a UUID or returns nil. Both the UUID form of
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx are decoded.
func Parse(s string) UUID {
if len(s) == 36+9 {
if strings.ToLower(s[:9]) != "urn:uuid:" {
return nil
}
s = s[9:]
} else if len(s) != 36 {
return nil
}
if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
return nil
}
uuid := make([]byte, 16)
for i, x := range []int{
0, 2, 4, 6,
9, 11,
14, 16,
19, 21,
24, 26, 28, 30, 32, 34} {
if v, ok := xtob(s[x:]); !ok {
return nil
} else {
uuid[i] = v
}
}
return uuid
}
// Equal returns true if uuid1 and uuid2 are equal.
func Equal(uuid1, uuid2 UUID) bool {
return bytes.Equal(uuid1, uuid2)
}
// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
// , or "" if uuid is invalid.
func (uuid UUID) String() string {
if uuid == nil || len(uuid) != 16 {
return ""
}
b := []byte(uuid)
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
b[:4], b[4:6], b[6:8], b[8:10], b[10:])
}
// URN returns the RFC 2141 URN form of uuid,
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, or "" if uuid is invalid.
func (uuid UUID) URN() string {
if uuid == nil || len(uuid) != 16 {
return ""
}
b := []byte(uuid)
return fmt.Sprintf("urn:uuid:%08x-%04x-%04x-%04x-%012x",
b[:4], b[4:6], b[6:8], b[8:10], b[10:])
}
// Variant returns the variant encoded in uuid. It returns Invalid if
// uuid is invalid.
func (uuid UUID) Variant() Variant {
if len(uuid) != 16 {
return Invalid
}
switch {
case (uuid[8] & 0xc0) == 0x80:
return RFC4122
case (uuid[8] & 0xe0) == 0xc0:
return Microsoft
case (uuid[8] & 0xe0) == 0xe0:
return Future
default:
return Reserved
}
panic("unreachable")
}
// Version returns the verison of uuid. It returns false if uuid is not
// valid.
func (uuid UUID) Version() (Version, bool) {
if len(uuid) != 16 {
return 0, false
}
return Version(uuid[6] >> 4), true
}
func (v Version) String() string {
if v > 15 {
return fmt.Sprintf("BAD_VERSION_%d", v)
}
return fmt.Sprintf("VERSION_%d", v)
}
func (v Variant) String() string {
switch v {
case RFC4122:
return "RFC4122"
case Reserved:
return "Reserved"
case Microsoft:
return "Microsoft"
case Future:
return "Future"
case Invalid:
return "Invalid"
}
return fmt.Sprintf("BadVariant%d", int(v))
}
// SetRand sets the random number generator to r, which implents io.Reader.
// If r.Read returns an error when the package requests random data then
// a panic will be issued.
//
// Calling SetRand with nil sets the random number generator to the default
// generator.
func SetRand(r io.Reader) {
if r == nil {
rander = rand.Reader
return
}
rander = r
}

View File

@ -0,0 +1,390 @@
// Copyright 2011 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"bytes"
"fmt"
"os"
"strings"
"testing"
"time"
)
type test struct {
in string
version Version
variant Variant
isuuid bool
}
var tests = []test{
{"f47ac10b-58cc-0372-8567-0e02b2c3d479", 0, RFC4122, true},
{"f47ac10b-58cc-1372-8567-0e02b2c3d479", 1, RFC4122, true},
{"f47ac10b-58cc-2372-8567-0e02b2c3d479", 2, RFC4122, true},
{"f47ac10b-58cc-3372-8567-0e02b2c3d479", 3, RFC4122, true},
{"f47ac10b-58cc-4372-8567-0e02b2c3d479", 4, RFC4122, true},
{"f47ac10b-58cc-5372-8567-0e02b2c3d479", 5, RFC4122, true},
{"f47ac10b-58cc-6372-8567-0e02b2c3d479", 6, RFC4122, true},
{"f47ac10b-58cc-7372-8567-0e02b2c3d479", 7, RFC4122, true},
{"f47ac10b-58cc-8372-8567-0e02b2c3d479", 8, RFC4122, true},
{"f47ac10b-58cc-9372-8567-0e02b2c3d479", 9, RFC4122, true},
{"f47ac10b-58cc-a372-8567-0e02b2c3d479", 10, RFC4122, true},
{"f47ac10b-58cc-b372-8567-0e02b2c3d479", 11, RFC4122, true},
{"f47ac10b-58cc-c372-8567-0e02b2c3d479", 12, RFC4122, true},
{"f47ac10b-58cc-d372-8567-0e02b2c3d479", 13, RFC4122, true},
{"f47ac10b-58cc-e372-8567-0e02b2c3d479", 14, RFC4122, true},
{"f47ac10b-58cc-f372-8567-0e02b2c3d479", 15, RFC4122, true},
{"urn:uuid:f47ac10b-58cc-4372-0567-0e02b2c3d479", 4, Reserved, true},
{"URN:UUID:f47ac10b-58cc-4372-0567-0e02b2c3d479", 4, Reserved, true},
{"f47ac10b-58cc-4372-0567-0e02b2c3d479", 4, Reserved, true},
{"f47ac10b-58cc-4372-1567-0e02b2c3d479", 4, Reserved, true},
{"f47ac10b-58cc-4372-2567-0e02b2c3d479", 4, Reserved, true},
{"f47ac10b-58cc-4372-3567-0e02b2c3d479", 4, Reserved, true},
{"f47ac10b-58cc-4372-4567-0e02b2c3d479", 4, Reserved, true},
{"f47ac10b-58cc-4372-5567-0e02b2c3d479", 4, Reserved, true},
{"f47ac10b-58cc-4372-6567-0e02b2c3d479", 4, Reserved, true},
{"f47ac10b-58cc-4372-7567-0e02b2c3d479", 4, Reserved, true},
{"f47ac10b-58cc-4372-8567-0e02b2c3d479", 4, RFC4122, true},
{"f47ac10b-58cc-4372-9567-0e02b2c3d479", 4, RFC4122, true},
{"f47ac10b-58cc-4372-a567-0e02b2c3d479", 4, RFC4122, true},
{"f47ac10b-58cc-4372-b567-0e02b2c3d479", 4, RFC4122, true},
{"f47ac10b-58cc-4372-c567-0e02b2c3d479", 4, Microsoft, true},
{"f47ac10b-58cc-4372-d567-0e02b2c3d479", 4, Microsoft, true},
{"f47ac10b-58cc-4372-e567-0e02b2c3d479", 4, Future, true},
{"f47ac10b-58cc-4372-f567-0e02b2c3d479", 4, Future, true},
{"f47ac10b158cc-5372-a567-0e02b2c3d479", 0, Invalid, false},
{"f47ac10b-58cc25372-a567-0e02b2c3d479", 0, Invalid, false},
{"f47ac10b-58cc-53723a567-0e02b2c3d479", 0, Invalid, false},
{"f47ac10b-58cc-5372-a56740e02b2c3d479", 0, Invalid, false},
{"f47ac10b-58cc-5372-a567-0e02-2c3d479", 0, Invalid, false},
{"g47ac10b-58cc-4372-a567-0e02b2c3d479", 0, Invalid, false},
}
var constants = []struct {
c interface{}
name string
}{
{Person, "Person"},
{Group, "Group"},
{Org, "Org"},
{Invalid, "Invalid"},
{RFC4122, "RFC4122"},
{Reserved, "Reserved"},
{Microsoft, "Microsoft"},
{Future, "Future"},
{Domain(17), "Domain17"},
{Variant(42), "BadVariant42"},
}
func testTest(t *testing.T, in string, tt test) {
uuid := Parse(in)
if ok := (uuid != nil); ok != tt.isuuid {
t.Errorf("Parse(%s) got %v expected %v\b", in, ok, tt.isuuid)
}
if uuid == nil {
return
}
if v := uuid.Variant(); v != tt.variant {
t.Errorf("Variant(%s) got %d expected %d\b", in, v, tt.variant)
}
if v, _ := uuid.Version(); v != tt.version {
t.Errorf("Version(%s) got %d expected %d\b", in, v, tt.version)
}
}
func TestUUID(t *testing.T) {
for _, tt := range tests {
testTest(t, tt.in, tt)
testTest(t, strings.ToUpper(tt.in), tt)
}
}
func TestConstants(t *testing.T) {
for x, tt := range constants {
v, ok := tt.c.(fmt.Stringer)
if !ok {
t.Errorf("%x: %v: not a stringer", x, v)
} else if s := v.String(); s != tt.name {
v, _ := tt.c.(int)
t.Errorf("%x: Constant %T:%d gives %q, expected %q\n", x, tt.c, v, s, tt.name)
}
}
}
func TestRandomUUID(t *testing.T) {
m := make(map[string]bool)
for x := 1; x < 32; x++ {
uuid := NewRandom()
s := uuid.String()
if m[s] {
t.Errorf("NewRandom returned duplicated UUID %s\n", s)
}
m[s] = true
if v, _ := uuid.Version(); v != 4 {
t.Errorf("Random UUID of version %s\n", v)
}
if uuid.Variant() != RFC4122 {
t.Errorf("Random UUID is variant %d\n", uuid.Variant())
}
}
}
func TestNew(t *testing.T) {
m := make(map[string]bool)
for x := 1; x < 32; x++ {
s := New()
if m[s] {
t.Errorf("New returned duplicated UUID %s\n", s)
}
m[s] = true
uuid := Parse(s)
if uuid == nil {
t.Errorf("New returned %q which does not decode\n", s)
continue
}
if v, _ := uuid.Version(); v != 4 {
t.Errorf("Random UUID of version %s\n", v)
}
if uuid.Variant() != RFC4122 {
t.Errorf("Random UUID is variant %d\n", uuid.Variant())
}
}
}
func clockSeq(t *testing.T, uuid UUID) int {
seq, ok := uuid.ClockSequence()
if !ok {
t.Fatalf("%s: invalid clock sequence\n", uuid)
}
return seq
}
func TestClockSeq(t *testing.T) {
// Fake time.Now for this test to return a monotonically advancing time; restore it at end.
defer func(orig func() time.Time) { timeNow = orig }(timeNow)
monTime := time.Now()
timeNow = func() time.Time {
monTime = monTime.Add(1 * time.Second)
return monTime
}
SetClockSequence(-1)
uuid1 := NewUUID()
uuid2 := NewUUID()
if clockSeq(t, uuid1) != clockSeq(t, uuid2) {
t.Errorf("clock sequence %d != %d\n", clockSeq(t, uuid1), clockSeq(t, uuid2))
}
SetClockSequence(-1)
uuid2 = NewUUID()
// Just on the very off chance we generated the same sequence
// two times we try again.
if clockSeq(t, uuid1) == clockSeq(t, uuid2) {
SetClockSequence(-1)
uuid2 = NewUUID()
}
if clockSeq(t, uuid1) == clockSeq(t, uuid2) {
t.Errorf("Duplicate clock sequence %d\n", clockSeq(t, uuid1))
}
SetClockSequence(0x1234)
uuid1 = NewUUID()
if seq := clockSeq(t, uuid1); seq != 0x1234 {
t.Errorf("%s: expected seq 0x1234 got 0x%04x\n", uuid1, seq)
}
}
func TestCoding(t *testing.T) {
text := "7d444840-9dc0-11d1-b245-5ffdce74fad2"
urn := "urn:uuid:7d444840-9dc0-11d1-b245-5ffdce74fad2"
data := UUID{
0x7d, 0x44, 0x48, 0x40,
0x9d, 0xc0,
0x11, 0xd1,
0xb2, 0x45,
0x5f, 0xfd, 0xce, 0x74, 0xfa, 0xd2,
}
if v := data.String(); v != text {
t.Errorf("%x: encoded to %s, expected %s\n", data, v, text)
}
if v := data.URN(); v != urn {
t.Errorf("%x: urn is %s, expected %s\n", data, v, urn)
}
uuid := Parse(text)
if !Equal(uuid, data) {
t.Errorf("%s: decoded to %s, expected %s\n", text, uuid, data)
}
}
func TestVersion1(t *testing.T) {
uuid1 := NewUUID()
uuid2 := NewUUID()
if Equal(uuid1, uuid2) {
t.Errorf("%s:duplicate uuid\n", uuid1)
}
if v, _ := uuid1.Version(); v != 1 {
t.Errorf("%s: version %s expected 1\n", uuid1, v)
}
if v, _ := uuid2.Version(); v != 1 {
t.Errorf("%s: version %s expected 1\n", uuid2, v)
}
n1 := uuid1.NodeID()
n2 := uuid2.NodeID()
if !bytes.Equal(n1, n2) {
t.Errorf("Different nodes %x != %x\n", n1, n2)
}
t1, ok := uuid1.Time()
if !ok {
t.Errorf("%s: invalid time\n", uuid1)
}
t2, ok := uuid2.Time()
if !ok {
t.Errorf("%s: invalid time\n", uuid2)
}
q1, ok := uuid1.ClockSequence()
if !ok {
t.Errorf("%s: invalid clock sequence\n", uuid1)
}
q2, ok := uuid2.ClockSequence()
if !ok {
t.Errorf("%s: invalid clock sequence", uuid2)
}
switch {
case t1 == t2 && q1 == q2:
t.Errorf("time stopped\n")
case t1 > t2 && q1 == q2:
t.Errorf("time reversed\n")
case t1 < t2 && q1 != q2:
t.Errorf("clock sequence chaned unexpectedly\n")
}
}
func TestNodeAndTime(t *testing.T) {
// Time is February 5, 1998 12:30:23.136364800 AM GMT
uuid := Parse("7d444840-9dc0-11d1-b245-5ffdce74fad2")
node := []byte{0x5f, 0xfd, 0xce, 0x74, 0xfa, 0xd2}
ts, ok := uuid.Time()
if ok {
c := time.Unix(ts.UnixTime())
want := time.Date(1998, 2, 5, 0, 30, 23, 136364800, time.UTC)
if !c.Equal(want) {
t.Errorf("Got time %v, want %v", c, want)
}
} else {
t.Errorf("%s: bad time\n", uuid)
}
if !bytes.Equal(node, uuid.NodeID()) {
t.Errorf("Expected node %v got %v\n", node, uuid.NodeID())
}
}
func TestMD5(t *testing.T) {
uuid := NewMD5(NameSpace_DNS, []byte("python.org")).String()
want := "6fa459ea-ee8a-3ca4-894e-db77e160355e"
if uuid != want {
t.Errorf("MD5: got %q expected %q\n", uuid, want)
}
}
func TestSHA1(t *testing.T) {
uuid := NewSHA1(NameSpace_DNS, []byte("python.org")).String()
want := "886313e1-3b8a-5372-9b90-0c9aee199e5d"
if uuid != want {
t.Errorf("SHA1: got %q expected %q\n", uuid, want)
}
}
func TestNodeID(t *testing.T) {
nid := []byte{1, 2, 3, 4, 5, 6}
SetNodeInterface("")
s := NodeInterface()
if s == "" || s == "user" {
t.Errorf("NodeInterface %q after SetInteface\n", s)
}
node1 := NodeID()
if node1 == nil {
t.Errorf("NodeID nil after SetNodeInterface\n", s)
}
SetNodeID(nid)
s = NodeInterface()
if s != "user" {
t.Errorf("Expected NodeInterface %q got %q\n", "user", s)
}
node2 := NodeID()
if node2 == nil {
t.Errorf("NodeID nil after SetNodeID\n", s)
}
if bytes.Equal(node1, node2) {
t.Errorf("NodeID not changed after SetNodeID\n", s)
} else if !bytes.Equal(nid, node2) {
t.Errorf("NodeID is %x, expected %x\n", node2, nid)
}
}
func testDCE(t *testing.T, name string, uuid UUID, domain Domain, id uint32) {
if uuid == nil {
t.Errorf("%s failed\n", name)
return
}
if v, _ := uuid.Version(); v != 2 {
t.Errorf("%s: %s: expected version 2, got %s\n", name, uuid, v)
return
}
if v, ok := uuid.Domain(); !ok || v != domain {
if !ok {
t.Errorf("%s: %d: Domain failed\n", name, uuid)
} else {
t.Errorf("%s: %s: expected domain %d, got %d\n", name, uuid, domain, v)
}
}
if v, ok := uuid.Id(); !ok || v != id {
if !ok {
t.Errorf("%s: %d: Id failed\n", name, uuid)
} else {
t.Errorf("%s: %s: expected id %d, got %d\n", name, uuid, id, v)
}
}
}
func TestDCE(t *testing.T) {
testDCE(t, "NewDCESecurity", NewDCESecurity(42, 12345678), 42, 12345678)
testDCE(t, "NewDCEPerson", NewDCEPerson(), Person, uint32(os.Getuid()))
testDCE(t, "NewDCEGroup", NewDCEGroup(), Group, uint32(os.Getgid()))
}
type badRand struct{}
func (r badRand) Read(buf []byte) (int, error) {
for i, _ := range buf {
buf[i] = byte(i)
}
return len(buf), nil
}
func TestBadRand(t *testing.T) {
SetRand(badRand{})
uuid1 := New()
uuid2 := New()
if uuid1 != uuid2 {
t.Errorf("execpted duplicates, got %q and %q\n", uuid1, uuid2)
}
SetRand(nil)
uuid1 = New()
uuid2 = New()
if uuid1 == uuid2 {
t.Errorf("unexecpted duplicates, got %q\n", uuid1)
}
}

View File

@ -0,0 +1,41 @@
// Copyright 2011 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"encoding/binary"
)
// NewUUID returns a Version 1 UUID based on the current NodeID and clock
// sequence, and the current time. If the NodeID has not been set by SetNodeID
// or SetNodeInterface then it will be set automatically. If the NodeID cannot
// be set NewUUID returns nil. If clock sequence has not been set by
// SetClockSequence then it will be set automatically. If GetTime fails to
// return the current NewUUID returns nil.
func NewUUID() UUID {
if nodeID == nil {
SetNodeInterface("")
}
now, err := GetTime()
if err != nil {
return nil
}
uuid := make([]byte, 16)
time_low := uint32(now & 0xffffffff)
time_mid := uint16((now >> 32) & 0xffff)
time_hi := uint16((now >> 48) & 0x0fff)
time_hi |= 0x1000 // Version 1
binary.BigEndian.PutUint32(uuid[0:], time_low)
binary.BigEndian.PutUint16(uuid[4:], time_mid)
binary.BigEndian.PutUint16(uuid[6:], time_hi)
binary.BigEndian.PutUint16(uuid[8:], clock_seq)
copy(uuid[10:], nodeID)
return uuid
}

View File

@ -0,0 +1,25 @@
// Copyright 2011 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
// Random returns a Random (Version 4) UUID or panics.
//
// The strength of the UUIDs is based on the strength of the crypto/rand
// package.
//
// A note about uniqueness derived from from the UUID Wikipedia entry:
//
// Randomly generated UUIDs have 122 random bits. One's annual risk of being
// hit by a meteorite is estimated to be one chance in 17 billion, that
// means the probability is about 0.00000000006 (6 × 1011),
// equivalent to the odds of creating a few tens of trillions of UUIDs in a
// year and having one duplicate.
func NewRandom() UUID {
uuid := make([]byte, 16)
randomBits([]byte(uuid))
uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
return uuid
}

View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 Christian Joudrey
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,231 @@
# gluahttp
[![](https://travis-ci.org/cjoudrey/gluahttp.svg)](https://travis-ci.org/cjoudrey/gluahttp)
gluahttp provides an easy way to make HTTP requests from within [GopherLua](https://github.com/yuin/gopher-lua).
## Installation
```
go get github.com/cjoudrey/gluahttp
```
## Usage
```go
import "github.com/yuin/gopher-lua"
import "github.com/cjoudrey/gluahttp"
func main() {
L := lua.NewState()
defer L.Close()
L.PreloadModule("http", NewHttpModule(&http.Client{}).Loader)
if err := L.DoString(`
local http = require("http")
response, error_message = http.request("GET", "http://example.com", {
query="page=1"
headers={
Accept="*/*"
}
})
`); err != nil {
panic(err)
}
}
```
## API
- [`http.delete(url [, options])`](#httpdeleteurl--options)
- [`http.get(url [, options])`](#httpgeturl--options)
- [`http.head(url [, options])`](#httpheadurl--options)
- [`http.patch(url [, options])`](#httppatchurl--options)
- [`http.post(url [, options])`](#httpposturl--options)
- [`http.put(url [, options])`](#httpputurl--options)
- [`http.request(method, url [, options])`](#httprequestmethod-url--options)
- [`http.request_batch(requests)`](#httprequest_batchrequests)
- [`http.response`](#httpresponse)
### http.delete(url [, options])
**Attributes**
| Name | Type | Description |
| ------- | ------ | ----------- |
| url | String | URL of the resource to load |
| options | Table | Additional options |
**Options**
| Name | Type | Description |
| ------- | ------ | ----------- |
| query | String | URL encoded query params |
| cookies | Table | Additional cookies to send with the request |
| headers | Table | Additional headers to send with the request |
**Returns**
[http.response](#httpresponse) or (nil, error message)
### http.get(url [, options])
**Attributes**
| Name | Type | Description |
| ------- | ------ | ----------- |
| url | String | URL of the resource to load |
| options | Table | Additional options |
**Options**
| Name | Type | Description |
| ------- | ------ | ----------- |
| query | String | URL encoded query params |
| cookies | Table | Additional cookies to send with the request |
| headers | Table | Additional headers to send with the request |
**Returns**
[http.response](#httpresponse) or (nil, error message)
### http.head(url [, options])
**Attributes**
| Name | Type | Description |
| ------- | ------ | ----------- |
| url | String | URL of the resource to load |
| options | Table | Additional options |
**Options**
| Name | Type | Description |
| ------- | ------ | ----------- |
| query | String | URL encoded query params |
| cookies | Table | Additional cookies to send with the request |
| headers | Table | Additional headers to send with the request |
**Returns**
[http.response](#httpresponse) or (nil, error message)
### http.patch(url [, options])
**Attributes**
| Name | Type | Description |
| ------- | ------ | ----------- |
| url | String | URL of the resource to load |
| options | Table | Additional options |
**Options**
| Name | Type | Description |
| ------- | ------ | ----------- |
| query | String | URL encoded query params |
| cookies | Table | Additional cookies to send with the request |
| form | String | URL encoded request body. This will also set the `Content-Type` header to `application/x-www-form-urlencoded` |
| headers | Table | Additional headers to send with the request |
**Returns**
[http.response](#httpresponse) or (nil, error message)
### http.post(url [, options])
**Attributes**
| Name | Type | Description |
| ------- | ------ | ----------- |
| url | String | URL of the resource to load |
| options | Table | Additional options |
**Options**
| Name | Type | Description |
| ------- | ------ | ----------- |
| query | String | URL encoded query params |
| cookies | Table | Additional cookies to send with the request |
| form | String | URL encoded request body. This will also set the `Content-Type` header to `application/x-www-form-urlencoded` |
| headers | Table | Additional headers to send with the request |
**Returns**
[http.response](#httpresponse) or (nil, error message)
### http.put(url [, options])
**Attributes**
| Name | Type | Description |
| ------- | ------ | ----------- |
| url | String | URL of the resource to load |
| options | Table | Additional options |
**Options**
| Name | Type | Description |
| ------- | ------ | ----------- |
| query | String | URL encoded query params |
| cookies | Table | Additional cookies to send with the request |
| form | String | URL encoded request body. This will also set the `Content-Type` header to `application/x-www-form-urlencoded` |
| headers | Table | Additional headers to send with the request |
**Returns**
[http.response](#httpresponse) or (nil, error message)
### http.request(method, url [, options])
**Attributes**
| Name | Type | Description |
| ------- | ------ | ----------- |
| method | String | The HTTP request method |
| url | String | URL of the resource to load |
| options | Table | Additional options |
**Options**
| Name | Type | Description |
| ------- | ------ | ----------- |
| query | String | URL encoded query params |
| cookies | Table | Additional cookies to send with the request |
| form | String | URL encoded request body. This will also set the `Content-Type` header to `application/x-www-form-urlencoded` |
| headers | Table | Additional headers to send with the request |
**Returns**
[http.response](#httpresponse) or (nil, error message)
### http.request_batch(requests)
**Attributes**
| Name | Type | Description |
| -------- | ----- | ----------- |
| requests | Table | A table of requests to send. Each request item is by itself a table containing [http.request](#httprequestmethod-url--options) parameters for the request |
**Returns**
[[http.response](#httpresponse)] or ([[http.response](#httpresponse)], [error message])
### http.response
The `http.response` table contains information about a completed HTTP request.
**Attributes**
| Name | Type | Description |
| ----------- | ------ | ----------- |
| body | String | The HTTP response body |
| body_size | Number | The size of the HTTP reponse body in bytes |
| headers | Table | The HTTP response headers |
| cookies | Table | The cookies sent by the server in the HTTP response |
| status_code | Number | The HTTP response status code |
| url | String | The final URL the request ended pointing to after redirects |

View File

@ -0,0 +1,214 @@
package gluahttp
import "github.com/yuin/gopher-lua"
import "net/http"
import "fmt"
import "errors"
import "io"
import "io/ioutil"
import "strings"
type httpModule struct {
client *http.Client
}
type empty struct{}
func NewHttpModule(client *http.Client) *httpModule {
return &httpModule{
client: client,
}
}
func (h *httpModule) Loader(L *lua.LState) int {
mod := L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{
"get": h.get,
"delete": h.delete,
"head": h.head,
"patch": h.patch,
"post": h.post,
"put": h.put,
"request": h.request,
"request_batch": h.requestBatch,
})
registerHttpResponseType(mod, L)
L.Push(mod)
return 1
}
func (h *httpModule) get(L *lua.LState) int {
return h.doRequestAndPush(L, "get", L.ToString(1), L.ToTable(2))
}
func (h *httpModule) delete(L *lua.LState) int {
return h.doRequestAndPush(L, "delete", L.ToString(1), L.ToTable(2))
}
func (h *httpModule) head(L *lua.LState) int {
return h.doRequestAndPush(L, "head", L.ToString(1), L.ToTable(2))
}
func (h *httpModule) patch(L *lua.LState) int {
return h.doRequestAndPush(L, "patch", L.ToString(1), L.ToTable(2))
}
func (h *httpModule) post(L *lua.LState) int {
return h.doRequestAndPush(L, "post", L.ToString(1), L.ToTable(2))
}
func (h *httpModule) put(L *lua.LState) int {
return h.doRequestAndPush(L, "put", L.ToString(1), L.ToTable(2))
}
func (h *httpModule) request(L *lua.LState) int {
return h.doRequestAndPush(L, L.ToString(1), L.ToString(2), L.ToTable(3))
}
func (h *httpModule) requestBatch(L *lua.LState) int {
requests := L.ToTable(1)
amountRequests := requests.Len()
errs := make([]error, amountRequests)
responses := make([]*lua.LUserData, amountRequests)
sem := make(chan empty, amountRequests)
i := 0
requests.ForEach(func(_ lua.LValue, value lua.LValue) {
requestTable := toTable(value)
if requestTable != nil {
method := requestTable.RawGet(lua.LNumber(1)).String()
url := requestTable.RawGet(lua.LNumber(2)).String()
options := toTable(requestTable.RawGet(lua.LNumber(3)))
go func(i int, L *lua.LState, method string, url string, options *lua.LTable) {
response, err := h.doRequest(L, method, url, options)
if err == nil {
errs[i] = nil
responses[i] = response
} else {
errs[i] = err
responses[i] = nil
}
sem <- empty{}
}(i, L, method, url, options)
} else {
errs[i] = errors.New("Request must be a table")
responses[i] = nil
sem <- empty{}
}
i = i + 1
})
for i = 0; i < amountRequests; i++ {
<-sem
}
hasErrors := false
errorsTable := L.NewTable()
responsesTable := L.NewTable()
for i = 0; i < amountRequests; i++ {
if errs[i] == nil {
responsesTable.Append(responses[i])
errorsTable.Append(lua.LNil)
} else {
responsesTable.Append(lua.LNil)
errorsTable.Append(lua.LString(fmt.Sprintf("%s", errs[i])))
hasErrors = true
}
}
if hasErrors {
L.Push(responsesTable)
L.Push(errorsTable)
return 2
} else {
L.Push(responsesTable)
return 1
}
}
func (h *httpModule) doRequest(L *lua.LState, method string, url string, options *lua.LTable) (*lua.LUserData, error) {
req, err := http.NewRequest(strings.ToUpper(method), url, nil)
if err != nil {
return nil, err
}
if options != nil {
if reqHeaders, ok := options.RawGet(lua.LString("headers")).(*lua.LTable); ok {
reqHeaders.ForEach(func(key lua.LValue, value lua.LValue) {
req.Header.Set(key.String(), value.String())
})
}
if reqCookies, ok := options.RawGet(lua.LString("cookies")).(*lua.LTable); ok {
reqCookies.ForEach(func(key lua.LValue, value lua.LValue) {
req.AddCookie(&http.Cookie{Name: key.String(), Value: value.String()})
})
}
switch reqQuery := options.RawGet(lua.LString("query")).(type) {
case *lua.LNilType:
break
case lua.LString:
req.URL.RawQuery = reqQuery.String()
break
}
switch reqForm := options.RawGet(lua.LString("form")).(type) {
case *lua.LNilType:
break
case lua.LString:
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Body = ioutil.NopCloser(strings.NewReader(reqForm.String()))
break
}
}
res, err := h.client.Do(req)
if err != nil {
if res != nil {
io.Copy(ioutil.Discard, res.Body)
defer res.Body.Close()
}
return nil, err
}
// TODO: Add a way to discard body
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
return newHttpResponse(res, &body, len(body), L), nil
}
func (h *httpModule) doRequestAndPush(L *lua.LState, method string, url string, options *lua.LTable) int {
response, err := h.doRequest(L, method, url, options)
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(fmt.Sprintf("%s", err)))
return 2
}
L.Push(response)
return 1
}
func toTable(v lua.LValue) *lua.LTable {
if lv, ok := v.(*lua.LTable); ok {
return lv
}
return nil
}

View File

@ -0,0 +1,395 @@
package gluahttp
import "github.com/yuin/gopher-lua"
import "testing"
import "io/ioutil"
import "net/http"
import "net"
import "fmt"
import "net/http/cookiejar"
func TestRequestNoMethod(t *testing.T) {
if err := evalLua(t, `
local http = require("http")
response, error = http.request()
assert_equal(nil, response)
assert_equal('unsupported protocol scheme ""', error)
`); err != nil {
t.Errorf("Failed to evaluate script: %s", err)
}
}
func TestRequestNoUrl(t *testing.T) {
if err := evalLua(t, `
local http = require("http")
response, error = http.request("get")
assert_equal(nil, response)
assert_equal('Get : unsupported protocol scheme ""', error)
`); err != nil {
t.Errorf("Failed to evaluate script: %s", err)
}
}
func TestRequestBatch(t *testing.T) {
listener, _ := net.Listen("tcp", "127.0.0.1:0")
setupServer(listener)
if err := evalLua(t, `
local http = require("http")
responses, errors = http.request_batch({
{"get", "http://`+listener.Addr().String()+`", {query="page=1"}},
{"post", "http://`+listener.Addr().String()+`/set_cookie"},
{"post", ""},
1
})
assert_equal(nil, errors[1])
assert_equal(nil, errors[2])
assert_equal('Post : unsupported protocol scheme ""', errors[3])
assert_equal('Request must be a table', errors[4])
assert_equal('Requested GET / with query "page=1"', responses[1]["body"])
assert_equal('Cookie set!', responses[2]["body"])
assert_equal('12345', responses[2]["cookies"]["session_id"])
assert_equal(nil, responses[3])
assert_equal(nil, responses[4])
responses, errors = http.request_batch({
{"get", "http://`+listener.Addr().String()+`/get_cookie"}
})
assert_equal(nil, errors)
assert_equal("session_id=12345", responses[1]["body"])
`); err != nil {
t.Errorf("Failed to evaluate script: %s", err)
}
}
func TestRequestGet(t *testing.T) {
listener, _ := net.Listen("tcp", "127.0.0.1:0")
setupServer(listener)
if err := evalLua(t, `
local http = require("http")
response, error = http.request("get", "http://`+listener.Addr().String()+`")
assert_equal('Requested GET / with query ""', response['body'])
assert_equal(200, response['status_code'])
assert_equal('29', response['headers']['Content-Length'])
assert_equal('text/plain; charset=utf-8', response['headers']['Content-Type'])
`); err != nil {
t.Errorf("Failed to evaluate script: %s", err)
}
}
func TestRequestGetWithRedirect(t *testing.T) {
listener, _ := net.Listen("tcp", "127.0.0.1:0")
setupServer(listener)
if err := evalLua(t, `
local http = require("http")
response, error = http.request("get", "http://`+listener.Addr().String()+`/redirect")
assert_equal('Requested GET / with query ""', response['body'])
assert_equal(200, response['status_code'])
assert_equal('http://`+listener.Addr().String()+`/', response['url'])
`); err != nil {
t.Errorf("Failed to evaluate script: %s", err)
}
}
func TestRequestPostForm(t *testing.T) {
listener, _ := net.Listen("tcp", "127.0.0.1:0")
setupServer(listener)
if err := evalLua(t, `
local http = require("http")
response, error = http.request("post", "http://`+listener.Addr().String()+`", {
form="username=bob&password=secret"
})
assert_equal(
'Requested POST / with query ""' ..
'Content-Type: application/x-www-form-urlencoded' ..
'Body: username=bob&password=secret', response['body'])
`); err != nil {
t.Errorf("Failed to evaluate script: %s", err)
}
}
func TestRequestHeaders(t *testing.T) {
listener, _ := net.Listen("tcp", "127.0.0.1:0")
setupServer(listener)
if err := evalLua(t, `
local http = require("http")
response, error = http.request("post", "http://`+listener.Addr().String()+`", {
headers={
["Content-Type"]="application/json"
}
})
assert_equal(
'Requested POST / with query ""' ..
'Content-Type: application/json' ..
'Body: ', response['body'])
`); err != nil {
t.Errorf("Failed to evaluate script: %s", err)
}
}
func TestRequestQuery(t *testing.T) {
listener, _ := net.Listen("tcp", "127.0.0.1:0")
setupServer(listener)
if err := evalLua(t, `
local http = require("http")
response, error = http.request("get", "http://`+listener.Addr().String()+`", {
query="page=2"
})
assert_equal('Requested GET / with query "page=2"', response['body'])
`); err != nil {
t.Errorf("Failed to evaluate script: %s", err)
}
}
func TestGet(t *testing.T) {
listener, _ := net.Listen("tcp", "127.0.0.1:0")
setupServer(listener)
if err := evalLua(t, `
local http = require("http")
response, error = http.get("http://`+listener.Addr().String()+`", {
query="page=1"
})
assert_equal('Requested GET / with query "page=1"', response['body'])
assert_equal(200, response['status_code'])
assert_equal('35', response['headers']['Content-Length'])
assert_equal('text/plain; charset=utf-8', response['headers']['Content-Type'])
`); err != nil {
t.Errorf("Failed to evaluate script: %s", err)
}
}
func TestDelete(t *testing.T) {
listener, _ := net.Listen("tcp", "127.0.0.1:0")
setupServer(listener)
if err := evalLua(t, `
local http = require("http")
response, error = http.delete("http://`+listener.Addr().String()+`", {
query="page=1"
})
assert_equal('Requested DELETE / with query "page=1"', response['body'])
assert_equal(200, response['status_code'])
assert_equal('38', response['headers']['Content-Length'])
assert_equal('text/plain; charset=utf-8', response['headers']['Content-Type'])
`); err != nil {
t.Errorf("Failed to evaluate script: %s", err)
}
}
func TestHead(t *testing.T) {
listener, _ := net.Listen("tcp", "127.0.0.1:0")
setupServer(listener)
if err := evalLua(t, `
local http = require("http")
response, error = http.head("http://`+listener.Addr().String()+`/head", {
query="page=1"
})
assert_equal(200, response['status_code'])
assert_equal("/head?page=1", response['headers']['X-Request-Uri'])
`); err != nil {
t.Errorf("Failed to evaluate script: %s", err)
}
}
func TestPost(t *testing.T) {
listener, _ := net.Listen("tcp", "127.0.0.1:0")
setupServer(listener)
if err := evalLua(t, `
local http = require("http")
response, error = http.post("http://`+listener.Addr().String()+`", {
form="username=bob&password=secret"
})
assert_equal(
'Requested POST / with query ""' ..
'Content-Type: application/x-www-form-urlencoded' ..
'Body: username=bob&password=secret', response['body'])
`); err != nil {
t.Errorf("Failed to evaluate script: %s", err)
}
}
func TestPatch(t *testing.T) {
listener, _ := net.Listen("tcp", "127.0.0.1:0")
setupServer(listener)
if err := evalLua(t, `
local http = require("http")
response, error = http.patch("http://`+listener.Addr().String()+`", {
form="username=bob&password=secret"
})
assert_equal(
'Requested PATCH / with query ""' ..
'Content-Type: application/x-www-form-urlencoded' ..
'Body: username=bob&password=secret', response['body'])
`); err != nil {
t.Errorf("Failed to evaluate script: %s", err)
}
}
func TestPut(t *testing.T) {
listener, _ := net.Listen("tcp", "127.0.0.1:0")
setupServer(listener)
if err := evalLua(t, `
local http = require("http")
response, error = http.put("http://`+listener.Addr().String()+`", {
form="username=bob&password=secret"
})
assert_equal(
'Requested PUT / with query ""' ..
'Content-Type: application/x-www-form-urlencoded' ..
'Body: username=bob&password=secret', response['body'])
`); err != nil {
t.Errorf("Failed to evaluate script: %s", err)
}
}
func TestResponseCookies(t *testing.T) {
listener, _ := net.Listen("tcp", "127.0.0.1:0")
setupServer(listener)
if err := evalLua(t, `
local http = require("http")
response, error = http.get("http://`+listener.Addr().String()+`/set_cookie")
assert_equal('Cookie set!', response["body"])
assert_equal('12345', response["cookies"]["session_id"])
`); err != nil {
t.Errorf("Failed to evaluate script: %s", err)
}
}
func TestRequestCookies(t *testing.T) {
listener, _ := net.Listen("tcp", "127.0.0.1:0")
setupServer(listener)
if err := evalLua(t, `
local http = require("http")
response, error = http.get("http://`+listener.Addr().String()+`/get_cookie", {
cookies={
["session_id"]="test"
}
})
assert_equal('session_id=test', response["body"])
assert_equal(15, response["body_size"])
`); err != nil {
t.Errorf("Failed to evaluate script: %s", err)
}
}
func TestResponseBodySize(t *testing.T) {
listener, _ := net.Listen("tcp", "127.0.0.1:0")
setupServer(listener)
if err := evalLua(t, `
local http = require("http")
response, error = http.get("http://`+listener.Addr().String()+`/")
assert_equal(29, response["body_size"])
`); err != nil {
t.Errorf("Failed to evaluate script: %s", err)
}
}
func TestResponseUrl(t *testing.T) {
listener, _ := net.Listen("tcp", "127.0.0.1:0")
setupServer(listener)
if err := evalLua(t, `
local http = require("http")
response, error = http.get("http://`+listener.Addr().String()+`/redirect")
assert_equal("http://`+listener.Addr().String()+`/", response["url"])
response, error = http.get("http://`+listener.Addr().String()+`/get_cookie")
assert_equal("http://`+listener.Addr().String()+`/get_cookie", response["url"])
`); err != nil {
t.Errorf("Failed to evaluate script: %s", err)
}
}
func evalLua(t *testing.T, script string) error {
L := lua.NewState()
defer L.Close()
cookieJar, _ := cookiejar.New(nil)
L.PreloadModule("http", NewHttpModule(&http.Client{
Jar: cookieJar,
},
).Loader)
L.SetGlobal("assert_equal", L.NewFunction(func(L *lua.LState) int {
expected := L.Get(1)
actual := L.Get(2)
if expected.Type() != actual.Type() || expected.String() != actual.String() {
t.Errorf("Expected %s %q, got %s %q", expected.Type(), expected, actual.Type(), actual)
}
return 0
}))
return L.DoString(script)
}
func setupServer(listener net.Listener) {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "Requested %s / with query %q", req.Method, req.URL.RawQuery)
if req.Method == "POST" || req.Method == "PATCH" || req.Method == "PUT" {
body, _ := ioutil.ReadAll(req.Body)
fmt.Fprintf(w, "Content-Type: %s", req.Header.Get("Content-Type"))
fmt.Fprintf(w, "Body: %s", body)
}
})
mux.HandleFunc("/head", func(w http.ResponseWriter, req *http.Request) {
if req.Method == "HEAD" {
w.Header().Set("X-Request-Uri", req.URL.String())
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusNotFound)
}
})
mux.HandleFunc("/set_cookie", func(w http.ResponseWriter, req *http.Request) {
http.SetCookie(w, &http.Cookie{Name: "session_id", Value: "12345"})
fmt.Fprint(w, "Cookie set!")
})
mux.HandleFunc("/get_cookie", func(w http.ResponseWriter, req *http.Request) {
session_id, _ := req.Cookie("session_id")
fmt.Fprint(w, session_id)
})
mux.HandleFunc("/redirect", func(w http.ResponseWriter, req *http.Request) {
http.Redirect(w, req, "/", http.StatusFound)
})
s := &http.Server{
Handler: mux,
}
go s.Serve(listener)
}

View File

@ -0,0 +1,98 @@
package gluahttp
import "github.com/yuin/gopher-lua"
import "net/http"
const luaHttpResponseTypeName = "http.response"
type luaHttpResponse struct {
res *http.Response
body lua.LString
bodySize int
}
func registerHttpResponseType(module *lua.LTable, L *lua.LState) {
mt := L.NewTypeMetatable(luaHttpResponseTypeName)
L.SetField(mt, "__index", L.NewFunction(httpResponseIndex))
L.SetField(module, "response", mt)
}
func newHttpResponse(res *http.Response, body *[]byte, bodySize int, L *lua.LState) *lua.LUserData {
ud := L.NewUserData()
ud.Value = &luaHttpResponse{
res: res,
body: lua.LString(*body),
bodySize: bodySize,
}
L.SetMetatable(ud, L.GetTypeMetatable(luaHttpResponseTypeName))
return ud
}
func checkHttpResponse(L *lua.LState) *luaHttpResponse {
ud := L.CheckUserData(1)
if v, ok := ud.Value.(*luaHttpResponse); ok {
return v
}
L.ArgError(1, "http.response expected")
return nil
}
func httpResponseIndex(L *lua.LState) int {
res := checkHttpResponse(L)
switch L.CheckString(2) {
case "headers":
return httpResponseHeaders(res, L)
case "cookies":
return httpResponseCookies(res, L)
case "status_code":
return httpResponseStatusCode(res, L)
case "url":
return httpResponseUrl(res, L)
case "body":
return httpResponseBody(res, L)
case "body_size":
return httpResponseBodySize(res, L)
}
return 0
}
func httpResponseHeaders(res *luaHttpResponse, L *lua.LState) int {
headers := L.NewTable()
for key, _ := range res.res.Header {
headers.RawSetString(key, lua.LString(res.res.Header.Get(key)))
}
L.Push(headers)
return 1
}
func httpResponseCookies(res *luaHttpResponse, L *lua.LState) int {
cookies := L.NewTable()
for _, cookie := range res.res.Cookies() {
cookies.RawSetString(cookie.Name, lua.LString(cookie.Value))
}
L.Push(cookies)
return 1
}
func httpResponseStatusCode(res *luaHttpResponse, L *lua.LState) int {
L.Push(lua.LNumber(res.res.StatusCode))
return 1
}
func httpResponseUrl(res *luaHttpResponse, L *lua.LState) int {
L.Push(lua.LString(res.res.Request.URL.String()))
return 1
}
func httpResponseBody(res *luaHttpResponse, L *lua.LState) int {
L.Push(&res.body)
return 1
}
func httpResponseBodySize(res *luaHttpResponse, L *lua.LState) int {
L.Push(lua.LNumber(res.bodySize))
return 1
}

View File

@ -0,0 +1,22 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,7 @@
# gopher-json [![GoDoc](https://godoc.org/github.com/layeh/gopher-json?status.svg)](https://godoc.org/github.com/layeh/gopher-json)
Package json is a simple JSON encoder/decoder for [gopher-lua](https://github.com/yuin/gopher-lua).
## License
Public domain.

View File

@ -0,0 +1,40 @@
package json
import (
"encoding/json"
"github.com/yuin/gopher-lua"
)
var api = map[string]lua.LGFunction{
"decode": apiDecode,
"encode": apiEncode,
}
func apiDecode(L *lua.LState) int {
str := L.CheckString(1)
var value interface{}
err := json.Unmarshal([]byte(str), &value)
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
L.Push(fromJSON(L, value))
return 1
}
func apiEncode(L *lua.LState) int {
value := L.CheckAny(1)
visited := make(map[*lua.LTable]bool)
data, err := toJSON(value, visited)
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
L.Push(lua.LString(string(data)))
return 1
}

View File

@ -0,0 +1,16 @@
// Package json is a simple JSON encoder/decoder for gopher-lua.
//
// Documentation
//
// The following functions are exposed by the library:
// decode(string): Decodes a JSON string. Returns nil and an error string if
// the string could not be decoded.
// encode(value): Encodes a value into a JSON string. Returns nil and an error
// string if the value could not be encoded.
//
// Example
//
// Below is an example usage of the library:
// L := lua.NewState()
// luajson.Preload(s)
package json

View File

@ -0,0 +1,20 @@
package json
import (
"github.com/yuin/gopher-lua"
)
// Preload adds json to the given Lua state's package.preload table. After it
// has been preloaded, it can be loaded using require:
//
// local json = require("json")
func Preload(L *lua.LState) {
L.PreloadModule("json", load)
}
func load(L *lua.LState) int {
t := L.NewTable()
L.SetFuncs(t, api)
L.Push(t)
return 1
}

View File

@ -0,0 +1,61 @@
package json
import (
"testing"
"github.com/yuin/gopher-lua"
)
func TestSimple(t *testing.T) {
const str = `
local json = require("json")
assert(type(json) == "table")
assert(type(json.decode) == "function")
assert(type(json.encode) == "function")
assert(json.encode(true) == "true")
assert(json.encode(1) == "1")
assert(json.encode(-10) == "-10")
assert(json.encode(nil) == "{}")
local obj = {"a",1,"b",2,"c",3}
local jsonStr = json.encode(obj)
local jsonObj = json.decode(jsonStr)
for i = 1, #obj do
assert(obj[i] == jsonObj[i])
end
local obj = {name="Tim",number=12345}
local jsonStr = json.encode(obj)
local jsonObj = json.decode(jsonStr)
assert(obj.name == jsonObj.name)
assert(obj.number == jsonObj.number)
local obj = {"a","b",what="c",[5]="asd"}
local jsonStr = json.encode(obj)
local jsonObj = json.decode(jsonStr)
assert(obj[1] == jsonObj["1"])
assert(obj[2] == jsonObj["2"])
assert(obj.what == jsonObj["what"])
assert(obj[5] == jsonObj["5"])
assert(json.decode("null") == nil)
assert(json.decode(json.encode({person={name = "tim",}})).person.name == "tim")
local obj = {
abc = 123,
def = nil,
}
local obj2 = {
obj = obj,
}
obj.obj2 = obj2
assert(json.encode(obj) == nil)
`
s := lua.NewState()
Preload(s)
if err := s.DoString(str); err != nil {
t.Error(err)
}
}

View File

@ -0,0 +1,112 @@
package json
import (
"encoding/json"
"errors"
"strconv"
"github.com/yuin/gopher-lua"
)
var (
errFunction = errors.New("cannot encode function to JSON")
errChannel = errors.New("cannot encode channel to JSON")
errState = errors.New("cannot encode state to JSON")
errUserData = errors.New("cannot encode userdata to JSON")
errNested = errors.New("cannot encode recursively nested tables to JSON")
)
type jsonValue struct {
lua.LValue
visited map[*lua.LTable]bool
}
func (j jsonValue) MarshalJSON() ([]byte, error) {
return toJSON(j.LValue, j.visited)
}
func toJSON(value lua.LValue, visited map[*lua.LTable]bool) (data []byte, err error) {
switch converted := value.(type) {
case lua.LBool:
data, err = json.Marshal(converted)
case lua.LChannel:
err = errChannel
case lua.LNumber:
data, err = json.Marshal(converted)
case *lua.LFunction:
err = errFunction
case *lua.LNilType:
data, err = json.Marshal(converted)
case *lua.LState:
err = errState
case lua.LString:
data, err = json.Marshal(converted)
case *lua.LTable:
var arr []jsonValue
var obj map[string]jsonValue
if visited[converted] {
panic(errNested)
return // unreachable
}
visited[converted] = true
converted.ForEach(func(k lua.LValue, v lua.LValue) {
i, numberKey := k.(lua.LNumber)
if numberKey && obj == nil {
index := int(i) - 1
if index != len(arr) {
// map out of order; convert to map
obj = make(map[string]jsonValue)
for i, value := range arr {
obj[strconv.Itoa(i+1)] = value
}
obj[strconv.Itoa(index+1)] = jsonValue{v, visited}
return
}
arr = append(arr, jsonValue{v, visited})
return
}
if obj == nil {
obj = make(map[string]jsonValue)
for i, value := range arr {
obj[strconv.Itoa(i+1)] = value
}
}
obj[k.String()] = jsonValue{v, visited}
})
if obj != nil {
data, err = json.Marshal(obj)
} else {
data, err = json.Marshal(arr)
}
case *lua.LUserData:
// TODO: call metatable __tostring?
err = errUserData
}
return
}
func fromJSON(L *lua.LState, value interface{}) lua.LValue {
switch converted := value.(type) {
case bool:
return lua.LBool(converted)
case float64:
return lua.LNumber(converted)
case string:
return lua.LString(converted)
case []interface{}:
arr := L.CreateTable(len(converted), 0)
for _, item := range converted {
arr.Append(fromJSON(L, item))
}
return arr
case map[string]interface{}:
tbl := L.CreateTable(0, len(converted))
for key, item := range converted {
tbl.RawSetH(lua.LString(key), fromJSON(L, item))
}
return tbl
}
return lua.LNil
}

View File

@ -0,0 +1,19 @@
Copyright (c) 2015 Tim Cooper
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,7 @@
# gopher-luar [![GoDoc](https://godoc.org/github.com/layeh/gopher-luar?status.svg)](https://godoc.org/github.com/layeh/gopher-luar)
custom type reflection for [gopher-lua](https://github.com/yuin/gopher-lua).
## License
MIT

View File

@ -0,0 +1,79 @@
package luar
import (
"reflect"
"github.com/yuin/gopher-lua"
)
func checkChan(L *lua.LState, idx int) reflect.Value {
ud := L.CheckUserData(idx)
ref := reflect.ValueOf(ud.Value)
if ref.Kind() != reflect.Chan {
L.ArgError(idx, "expecting chan")
}
return ref
}
func chanIndex(L *lua.LState) int {
_ = checkChan(L, 1)
key := L.CheckString(2)
switch key {
case "send":
L.Push(L.NewFunction(chanSend))
case "receive":
L.Push(L.NewFunction(chanReceive))
case "close":
L.Push(L.NewFunction(chanClose))
default:
return 0
}
return 1
}
func chanLen(L *lua.LState) int {
ref := checkChan(L, 1)
L.Push(lua.LNumber(ref.Len()))
return 1
}
func chanEq(L *lua.LState) int {
chan1 := checkChan(L, 1)
chan2 := checkChan(L, 2)
L.Push(lua.LBool(chan1 == chan2))
return 1
}
// chan methods
func chanSend(L *lua.LState) int {
ref := checkChan(L, 1)
value := L.CheckAny(2)
convertedValue := lValueToReflect(value, ref.Type().Elem())
if convertedValue.Type() != ref.Type().Elem() {
L.ArgError(2, "incorrect type")
}
ref.Send(convertedValue)
return 0
}
func chanReceive(L *lua.LState) int {
ref := checkChan(L, 1)
value, ok := ref.Recv()
if !ok {
L.Push(lua.LNil)
L.Push(lua.LBool(false))
return 2
}
L.Push(New(L, value.Interface()))
L.Push(lua.LBool(true))
return 2
}
func chanClose(L *lua.LState) int {
ref := checkChan(L, 1)
ref.Close()
return 0
}

View File

@ -0,0 +1,163 @@
// Package luar provides custom type reflection to gopher-lua.
//
// Notice
//
// This package is currently in development, and its behavior may change. This
// message will be removed once the package is considered stable.
//
// Basic types
//
// Go bool, number, and string types are converted to the equivalent basic
// Lua type.
//
// Example:
// New(L, "Hello World") -> lua.LString("Hello World")
// New(L, uint(834)) -> lua.LNumber(uint(834))
//
// Channel types
//
// Channel types have the following methods defined:
// receive(): Receives data from the channel. Returns nil plus false if the
// channel is closed.
// send(data): Sends data to the channel.
// close(): Closes the channel.
//
// Taking the length (#) of a channel returns how many unread items are in its
// buffer.
//
// Example:
// ch := make(chan string)
// L.SetGlobal("ch", New(L, ch))
// ---
// ch:receive() -- equivalent to v, ok := ch
// ch:send("hello") -- equivalent to ch <- "hello"
// ch:close() -- equivalent to close(ch)
//
// Function types
//
// Function types can be called from Lua. Its arguments and returned values
// will be automatically converted from and to Lua types, respectively (see
// exception below). However, a function that uses luar.LState can bypass the
// automatic argument and return value conversion (see luar.LState
// documentation for example).
//
// Example:
// fn := func(name string, age uint) string {
// return fmt.Sprintf("Hello %s, age %d", name, age)
// }
// L.SetGlobal("fn", New(L, fn))
// ---
// print(fn("Tim", 5)) -- prints "Hello Tim, age 5"
//
// A special conversion case happens when function returns a lua.LValue slice.
// In that case, luar will automatically unpack the slice.
//
// Example:
// fn := func() []lua.LValue {
// return []lua.LValue{lua.LString("Hello"), lua.LNumber(2.5)}
// }
// L.SetGlobal("fn", New(L, fn))
// ---
// x, y = fn()
// print(x) -- prints "Hello"
// print(y) -- prints "2.5"
//
// Map types
//
// Map types can be accessed and modified like a normal Lua table a meta table.
// Its length can also be queried using the # operator.
//
// Rather than using pairs to create an map iterator, calling the value (e.g.
// map_variable()) will return an iterator for the map.
//
// Example:
// places := map[string]string{
// "NA": "North America",
// "EU": "European Union",
// }
// L.SetGlobal("places", New(L, places))
// ---
// print(#places) -- prints "2"
// print(places.NA) -- prints "North America"
// print(places["EU"]) -- prints "European Union"
// for k, v in places() do
// print(k .. ": " .. v)
// end
//
// Slice types
//
// Like map types, slices be accessed, be modified, and have their length
// queried. Additionally, the following methods are defined for slices:
// append(items...): Appends the items to the slice. Returns a slice with
// the items appended.
// capacity(): Returns the slice capacity.
//
// For consistency with other Lua code, slices use one-based indexing.
//
// Example:
// letters := []string{"a", "e", "i"}
// L.SetGlobal("letters", New(L, letters))
// ---
// letters = letters:append("o", "u")
//
// Struct types
//
// Struct types can have their fields accessed and modified and their methods
// called. First letters of field/method names are automatically converted to
// uppercase.
//
// Example:
// type Person {
// Name string
// }
// func (p Person) SayHello() {
// fmt.Printf("Hello, %s\n", p.Name)
// }
//
// tim := Person{"Tim"}
// L.SetGlobal("tim", New(L, tim))
// ---
// tim:SayHello() -- same as tim:sayHello()
//
// Pointer types
//
// Pointers to structs operate the same way structs do. Pointers can also be
// dereferenced using the unary minus (-) operator.
//
// Example:
// str := "hello"
// L.SetGlobal("strptr", New(L, &str))
// ---
// print(-strptr) -- prints "hello"
//
// The pointed to value can changed using the pow (^) operator.
//
// Example:
// str := "hello"
// L.SetGlobal("strptr", New(L, &str))
// ---
// print(str^"world") -- prints "world", and str's value is now "world"
//
// Type types
//
// Type constructors can be created using NewType. When called, it returns a
// new variable which is of the same type that was passed to NewType. Its
// behavior is dependent on the kind of value passed, as described below:
//
// Kind Constructor arguments Return value
// -----------------------------------------------------
// Channel Buffer size (opt) Channel
// Map None Map
// Slice Length (opt), Capacity (opt) Slice
// Default None Pointer to the newly allocated value
//
// Example:
// type Person struct {
// Name string
// }
// L.SetGlobal("Person", NewType(L, Person{}))
// ---
// p = Person()
// p.Name = "John"
// print("Hello, " .. p.Name) // prints "Hello, John"
package luar

View File

@ -0,0 +1,781 @@
package luar_test
import (
"fmt"
"strconv"
"github.com/layeh/gopher-luar"
"github.com/yuin/gopher-lua"
)
type Person struct {
Name string
Age int
Friend *Person
}
func (p Person) Hello() string {
return "Hello, " + p.Name
}
func (p Person) String() string {
return p.Name + " (" + strconv.Itoa(p.Age) + ")"
}
func (p *Person) AddNumbers(L *luar.LState) int {
sum := 0
for i := L.GetTop(); i >= 1; i-- {
sum += L.CheckInt(i)
}
L.Push(lua.LString("Tim counts: " + strconv.Itoa(sum)))
return 1
}
type Proxy struct {
XYZ string
}
func (p *Proxy) LuarCall(args ...lua.LValue) {
fmt.Printf("I was called with %d arguments!\n", len(args))
}
func (p *Proxy) LuarNewIndex(key string, value lua.LValue) {
str := value.String()
p.XYZ = str + str
}
func Example__1() {
const code = `
print(user1.Name)
print(user1.Age)
print(user1:Hello())
print(user2.Name)
print(user2.Age)
hello = user2.Hello
print(hello(user2))
`
L := lua.NewState()
defer L.Close()
tim := &Person{
Name: "Tim",
Age: 30,
}
john := Person{
Name: "John",
Age: 40,
}
L.SetGlobal("user1", luar.New(L, tim))
L.SetGlobal("user2", luar.New(L, john))
if err := L.DoString(code); err != nil {
panic(err)
}
// Output:
// Tim
// 30
// Hello, Tim
// John
// 40
// Hello, John
}
func Example__2() {
const code = `
for i = 1, #things do
print(things[i])
end
things[1] = "cookie"
print()
print(thangs.ABC)
print(thangs.DEF)
print(thangs.GHI)
thangs.GHI = 789
thangs.ABC = nil
`
L := lua.NewState()
defer L.Close()
things := []string{
"cake",
"wallet",
"calendar",
"phone",
"speaker",
}
thangs := map[string]int{
"ABC": 123,
"DEF": 456,
}
L.SetGlobal("things", luar.New(L, things))
L.SetGlobal("thangs", luar.New(L, thangs))
if err := L.DoString(code); err != nil {
panic(err)
}
fmt.Println()
fmt.Println(things[0])
fmt.Println(thangs["GHI"])
_, ok := thangs["ABC"]
fmt.Println(ok)
// Output:
// cake
// wallet
// calendar
// phone
// speaker
//
// 123
// 456
// nil
//
// cookie
// 789
// false
}
func Example__3() {
const code = `
user2 = Person()
user2.Name = "John"
user2.Friend = user1
print(user2.Name)
print(user2.Friend.Name)
everyone = People()
everyone["tim"] = user1
everyone["john"] = user2
`
L := lua.NewState()
defer L.Close()
tim := &Person{
Name: "Tim",
}
L.SetGlobal("user1", luar.New(L, tim))
L.SetGlobal("Person", luar.NewType(L, Person{}))
L.SetGlobal("People", luar.NewType(L, map[string]*Person{}))
if err := L.DoString(code); err != nil {
panic(err)
}
everyone := L.GetGlobal("everyone").(*lua.LUserData).Value.(map[string]*Person)
fmt.Println(len(everyone))
// Output:
// John
// Tim
// 2
}
func Example__4() {
const code = `
print(getHello(person))
`
L := lua.NewState()
defer L.Close()
tim := &Person{
Name: "Tim",
}
fn := func(p *Person) string {
return "Hello, " + p.Name
}
L.SetGlobal("person", luar.New(L, tim))
L.SetGlobal("getHello", luar.New(L, fn))
if err := L.DoString(code); err != nil {
panic(err)
}
// Output:
// Hello, Tim
}
func Example__5() {
const code = `
print(ch:receive())
ch:send("John")
print(ch:receive())
`
L := lua.NewState()
defer L.Close()
ch := make(chan string)
go func() {
ch <- "Tim"
name, ok := <-ch
fmt.Printf("%s\t%v\n", name, ok)
close(ch)
}()
L.SetGlobal("ch", luar.New(L, ch))
if err := L.DoString(code); err != nil {
panic(err)
}
// Output:
// Tim true
// John true
// nil false
}
func Example__6() {
const code = `
local sorted = {}
for k, v in countries() do
table.insert(sorted, v)
end
table.sort(sorted)
for i = 1, #sorted do
print(sorted[i])
end
`
L := lua.NewState()
defer L.Close()
countries := map[string]string{
"JP": "Japan",
"CA": "Canada",
"FR": "France",
}
L.SetGlobal("countries", luar.New(L, countries))
if err := L.DoString(code); err != nil {
panic(err)
}
// Output:
// Canada
// France
// Japan
}
func Example__7() {
const code = `
fn("a", 1, 2, 3)
fn("b")
fn("c", 4)
`
L := lua.NewState()
defer L.Close()
fn := func(str string, extra ...int) {
fmt.Printf("%s\n", str)
for _, x := range extra {
fmt.Printf("%d\n", x)
}
}
L.SetGlobal("fn", luar.New(L, fn))
if err := L.DoString(code); err != nil {
panic(err)
}
// Output:
// a
// 1
// 2
// 3
// b
// c
// 4
}
func Example__8() {
const code = `
for _, x in ipairs(fn(1, 2, 3)) do
print(x)
end
for _, x in ipairs(fn()) do
print(x)
end
for _, x in ipairs(fn(4)) do
print(x)
end
`
L := lua.NewState()
defer L.Close()
fn := func(x ...float64) *lua.LTable {
tbl := L.NewTable()
for i := len(x) - 1; i >= 0; i-- {
tbl.Insert(len(x)-i, lua.LNumber(x[i]))
}
return tbl
}
L.SetGlobal("fn", luar.New(L, fn))
if err := L.DoString(code); err != nil {
panic(err)
}
// Output:
// 3
// 2
// 1
// 4
}
func Example__9() {
const code = `
print(#items)
print(items:capacity())
items = items:append("hello", "world")
print(#items)
print(items:capacity())
print(items[1])
print(items[2])
`
L := lua.NewState()
defer L.Close()
items := make([]string, 0, 10)
L.SetGlobal("items", luar.New(L, items))
if err := L.DoString(code); err != nil {
panic(err)
}
// Output:
// 0
// 10
// 2
// 10
// hello
// world
}
func Example__10() {
const code = `
ints = newInts(1)
print(#ints, ints:capacity())
ints = newInts(0, 10)
print(#ints, ints:capacity())
`
L := lua.NewState()
defer L.Close()
type ints []int
L.SetGlobal("newInts", luar.NewType(L, ints{}))
if err := L.DoString(code); err != nil {
panic(err)
}
// Output:
// 1 1
// 0 10
}
func Example__11() {
const code = `
print(p1 == p1)
print(p1 == p1_alias)
print(p1 == p2)
`
L := lua.NewState()
defer L.Close()
p1 := Person{
Name: "Tim",
}
p2 := Person{
Name: "John",
}
L.SetGlobal("p1", luar.New(L, &p1))
L.SetGlobal("p1_alias", luar.New(L, &p1))
L.SetGlobal("p2", luar.New(L, &p2))
if err := L.DoString(code); err != nil {
panic(err)
}
// Output:
// true
// true
// false
}
func Example__12() {
const code = `
print(p1)
print(p2)
`
L := lua.NewState()
defer L.Close()
p1 := Person{
Name: "Tim",
Age: 99,
}
p2 := Person{
Name: "John",
Age: 2,
}
L.SetGlobal("p1", luar.New(L, &p1))
L.SetGlobal("p2", luar.New(L, &p2))
if err := L.DoString(code); err != nil {
panic(err)
}
// Output:
// Tim (99)
// John (2)
}
func Example__13() {
const code = `
print(p:AddNumbers(1, 2, 3, 4, 5))
`
L := lua.NewState()
defer L.Close()
p := Person{
Name: "Tim",
}
L.SetGlobal("p", luar.New(L, &p))
if err := L.DoString(code); err != nil {
panic(err)
}
// Output:
// Tim counts: 15
}
func Example__14() {
const code = `
print(p:hello())
print(p.age)
`
L := lua.NewState()
defer L.Close()
p := Person{
Name: "Tim",
Age: 66,
}
L.SetGlobal("p", luar.New(L, &p))
if err := L.DoString(code); err != nil {
panic(err)
}
// Output:
// Hello, Tim
// 66
}
func Example__15() {
const code = `
print(p.XYZ)
p("Hello", "World")
p.nothing = "nice"
`
L := lua.NewState()
defer L.Close()
p := Proxy{
XYZ: "1000+",
}
L.SetGlobal("p", luar.New(L, &p))
if err := L.DoString(code); err != nil {
panic(err)
}
fmt.Println(p.XYZ)
// Output:
// 1000+
// I was called with 2 arguments!
// nicenice
}
func Example__16() {
const code = `
print(fn("tim", 5))
`
L := lua.NewState()
defer L.Close()
fn := func(name string, count int) []lua.LValue {
s := make([]lua.LValue, count)
for i := 0; i < count; i++ {
s[i] = lua.LString(name)
}
return s
}
L.SetGlobal("fn", luar.New(L, fn))
if err := L.DoString(code); err != nil {
panic(err)
}
// Output:
// tim tim tim tim tim
}
func Example__17() {
const code = `
print(-ptr)
`
L := lua.NewState()
defer L.Close()
str := "hello"
L.SetGlobal("ptr", luar.New(L, &str))
if err := L.DoString(code); err != nil {
panic(err)
}
// Output:
// hello
}
func Example__18() {
const code = `
print(ptr1 == nil)
print(ptr2 == nil)
print(ptr1 == ptr2)
`
L := lua.NewState()
defer L.Close()
var ptr1 *string
str := "hello"
L.SetGlobal("ptr1", luar.New(L, ptr1))
L.SetGlobal("ptr2", luar.New(L, &str))
if err := L.DoString(code); err != nil {
panic(err)
}
// Output:
// true
// false
// false
}
func Example__19() {
const code = `
print(-str)
print(str ^ "world")
print(-str)
`
L := lua.NewState()
defer L.Close()
str := "hello"
L.SetGlobal("str", luar.New(L, &str))
if err := L.DoString(code); err != nil {
panic(err)
}
// Output:
// hello
// world
// world
}
type Example__20_A struct {
*Example__20_B
}
type Example__20_B struct {
Value *string
}
func Example__20() {
const code = `
print(a.Value == nil)
a.Value = str_ptr()
_ = a.Value ^ "hello"
print(a.Value == nil)
print(-a.Value)
`
L := lua.NewState()
defer L.Close()
a := Example__20_A{
Example__20_B: &Example__20_B{},
}
L.SetGlobal("a", luar.New(L, a))
L.SetGlobal("str_ptr", luar.NewType(L, ""))
if err := L.DoString(code); err != nil {
panic(err)
}
// Output:
// true
// false
// hello
}
func Example__21() {
const code = `
print(fn == nil)
`
L := lua.NewState()
defer L.Close()
var fn func()
L.SetGlobal("fn", luar.New(L, fn))
if err := L.DoString(code); err != nil {
panic(err)
}
// Output:
// true
}
func Example__22() {
const code = `
fn(arr)
`
L := lua.NewState()
defer L.Close()
arr := [3]int{1, 2, 3}
fn := func(val [3]int) {
fmt.Printf("%d %d %d\n", val[0], val[1], val[2])
}
L.SetGlobal("fn", luar.New(L, fn))
L.SetGlobal("arr", luar.New(L, arr))
if err := L.DoString(code); err != nil {
panic(err)
}
// Output:
// 1 2 3
}
func Example__23() {
const code = `
b = a
`
L := lua.NewState()
defer L.Close()
a := complex(float64(1), float64(2))
L.SetGlobal("a", luar.New(L, a))
if err := L.DoString(code); err != nil {
panic(err)
}
b := L.GetGlobal("b").(*lua.LUserData).Value.(complex128)
fmt.Println(a == b)
// Output:
// true
}
func ExampleMeta() {
const code = `
proxy(234, nil, "asd", {})
`
L := lua.NewState()
defer L.Close()
// Proxy has the following method defined:
// func (p *Proxy) LuarCall(args ...lua.LValue) {
// fmt.Printf("I was called with %d arguments!\n", len(args))
// }
//
proxy := &Proxy{}
L.SetGlobal("proxy", luar.New(L, proxy))
if err := L.DoString(code); err != nil {
panic(err)
}
// Output:
// I was called with 4 arguments!
}
func ExampleLState() {
const code = `
print(sum(1, 2, 3, 4, 5))
`
L := lua.NewState()
defer L.Close()
sum := func(L *luar.LState) int {
total := 0
for i := 1; i <= L.GetTop(); i++ {
total += L.CheckInt(i)
}
L.Push(lua.LNumber(total))
return 1
}
L.SetGlobal("sum", luar.New(L, sum))
if err := L.DoString(code); err != nil {
panic(err)
}
// Output:
// 15
}
func ExampleNewType() {
L := lua.NewState()
defer L.Close()
type Song struct {
Title string
Artist string
}
L.SetGlobal("Song", luar.NewType(L, Song{}))
L.DoString(`
s = Song()
s.Title = "Montana"
s.Artist = "Tycho"
print(s.Artist .. " - " .. s.Title)
`)
// Output:
// Tycho - Montana
}

View File

@ -0,0 +1,96 @@
package luar
import (
"reflect"
"github.com/yuin/gopher-lua"
)
// LState is an wrapper for gopher-lua's LState. It should be used when you
// wish to have a function/method with the standard "func(*lua.LState) int"
// signature.
type LState struct {
*lua.LState
}
var (
refTypeLStatePtr reflect.Type
refTypeLuaLValueSlice reflect.Type
refTypeLuaLValue reflect.Type
refTypeInt reflect.Type
)
func init() {
refTypeLStatePtr = reflect.TypeOf(&LState{})
refTypeLuaLValueSlice = reflect.TypeOf([]lua.LValue{})
refTypeLuaLValue = reflect.TypeOf((*lua.LValue)(nil)).Elem()
refTypeInt = reflect.TypeOf(int(0))
}
func funcIsBypass(t reflect.Type) bool {
if t.NumIn() == 1 && t.NumOut() == 1 && t.In(0) == refTypeLStatePtr && t.Out(0) == refTypeInt {
return true
}
if t.NumIn() == 2 && t.NumOut() == 1 && t.In(1) == refTypeLStatePtr && t.Out(0) == refTypeInt {
return true
}
return false
}
func funcEvaluate(L *lua.LState, fn reflect.Value) int {
fnType := fn.Type()
if funcIsBypass(fnType) {
luarState := LState{L}
args := make([]reflect.Value, 0, 2)
if fnType.NumIn() == 2 {
receiverHint := fnType.In(0)
receiver := lValueToReflect(L.Get(1), receiverHint)
if receiver.Type() != receiverHint {
L.RaiseError("incorrect receiver type")
}
args = append(args, receiver)
L.Remove(1)
}
args = append(args, reflect.ValueOf(&luarState))
return fn.Call(args)[0].Interface().(int)
}
top := L.GetTop()
expected := fnType.NumIn()
variadic := fnType.IsVariadic()
if !variadic && top != expected {
L.RaiseError("invalid number of function arguments (%d expected, got %d)", expected, top)
}
if variadic && top < expected-1 {
L.RaiseError("invalid number of function arguments (%d or more expected, got %d)", expected-1, top)
}
args := make([]reflect.Value, top)
for i := 0; i < L.GetTop(); i++ {
var hint reflect.Type
if variadic && i >= expected-1 {
hint = fnType.In(expected - 1).Elem()
} else {
hint = fnType.In(i)
}
args[i] = lValueToReflect(L.Get(i+1), hint)
}
ret := fn.Call(args)
if len(ret) == 1 && ret[0].Type() == refTypeLuaLValueSlice {
values := ret[0].Interface().([]lua.LValue)
for _, value := range values {
L.Push(value)
}
return len(values)
}
for _, val := range ret {
L.Push(New(L, val.Interface()))
}
return len(ret)
}
func funcWrapper(L *lua.LState, fn reflect.Value) *lua.LFunction {
wrapper := func(L *lua.LState) int {
return funcEvaluate(L, fn)
}
return L.NewFunction(wrapper)
}

View File

@ -0,0 +1,226 @@
package luar
import (
"fmt"
"reflect"
"github.com/yuin/gopher-lua"
)
var wrapperMetatable map[string]map[string]lua.LGFunction
func init() {
wrapperMetatable = map[string]map[string]lua.LGFunction{
"chan": {
"__index": chanIndex,
"__len": chanLen,
"__tostring": allTostring,
"__eq": chanEq,
},
"map": {
"__index": mapIndex,
"__newindex": mapNewIndex,
"__len": mapLen,
"__call": mapCall,
"__tostring": allTostring,
"__eq": mapEq,
},
"ptr": {
"__index": ptrIndex,
"__newindex": ptrNewIndex,
"__pow": ptrPow,
"__call": ptrCall,
"__tostring": allTostring,
"__unm": ptrUnm,
"__eq": ptrEq,
},
"slice": {
"__index": sliceIndex,
"__newindex": sliceNewIndex,
"__len": sliceLen,
"__tostring": allTostring,
"__eq": sliceEq,
},
"struct": {
"__index": structIndex,
"__newindex": structNewIndex,
"__call": structCall,
"__tostring": allTostring,
},
"type": {
"__call": typeCall,
"__tostring": allTostring,
"__eq": typeEq,
},
}
}
func ensureMetatable(L *lua.LState) *lua.LTable {
const metatableKey = lua.LString("github.com/layeh/gopher-luar")
v := L.G.Registry.RawGetH(metatableKey)
if v != lua.LNil {
return v.(*lua.LTable)
}
newTable := L.NewTable()
for typeName, meta := range wrapperMetatable {
typeTable := L.NewTable()
typeTable.RawSetH(lua.LString("__metatable"), lua.LTrue)
for methodName, methodFunc := range meta {
typeTable.RawSetH(lua.LString(methodName), L.NewFunction(methodFunc))
}
newTable.RawSetH(lua.LString(typeName), typeTable)
}
L.G.Registry.RawSetH(metatableKey, newTable)
return newTable
}
func allTostring(L *lua.LState) int {
ud := L.CheckUserData(1)
value := ud.Value
if stringer, ok := value.(fmt.Stringer); ok {
L.Push(lua.LString(stringer.String()))
} else {
L.Push(lua.LString(fmt.Sprintf("userdata (luar): %p", ud)))
}
return 1
}
// New creates and returns a new lua.LValue for the given value.
//
// The following types are supported:
// Kind gopher-lua Type
// -------------------------------
// nil LNil
// Bool LBool
// Int LNumber
// Int8 LNumber
// Int16 LNumber
// Int32 LNumber
// Int64 LNumber
// Uint LNumber
// Uint8 LNumber
// Uint32 LNumber
// Uint64 LNumber
// Float32 LNumber
// Float64 LNumber
// Complex64 *LUserData
// Complex128 *LUserData
// Array *LUserData
// Chan *LUserData
// Interface *LUserData
// Func *lua.LFunction
// Map *LUserData
// Ptr *LUserData
// Slice *LUserData
// String LString
// Struct *LUserData
// UnsafePointer *LUserData
func New(L *lua.LState, value interface{}) lua.LValue {
if value == nil {
return lua.LNil
}
if lval, ok := value.(lua.LValue); ok {
return lval
}
val := reflect.ValueOf(value)
switch val.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
if val.IsNil() {
return lua.LNil
}
}
table := ensureMetatable(L)
switch val.Kind() {
case reflect.Bool:
return lua.LBool(val.Bool())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return lua.LNumber(float64(val.Int()))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return lua.LNumber(float64(val.Uint()))
case reflect.Float32, reflect.Float64:
return lua.LNumber(val.Float())
case reflect.Chan:
ud := L.NewUserData()
ud.Value = val.Interface()
ud.Metatable = table.RawGetH(lua.LString("chan"))
return ud
case reflect.Func:
return funcWrapper(L, val)
case reflect.Interface:
ud := L.NewUserData()
ud.Value = val.Interface()
return ud
case reflect.Map:
ud := L.NewUserData()
ud.Value = val.Interface()
ud.Metatable = table.RawGetH(lua.LString("map"))
return ud
case reflect.Ptr:
ud := L.NewUserData()
ud.Value = val.Interface()
ud.Metatable = table.RawGetH(lua.LString("ptr"))
return ud
case reflect.Slice:
ud := L.NewUserData()
ud.Value = val.Interface()
ud.Metatable = table.RawGetH(lua.LString("slice"))
return ud
case reflect.String:
return lua.LString(val.String())
case reflect.Struct:
ud := L.NewUserData()
ud.Value = val.Interface()
ud.Metatable = table.RawGetH(lua.LString("struct"))
return ud
default:
ud := L.NewUserData()
ud.Value = val.Interface()
return ud
}
}
// NewType returns a new type creator for the given value's type.
//
// When the lua.LValue is called, a new value will be created that is the
// same type as value's type.
func NewType(L *lua.LState, value interface{}) lua.LValue {
table := ensureMetatable(L)
val := reflect.TypeOf(value)
ud := L.NewUserData()
ud.Value = val
ud.Metatable = table.RawGetH(lua.LString("type"))
return ud
}
func lValueToReflect(v lua.LValue, hint reflect.Type) reflect.Value {
if hint == refTypeLuaLValue {
return reflect.ValueOf(v)
}
switch converted := v.(type) {
case lua.LBool:
return reflect.ValueOf(bool(converted))
case lua.LChannel:
return reflect.ValueOf(converted)
case lua.LNumber:
return reflect.ValueOf(converted).Convert(hint)
case *lua.LFunction:
return reflect.ValueOf(converted)
case *lua.LNilType:
return reflect.Zero(hint)
case *lua.LState:
return reflect.ValueOf(converted)
case lua.LString:
return reflect.ValueOf(string(converted))
case *lua.LTable:
return reflect.ValueOf(converted)
case *lua.LUserData:
return reflect.ValueOf(converted.Value)
}
panic("fatal lValueToReflect error")
}

View File

@ -0,0 +1,79 @@
package luar
import (
"reflect"
"github.com/yuin/gopher-lua"
)
func checkMap(L *lua.LState, idx int) reflect.Value {
ud := L.CheckUserData(idx)
ref := reflect.ValueOf(ud.Value)
if ref.Kind() != reflect.Map {
L.ArgError(idx, "expecting map")
}
return ref
}
func mapIndex(L *lua.LState) int {
ref := checkMap(L, 1)
key := L.CheckAny(2)
convertedKey := lValueToReflect(key, ref.Type().Key())
item := ref.MapIndex(convertedKey)
if !item.IsValid() {
return 0
}
L.Push(New(L, item.Interface()))
return 1
}
func mapNewIndex(L *lua.LState) int {
ref := checkMap(L, 1)
key := L.CheckAny(2)
value := L.CheckAny(3)
convertedKey := lValueToReflect(key, ref.Type().Key())
if convertedKey.Type() != ref.Type().Key() {
L.ArgError(2, "invalid map key type")
}
var convertedValue reflect.Value
if value != lua.LNil {
convertedValue = lValueToReflect(value, ref.Type().Elem())
if convertedValue.Type() != ref.Type().Elem() {
L.ArgError(3, "invalid map value type")
}
}
ref.SetMapIndex(convertedKey, convertedValue)
return 0
}
func mapLen(L *lua.LState) int {
ref := checkMap(L, 1)
L.Push(lua.LNumber(ref.Len()))
return 1
}
func mapCall(L *lua.LState) int {
ref := checkMap(L, 1)
keys := ref.MapKeys()
i := 0
fn := func(L *lua.LState) int {
if i >= len(keys) {
return 0
}
L.Push(New(L, keys[i].Interface()))
L.Push(New(L, ref.MapIndex(keys[i]).Interface()))
i++
return 2
}
L.Push(L.NewFunction(fn))
return 1
}
func mapEq(L *lua.LState) int {
map1 := checkMap(L, 1)
map2 := checkMap(L, 2)
L.Push(lua.LBool(map1 == map2))
return 1
}

View File

@ -0,0 +1,34 @@
package luar
import (
"reflect"
"github.com/yuin/gopher-lua"
)
// Meta can be implemented by a struct or struct pointer. Each method defines
// a fallback action for the corresponding Lua metamethod.
//
// The signature of the methods does not matter; they will be converted using
// the standard function conversion rules. Also, a type is allowed to implement
// only a subset of the interface.
type Meta interface {
LuarCall(arguments ...interface{}) interface{}
LuarIndex(key interface{}) interface{}
LuarNewIndex(key, value interface{})
}
const (
luarCallFunc = "LuarCall"
luarIndexFunc = "LuarIndex"
luarNewIndexFunc = "LuarNewIndex"
)
func metaFunction(L *lua.LState, name string, ref reflect.Value) int {
refType := ref.Type()
method, ok := refType.MethodByName(name)
if !ok {
return -1
}
return funcEvaluate(L, method.Func)
}

View File

@ -0,0 +1,130 @@
package luar
import (
"reflect"
"github.com/yuin/gopher-lua"
)
func checkPtr(L *lua.LState, idx int) reflect.Value {
ud := L.CheckUserData(idx)
ref := reflect.ValueOf(ud.Value)
if ref.Kind() != reflect.Ptr {
L.ArgError(idx, "expecting ptr")
}
return ref
}
func ptrIndex(L *lua.LState) int {
ref := checkPtr(L, 1)
deref := ref.Elem()
if deref.Kind() != reflect.Struct {
L.RaiseError("cannot index non-struct pointer")
}
refType := ref.Type()
// Check for method
key := L.OptString(2, "")
exKey := getExportedName(key)
if key != "" {
if method, ok := refType.MethodByName(exKey); ok {
L.Push(New(L, method.Func.Interface()))
return 1
}
}
// Check for field
if field := deref.FieldByName(exKey); field.IsValid() {
if !field.CanInterface() {
L.RaiseError("cannot interface field " + exKey)
}
if val := New(L, field.Interface()); val != nil {
L.Push(val)
return 1
}
L.RaiseError("could not convert field " + exKey)
}
if ret := metaFunction(L, luarIndexFunc, ref); ret >= 0 {
return ret
}
return 0
}
func ptrNewIndex(L *lua.LState) int {
ref := checkPtr(L, 1)
deref := ref.Elem()
if deref.Kind() != reflect.Struct {
L.RaiseError("cannot new index non-struct pointer")
}
key := L.OptString(2, "")
value := L.CheckAny(3)
if key == "" {
if ret := metaFunction(L, luarNewIndexFunc, ref); ret >= 0 {
return ret
}
L.TypeError(2, lua.LTString)
return 0
}
exKey := getExportedName(key)
field := deref.FieldByName(exKey)
if !field.IsValid() {
if ret := metaFunction(L, luarNewIndexFunc, ref); ret >= 0 {
return ret
}
L.ArgError(2, "unknown field "+exKey)
}
if !field.CanSet() {
L.ArgError(2, "cannot set field "+exKey)
}
field.Set(lValueToReflect(value, field.Type()))
return 0
}
func ptrPow(L *lua.LState) int {
ref := checkPtr(L, 1)
val := L.CheckAny(2)
if ref.IsNil() {
L.RaiseError("cannot dereference nil pointer")
}
elem := ref.Elem()
if !elem.CanSet() {
L.RaiseError("unable to set pointer value")
}
value := lValueToReflect(val, elem.Type())
elem.Set(value)
return 1
}
func ptrCall(L *lua.LState) int {
ref := checkPtr(L, 1)
if ret := metaFunction(L, luarCallFunc, ref); ret >= 0 {
return ret
}
L.RaiseError("attempt to call a non-function object")
return 0
}
func ptrUnm(L *lua.LState) int {
ref := checkPtr(L, 1)
elem := ref.Elem()
if !elem.CanInterface() {
L.RaiseError("cannot interface pointer type " + elem.String())
}
L.Push(New(L, elem.Interface()))
return 1
}
func ptrEq(L *lua.LState) int {
ptr1 := checkPtr(L, 1)
ptr2 := checkPtr(L, 2)
L.Push(lua.LBool(ptr1 == ptr2))
return 1
}

View File

@ -0,0 +1,93 @@
package luar
import (
"reflect"
"github.com/yuin/gopher-lua"
)
func checkSlice(L *lua.LState, idx int) reflect.Value {
ud := L.CheckUserData(idx)
ref := reflect.ValueOf(ud.Value)
if ref.Kind() != reflect.Slice {
L.ArgError(idx, "expecting slice")
}
return ref
}
func sliceIndex(L *lua.LState) int {
ref := checkSlice(L, 1)
key := L.CheckAny(2)
switch converted := key.(type) {
case lua.LNumber:
index := int(converted)
if index < 1 || index > ref.Len() {
L.ArgError(2, "index out of range")
}
L.Push(New(L, ref.Index(index-1).Interface()))
case lua.LString:
switch string(converted) {
case "capacity":
L.Push(L.NewFunction(sliceCapacity))
case "append":
L.Push(L.NewFunction(sliceAppend))
default:
return 0
}
default:
L.ArgError(2, "must be a number or string")
}
return 1
}
func sliceNewIndex(L *lua.LState) int {
ref := checkSlice(L, 1)
index := L.CheckInt(2)
value := L.CheckAny(3)
if index < 1 || index > ref.Len() {
L.ArgError(2, "index out of range")
}
ref.Index(index - 1).Set(lValueToReflect(value, ref.Type().Elem()))
return 0
}
func sliceLen(L *lua.LState) int {
ref := checkSlice(L, 1)
L.Push(lua.LNumber(ref.Len()))
return 1
}
func sliceEq(L *lua.LState) int {
slice1 := checkSlice(L, 1)
slice2 := checkSlice(L, 2)
L.Push(lua.LBool(slice1 == slice2))
return 1
}
// slice methods
func sliceCapacity(L *lua.LState) int {
ref := checkSlice(L, 1)
L.Push(lua.LNumber(ref.Cap()))
return 1
}
func sliceAppend(L *lua.LState) int {
ref := checkSlice(L, 1)
hint := ref.Type().Elem()
values := make([]reflect.Value, L.GetTop()-1)
for i := 2; i <= L.GetTop(); i++ {
value := lValueToReflect(L.Get(i), hint)
if value.Type() != hint {
L.ArgError(i, "invalid type")
}
values[i-2] = value
}
newSlice := reflect.Append(ref, values...)
L.Push(New(L, newSlice.Interface()))
return 1
}

View File

@ -0,0 +1,84 @@
package luar
import (
"reflect"
"github.com/yuin/gopher-lua"
)
func checkStruct(L *lua.LState, idx int) reflect.Value {
ud := L.CheckUserData(idx)
ref := reflect.ValueOf(ud.Value)
if ref.Kind() != reflect.Struct {
L.ArgError(idx, "expecting struct")
}
return ref
}
func structIndex(L *lua.LState) int {
ref := checkStruct(L, 1)
refType := ref.Type()
// Check for method
key := L.OptString(2, "")
exKey := getExportedName(key)
if exKey != "" {
if method, ok := refType.MethodByName(exKey); ok {
L.Push(New(L, method.Func.Interface()))
return 1
}
}
// Check for field
if field := ref.FieldByName(exKey); field.IsValid() {
if !field.CanInterface() {
L.RaiseError("cannot interface field " + exKey)
}
L.Push(New(L, field.Interface()))
return 1
}
if ret := metaFunction(L, luarIndexFunc, ref); ret >= 0 {
return ret
}
return 0
}
func structNewIndex(L *lua.LState) int {
ref := checkStruct(L, 1)
key := L.OptString(2, "")
value := L.CheckAny(3)
if key == "" {
if ret := metaFunction(L, luarNewIndexFunc, ref); ret >= 0 {
return ret
}
L.TypeError(2, lua.LTString)
return 0
}
exKey := getExportedName(key)
field := ref.FieldByName(exKey)
if !field.IsValid() {
if ret := metaFunction(L, luarNewIndexFunc, ref); ret >= 0 {
return ret
}
L.ArgError(2, "unknown field "+exKey)
}
if !field.CanSet() {
L.ArgError(2, "cannot set field "+exKey)
}
field.Set(lValueToReflect(value, field.Type()))
return 0
}
func structCall(L *lua.LState) int {
ref := checkStruct(L, 1)
if ret := metaFunction(L, luarCallFunc, ref); ret >= 0 {
return ret
}
L.RaiseError("attempt to call a non-function object")
return 0
}

View File

@ -0,0 +1,44 @@
package luar
import (
"reflect"
"github.com/yuin/gopher-lua"
)
func checkType(L *lua.LState, idx int) reflect.Type {
ud := L.CheckUserData(idx)
ref, ok := ud.Value.(reflect.Type)
if !ok {
L.ArgError(idx, "expecting type")
}
return ref
}
func typeCall(L *lua.LState) int {
ref := checkType(L, 1)
var value reflect.Value
switch ref.Kind() {
case reflect.Chan:
buffer := L.OptInt(2, 0)
value = reflect.MakeChan(ref, buffer)
case reflect.Map:
value = reflect.MakeMap(ref)
case reflect.Slice:
length := L.OptInt(2, 0)
capacity := L.OptInt(3, length)
value = reflect.MakeSlice(ref, length, capacity)
default:
value = reflect.New(ref)
}
L.Push(New(L, value.Interface()))
return 1
}
func typeEq(L *lua.LState) int {
type1 := checkType(L, 1)
type2 := checkType(L, 2)
L.Push(lua.LBool(type1 == type2))
return 1
}

View File

@ -0,0 +1,15 @@
package luar
import (
"unicode"
"unicode/utf8"
)
func getExportedName(name string) string {
buf := []byte(name)
first, n := utf8.DecodeRune(buf)
if n == 0 {
return name
}
return string(unicode.ToUpper(first)) + string(buf[n:])
}

View File

@ -0,0 +1,21 @@
Copyright (C) 2012 Rob Figueiredo
All Rights Reserved.
MIT LICENSE
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1 @@
[![GoDoc](http://godoc.org/github.com/robfig/cron?status.png)](http://godoc.org/github.com/robfig/cron)

View File

@ -0,0 +1,27 @@
package cron
import "time"
// ConstantDelaySchedule represents a simple recurring duty cycle, e.g. "Every 5 minutes".
// It does not support jobs more frequent than once a second.
type ConstantDelaySchedule struct {
Delay time.Duration
}
// Every returns a crontab Schedule that activates once every duration.
// Delays of less than a second are not supported (will round up to 1 second).
// Any fields less than a Second are truncated.
func Every(duration time.Duration) ConstantDelaySchedule {
if duration < time.Second {
duration = time.Second
}
return ConstantDelaySchedule{
Delay: duration - time.Duration(duration.Nanoseconds())%time.Second,
}
}
// Next returns the next time this should be run.
// This rounds so that the next activation time will be on the second.
func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time {
return t.Add(schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond)
}

View File

@ -0,0 +1,54 @@
package cron
import (
"testing"
"time"
)
func TestConstantDelayNext(t *testing.T) {
tests := []struct {
time string
delay time.Duration
expected string
}{
// Simple cases
{"Mon Jul 9 14:45 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"},
{"Mon Jul 9 14:59 2012", 15 * time.Minute, "Mon Jul 9 15:14 2012"},
{"Mon Jul 9 14:59:59 2012", 15 * time.Minute, "Mon Jul 9 15:14:59 2012"},
// Wrap around hours
{"Mon Jul 9 15:45 2012", 35 * time.Minute, "Mon Jul 9 16:20 2012"},
// Wrap around days
{"Mon Jul 9 23:46 2012", 14 * time.Minute, "Tue Jul 10 00:00 2012"},
{"Mon Jul 9 23:45 2012", 35 * time.Minute, "Tue Jul 10 00:20 2012"},
{"Mon Jul 9 23:35:51 2012", 44*time.Minute + 24*time.Second, "Tue Jul 10 00:20:15 2012"},
{"Mon Jul 9 23:35:51 2012", 25*time.Hour + 44*time.Minute + 24*time.Second, "Thu Jul 11 01:20:15 2012"},
// Wrap around months
{"Mon Jul 9 23:35 2012", 91*24*time.Hour + 25*time.Minute, "Thu Oct 9 00:00 2012"},
// Wrap around minute, hour, day, month, and year
{"Mon Dec 31 23:59:45 2012", 15 * time.Second, "Tue Jan 1 00:00:00 2013"},
// Round to nearest second on the delay
{"Mon Jul 9 14:45 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"},
// Round up to 1 second if the duration is less.
{"Mon Jul 9 14:45:00 2012", 15 * time.Millisecond, "Mon Jul 9 14:45:01 2012"},
// Round to nearest second when calculating the next time.
{"Mon Jul 9 14:45:00.005 2012", 15 * time.Minute, "Mon Jul 9 15:00 2012"},
// Round to nearest second for both.
{"Mon Jul 9 14:45:00.005 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"},
}
for _, c := range tests {
actual := Every(c.delay).Next(getTime(c.time))
expected := getTime(c.expected)
if actual != expected {
t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.delay, expected, actual)
}
}
}

View File

@ -0,0 +1,199 @@
// This library implements a cron spec parser and runner. See the README for
// more details.
package cron
import (
"sort"
"time"
)
// Cron keeps track of any number of entries, invoking the associated func as
// specified by the schedule. It may be started, stopped, and the entries may
// be inspected while running.
type Cron struct {
entries []*Entry
stop chan struct{}
add chan *Entry
snapshot chan []*Entry
running bool
}
// Job is an interface for submitted cron jobs.
type Job interface {
Run()
}
// The Schedule describes a job's duty cycle.
type Schedule interface {
// Return the next activation time, later than the given time.
// Next is invoked initially, and then each time the job is run.
Next(time.Time) time.Time
}
// Entry consists of a schedule and the func to execute on that schedule.
type Entry struct {
// The schedule on which this job should be run.
Schedule Schedule
// The next time the job will run. This is the zero time if Cron has not been
// started or this entry's schedule is unsatisfiable
Next time.Time
// The last time this job was run. This is the zero time if the job has never
// been run.
Prev time.Time
// The Job to run.
Job Job
}
// byTime is a wrapper for sorting the entry array by time
// (with zero time at the end).
type byTime []*Entry
func (s byTime) Len() int { return len(s) }
func (s byTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s byTime) Less(i, j int) bool {
// Two zero times should return false.
// Otherwise, zero is "greater" than any other time.
// (To sort it at the end of the list.)
if s[i].Next.IsZero() {
return false
}
if s[j].Next.IsZero() {
return true
}
return s[i].Next.Before(s[j].Next)
}
// New returns a new Cron job runner.
func New() *Cron {
return &Cron{
entries: nil,
add: make(chan *Entry),
stop: make(chan struct{}),
snapshot: make(chan []*Entry),
running: false,
}
}
// A wrapper that turns a func() into a cron.Job
type FuncJob func()
func (f FuncJob) Run() { f() }
// AddFunc adds a func to the Cron to be run on the given schedule.
func (c *Cron) AddFunc(spec string, cmd func()) error {
return c.AddJob(spec, FuncJob(cmd))
}
// AddFunc adds a Job to the Cron to be run on the given schedule.
func (c *Cron) AddJob(spec string, cmd Job) error {
schedule, err := Parse(spec)
if err != nil {
return err
}
c.Schedule(schedule, cmd)
return nil
}
// Schedule adds a Job to the Cron to be run on the given schedule.
func (c *Cron) Schedule(schedule Schedule, cmd Job) {
entry := &Entry{
Schedule: schedule,
Job: cmd,
}
if !c.running {
c.entries = append(c.entries, entry)
return
}
c.add <- entry
}
// Entries returns a snapshot of the cron entries.
func (c *Cron) Entries() []*Entry {
if c.running {
c.snapshot <- nil
x := <-c.snapshot
return x
}
return c.entrySnapshot()
}
// Start the cron scheduler in its own go-routine.
func (c *Cron) Start() {
c.running = true
go c.run()
}
// Run the scheduler.. this is private just due to the need to synchronize
// access to the 'running' state variable.
func (c *Cron) run() {
// Figure out the next activation times for each entry.
now := time.Now().Local()
for _, entry := range c.entries {
entry.Next = entry.Schedule.Next(now)
}
for {
// Determine the next entry to run.
sort.Sort(byTime(c.entries))
var effective time.Time
if len(c.entries) == 0 || c.entries[0].Next.IsZero() {
// If there are no entries yet, just sleep - it still handles new entries
// and stop requests.
effective = now.AddDate(10, 0, 0)
} else {
effective = c.entries[0].Next
}
select {
case now = <-time.After(effective.Sub(now)):
// Run every entry whose next time was this effective time.
for _, e := range c.entries {
if e.Next != effective {
break
}
go e.Job.Run()
e.Prev = e.Next
e.Next = e.Schedule.Next(effective)
}
continue
case newEntry := <-c.add:
c.entries = append(c.entries, newEntry)
newEntry.Next = newEntry.Schedule.Next(now)
case <-c.snapshot:
c.snapshot <- c.entrySnapshot()
case <-c.stop:
return
}
// 'now' should be updated after newEntry and snapshot cases.
now = time.Now().Local()
}
}
// Stop the cron scheduler.
func (c *Cron) Stop() {
c.stop <- struct{}{}
c.running = false
}
// entrySnapshot returns a copy of the current cron entry list.
func (c *Cron) entrySnapshot() []*Entry {
entries := []*Entry{}
for _, e := range c.entries {
entries = append(entries, &Entry{
Schedule: e.Schedule,
Next: e.Next,
Prev: e.Prev,
Job: e.Job,
})
}
return entries
}

View File

@ -0,0 +1,255 @@
package cron
import (
"fmt"
"sync"
"testing"
"time"
)
// Many tests schedule a job for every second, and then wait at most a second
// for it to run. This amount is just slightly larger than 1 second to
// compensate for a few milliseconds of runtime.
const ONE_SECOND = 1*time.Second + 10*time.Millisecond
// Start and stop cron with no entries.
func TestNoEntries(t *testing.T) {
cron := New()
cron.Start()
select {
case <-time.After(ONE_SECOND):
t.FailNow()
case <-stop(cron):
}
}
// Start, stop, then add an entry. Verify entry doesn't run.
func TestStopCausesJobsToNotRun(t *testing.T) {
wg := &sync.WaitGroup{}
wg.Add(1)
cron := New()
cron.Start()
cron.Stop()
cron.AddFunc("* * * * * ?", func() { wg.Done() })
select {
case <-time.After(ONE_SECOND):
// No job ran!
case <-wait(wg):
t.FailNow()
}
}
// Add a job, start cron, expect it runs.
func TestAddBeforeRunning(t *testing.T) {
wg := &sync.WaitGroup{}
wg.Add(1)
cron := New()
cron.AddFunc("* * * * * ?", func() { wg.Done() })
cron.Start()
defer cron.Stop()
// Give cron 2 seconds to run our job (which is always activated).
select {
case <-time.After(ONE_SECOND):
t.FailNow()
case <-wait(wg):
}
}
// Start cron, add a job, expect it runs.
func TestAddWhileRunning(t *testing.T) {
wg := &sync.WaitGroup{}
wg.Add(1)
cron := New()
cron.Start()
defer cron.Stop()
cron.AddFunc("* * * * * ?", func() { wg.Done() })
select {
case <-time.After(ONE_SECOND):
t.FailNow()
case <-wait(wg):
}
}
// Test timing with Entries.
func TestSnapshotEntries(t *testing.T) {
wg := &sync.WaitGroup{}
wg.Add(1)
cron := New()
cron.AddFunc("@every 2s", func() { wg.Done() })
cron.Start()
defer cron.Stop()
// Cron should fire in 2 seconds. After 1 second, call Entries.
select {
case <-time.After(ONE_SECOND):
cron.Entries()
}
// Even though Entries was called, the cron should fire at the 2 second mark.
select {
case <-time.After(ONE_SECOND):
t.FailNow()
case <-wait(wg):
}
}
// Test that the entries are correctly sorted.
// Add a bunch of long-in-the-future entries, and an immediate entry, and ensure
// that the immediate entry runs immediately.
// Also: Test that multiple jobs run in the same instant.
func TestMultipleEntries(t *testing.T) {
wg := &sync.WaitGroup{}
wg.Add(2)
cron := New()
cron.AddFunc("0 0 0 1 1 ?", func() {})
cron.AddFunc("* * * * * ?", func() { wg.Done() })
cron.AddFunc("0 0 0 31 12 ?", func() {})
cron.AddFunc("* * * * * ?", func() { wg.Done() })
cron.Start()
defer cron.Stop()
select {
case <-time.After(ONE_SECOND):
t.FailNow()
case <-wait(wg):
}
}
// Test running the same job twice.
func TestRunningJobTwice(t *testing.T) {
wg := &sync.WaitGroup{}
wg.Add(2)
cron := New()
cron.AddFunc("0 0 0 1 1 ?", func() {})
cron.AddFunc("0 0 0 31 12 ?", func() {})
cron.AddFunc("* * * * * ?", func() { wg.Done() })
cron.Start()
defer cron.Stop()
select {
case <-time.After(2 * ONE_SECOND):
t.FailNow()
case <-wait(wg):
}
}
func TestRunningMultipleSchedules(t *testing.T) {
wg := &sync.WaitGroup{}
wg.Add(2)
cron := New()
cron.AddFunc("0 0 0 1 1 ?", func() {})
cron.AddFunc("0 0 0 31 12 ?", func() {})
cron.AddFunc("* * * * * ?", func() { wg.Done() })
cron.Schedule(Every(time.Minute), FuncJob(func() {}))
cron.Schedule(Every(time.Second), FuncJob(func() { wg.Done() }))
cron.Schedule(Every(time.Hour), FuncJob(func() {}))
cron.Start()
defer cron.Stop()
select {
case <-time.After(2 * ONE_SECOND):
t.FailNow()
case <-wait(wg):
}
}
// Test that the cron is run in the local time zone (as opposed to UTC).
func TestLocalTimezone(t *testing.T) {
wg := &sync.WaitGroup{}
wg.Add(1)
now := time.Now().Local()
spec := fmt.Sprintf("%d %d %d %d %d ?",
now.Second()+1, now.Minute(), now.Hour(), now.Day(), now.Month())
cron := New()
cron.AddFunc(spec, func() { wg.Done() })
cron.Start()
defer cron.Stop()
select {
case <-time.After(ONE_SECOND):
t.FailNow()
case <-wait(wg):
}
}
type testJob struct {
wg *sync.WaitGroup
name string
}
func (t testJob) Run() {
t.wg.Done()
}
// Simple test using Runnables.
func TestJob(t *testing.T) {
wg := &sync.WaitGroup{}
wg.Add(1)
cron := New()
cron.AddJob("0 0 0 30 Feb ?", testJob{wg, "job0"})
cron.AddJob("0 0 0 1 1 ?", testJob{wg, "job1"})
cron.AddJob("* * * * * ?", testJob{wg, "job2"})
cron.AddJob("1 0 0 1 1 ?", testJob{wg, "job3"})
cron.Schedule(Every(5*time.Second+5*time.Nanosecond), testJob{wg, "job4"})
cron.Schedule(Every(5*time.Minute), testJob{wg, "job5"})
cron.Start()
defer cron.Stop()
select {
case <-time.After(ONE_SECOND):
t.FailNow()
case <-wait(wg):
}
// Ensure the entries are in the right order.
expecteds := []string{"job2", "job4", "job5", "job1", "job3", "job0"}
var actuals []string
for _, entry := range cron.Entries() {
actuals = append(actuals, entry.Job.(testJob).name)
}
for i, expected := range expecteds {
if actuals[i] != expected {
t.Errorf("Jobs not in the right order. (expected) %s != %s (actual)", expecteds, actuals)
t.FailNow()
}
}
}
func wait(wg *sync.WaitGroup) chan bool {
ch := make(chan bool)
go func() {
wg.Wait()
ch <- true
}()
return ch
}
func stop(cron *Cron) chan bool {
ch := make(chan bool)
go func() {
cron.Stop()
ch <- true
}()
return ch
}

129
vendor/src/github.com/robfig/cron/doc.go vendored Normal file
View File

@ -0,0 +1,129 @@
/*
Package cron implements a cron spec parser and job runner.
Usage
Callers may register Funcs to be invoked on a given schedule. Cron will run
them in their own goroutines.
c := cron.New()
c.AddFunc("0 30 * * * *", func() { fmt.Println("Every hour on the half hour") })
c.AddFunc("@hourly", func() { fmt.Println("Every hour") })
c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty") })
c.Start()
..
// Funcs are invoked in their own goroutine, asynchronously.
...
// Funcs may also be added to a running Cron
c.AddFunc("@daily", func() { fmt.Println("Every day") })
..
// Inspect the cron job entries' next and previous run times.
inspect(c.Entries())
..
c.Stop() // Stop the scheduler (does not stop any jobs already running).
CRON Expression Format
A cron expression represents a set of times, using 6 space-separated fields.
Field name | Mandatory? | Allowed values | Allowed special characters
---------- | ---------- | -------------- | --------------------------
Seconds | Yes | 0-59 | * / , -
Minutes | Yes | 0-59 | * / , -
Hours | Yes | 0-23 | * / , -
Day of month | Yes | 1-31 | * / , - ?
Month | Yes | 1-12 or JAN-DEC | * / , -
Day of week | Yes | 0-6 or SUN-SAT | * / , - ?
Note: Month and Day-of-week field values are case insensitive. "SUN", "Sun",
and "sun" are equally accepted.
Special Characters
Asterisk ( * )
The asterisk indicates that the cron expression will match for all values of the
field; e.g., using an asterisk in the 5th field (month) would indicate every
month.
Slash ( / )
Slashes are used to describe increments of ranges. For example 3-59/15 in the
1st field (minutes) would indicate the 3rd minute of the hour and every 15
minutes thereafter. The form "*\/..." is equivalent to the form "first-last/...",
that is, an increment over the largest possible range of the field. The form
"N/..." is accepted as meaning "N-MAX/...", that is, starting at N, use the
increment until the end of that specific range. It does not wrap around.
Comma ( , )
Commas are used to separate items of a list. For example, using "MON,WED,FRI" in
the 5th field (day of week) would mean Mondays, Wednesdays and Fridays.
Hyphen ( - )
Hyphens are used to define ranges. For example, 9-17 would indicate every
hour between 9am and 5pm inclusive.
Question mark ( ? )
Question mark may be used instead of '*' for leaving either day-of-month or
day-of-week blank.
Predefined schedules
You may use one of several pre-defined schedules in place of a cron expression.
Entry | Description | Equivalent To
----- | ----------- | -------------
@yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 *
@monthly | Run once a month, midnight, first of month | 0 0 0 1 * *
@weekly | Run once a week, midnight on Sunday | 0 0 0 * * 0
@daily (or @midnight) | Run once a day, midnight | 0 0 0 * * *
@hourly | Run once an hour, beginning of hour | 0 0 * * * *
Intervals
You may also schedule a job to execute at fixed intervals. This is supported by
formatting the cron spec like this:
@every <duration>
where "duration" is a string accepted by time.ParseDuration
(http://golang.org/pkg/time/#ParseDuration).
For example, "@every 1h30m10s" would indicate a schedule that activates every
1 hour, 30 minutes, 10 seconds.
Note: The interval does not take the job runtime into account. For example,
if a job takes 3 minutes to run, and it is scheduled to run every 5 minutes,
it will have only 2 minutes of idle time between each run.
Time zones
All interpretation and scheduling is done in the machine's local time zone (as
provided by the Go time package (http://www.golang.org/pkg/time).
Be aware that jobs scheduled during daylight-savings leap-ahead transitions will
not be run!
Thread safety
Since the Cron service runs concurrently with the calling code, some amount of
care must be taken to ensure proper synchronization.
All cron methods are designed to be correctly synchronized as long as the caller
ensures that invocations have a clear happens-before ordering between them.
Implementation
Cron entries are stored in an array, sorted by their next activation time. Cron
sleeps until the next job is due to be run.
Upon waking:
- it runs each entry that is active on that second
- it calculates the next run times for the jobs that were run
- it re-sorts the array of entries by next activation time.
- it goes to sleep until the soonest job.
*/
package cron

View File

@ -0,0 +1,231 @@
package cron
import (
"fmt"
"log"
"math"
"strconv"
"strings"
"time"
)
// Parse returns a new crontab schedule representing the given spec.
// It returns a descriptive error if the spec is not valid.
//
// It accepts
// - Full crontab specs, e.g. "* * * * * ?"
// - Descriptors, e.g. "@midnight", "@every 1h30m"
func Parse(spec string) (_ Schedule, err error) {
// Convert panics into errors
defer func() {
if recovered := recover(); recovered != nil {
err = fmt.Errorf("%v", recovered)
}
}()
if spec[0] == '@' {
return parseDescriptor(spec), nil
}
// Split on whitespace. We require 5 or 6 fields.
// (second) (minute) (hour) (day of month) (month) (day of week, optional)
fields := strings.Fields(spec)
if len(fields) != 5 && len(fields) != 6 {
log.Panicf("Expected 5 or 6 fields, found %d: %s", len(fields), spec)
}
// If a sixth field is not provided (DayOfWeek), then it is equivalent to star.
if len(fields) == 5 {
fields = append(fields, "*")
}
schedule := &SpecSchedule{
Second: getField(fields[0], seconds),
Minute: getField(fields[1], minutes),
Hour: getField(fields[2], hours),
Dom: getField(fields[3], dom),
Month: getField(fields[4], months),
Dow: getField(fields[5], dow),
}
return schedule, nil
}
// getField returns an Int with the bits set representing all of the times that
// the field represents. A "field" is a comma-separated list of "ranges".
func getField(field string, r bounds) uint64 {
// list = range {"," range}
var bits uint64
ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
for _, expr := range ranges {
bits |= getRange(expr, r)
}
return bits
}
// getRange returns the bits indicated by the given expression:
// number | number "-" number [ "/" number ]
func getRange(expr string, r bounds) uint64 {
var (
start, end, step uint
rangeAndStep = strings.Split(expr, "/")
lowAndHigh = strings.Split(rangeAndStep[0], "-")
singleDigit = len(lowAndHigh) == 1
)
var extra_star uint64
if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
start = r.min
end = r.max
extra_star = starBit
} else {
start = parseIntOrName(lowAndHigh[0], r.names)
switch len(lowAndHigh) {
case 1:
end = start
case 2:
end = parseIntOrName(lowAndHigh[1], r.names)
default:
log.Panicf("Too many hyphens: %s", expr)
}
}
switch len(rangeAndStep) {
case 1:
step = 1
case 2:
step = mustParseInt(rangeAndStep[1])
// Special handling: "N/step" means "N-max/step".
if singleDigit {
end = r.max
}
default:
log.Panicf("Too many slashes: %s", expr)
}
if start < r.min {
log.Panicf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
}
if end > r.max {
log.Panicf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
}
if start > end {
log.Panicf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
}
return getBits(start, end, step) | extra_star
}
// parseIntOrName returns the (possibly-named) integer contained in expr.
func parseIntOrName(expr string, names map[string]uint) uint {
if names != nil {
if namedInt, ok := names[strings.ToLower(expr)]; ok {
return namedInt
}
}
return mustParseInt(expr)
}
// mustParseInt parses the given expression as an int or panics.
func mustParseInt(expr string) uint {
num, err := strconv.Atoi(expr)
if err != nil {
log.Panicf("Failed to parse int from %s: %s", expr, err)
}
if num < 0 {
log.Panicf("Negative number (%d) not allowed: %s", num, expr)
}
return uint(num)
}
// getBits sets all bits in the range [min, max], modulo the given step size.
func getBits(min, max, step uint) uint64 {
var bits uint64
// If step is 1, use shifts.
if step == 1 {
return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)
}
// Else, use a simple loop.
for i := min; i <= max; i += step {
bits |= 1 << i
}
return bits
}
// all returns all bits within the given bounds. (plus the star bit)
func all(r bounds) uint64 {
return getBits(r.min, r.max, 1) | starBit
}
// parseDescriptor returns a pre-defined schedule for the expression, or panics
// if none matches.
func parseDescriptor(spec string) Schedule {
switch spec {
case "@yearly", "@annually":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: 1 << hours.min,
Dom: 1 << dom.min,
Month: 1 << months.min,
Dow: all(dow),
}
case "@monthly":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: 1 << hours.min,
Dom: 1 << dom.min,
Month: all(months),
Dow: all(dow),
}
case "@weekly":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: 1 << hours.min,
Dom: all(dom),
Month: all(months),
Dow: 1 << dow.min,
}
case "@daily", "@midnight":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: 1 << hours.min,
Dom: all(dom),
Month: all(months),
Dow: all(dow),
}
case "@hourly":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: all(hours),
Dom: all(dom),
Month: all(months),
Dow: all(dow),
}
}
const every = "@every "
if strings.HasPrefix(spec, every) {
duration, err := time.ParseDuration(spec[len(every):])
if err != nil {
log.Panicf("Failed to parse duration %s: %s", spec, err)
}
return Every(duration)
}
log.Panicf("Unrecognized descriptor: %s", spec)
return nil
}

View File

@ -0,0 +1,117 @@
package cron
import (
"reflect"
"testing"
"time"
)
func TestRange(t *testing.T) {
ranges := []struct {
expr string
min, max uint
expected uint64
}{
{"5", 0, 7, 1 << 5},
{"0", 0, 7, 1 << 0},
{"7", 0, 7, 1 << 7},
{"5-5", 0, 7, 1 << 5},
{"5-6", 0, 7, 1<<5 | 1<<6},
{"5-7", 0, 7, 1<<5 | 1<<6 | 1<<7},
{"5-6/2", 0, 7, 1 << 5},
{"5-7/2", 0, 7, 1<<5 | 1<<7},
{"5-7/1", 0, 7, 1<<5 | 1<<6 | 1<<7},
{"*", 1, 3, 1<<1 | 1<<2 | 1<<3 | starBit},
{"*/2", 1, 3, 1<<1 | 1<<3 | starBit},
}
for _, c := range ranges {
actual := getRange(c.expr, bounds{c.min, c.max, nil})
if actual != c.expected {
t.Errorf("%s => (expected) %d != %d (actual)", c.expr, c.expected, actual)
}
}
}
func TestField(t *testing.T) {
fields := []struct {
expr string
min, max uint
expected uint64
}{
{"5", 1, 7, 1 << 5},
{"5,6", 1, 7, 1<<5 | 1<<6},
{"5,6,7", 1, 7, 1<<5 | 1<<6 | 1<<7},
{"1,5-7/2,3", 1, 7, 1<<1 | 1<<5 | 1<<7 | 1<<3},
}
for _, c := range fields {
actual := getField(c.expr, bounds{c.min, c.max, nil})
if actual != c.expected {
t.Errorf("%s => (expected) %d != %d (actual)", c.expr, c.expected, actual)
}
}
}
func TestBits(t *testing.T) {
allBits := []struct {
r bounds
expected uint64
}{
{minutes, 0xfffffffffffffff}, // 0-59: 60 ones
{hours, 0xffffff}, // 0-23: 24 ones
{dom, 0xfffffffe}, // 1-31: 31 ones, 1 zero
{months, 0x1ffe}, // 1-12: 12 ones, 1 zero
{dow, 0x7f}, // 0-6: 7 ones
}
for _, c := range allBits {
actual := all(c.r) // all() adds the starBit, so compensate for that..
if c.expected|starBit != actual {
t.Errorf("%d-%d/%d => (expected) %b != %b (actual)",
c.r.min, c.r.max, 1, c.expected|starBit, actual)
}
}
bits := []struct {
min, max, step uint
expected uint64
}{
{0, 0, 1, 0x1},
{1, 1, 1, 0x2},
{1, 5, 2, 0x2a}, // 101010
{1, 4, 2, 0xa}, // 1010
}
for _, c := range bits {
actual := getBits(c.min, c.max, c.step)
if c.expected != actual {
t.Errorf("%d-%d/%d => (expected) %b != %b (actual)",
c.min, c.max, c.step, c.expected, actual)
}
}
}
func TestSpecSchedule(t *testing.T) {
entries := []struct {
expr string
expected Schedule
}{
{"* 5 * * * *", &SpecSchedule{all(seconds), 1 << 5, all(hours), all(dom), all(months), all(dow)}},
{"@every 5m", ConstantDelaySchedule{time.Duration(5) * time.Minute}},
}
for _, c := range entries {
actual, err := Parse(c.expr)
if err != nil {
t.Error(err)
}
if !reflect.DeepEqual(actual, c.expected) {
t.Errorf("%s => (expected) %b != %b (actual)", c.expr, c.expected, actual)
}
}
}

View File

@ -0,0 +1,159 @@
package cron
import "time"
// SpecSchedule specifies a duty cycle (to the second granularity), based on a
// traditional crontab specification. It is computed initially and stored as bit sets.
type SpecSchedule struct {
Second, Minute, Hour, Dom, Month, Dow uint64
}
// bounds provides a range of acceptable values (plus a map of name to value).
type bounds struct {
min, max uint
names map[string]uint
}
// The bounds for each field.
var (
seconds = bounds{0, 59, nil}
minutes = bounds{0, 59, nil}
hours = bounds{0, 23, nil}
dom = bounds{1, 31, nil}
months = bounds{1, 12, map[string]uint{
"jan": 1,
"feb": 2,
"mar": 3,
"apr": 4,
"may": 5,
"jun": 6,
"jul": 7,
"aug": 8,
"sep": 9,
"oct": 10,
"nov": 11,
"dec": 12,
}}
dow = bounds{0, 6, map[string]uint{
"sun": 0,
"mon": 1,
"tue": 2,
"wed": 3,
"thu": 4,
"fri": 5,
"sat": 6,
}}
)
const (
// Set the top bit if a star was included in the expression.
starBit = 1 << 63
)
// Next returns the next time this schedule is activated, greater than the given
// time. If no time can be found to satisfy the schedule, return the zero time.
func (s *SpecSchedule) Next(t time.Time) time.Time {
// General approach:
// For Month, Day, Hour, Minute, Second:
// Check if the time value matches. If yes, continue to the next field.
// If the field doesn't match the schedule, then increment the field until it matches.
// While incrementing the field, a wrap-around brings it back to the beginning
// of the field list (since it is necessary to re-verify previous field
// values)
// Start at the earliest possible time (the upcoming second).
t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond)
// This flag indicates whether a field has been incremented.
added := false
// If no time is found within five years, return zero.
yearLimit := t.Year() + 5
WRAP:
if t.Year() > yearLimit {
return time.Time{}
}
// Find the first applicable month.
// If it's this month, then do nothing.
for 1<<uint(t.Month())&s.Month == 0 {
// If we have to add a month, reset the other parts to 0.
if !added {
added = true
// Otherwise, set the date at the beginning (since the current time is irrelevant).
t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())
}
t = t.AddDate(0, 1, 0)
// Wrapped around.
if t.Month() == time.January {
goto WRAP
}
}
// Now get a day in that month.
for !dayMatches(s, t) {
if !added {
added = true
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
}
t = t.AddDate(0, 0, 1)
if t.Day() == 1 {
goto WRAP
}
}
for 1<<uint(t.Hour())&s.Hour == 0 {
if !added {
added = true
t = t.Truncate(time.Hour)
}
t = t.Add(1 * time.Hour)
if t.Hour() == 0 {
goto WRAP
}
}
for 1<<uint(t.Minute())&s.Minute == 0 {
if !added {
added = true
t = t.Truncate(time.Minute)
}
t = t.Add(1 * time.Minute)
if t.Minute() == 0 {
goto WRAP
}
}
for 1<<uint(t.Second())&s.Second == 0 {
if !added {
added = true
t = t.Truncate(time.Second)
}
t = t.Add(1 * time.Second)
if t.Second() == 0 {
goto WRAP
}
}
return t
}
// dayMatches returns true if the schedule's day-of-week and day-of-month
// restrictions are satisfied by the given time.
func dayMatches(s *SpecSchedule, t time.Time) bool {
var (
domMatch bool = 1<<uint(t.Day())&s.Dom > 0
dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0
)
if s.Dom&starBit > 0 || s.Dow&starBit > 0 {
return domMatch && dowMatch
}
return domMatch || dowMatch
}

View File

@ -0,0 +1,204 @@
package cron
import (
"testing"
"time"
)
func TestActivation(t *testing.T) {
tests := []struct {
time, spec string
expected bool
}{
// Every fifteen minutes.
{"Mon Jul 9 15:00 2012", "0 0/15 * * *", true},
{"Mon Jul 9 15:45 2012", "0 0/15 * * *", true},
{"Mon Jul 9 15:40 2012", "0 0/15 * * *", false},
// Every fifteen minutes, starting at 5 minutes.
{"Mon Jul 9 15:05 2012", "0 5/15 * * *", true},
{"Mon Jul 9 15:20 2012", "0 5/15 * * *", true},
{"Mon Jul 9 15:50 2012", "0 5/15 * * *", true},
// Named months
{"Sun Jul 15 15:00 2012", "0 0/15 * * Jul", true},
{"Sun Jul 15 15:00 2012", "0 0/15 * * Jun", false},
// Everything set.
{"Sun Jul 15 08:30 2012", "0 30 08 ? Jul Sun", true},
{"Sun Jul 15 08:30 2012", "0 30 08 15 Jul ?", true},
{"Mon Jul 16 08:30 2012", "0 30 08 ? Jul Sun", false},
{"Mon Jul 16 08:30 2012", "0 30 08 15 Jul ?", false},
// Predefined schedules
{"Mon Jul 9 15:00 2012", "@hourly", true},
{"Mon Jul 9 15:04 2012", "@hourly", false},
{"Mon Jul 9 15:00 2012", "@daily", false},
{"Mon Jul 9 00:00 2012", "@daily", true},
{"Mon Jul 9 00:00 2012", "@weekly", false},
{"Sun Jul 8 00:00 2012", "@weekly", true},
{"Sun Jul 8 01:00 2012", "@weekly", false},
{"Sun Jul 8 00:00 2012", "@monthly", false},
{"Sun Jul 1 00:00 2012", "@monthly", true},
// Test interaction of DOW and DOM.
// If both are specified, then only one needs to match.
{"Sun Jul 15 00:00 2012", "0 * * 1,15 * Sun", true},
{"Fri Jun 15 00:00 2012", "0 * * 1,15 * Sun", true},
{"Wed Aug 1 00:00 2012", "0 * * 1,15 * Sun", true},
// However, if one has a star, then both need to match.
{"Sun Jul 15 00:00 2012", "0 * * * * Mon", false},
{"Sun Jul 15 00:00 2012", "0 * * */10 * Sun", false},
{"Mon Jul 9 00:00 2012", "0 * * 1,15 * *", false},
{"Sun Jul 15 00:00 2012", "0 * * 1,15 * *", true},
{"Sun Jul 15 00:00 2012", "0 * * */2 * Sun", true},
}
for _, test := range tests {
sched, err := Parse(test.spec)
if err != nil {
t.Error(err)
continue
}
actual := sched.Next(getTime(test.time).Add(-1 * time.Second))
expected := getTime(test.time)
if test.expected && expected != actual || !test.expected && expected == actual {
t.Errorf("Fail evaluating %s on %s: (expected) %s != %s (actual)",
test.spec, test.time, expected, actual)
}
}
}
func TestNext(t *testing.T) {
runs := []struct {
time, spec string
expected string
}{
// Simple cases
{"Mon Jul 9 14:45 2012", "0 0/15 * * *", "Mon Jul 9 15:00 2012"},
{"Mon Jul 9 14:59 2012", "0 0/15 * * *", "Mon Jul 9 15:00 2012"},
{"Mon Jul 9 14:59:59 2012", "0 0/15 * * *", "Mon Jul 9 15:00 2012"},
// Wrap around hours
{"Mon Jul 9 15:45 2012", "0 20-35/15 * * *", "Mon Jul 9 16:20 2012"},
// Wrap around days
{"Mon Jul 9 23:46 2012", "0 */15 * * *", "Tue Jul 10 00:00 2012"},
{"Mon Jul 9 23:45 2012", "0 20-35/15 * * *", "Tue Jul 10 00:20 2012"},
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * * *", "Tue Jul 10 00:20:15 2012"},
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 * *", "Tue Jul 10 01:20:15 2012"},
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 10-12 * *", "Tue Jul 10 10:20:15 2012"},
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 */2 * *", "Thu Jul 11 01:20:15 2012"},
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 9-20 * *", "Wed Jul 10 00:20:15 2012"},
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 9-20 Jul *", "Wed Jul 10 00:20:15 2012"},
// Wrap around months
{"Mon Jul 9 23:35 2012", "0 0 0 9 Apr-Oct ?", "Thu Aug 9 00:00 2012"},
{"Mon Jul 9 23:35 2012", "0 0 0 */5 Apr,Aug,Oct Mon", "Mon Aug 6 00:00 2012"},
{"Mon Jul 9 23:35 2012", "0 0 0 */5 Oct Mon", "Mon Oct 1 00:00 2012"},
// Wrap around years
{"Mon Jul 9 23:35 2012", "0 0 0 * Feb Mon", "Mon Feb 4 00:00 2013"},
{"Mon Jul 9 23:35 2012", "0 0 0 * Feb Mon/2", "Fri Feb 1 00:00 2013"},
// Wrap around minute, hour, day, month, and year
{"Mon Dec 31 23:59:45 2012", "0 * * * * *", "Tue Jan 1 00:00:00 2013"},
// Leap year
{"Mon Jul 9 23:35 2012", "0 0 0 29 Feb ?", "Mon Feb 29 00:00 2016"},
// Daylight savings time 2am EST (-5) -> 3am EDT (-4)
{"2012-03-11T00:00:00-0500", "0 30 2 11 Mar ?", "2013-03-11T02:30:00-0400"},
// hourly job
{"2012-03-11T00:00:00-0500", "0 0 * * * ?", "2012-03-11T01:00:00-0500"},
{"2012-03-11T01:00:00-0500", "0 0 * * * ?", "2012-03-11T03:00:00-0400"},
{"2012-03-11T03:00:00-0400", "0 0 * * * ?", "2012-03-11T04:00:00-0400"},
{"2012-03-11T04:00:00-0400", "0 0 * * * ?", "2012-03-11T05:00:00-0400"},
// 1am nightly job
{"2012-03-11T00:00:00-0500", "0 0 1 * * ?", "2012-03-11T01:00:00-0500"},
{"2012-03-11T01:00:00-0500", "0 0 1 * * ?", "2012-03-12T01:00:00-0400"},
// 2am nightly job (skipped)
{"2012-03-11T00:00:00-0500", "0 0 2 * * ?", "2012-03-12T02:00:00-0400"},
// Daylight savings time 2am EDT (-4) => 1am EST (-5)
{"2012-11-04T00:00:00-0400", "0 30 2 04 Nov ?", "2012-11-04T02:30:00-0500"},
{"2012-11-04T01:45:00-0400", "0 30 1 04 Nov ?", "2012-11-04T01:30:00-0500"},
// hourly job
{"2012-11-04T00:00:00-0400", "0 0 * * * ?", "2012-11-04T01:00:00-0400"},
{"2012-11-04T01:00:00-0400", "0 0 * * * ?", "2012-11-04T01:00:00-0500"},
{"2012-11-04T01:00:00-0500", "0 0 * * * ?", "2012-11-04T02:00:00-0500"},
// 1am nightly job (runs twice)
{"2012-11-04T00:00:00-0400", "0 0 1 * * ?", "2012-11-04T01:00:00-0400"},
{"2012-11-04T01:00:00-0400", "0 0 1 * * ?", "2012-11-04T01:00:00-0500"},
{"2012-11-04T01:00:00-0500", "0 0 1 * * ?", "2012-11-05T01:00:00-0500"},
// 2am nightly job
{"2012-11-04T00:00:00-0400", "0 0 2 * * ?", "2012-11-04T02:00:00-0500"},
{"2012-11-04T02:00:00-0500", "0 0 2 * * ?", "2012-11-05T02:00:00-0500"},
// 3am nightly job
{"2012-11-04T00:00:00-0400", "0 0 3 * * ?", "2012-11-04T03:00:00-0500"},
{"2012-11-04T03:00:00-0500", "0 0 3 * * ?", "2012-11-05T03:00:00-0500"},
// Unsatisfiable
{"Mon Jul 9 23:35 2012", "0 0 0 30 Feb ?", ""},
{"Mon Jul 9 23:35 2012", "0 0 0 31 Apr ?", ""},
}
for _, c := range runs {
sched, err := Parse(c.spec)
if err != nil {
t.Error(err)
continue
}
actual := sched.Next(getTime(c.time))
expected := getTime(c.expected)
if !actual.Equal(expected) {
t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.spec, expected, actual)
}
}
}
func TestErrors(t *testing.T) {
invalidSpecs := []string{
"xyz",
"60 0 * * *",
"0 60 * * *",
"0 0 * * XYZ",
}
for _, spec := range invalidSpecs {
_, err := Parse(spec)
if err == nil {
t.Error("expected an error parsing: ", spec)
}
}
}
func getTime(value string) time.Time {
if value == "" {
return time.Time{}
}
t, err := time.Parse("Mon Jan 2 15:04 2006", value)
if err != nil {
t, err = time.Parse("Mon Jan 2 15:04:05 2006", value)
if err != nil {
t, err = time.Parse("2006-01-02T15:04:05-0700", value)
if err != nil {
panic(err)
}
// Daylight savings time tests require location
if ny, err := time.LoadLocation("America/New_York"); err == nil {
t = t.In(ny)
}
}
}
return t
}

View File

@ -0,0 +1,57 @@
Copyright (c) 2012 Péter Surányi. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
----------------------------------------------------------------------
Portions of gcfg's source code have been derived from Go, and are
covered by the following license:
----------------------------------------------------------------------
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,7 @@
Gcfg reads INI-style configuration files into Go structs;
supports user-defined types and subsections.
Project page: https://code.google.com/p/gcfg
Package docs: http://godoc.org/code.google.com/p/gcfg
My other projects: https://speter.net

View File

@ -0,0 +1,118 @@
// Package gcfg reads "INI-style" text-based configuration files with
// "name=value" pairs grouped into sections (gcfg files).
//
// This package is still a work in progress; see the sections below for planned
// changes.
//
// Syntax
//
// The syntax is based on that used by git config:
// http://git-scm.com/docs/git-config#_syntax .
// There are some (planned) differences compared to the git config format:
// - improve data portability:
// - must be encoded in UTF-8 (for now) and must not contain the 0 byte
// - include and "path" type is not supported
// (path type may be implementable as a user-defined type)
// - internationalization
// - section and variable names can contain unicode letters, unicode digits
// (as defined in http://golang.org/ref/spec#Characters ) and hyphens
// (U+002D), starting with a unicode letter
// - disallow potentially ambiguous or misleading definitions:
// - `[sec.sub]` format is not allowed (deprecated in gitconfig)
// - `[sec ""]` is not allowed
// - use `[sec]` for section name "sec" and empty subsection name
// - (planned) within a single file, definitions must be contiguous for each:
// - section: '[secA]' -> '[secB]' -> '[secA]' is an error
// - subsection: '[sec "A"]' -> '[sec "B"]' -> '[sec "A"]' is an error
// - multivalued variable: 'multi=a' -> 'other=x' -> 'multi=b' is an error
//
// Data structure
//
// The functions in this package read values into a user-defined struct.
// Each section corresponds to a struct field in the config struct, and each
// variable in a section corresponds to a data field in the section struct.
// The mapping of each section or variable name to fields is done either based
// on the "gcfg" struct tag or by matching the name of the section or variable,
// ignoring case. In the latter case, hyphens '-' in section and variable names
// correspond to underscores '_' in field names.
// Fields must be exported; to use a section or variable name starting with a
// letter that is neither upper- or lower-case, prefix the field name with 'X'.
// (See https://code.google.com/p/go/issues/detail?id=5763#c4 .)
//
// For sections with subsections, the corresponding field in config must be a
// map, rather than a struct, with string keys and pointer-to-struct values.
// Values for subsection variables are stored in the map with the subsection
// name used as the map key.
// (Note that unlike section and variable names, subsection names are case
// sensitive.)
// When using a map, and there is a section with the same section name but
// without a subsection name, its values are stored with the empty string used
// as the key.
//
// The functions in this package panic if config is not a pointer to a struct,
// or when a field is not of a suitable type (either a struct or a map with
// string keys and pointer-to-struct values).
//
// Parsing of values
//
// The section structs in the config struct may contain single-valued or
// multi-valued variables. Variables of unnamed slice type (that is, a type
// starting with `[]`) are treated as multi-value; all others (including named
// slice types) are treated as single-valued variables.
//
// Single-valued variables are handled based on the type as follows.
// Unnamed pointer types (that is, types starting with `*`) are dereferenced,
// and if necessary, a new instance is allocated.
//
// For types implementing the encoding.TextUnmarshaler interface, the
// UnmarshalText method is used to set the value. Implementing this method is
// the recommended way for parsing user-defined types.
//
// For fields of string kind, the value string is assigned to the field, after
// unquoting and unescaping as needed.
// For fields of bool kind, the field is set to true if the value is "true",
// "yes", "on" or "1", and set to false if the value is "false", "no", "off" or
// "0", ignoring case. In addition, single-valued bool fields can be specified
// with a "blank" value (variable name without equals sign and value); in such
// case the value is set to true.
//
// Predefined integer types [u]int(|8|16|32|64) and big.Int are parsed as
// decimal or hexadecimal (if having '0x' prefix). (This is to prevent
// unintuitively handling zero-padded numbers as octal.) Other types having
// [u]int* as the underlying type, such as os.FileMode and uintptr allow
// decimal, hexadecimal, or octal values.
// Parsing mode for integer types can be overridden using the struct tag option
// ",int=mode" where mode is a combination of the 'd', 'h', and 'o' characters
// (each standing for decimal, hexadecimal, and octal, respectively.)
//
// All other types are parsed using fmt.Sscanf with the "%v" verb.
//
// For multi-valued variables, each individual value is parsed as above and
// appended to the slice. If the first value is specified as a "blank" value
// (variable name without equals sign and value), a new slice is allocated;
// that is any values previously set in the slice will be ignored.
//
// The types subpackage for provides helpers for parsing "enum-like" and integer
// types.
//
// TODO
//
// The following is a list of changes under consideration:
// - documentation
// - self-contained syntax documentation
// - more practical examples
// - move TODOs to issue tracker (eventually)
// - syntax
// - reconsider valid escape sequences
// (gitconfig doesn't support \r in value, \t in subsection name, etc.)
// - reading / parsing gcfg files
// - define internal representation structure
// - support multiple inputs (readers, strings, files)
// - support declaring encoding (?)
// - support varying fields sets for subsections (?)
// - writing gcfg files
// - error handling
// - make error context accessible programmatically?
// - limit input size?
//
package gcfg

View File

@ -0,0 +1,132 @@
package gcfg_test
import (
"fmt"
"log"
)
import "github.com/scalingdata/gcfg"
func ExampleReadStringInto() {
cfgStr := `; Comment line
[section]
name=value # comment`
cfg := struct {
Section struct {
Name string
}
}{}
err := gcfg.ReadStringInto(&cfg, cfgStr)
if err != nil {
log.Fatalf("Failed to parse gcfg data: %s", err)
}
fmt.Println(cfg.Section.Name)
// Output: value
}
func ExampleReadStringInto_bool() {
cfgStr := `; Comment line
[section]
switch=on`
cfg := struct {
Section struct {
Switch bool
}
}{}
err := gcfg.ReadStringInto(&cfg, cfgStr)
if err != nil {
log.Fatalf("Failed to parse gcfg data: %s", err)
}
fmt.Println(cfg.Section.Switch)
// Output: true
}
func ExampleReadStringInto_hyphens() {
cfgStr := `; Comment line
[section-name]
variable-name=value # comment`
cfg := struct {
Section_Name struct {
Variable_Name string
}
}{}
err := gcfg.ReadStringInto(&cfg, cfgStr)
if err != nil {
log.Fatalf("Failed to parse gcfg data: %s", err)
}
fmt.Println(cfg.Section_Name.Variable_Name)
// Output: value
}
func ExampleReadStringInto_tags() {
cfgStr := `; Comment line
[section]
var-name=value # comment`
cfg := struct {
Section struct {
FieldName string `gcfg:"var-name"`
}
}{}
err := gcfg.ReadStringInto(&cfg, cfgStr)
if err != nil {
log.Fatalf("Failed to parse gcfg data: %s", err)
}
fmt.Println(cfg.Section.FieldName)
// Output: value
}
func ExampleReadStringInto_subsections() {
cfgStr := `; Comment line
[profile "A"]
color = white
[profile "B"]
color = black
`
cfg := struct {
Profile map[string]*struct {
Color string
}
}{}
err := gcfg.ReadStringInto(&cfg, cfgStr)
if err != nil {
log.Fatalf("Failed to parse gcfg data: %s", err)
}
fmt.Printf("%s %s\n", cfg.Profile["A"].Color, cfg.Profile["B"].Color)
// Output: white black
}
func ExampleReadStringInto_multivalue() {
cfgStr := `; Comment line
[section]
multi=value1
multi=value2`
cfg := struct {
Section struct {
Multi []string
}
}{}
err := gcfg.ReadStringInto(&cfg, cfgStr)
if err != nil {
log.Fatalf("Failed to parse gcfg data: %s", err)
}
fmt.Println(cfg.Section.Multi)
// Output: [value1 value2]
}
func ExampleReadStringInto_unicode() {
cfgStr := `; Comment line
[]
= # comment`
cfg := struct {
X甲 struct {
X乙 string
}
}{}
err := gcfg.ReadStringInto(&cfg, cfgStr)
if err != nil {
log.Fatalf("Failed to parse gcfg data: %s", err)
}
fmt.Println(cfg.X甲.X乙)
// Output: 丙
}

View File

@ -0,0 +1,7 @@
// +build !go1.2
package gcfg
type textUnmarshaler interface {
UnmarshalText(text []byte) error
}

View File

@ -0,0 +1,9 @@
// +build go1.2
package gcfg
import (
"encoding"
)
type textUnmarshaler encoding.TextUnmarshaler

View File

@ -0,0 +1,63 @@
package gcfg
import (
"fmt"
"math/big"
"strings"
"testing"
)
type Config1 struct {
Section struct {
Int int
BigInt big.Int
}
}
var testsIssue1 = []struct {
cfg string
typename string
}{
{"[section]\nint=X", "int"},
{"[section]\nint=", "int"},
{"[section]\nint=1A", "int"},
{"[section]\nbigint=X", "big.Int"},
{"[section]\nbigint=", "big.Int"},
{"[section]\nbigint=1A", "big.Int"},
}
// Value parse error should:
// - include plain type name
// - not include reflect internals
func TestIssue1(t *testing.T) {
for i, tt := range testsIssue1 {
var c Config1
err := ReadStringInto(&c, tt.cfg)
switch {
case err == nil:
t.Errorf("%d fail: got ok; wanted error", i)
case !strings.Contains(err.Error(), tt.typename):
t.Errorf("%d fail: error message doesn't contain type name %q: %v",
i, tt.typename, err)
case strings.Contains(err.Error(), "reflect"):
t.Errorf("%d fail: error message includes reflect internals: %v",
i, err)
default:
t.Logf("%d pass: %v", i, err)
}
}
}
type confIssue2 struct{ Main struct{ Foo string } }
var testsIssue2 = []readtest{
{"[main]\n;\nfoo = bar\n", &confIssue2{struct{ Foo string }{"bar"}}, true},
{"[main]\r\n;\r\nfoo = bar\r\n", &confIssue2{struct{ Foo string }{"bar"}}, true},
}
func TestIssue2(t *testing.T) {
for i, tt := range testsIssue2 {
id := fmt.Sprintf("issue2:%d", i)
testRead(t, id, tt)
}
}

View File

@ -0,0 +1,181 @@
package gcfg
import (
"fmt"
"io"
"io/ioutil"
"os"
"strings"
)
import (
"github.com/scalingdata/gcfg/scanner"
"github.com/scalingdata/gcfg/token"
)
var unescape = map[rune]rune{'\\': '\\', '"': '"', 'n': '\n', 't': '\t'}
// no error: invalid literals should be caught by scanner
func unquote(s string) string {
u, q, esc := make([]rune, 0, len(s)), false, false
for _, c := range s {
if esc {
uc, ok := unescape[c]
switch {
case ok:
u = append(u, uc)
fallthrough
case !q && c == '\n':
esc = false
continue
}
panic("invalid escape sequence")
}
switch c {
case '"':
q = !q
case '\\':
esc = true
default:
u = append(u, c)
}
}
if q {
panic("missing end quote")
}
if esc {
panic("invalid escape sequence")
}
return string(u)
}
func readInto(config interface{}, fset *token.FileSet, file *token.File, src []byte) error {
var s scanner.Scanner
var errs scanner.ErrorList
s.Init(file, src, func(p token.Position, m string) { errs.Add(p, m) }, 0)
sect, sectsub := "", ""
pos, tok, lit := s.Scan()
errfn := func(msg string) error {
return fmt.Errorf("%s: %s", fset.Position(pos), msg)
}
for {
if errs.Len() > 0 {
return errs.Err()
}
switch tok {
case token.EOF:
return nil
case token.EOL, token.COMMENT:
pos, tok, lit = s.Scan()
case token.LBRACK:
pos, tok, lit = s.Scan()
if errs.Len() > 0 {
return errs.Err()
}
if tok != token.IDENT {
return errfn("expected section name")
}
sect, sectsub = lit, ""
pos, tok, lit = s.Scan()
if errs.Len() > 0 {
return errs.Err()
}
if tok == token.STRING {
sectsub = unquote(lit)
if sectsub == "" {
return errfn("empty subsection name")
}
pos, tok, lit = s.Scan()
if errs.Len() > 0 {
return errs.Err()
}
}
if tok != token.RBRACK {
if sectsub == "" {
return errfn("expected subsection name or right bracket")
}
return errfn("expected right bracket")
}
pos, tok, lit = s.Scan()
if tok != token.EOL && tok != token.EOF && tok != token.COMMENT {
return errfn("expected EOL, EOF, or comment")
}
case token.IDENT:
if sect == "" {
return errfn("expected section header")
}
n := lit
pos, tok, lit = s.Scan()
if errs.Len() > 0 {
return errs.Err()
}
blank, v := tok == token.EOF || tok == token.EOL || tok == token.COMMENT, ""
if !blank {
if tok != token.ASSIGN {
return errfn("expected '='")
}
pos, tok, lit = s.Scan()
if errs.Len() > 0 {
return errs.Err()
}
if tok != token.STRING {
return errfn("expected value")
}
v = unquote(lit)
pos, tok, lit = s.Scan()
if errs.Len() > 0 {
return errs.Err()
}
if tok != token.EOL && tok != token.EOF && tok != token.COMMENT {
return errfn("expected EOL, EOF, or comment")
}
}
err := set(config, sect, sectsub, n, blank, v)
if err != nil {
return err
}
default:
if sect == "" {
return errfn("expected section header")
}
return errfn("expected section header or variable declaration")
}
}
panic("never reached")
}
// ReadInto reads gcfg formatted data from reader and sets the values into the
// corresponding fields in config.
func ReadInto(config interface{}, reader io.Reader) error {
src, err := ioutil.ReadAll(reader)
if err != nil {
return err
}
fset := token.NewFileSet()
file := fset.AddFile("", fset.Base(), len(src))
return readInto(config, fset, file, src)
}
// ReadStringInto reads gcfg formatted data from str and sets the values into
// the corresponding fields in config.
func ReadStringInto(config interface{}, str string) error {
r := strings.NewReader(str)
return ReadInto(config, r)
}
// ReadFileInto reads gcfg formatted data from the file filename and sets the
// values into the corresponding fields in config.
func ReadFileInto(config interface{}, filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
src, err := ioutil.ReadAll(f)
if err != nil {
return err
}
fset := token.NewFileSet()
file := fset.AddFile(filename, fset.Base(), len(src))
return readInto(config, fset, file, src)
}

View File

@ -0,0 +1,333 @@
package gcfg
import (
"fmt"
"math/big"
"os"
"reflect"
"testing"
)
const (
// 64 spaces
sp64 = " "
// 512 spaces
sp512 = sp64 + sp64 + sp64 + sp64 + sp64 + sp64 + sp64 + sp64
// 4096 spaces
sp4096 = sp512 + sp512 + sp512 + sp512 + sp512 + sp512 + sp512 + sp512
)
type cBasic struct {
Section cBasicS1
Hyphen_In_Section cBasicS2
unexported cBasicS1
Exported cBasicS3
TagName cBasicS1 `gcfg:"tag-name"`
}
type cBasicS1 struct {
Name string
Int int
PName *string
}
type cBasicS2 struct {
Hyphen_In_Name string
}
type cBasicS3 struct {
unexported string
}
type nonMulti []string
type unmarshalable string
func (u *unmarshalable) UnmarshalText(text []byte) error {
s := string(text)
if s == "error" {
return fmt.Errorf("%s", s)
}
*u = unmarshalable(s)
return nil
}
var _ textUnmarshaler = new(unmarshalable)
type cUni struct {
X甲 cUniS1
XSection cUniS2
}
type cUniS1 struct {
X乙 string
}
type cUniS2 struct {
XName string
}
type cMulti struct {
M1 cMultiS1
M2 cMultiS2
M3 cMultiS3
}
type cMultiS1 struct{ Multi []string }
type cMultiS2 struct{ NonMulti nonMulti }
type cMultiS3 struct{ MultiInt []int }
type cSubs struct{ Sub map[string]*cSubsS1 }
type cSubsS1 struct{ Name string }
type cBool struct{ Section cBoolS1 }
type cBoolS1 struct{ Bool bool }
type cTxUnm struct{ Section cTxUnmS1 }
type cTxUnmS1 struct{ Name unmarshalable }
type cNum struct {
N1 cNumS1
N2 cNumS2
N3 cNumS3
}
type cNumS1 struct {
Int int
IntDHO int `gcfg:",int=dho"`
Big *big.Int
}
type cNumS2 struct {
MultiInt []int
MultiBig []*big.Int
}
type cNumS3 struct{ FileMode os.FileMode }
type readtest struct {
gcfg string
exp interface{}
ok bool
}
func newString(s string) *string {
return &s
}
var readtests = []struct {
group string
tests []readtest
}{{"scanning", []readtest{
{"[section]\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
// hyphen in name
{"[hyphen-in-section]\nhyphen-in-name=value", &cBasic{Hyphen_In_Section: cBasicS2{Hyphen_In_Name: "value"}}, true},
// quoted string value
{"[section]\nname=\"\"", &cBasic{Section: cBasicS1{Name: ""}}, true},
{"[section]\nname=\" \"", &cBasic{Section: cBasicS1{Name: " "}}, true},
{"[section]\nname=\"value\"", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"[section]\nname=\" value \"", &cBasic{Section: cBasicS1{Name: " value "}}, true},
{"\n[section]\nname=\"va ; lue\"", &cBasic{Section: cBasicS1{Name: "va ; lue"}}, true},
{"[section]\nname=\"val\" \"ue\"", &cBasic{Section: cBasicS1{Name: "val ue"}}, true},
{"[section]\nname=\"value", &cBasic{}, false},
// escape sequences
{"[section]\nname=\"va\\\\lue\"", &cBasic{Section: cBasicS1{Name: "va\\lue"}}, true},
{"[section]\nname=\"va\\\"lue\"", &cBasic{Section: cBasicS1{Name: "va\"lue"}}, true},
{"[section]\nname=\"va\\nlue\"", &cBasic{Section: cBasicS1{Name: "va\nlue"}}, true},
{"[section]\nname=\"va\\tlue\"", &cBasic{Section: cBasicS1{Name: "va\tlue"}}, true},
{"\n[section]\nname=\\", &cBasic{}, false},
{"\n[section]\nname=\\a", &cBasic{}, false},
{"\n[section]\nname=\"val\\a\"", &cBasic{}, false},
{"\n[section]\nname=val\\", &cBasic{}, false},
{"\n[sub \"A\\\n\"]\nname=value", &cSubs{}, false},
{"\n[sub \"A\\\t\"]\nname=value", &cSubs{}, false},
// broken line
{"[section]\nname=value \\\n value", &cBasic{Section: cBasicS1{Name: "value value"}}, true},
{"[section]\nname=\"value \\\n value\"", &cBasic{}, false},
}}, {"scanning:whitespace", []readtest{
{" \n[section]\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{" [section]\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"\t[section]\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"[ section]\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"[section ]\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"[section]\n name=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"[section]\nname =value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"[section]\nname= value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"[section]\nname=value ", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"[section]\r\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"[section]\r\nname=value\r\n", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{";cmnt\r\n[section]\r\nname=value\r\n", &cBasic{Section: cBasicS1{Name: "value"}}, true},
// long lines
{sp4096 + "[section]\nname=value\n", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"[" + sp4096 + "section]\nname=value\n", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"[section" + sp4096 + "]\nname=value\n", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"[section]" + sp4096 + "\nname=value\n", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"[section]\n" + sp4096 + "name=value\n", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"[section]\nname" + sp4096 + "=value\n", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"[section]\nname=" + sp4096 + "value\n", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"[section]\nname=value\n" + sp4096, &cBasic{Section: cBasicS1{Name: "value"}}, true},
}}, {"scanning:comments", []readtest{
{"; cmnt\n[section]\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"# cmnt\n[section]\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{" ; cmnt\n[section]\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"\t; cmnt\n[section]\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"\n[section]; cmnt\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"\n[section] ; cmnt\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"\n[section]\nname=value; cmnt", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"\n[section]\nname=value ; cmnt", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"\n[section]\nname=\"value\" ; cmnt", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"\n[section]\nname=value ; \"cmnt", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"\n[section]\nname=\"va ; lue\" ; cmnt", &cBasic{Section: cBasicS1{Name: "va ; lue"}}, true},
{"\n[section]\nname=; cmnt", &cBasic{Section: cBasicS1{Name: ""}}, true},
}}, {"scanning:subsections", []readtest{
{"\n[sub \"A\"]\nname=value", &cSubs{map[string]*cSubsS1{"A": &cSubsS1{"value"}}}, true},
{"\n[sub \"b\"]\nname=value", &cSubs{map[string]*cSubsS1{"b": &cSubsS1{"value"}}}, true},
{"\n[sub \"A\\\\\"]\nname=value", &cSubs{map[string]*cSubsS1{"A\\": &cSubsS1{"value"}}}, true},
{"\n[sub \"A\\\"\"]\nname=value", &cSubs{map[string]*cSubsS1{"A\"": &cSubsS1{"value"}}}, true},
}}, {"syntax", []readtest{
// invalid line
{"\n[section]\n=", &cBasic{}, false},
// no section
{"name=value", &cBasic{}, false},
// empty section
{"\n[]\nname=value", &cBasic{}, false},
// empty subsection
{"\n[sub \"\"]\nname=value", &cSubs{}, false},
}}, {"setting", []readtest{
{"[section]\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
// pointer
{"[section]", &cBasic{Section: cBasicS1{PName: nil}}, true},
{"[section]\npname=value", &cBasic{Section: cBasicS1{PName: newString("value")}}, true},
// section name not matched
{"\n[nonexistent]\nname=value", &cBasic{}, false},
// subsection name not matched
{"\n[section \"nonexistent\"]\nname=value", &cBasic{}, false},
// variable name not matched
{"\n[section]\nnonexistent=value", &cBasic{}, false},
// hyphen in name
{"[hyphen-in-section]\nhyphen-in-name=value", &cBasic{Hyphen_In_Section: cBasicS2{Hyphen_In_Name: "value"}}, true},
// ignore unexported fields
{"[unexported]\nname=value", &cBasic{}, false},
{"[exported]\nunexported=value", &cBasic{}, false},
// 'X' prefix for non-upper/lower-case letters
{"[甲]\n乙=丙", &cUni{X甲: cUniS1{X乙: "丙"}}, true},
//{"[section]\nxname=value", &cBasic{XSection: cBasicS4{XName: "value"}}, false},
//{"[xsection]\nname=value", &cBasic{XSection: cBasicS4{XName: "value"}}, false},
// name specified as struct tag
{"[tag-name]\nname=value", &cBasic{TagName: cBasicS1{Name: "value"}}, true},
}}, {"multivalue", []readtest{
// unnamed slice type: treat as multi-value
{"\n[m1]", &cMulti{M1: cMultiS1{}}, true},
{"\n[m1]\nmulti=value", &cMulti{M1: cMultiS1{[]string{"value"}}}, true},
{"\n[m1]\nmulti=value1\nmulti=value2", &cMulti{M1: cMultiS1{[]string{"value1", "value2"}}}, true},
// "blank" empties multi-valued slice -- here same result as above
{"\n[m1]\nmulti\nmulti=value1\nmulti=value2", &cMulti{M1: cMultiS1{[]string{"value1", "value2"}}}, true},
// named slice type: do not treat as multi-value
{"\n[m2]", &cMulti{}, true},
{"\n[m2]\nmulti=value", &cMulti{}, false},
{"\n[m2]\nmulti=value1\nmulti=value2", &cMulti{}, false},
}}, {"type:string", []readtest{
{"[section]\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
{"[section]\nname=", &cBasic{Section: cBasicS1{Name: ""}}, true},
}}, {"type:bool", []readtest{
// explicit values
{"[section]\nbool=true", &cBool{cBoolS1{true}}, true},
{"[section]\nbool=yes", &cBool{cBoolS1{true}}, true},
{"[section]\nbool=on", &cBool{cBoolS1{true}}, true},
{"[section]\nbool=1", &cBool{cBoolS1{true}}, true},
{"[section]\nbool=tRuE", &cBool{cBoolS1{true}}, true},
{"[section]\nbool=false", &cBool{cBoolS1{false}}, true},
{"[section]\nbool=no", &cBool{cBoolS1{false}}, true},
{"[section]\nbool=off", &cBool{cBoolS1{false}}, true},
{"[section]\nbool=0", &cBool{cBoolS1{false}}, true},
{"[section]\nbool=NO", &cBool{cBoolS1{false}}, true},
// "blank" value handled as true
{"[section]\nbool", &cBool{cBoolS1{true}}, true},
// bool parse errors
{"[section]\nbool=maybe", &cBool{}, false},
{"[section]\nbool=t", &cBool{}, false},
{"[section]\nbool=truer", &cBool{}, false},
{"[section]\nbool=2", &cBool{}, false},
{"[section]\nbool=-1", &cBool{}, false},
}}, {"type:numeric", []readtest{
{"[section]\nint=0", &cBasic{Section: cBasicS1{Int: 0}}, true},
{"[section]\nint=1", &cBasic{Section: cBasicS1{Int: 1}}, true},
{"[section]\nint=-1", &cBasic{Section: cBasicS1{Int: -1}}, true},
{"[section]\nint=0.2", &cBasic{}, false},
{"[section]\nint=1e3", &cBasic{}, false},
// primitive [u]int(|8|16|32|64) and big.Int is parsed as dec or hex (not octal)
{"[n1]\nint=010", &cNum{N1: cNumS1{Int: 10}}, true},
{"[n1]\nint=0x10", &cNum{N1: cNumS1{Int: 0x10}}, true},
{"[n1]\nbig=1", &cNum{N1: cNumS1{Big: big.NewInt(1)}}, true},
{"[n1]\nbig=0x10", &cNum{N1: cNumS1{Big: big.NewInt(0x10)}}, true},
{"[n1]\nbig=010", &cNum{N1: cNumS1{Big: big.NewInt(10)}}, true},
{"[n2]\nmultiint=010", &cNum{N2: cNumS2{MultiInt: []int{10}}}, true},
{"[n2]\nmultibig=010", &cNum{N2: cNumS2{MultiBig: []*big.Int{big.NewInt(10)}}}, true},
// set parse mode for int types via struct tag
{"[n1]\nintdho=010", &cNum{N1: cNumS1{IntDHO: 010}}, true},
// octal allowed for named type
{"[n3]\nfilemode=0777", &cNum{N3: cNumS3{FileMode: 0777}}, true},
}}, {"type:textUnmarshaler", []readtest{
{"[section]\nname=value", &cTxUnm{Section: cTxUnmS1{Name: "value"}}, true},
{"[section]\nname=error", &cTxUnm{}, false},
}},
}
func TestReadStringInto(t *testing.T) {
for _, tg := range readtests {
for i, tt := range tg.tests {
id := fmt.Sprintf("%s:%d", tg.group, i)
testRead(t, id, tt)
}
}
}
func TestReadStringIntoMultiBlankPreset(t *testing.T) {
tt := readtest{"\n[m1]\nmulti\nmulti=value1\nmulti=value2", &cMulti{M1: cMultiS1{[]string{"value1", "value2"}}}, true}
cfg := &cMulti{M1: cMultiS1{[]string{"preset1", "preset2"}}}
testReadInto(t, "multi:blank", tt, cfg)
}
func testRead(t *testing.T, id string, tt readtest) {
// get the type of the expected result
restyp := reflect.TypeOf(tt.exp).Elem()
// create a new instance to hold the actual result
res := reflect.New(restyp).Interface()
testReadInto(t, id, tt, res)
}
func testReadInto(t *testing.T, id string, tt readtest, res interface{}) {
err := ReadStringInto(res, tt.gcfg)
if tt.ok {
if err != nil {
t.Errorf("%s fail: got error %v, wanted ok", id, err)
return
} else if !reflect.DeepEqual(res, tt.exp) {
t.Errorf("%s fail: got value %#v, wanted value %#v", id, res, tt.exp)
return
}
if !testing.Short() {
t.Logf("%s pass: got value %#v", id, res)
}
} else { // !tt.ok
if err == nil {
t.Errorf("%s fail: got value %#v, wanted error", id, res)
return
}
if !testing.Short() {
t.Logf("%s pass: got error %v", id, err)
}
}
}
func TestReadFileInto(t *testing.T) {
res := &struct{ Section struct{ Name string } }{}
err := ReadFileInto(res, "testdata/gcfg_test.gcfg")
if err != nil {
t.Errorf(err.Error())
}
if "value" != res.Section.Name {
t.Errorf("got %q, wanted %q", res.Section.Name, "value")
}
}
func TestReadFileIntoUnicode(t *testing.T) {
res := &struct{ X甲 struct{ X乙 string } }{}
err := ReadFileInto(res, "testdata/gcfg_unicode_test.gcfg")
if err != nil {
t.Errorf(err.Error())
}
if "丙" != res.X甲.X乙 {
t.Errorf("got %q, wanted %q", res.X甲.X乙, "丙")
}
}

View File

@ -0,0 +1,121 @@
// 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.
package scanner
import (
"fmt"
"io"
"sort"
)
import (
"github.com/scalingdata/gcfg/token"
)
// In an ErrorList, an error is represented by an *Error.
// The position Pos, if valid, points to the beginning of
// the offending token, and the error condition is described
// by Msg.
//
type Error struct {
Pos token.Position
Msg string
}
// Error implements the error interface.
func (e Error) Error() string {
if e.Pos.Filename != "" || e.Pos.IsValid() {
// don't print "<unknown position>"
// TODO(gri) reconsider the semantics of Position.IsValid
return e.Pos.String() + ": " + e.Msg
}
return e.Msg
}
// ErrorList is a list of *Errors.
// The zero value for an ErrorList is an empty ErrorList ready to use.
//
type ErrorList []*Error
// Add adds an Error with given position and error message to an ErrorList.
func (p *ErrorList) Add(pos token.Position, msg string) {
*p = append(*p, &Error{pos, msg})
}
// Reset resets an ErrorList to no errors.
func (p *ErrorList) Reset() { *p = (*p)[0:0] }
// ErrorList implements the sort Interface.
func (p ErrorList) Len() int { return len(p) }
func (p ErrorList) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p ErrorList) Less(i, j int) bool {
e := &p[i].Pos
f := &p[j].Pos
if e.Filename < f.Filename {
return true
}
if e.Filename == f.Filename {
return e.Offset < f.Offset
}
return false
}
// Sort sorts an ErrorList. *Error entries are sorted by position,
// other errors are sorted by error message, and before any *Error
// entry.
//
func (p ErrorList) Sort() {
sort.Sort(p)
}
// RemoveMultiples sorts an ErrorList and removes all but the first error per line.
func (p *ErrorList) RemoveMultiples() {
sort.Sort(p)
var last token.Position // initial last.Line is != any legal error line
i := 0
for _, e := range *p {
if e.Pos.Filename != last.Filename || e.Pos.Line != last.Line {
last = e.Pos
(*p)[i] = e
i++
}
}
(*p) = (*p)[0:i]
}
// An ErrorList implements the error interface.
func (p ErrorList) Error() string {
switch len(p) {
case 0:
return "no errors"
case 1:
return p[0].Error()
}
return fmt.Sprintf("%s (and %d more errors)", p[0], len(p)-1)
}
// Err returns an error equivalent to this error list.
// If the list is empty, Err returns nil.
func (p ErrorList) Err() error {
if len(p) == 0 {
return nil
}
return p
}
// PrintError is a utility function that prints a list of errors to w,
// one error per line, if the err parameter is an ErrorList. Otherwise
// it prints the err string.
//
func PrintError(w io.Writer, err error) {
if list, ok := err.(ErrorList); ok {
for _, e := range list {
fmt.Fprintf(w, "%s\n", e)
}
} else if err != nil {
fmt.Fprintf(w, "%s\n", err)
}
}

View File

@ -0,0 +1,46 @@
// Copyright 2012 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 scanner_test
import (
"fmt"
)
import (
"github.com/scalingdata/gcfg/scanner"
"github.com/scalingdata/gcfg/token"
)
func ExampleScanner_Scan() {
// src is the input that we want to tokenize.
src := []byte(`[profile "A"]
color = blue ; Comment`)
// Initialize the scanner.
var s scanner.Scanner
fset := token.NewFileSet() // positions are relative to fset
file := fset.AddFile("", fset.Base(), len(src)) // register input "file"
s.Init(file, src, nil /* no error handler */, scanner.ScanComments)
// Repeated calls to Scan yield the token sequence found in the input.
for {
pos, tok, lit := s.Scan()
if tok == token.EOF {
break
}
fmt.Printf("%s\t%q\t%q\n", fset.Position(pos), tok, lit)
}
// output:
// 1:1 "[" ""
// 1:2 "IDENT" "profile"
// 1:10 "STRING" "\"A\""
// 1:13 "]" ""
// 1:14 "\n" ""
// 2:1 "IDENT" "color"
// 2:7 "=" ""
// 2:9 "STRING" "blue"
// 2:14 "COMMENT" "; Comment"
}

View File

@ -0,0 +1,342 @@
// 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.
// Package scanner implements a scanner for gcfg configuration text.
// It takes a []byte as source which can then be tokenized
// through repeated calls to the Scan method.
//
// Note that the API for the scanner package may change to accommodate new
// features or implementation changes in gcfg.
//
package scanner
import (
"fmt"
"path/filepath"
"unicode"
"unicode/utf8"
)
import (
"github.com/scalingdata/gcfg/token"
)
// An ErrorHandler may be provided to Scanner.Init. If a syntax error is
// encountered and a handler was installed, the handler is called with a
// position and an error message. The position points to the beginning of
// the offending token.
//
type ErrorHandler func(pos token.Position, msg string)
// A Scanner holds the scanner's internal state while processing
// a given text. It can be allocated as part of another data
// structure but must be initialized via Init before use.
//
type Scanner struct {
// immutable state
file *token.File // source file handle
dir string // directory portion of file.Name()
src []byte // source
err ErrorHandler // error reporting; or nil
mode Mode // scanning mode
// scanning state
ch rune // current character
offset int // character offset
rdOffset int // reading offset (position after current character)
lineOffset int // current line offset
nextVal bool // next token is expected to be a value
// public state - ok to modify
ErrorCount int // number of errors encountered
}
// Read the next Unicode char into s.ch.
// s.ch < 0 means end-of-file.
//
func (s *Scanner) next() {
if s.rdOffset < len(s.src) {
s.offset = s.rdOffset
if s.ch == '\n' {
s.lineOffset = s.offset
s.file.AddLine(s.offset)
}
r, w := rune(s.src[s.rdOffset]), 1
switch {
case r == 0:
s.error(s.offset, "illegal character NUL")
case r >= 0x80:
// not ASCII
r, w = utf8.DecodeRune(s.src[s.rdOffset:])
if r == utf8.RuneError && w == 1 {
s.error(s.offset, "illegal UTF-8 encoding")
}
}
s.rdOffset += w
s.ch = r
} else {
s.offset = len(s.src)
if s.ch == '\n' {
s.lineOffset = s.offset
s.file.AddLine(s.offset)
}
s.ch = -1 // eof
}
}
// A mode value is a set of flags (or 0).
// They control scanner behavior.
//
type Mode uint
const (
ScanComments Mode = 1 << iota // return comments as COMMENT tokens
)
// Init prepares the scanner s to tokenize the text src by setting the
// scanner at the beginning of src. The scanner uses the file set file
// for position information and it adds line information for each line.
// It is ok to re-use the same file when re-scanning the same file as
// line information which is already present is ignored. Init causes a
// panic if the file size does not match the src size.
//
// Calls to Scan will invoke the error handler err if they encounter a
// syntax error and err is not nil. Also, for each error encountered,
// the Scanner field ErrorCount is incremented by one. The mode parameter
// determines how comments are handled.
//
// Note that Init may call err if there is an error in the first character
// of the file.
//
func (s *Scanner) Init(file *token.File, src []byte, err ErrorHandler, mode Mode) {
// Explicitly initialize all fields since a scanner may be reused.
if file.Size() != len(src) {
panic(fmt.Sprintf("file size (%d) does not match src len (%d)", file.Size(), len(src)))
}
s.file = file
s.dir, _ = filepath.Split(file.Name())
s.src = src
s.err = err
s.mode = mode
s.ch = ' '
s.offset = 0
s.rdOffset = 0
s.lineOffset = 0
s.ErrorCount = 0
s.nextVal = false
s.next()
}
func (s *Scanner) error(offs int, msg string) {
if s.err != nil {
s.err(s.file.Position(s.file.Pos(offs)), msg)
}
s.ErrorCount++
}
func (s *Scanner) scanComment() string {
// initial [;#] already consumed
offs := s.offset - 1 // position of initial [;#]
for s.ch != '\n' && s.ch >= 0 {
s.next()
}
return string(s.src[offs:s.offset])
}
func isLetter(ch rune) bool {
return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch >= 0x80 && unicode.IsLetter(ch)
}
func isDigit(ch rune) bool {
return '0' <= ch && ch <= '9' || ch >= 0x80 && unicode.IsDigit(ch)
}
func (s *Scanner) scanIdentifier() string {
offs := s.offset
for isLetter(s.ch) || isDigit(s.ch) || s.ch == '-' {
s.next()
}
return string(s.src[offs:s.offset])
}
func (s *Scanner) scanEscape(val bool) {
offs := s.offset
ch := s.ch
s.next() // always make progress
switch ch {
case '\\', '"':
// ok
case 'n', 't':
if val {
break // ok
}
fallthrough
default:
s.error(offs, "unknown escape sequence")
}
}
func (s *Scanner) scanString() string {
// '"' opening already consumed
offs := s.offset - 1
for s.ch != '"' {
ch := s.ch
s.next()
if ch == '\n' || ch < 0 {
s.error(offs, "string not terminated")
break
}
if ch == '\\' {
s.scanEscape(false)
}
}
s.next()
return string(s.src[offs:s.offset])
}
func stripCR(b []byte) []byte {
c := make([]byte, len(b))
i := 0
for _, ch := range b {
if ch != '\r' {
c[i] = ch
i++
}
}
return c[:i]
}
func (s *Scanner) scanValString() string {
offs := s.offset
hasCR := false
end := offs
inQuote := false
loop:
for inQuote || s.ch >= 0 && s.ch != '\n' && s.ch != ';' && s.ch != '#' {
ch := s.ch
s.next()
switch {
case inQuote && ch == '\\':
s.scanEscape(true)
case !inQuote && ch == '\\':
if s.ch == '\r' {
hasCR = true
s.next()
}
if s.ch != '\n' {
s.error(offs, "unquoted '\\' must be followed by new line")
break loop
}
s.next()
case ch == '"':
inQuote = !inQuote
case ch == '\r':
hasCR = true
case ch < 0 || inQuote && ch == '\n':
s.error(offs, "string not terminated")
break loop
}
if inQuote || !isWhiteSpace(ch) {
end = s.offset
}
}
lit := s.src[offs:end]
if hasCR {
lit = stripCR(lit)
}
return string(lit)
}
func isWhiteSpace(ch rune) bool {
return ch == ' ' || ch == '\t' || ch == '\r'
}
func (s *Scanner) skipWhitespace() {
for isWhiteSpace(s.ch) {
s.next()
}
}
// Scan scans the next token and returns the token position, the token,
// and its literal string if applicable. The source end is indicated by
// token.EOF.
//
// If the returned token is a literal (token.IDENT, token.STRING) or
// token.COMMENT, the literal string has the corresponding value.
//
// If the returned token is token.ILLEGAL, the literal string is the
// offending character.
//
// In all other cases, Scan returns an empty literal string.
//
// For more tolerant parsing, Scan will return a valid token if
// possible even if a syntax error was encountered. Thus, even
// if the resulting token sequence contains no illegal tokens,
// a client may not assume that no error occurred. Instead it
// must check the scanner's ErrorCount or the number of calls
// of the error handler, if there was one installed.
//
// Scan adds line information to the file added to the file
// set with Init. Token positions are relative to that file
// and thus relative to the file set.
//
func (s *Scanner) Scan() (pos token.Pos, tok token.Token, lit string) {
scanAgain:
s.skipWhitespace()
// current token start
pos = s.file.Pos(s.offset)
// determine token value
switch ch := s.ch; {
case s.nextVal:
lit = s.scanValString()
tok = token.STRING
s.nextVal = false
case isLetter(ch):
lit = s.scanIdentifier()
tok = token.IDENT
default:
s.next() // always make progress
switch ch {
case -1:
tok = token.EOF
case '\n':
tok = token.EOL
case '"':
tok = token.STRING
lit = s.scanString()
case '[':
tok = token.LBRACK
case ']':
tok = token.RBRACK
case ';', '#':
// comment
lit = s.scanComment()
if s.mode&ScanComments == 0 {
// skip comment
goto scanAgain
}
tok = token.COMMENT
case '=':
tok = token.ASSIGN
s.nextVal = true
default:
s.error(s.file.Offset(pos), fmt.Sprintf("illegal character %#U", ch))
tok = token.ILLEGAL
lit = string(ch)
}
}
return
}

View File

@ -0,0 +1,417 @@
// 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.
package scanner
import (
"os"
"strings"
"testing"
)
import (
"github.com/scalingdata/gcfg/token"
)
var fset = token.NewFileSet()
const /* class */ (
special = iota
literal
operator
)
func tokenclass(tok token.Token) int {
switch {
case tok.IsLiteral():
return literal
case tok.IsOperator():
return operator
}
return special
}
type elt struct {
tok token.Token
lit string
class int
pre string
suf string
}
var tokens = [...]elt{
// Special tokens
{token.COMMENT, "; a comment", special, "", "\n"},
{token.COMMENT, "# a comment", special, "", "\n"},
// Operators and delimiters
{token.ASSIGN, "=", operator, "", "value"},
{token.LBRACK, "[", operator, "", ""},
{token.RBRACK, "]", operator, "", ""},
{token.EOL, "\n", operator, "", ""},
// Identifiers
{token.IDENT, "foobar", literal, "", ""},
{token.IDENT, "a۰۱۸", literal, "", ""},
{token.IDENT, "foo६४", literal, "", ""},
{token.IDENT, "bar", literal, "", ""},
{token.IDENT, "foo-bar", literal, "", ""},
{token.IDENT, "foo", literal, ";\n", ""},
// String literals (subsection names)
{token.STRING, `"foobar"`, literal, "", ""},
{token.STRING, `"\""`, literal, "", ""},
// String literals (values)
{token.STRING, `"\n"`, literal, "=", ""},
{token.STRING, `"foobar"`, literal, "=", ""},
{token.STRING, `"foo\nbar"`, literal, "=", ""},
{token.STRING, `"foo\"bar"`, literal, "=", ""},
{token.STRING, `"foo\\bar"`, literal, "=", ""},
{token.STRING, `"foobar"`, literal, "=", ""},
{token.STRING, `"foobar"`, literal, "= ", ""},
{token.STRING, `"foobar"`, literal, "=", "\n"},
{token.STRING, `"foobar"`, literal, "=", ";"},
{token.STRING, `"foobar"`, literal, "=", " ;"},
{token.STRING, `"foobar"`, literal, "=", "#"},
{token.STRING, `"foobar"`, literal, "=", " #"},
{token.STRING, "foobar", literal, "=", ""},
{token.STRING, "foobar", literal, "= ", ""},
{token.STRING, "foobar", literal, "=", " "},
{token.STRING, `"foo" "bar"`, literal, "=", " "},
{token.STRING, "foo\\\nbar", literal, "=", ""},
{token.STRING, "foo\\\r\nbar", literal, "=", ""},
}
const whitespace = " \t \n\n\n" // to separate tokens
var source = func() []byte {
var src []byte
for _, t := range tokens {
src = append(src, t.pre...)
src = append(src, t.lit...)
src = append(src, t.suf...)
src = append(src, whitespace...)
}
return src
}()
func newlineCount(s string) int {
n := 0
for i := 0; i < len(s); i++ {
if s[i] == '\n' {
n++
}
}
return n
}
func checkPos(t *testing.T, lit string, p token.Pos, expected token.Position) {
pos := fset.Position(p)
if pos.Filename != expected.Filename {
t.Errorf("bad filename for %q: got %s, expected %s", lit, pos.Filename, expected.Filename)
}
if pos.Offset != expected.Offset {
t.Errorf("bad position for %q: got %d, expected %d", lit, pos.Offset, expected.Offset)
}
if pos.Line != expected.Line {
t.Errorf("bad line for %q: got %d, expected %d", lit, pos.Line, expected.Line)
}
if pos.Column != expected.Column {
t.Errorf("bad column for %q: got %d, expected %d", lit, pos.Column, expected.Column)
}
}
// Verify that calling Scan() provides the correct results.
func TestScan(t *testing.T) {
// make source
src_linecount := newlineCount(string(source))
whitespace_linecount := newlineCount(whitespace)
index := 0
// error handler
eh := func(_ token.Position, msg string) {
t.Errorf("%d: error handler called (msg = %s)", index, msg)
}
// verify scan
var s Scanner
s.Init(fset.AddFile("", fset.Base(), len(source)), source, eh, ScanComments)
// epos is the expected position
epos := token.Position{
Filename: "",
Offset: 0,
Line: 1,
Column: 1,
}
for {
pos, tok, lit := s.Scan()
if lit == "" {
// no literal value for non-literal tokens
lit = tok.String()
}
e := elt{token.EOF, "", special, "", ""}
if index < len(tokens) {
e = tokens[index]
}
if tok == token.EOF {
lit = "<EOF>"
epos.Line = src_linecount
epos.Column = 2
}
if e.pre != "" && strings.ContainsRune("=;#", rune(e.pre[0])) {
epos.Column = 1
checkPos(t, lit, pos, epos)
var etok token.Token
if e.pre[0] == '=' {
etok = token.ASSIGN
} else {
etok = token.COMMENT
}
if tok != etok {
t.Errorf("bad token for %q: got %q, expected %q", lit, tok, etok)
}
pos, tok, lit = s.Scan()
}
epos.Offset += len(e.pre)
if tok != token.EOF {
epos.Column = 1 + len(e.pre)
}
if e.pre != "" && e.pre[len(e.pre)-1] == '\n' {
epos.Offset--
epos.Column--
checkPos(t, lit, pos, epos)
if tok != token.EOL {
t.Errorf("bad token for %q: got %q, expected %q", lit, tok, token.EOL)
}
epos.Line++
epos.Offset++
epos.Column = 1
pos, tok, lit = s.Scan()
}
checkPos(t, lit, pos, epos)
if tok != e.tok {
t.Errorf("bad token for %q: got %q, expected %q", lit, tok, e.tok)
}
if e.tok.IsLiteral() {
// no CRs in value string literals
elit := e.lit
if strings.ContainsRune(e.pre, '=') {
elit = string(stripCR([]byte(elit)))
epos.Offset += len(e.lit) - len(lit) // correct position
}
if lit != elit {
t.Errorf("bad literal for %q: got %q, expected %q", lit, lit, elit)
}
}
if tokenclass(tok) != e.class {
t.Errorf("bad class for %q: got %d, expected %d", lit, tokenclass(tok), e.class)
}
epos.Offset += len(lit) + len(e.suf) + len(whitespace)
epos.Line += newlineCount(lit) + newlineCount(e.suf) + whitespace_linecount
index++
if tok == token.EOF {
break
}
if e.suf == "value" {
pos, tok, lit = s.Scan()
if tok != token.STRING {
t.Errorf("bad token for %q: got %q, expected %q", lit, tok, token.STRING)
}
} else if strings.ContainsRune(e.suf, ';') || strings.ContainsRune(e.suf, '#') {
pos, tok, lit = s.Scan()
if tok != token.COMMENT {
t.Errorf("bad token for %q: got %q, expected %q", lit, tok, token.COMMENT)
}
}
// skip EOLs
for i := 0; i < whitespace_linecount+newlineCount(e.suf); i++ {
pos, tok, lit = s.Scan()
if tok != token.EOL {
t.Errorf("bad token for %q: got %q, expected %q", lit, tok, token.EOL)
}
}
}
if s.ErrorCount != 0 {
t.Errorf("found %d errors", s.ErrorCount)
}
}
func TestScanValStringEOF(t *testing.T) {
var s Scanner
src := "= value"
f := fset.AddFile("src", fset.Base(), len(src))
s.Init(f, []byte(src), nil, 0)
s.Scan() // =
s.Scan() // value
_, tok, _ := s.Scan() // EOF
if tok != token.EOF {
t.Errorf("bad token: got %s, expected %s", tok, token.EOF)
}
if s.ErrorCount > 0 {
t.Error("scanning error")
}
}
// Verify that initializing the same scanner more then once works correctly.
func TestInit(t *testing.T) {
var s Scanner
// 1st init
src1 := "\nname = value"
f1 := fset.AddFile("src1", fset.Base(), len(src1))
s.Init(f1, []byte(src1), nil, 0)
if f1.Size() != len(src1) {
t.Errorf("bad file size: got %d, expected %d", f1.Size(), len(src1))
}
s.Scan() // \n
s.Scan() // name
_, tok, _ := s.Scan() // =
if tok != token.ASSIGN {
t.Errorf("bad token: got %s, expected %s", tok, token.ASSIGN)
}
// 2nd init
src2 := "[section]"
f2 := fset.AddFile("src2", fset.Base(), len(src2))
s.Init(f2, []byte(src2), nil, 0)
if f2.Size() != len(src2) {
t.Errorf("bad file size: got %d, expected %d", f2.Size(), len(src2))
}
_, tok, _ = s.Scan() // [
if tok != token.LBRACK {
t.Errorf("bad token: got %s, expected %s", tok, token.LBRACK)
}
if s.ErrorCount != 0 {
t.Errorf("found %d errors", s.ErrorCount)
}
}
func TestStdErrorHandler(t *testing.T) {
const src = "@\n" + // illegal character, cause an error
"@ @\n" // two errors on the same line
var list ErrorList
eh := func(pos token.Position, msg string) { list.Add(pos, msg) }
var s Scanner
s.Init(fset.AddFile("File1", fset.Base(), len(src)), []byte(src), eh, 0)
for {
if _, tok, _ := s.Scan(); tok == token.EOF {
break
}
}
if len(list) != s.ErrorCount {
t.Errorf("found %d errors, expected %d", len(list), s.ErrorCount)
}
if len(list) != 3 {
t.Errorf("found %d raw errors, expected 3", len(list))
PrintError(os.Stderr, list)
}
list.Sort()
if len(list) != 3 {
t.Errorf("found %d sorted errors, expected 3", len(list))
PrintError(os.Stderr, list)
}
list.RemoveMultiples()
if len(list) != 2 {
t.Errorf("found %d one-per-line errors, expected 2", len(list))
PrintError(os.Stderr, list)
}
}
type errorCollector struct {
cnt int // number of errors encountered
msg string // last error message encountered
pos token.Position // last error position encountered
}
func checkError(t *testing.T, src string, tok token.Token, pos int, err string) {
var s Scanner
var h errorCollector
eh := func(pos token.Position, msg string) {
h.cnt++
h.msg = msg
h.pos = pos
}
s.Init(fset.AddFile("", fset.Base(), len(src)), []byte(src), eh, ScanComments)
if src[0] == '=' {
_, _, _ = s.Scan()
}
_, tok0, _ := s.Scan()
_, tok1, _ := s.Scan()
if tok0 != tok {
t.Errorf("%q: got %s, expected %s", src, tok0, tok)
}
if tok1 != token.EOF {
t.Errorf("%q: got %s, expected EOF", src, tok1)
}
cnt := 0
if err != "" {
cnt = 1
}
if h.cnt != cnt {
t.Errorf("%q: got cnt %d, expected %d", src, h.cnt, cnt)
}
if h.msg != err {
t.Errorf("%q: got msg %q, expected %q", src, h.msg, err)
}
if h.pos.Offset != pos {
t.Errorf("%q: got offset %d, expected %d", src, h.pos.Offset, pos)
}
}
var errors = []struct {
src string
tok token.Token
pos int
err string
}{
{"\a", token.ILLEGAL, 0, "illegal character U+0007"},
{"/", token.ILLEGAL, 0, "illegal character U+002F '/'"},
{"_", token.ILLEGAL, 0, "illegal character U+005F '_'"},
{``, token.ILLEGAL, 0, "illegal character U+2026 '…'"},
{`""`, token.STRING, 0, ""},
{`"`, token.STRING, 0, "string not terminated"},
{"\"\n", token.STRING, 0, "string not terminated"},
{`="`, token.STRING, 1, "string not terminated"},
{"=\"\n", token.STRING, 1, "string not terminated"},
{"=\\", token.STRING, 1, "unquoted '\\' must be followed by new line"},
{"=\\\r", token.STRING, 1, "unquoted '\\' must be followed by new line"},
{`"\z"`, token.STRING, 2, "unknown escape sequence"},
{`"\a"`, token.STRING, 2, "unknown escape sequence"},
{`"\b"`, token.STRING, 2, "unknown escape sequence"},
{`"\f"`, token.STRING, 2, "unknown escape sequence"},
{`"\r"`, token.STRING, 2, "unknown escape sequence"},
{`"\t"`, token.STRING, 2, "unknown escape sequence"},
{`"\v"`, token.STRING, 2, "unknown escape sequence"},
{`"\0"`, token.STRING, 2, "unknown escape sequence"},
}
func TestScanErrors(t *testing.T) {
for _, e := range errors {
checkError(t, e.src, e.tok, e.pos, e.err)
}
}
func BenchmarkScan(b *testing.B) {
b.StopTimer()
fset := token.NewFileSet()
file := fset.AddFile("", fset.Base(), len(source))
var s Scanner
b.StartTimer()
for i := b.N - 1; i >= 0; i-- {
s.Init(file, source, nil, ScanComments)
for {
_, tok, _ := s.Scan()
if tok == token.EOF {
break
}
}
}
}

View File

@ -0,0 +1,281 @@
package gcfg
import (
"fmt"
"math/big"
"reflect"
"strings"
"unicode"
"unicode/utf8"
"github.com/scalingdata/gcfg/types"
)
type tag struct {
ident string
intMode string
}
func newTag(ts string) tag {
t := tag{}
s := strings.Split(ts, ",")
t.ident = s[0]
for _, tse := range s[1:] {
if strings.HasPrefix(tse, "int=") {
t.intMode = tse[len("int="):]
}
}
return t
}
func fieldFold(v reflect.Value, name string) (reflect.Value, tag) {
var n string
r0, _ := utf8.DecodeRuneInString(name)
if unicode.IsLetter(r0) && !unicode.IsLower(r0) && !unicode.IsUpper(r0) {
n = "X"
}
n += strings.Replace(name, "-", "_", -1)
f, ok := v.Type().FieldByNameFunc(func(fieldName string) bool {
if !v.FieldByName(fieldName).CanSet() {
return false
}
f, _ := v.Type().FieldByName(fieldName)
t := newTag(f.Tag.Get("gcfg"))
if t.ident != "" {
return strings.EqualFold(t.ident, name)
}
return strings.EqualFold(n, fieldName)
})
if !ok {
return reflect.Value{}, tag{}
}
return v.FieldByName(f.Name), newTag(f.Tag.Get("gcfg"))
}
type setter func(destp interface{}, blank bool, val string, t tag) error
var errUnsupportedType = fmt.Errorf("unsupported type")
var errBlankUnsupported = fmt.Errorf("blank value not supported for type")
var setters = []setter{
typeSetter, textUnmarshalerSetter, kindSetter, scanSetter,
}
func textUnmarshalerSetter(d interface{}, blank bool, val string, t tag) error {
dtu, ok := d.(textUnmarshaler)
if !ok {
return errUnsupportedType
}
if blank {
return errBlankUnsupported
}
return dtu.UnmarshalText([]byte(val))
}
func boolSetter(d interface{}, blank bool, val string, t tag) error {
if blank {
reflect.ValueOf(d).Elem().Set(reflect.ValueOf(true))
return nil
}
b, err := types.ParseBool(val)
if err == nil {
reflect.ValueOf(d).Elem().Set(reflect.ValueOf(b))
}
return err
}
func intMode(mode string) types.IntMode {
var m types.IntMode
if strings.ContainsAny(mode, "dD") {
m |= types.Dec
}
if strings.ContainsAny(mode, "hH") {
m |= types.Hex
}
if strings.ContainsAny(mode, "oO") {
m |= types.Oct
}
return m
}
var typeModes = map[reflect.Type]types.IntMode{
reflect.TypeOf(int(0)): types.Dec | types.Hex,
reflect.TypeOf(int8(0)): types.Dec | types.Hex,
reflect.TypeOf(int16(0)): types.Dec | types.Hex,
reflect.TypeOf(int32(0)): types.Dec | types.Hex,
reflect.TypeOf(int64(0)): types.Dec | types.Hex,
reflect.TypeOf(uint(0)): types.Dec | types.Hex,
reflect.TypeOf(uint8(0)): types.Dec | types.Hex,
reflect.TypeOf(uint16(0)): types.Dec | types.Hex,
reflect.TypeOf(uint32(0)): types.Dec | types.Hex,
reflect.TypeOf(uint64(0)): types.Dec | types.Hex,
// use default mode (allow dec/hex/oct) for uintptr type
reflect.TypeOf(big.Int{}): types.Dec | types.Hex,
}
func intModeDefault(t reflect.Type) types.IntMode {
m, ok := typeModes[t]
if !ok {
m = types.Dec | types.Hex | types.Oct
}
return m
}
func intSetter(d interface{}, blank bool, val string, t tag) error {
if blank {
return errBlankUnsupported
}
mode := intMode(t.intMode)
if mode == 0 {
mode = intModeDefault(reflect.TypeOf(d).Elem())
}
return types.ParseInt(d, val, mode)
}
func stringSetter(d interface{}, blank bool, val string, t tag) error {
if blank {
return errBlankUnsupported
}
dsp, ok := d.(*string)
if !ok {
return errUnsupportedType
}
*dsp = val
return nil
}
var kindSetters = map[reflect.Kind]setter{
reflect.String: stringSetter,
reflect.Bool: boolSetter,
reflect.Int: intSetter,
reflect.Int8: intSetter,
reflect.Int16: intSetter,
reflect.Int32: intSetter,
reflect.Int64: intSetter,
reflect.Uint: intSetter,
reflect.Uint8: intSetter,
reflect.Uint16: intSetter,
reflect.Uint32: intSetter,
reflect.Uint64: intSetter,
reflect.Uintptr: intSetter,
}
var typeSetters = map[reflect.Type]setter{
reflect.TypeOf(big.Int{}): intSetter,
}
func typeSetter(d interface{}, blank bool, val string, tt tag) error {
t := reflect.ValueOf(d).Type().Elem()
setter, ok := typeSetters[t]
if !ok {
return errUnsupportedType
}
return setter(d, blank, val, tt)
}
func kindSetter(d interface{}, blank bool, val string, tt tag) error {
k := reflect.ValueOf(d).Type().Elem().Kind()
setter, ok := kindSetters[k]
if !ok {
return errUnsupportedType
}
return setter(d, blank, val, tt)
}
func scanSetter(d interface{}, blank bool, val string, tt tag) error {
if blank {
return errBlankUnsupported
}
return types.ScanFully(d, val, 'v')
}
func set(cfg interface{}, sect, sub, name string, blank bool, value string) error {
vPCfg := reflect.ValueOf(cfg)
if vPCfg.Kind() != reflect.Ptr || vPCfg.Elem().Kind() != reflect.Struct {
panic(fmt.Errorf("config must be a pointer to a struct"))
}
vCfg := vPCfg.Elem()
vSect, _ := fieldFold(vCfg, sect)
if !vSect.IsValid() {
return fmt.Errorf("invalid section: section %q", sect)
}
if vSect.Kind() == reflect.Map {
vst := vSect.Type()
if vst.Key().Kind() != reflect.String ||
vst.Elem().Kind() != reflect.Ptr ||
vst.Elem().Elem().Kind() != reflect.Struct {
panic(fmt.Errorf("map field for section must have string keys and "+
" pointer-to-struct values: section %q", sect))
}
if vSect.IsNil() {
vSect.Set(reflect.MakeMap(vst))
}
k := reflect.ValueOf(sub)
pv := vSect.MapIndex(k)
if !pv.IsValid() {
vType := vSect.Type().Elem().Elem()
pv = reflect.New(vType)
vSect.SetMapIndex(k, pv)
}
vSect = pv.Elem()
} else if vSect.Kind() != reflect.Struct {
panic(fmt.Errorf("field for section must be a map or a struct: "+
"section %q", sect))
} else if sub != "" {
return fmt.Errorf("invalid subsection: "+
"section %q subsection %q", sect, sub)
}
vVar, t := fieldFold(vSect, name)
if !vVar.IsValid() {
return fmt.Errorf("invalid variable: "+
"section %q subsection %q variable %q", sect, sub, name)
}
// vVal is either single-valued var, or newly allocated value within multi-valued var
var vVal reflect.Value
// multi-value if unnamed slice type
isMulti := vVar.Type().Name() == "" && vVar.Kind() == reflect.Slice
if isMulti && blank {
vVar.Set(reflect.Zero(vVar.Type()))
return nil
}
if isMulti {
vVal = reflect.New(vVar.Type().Elem()).Elem()
} else {
vVal = vVar
}
isDeref := vVal.Type().Name() == "" && vVal.Type().Kind() == reflect.Ptr
isNew := isDeref && vVal.IsNil()
// vAddr is address of value to set (dereferenced & allocated as needed)
var vAddr reflect.Value
switch {
case isNew:
vAddr = reflect.New(vVal.Type().Elem())
case isDeref && !isNew:
vAddr = vVal
default:
vAddr = vVal.Addr()
}
vAddrI := vAddr.Interface()
err, ok := error(nil), false
for _, s := range setters {
err = s(vAddrI, blank, value, t)
if err == nil {
ok = true
break
}
if err != errUnsupportedType {
return err
}
}
if !ok {
// in case all setters returned errUnsupportedType
return err
}
if isNew { // set reference if it was dereferenced and newly allocated
vVal.Set(vAddr)
}
if isMulti { // append if multi-valued
vVar.Set(reflect.Append(vVar, vVal))
}
return nil
}

View File

@ -0,0 +1,3 @@
; Comment line
[section]
name=value # comment

View File

@ -0,0 +1,3 @@
; Comment line
[甲]
乙=丙 # comment

View File

@ -0,0 +1,435 @@
// Copyright 2010 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.
// TODO(gri) consider making this a separate package outside the go directory.
package token
import (
"fmt"
"sort"
"sync"
)
// -----------------------------------------------------------------------------
// Positions
// Position describes an arbitrary source position
// including the file, line, and column location.
// A Position is valid if the line number is > 0.
//
type Position struct {
Filename string // filename, if any
Offset int // offset, starting at 0
Line int // line number, starting at 1
Column int // column number, starting at 1 (character count)
}
// IsValid returns true if the position is valid.
func (pos *Position) IsValid() bool { return pos.Line > 0 }
// String returns a string in one of several forms:
//
// file:line:column valid position with file name
// line:column valid position without file name
// file invalid position with file name
// - invalid position without file name
//
func (pos Position) String() string {
s := pos.Filename
if pos.IsValid() {
if s != "" {
s += ":"
}
s += fmt.Sprintf("%d:%d", pos.Line, pos.Column)
}
if s == "" {
s = "-"
}
return s
}
// Pos is a compact encoding of a source position within a file set.
// It can be converted into a Position for a more convenient, but much
// larger, representation.
//
// The Pos value for a given file is a number in the range [base, base+size],
// where base and size are specified when adding the file to the file set via
// AddFile.
//
// To create the Pos value for a specific source offset, first add
// the respective file to the current file set (via FileSet.AddFile)
// and then call File.Pos(offset) for that file. Given a Pos value p
// for a specific file set fset, the corresponding Position value is
// obtained by calling fset.Position(p).
//
// Pos values can be compared directly with the usual comparison operators:
// If two Pos values p and q are in the same file, comparing p and q is
// equivalent to comparing the respective source file offsets. If p and q
// are in different files, p < q is true if the file implied by p was added
// to the respective file set before the file implied by q.
//
type Pos int
// The zero value for Pos is NoPos; there is no file and line information
// associated with it, and NoPos().IsValid() is false. NoPos is always
// smaller than any other Pos value. The corresponding Position value
// for NoPos is the zero value for Position.
//
const NoPos Pos = 0
// IsValid returns true if the position is valid.
func (p Pos) IsValid() bool {
return p != NoPos
}
// -----------------------------------------------------------------------------
// File
// A File is a handle for a file belonging to a FileSet.
// A File has a name, size, and line offset table.
//
type File struct {
set *FileSet
name string // file name as provided to AddFile
base int // Pos value range for this file is [base...base+size]
size int // file size as provided to AddFile
// lines and infos are protected by set.mutex
lines []int
infos []lineInfo
}
// Name returns the file name of file f as registered with AddFile.
func (f *File) Name() string {
return f.name
}
// Base returns the base offset of file f as registered with AddFile.
func (f *File) Base() int {
return f.base
}
// Size returns the size of file f as registered with AddFile.
func (f *File) Size() int {
return f.size
}
// LineCount returns the number of lines in file f.
func (f *File) LineCount() int {
f.set.mutex.RLock()
n := len(f.lines)
f.set.mutex.RUnlock()
return n
}
// AddLine adds the line offset for a new line.
// The line offset must be larger than the offset for the previous line
// and smaller than the file size; otherwise the line offset is ignored.
//
func (f *File) AddLine(offset int) {
f.set.mutex.Lock()
if i := len(f.lines); (i == 0 || f.lines[i-1] < offset) && offset < f.size {
f.lines = append(f.lines, offset)
}
f.set.mutex.Unlock()
}
// SetLines sets the line offsets for a file and returns true if successful.
// The line offsets are the offsets of the first character of each line;
// for instance for the content "ab\nc\n" the line offsets are {0, 3}.
// An empty file has an empty line offset table.
// Each line offset must be larger than the offset for the previous line
// and smaller than the file size; otherwise SetLines fails and returns
// false.
//
func (f *File) SetLines(lines []int) bool {
// verify validity of lines table
size := f.size
for i, offset := range lines {
if i > 0 && offset <= lines[i-1] || size <= offset {
return false
}
}
// set lines table
f.set.mutex.Lock()
f.lines = lines
f.set.mutex.Unlock()
return true
}
// SetLinesForContent sets the line offsets for the given file content.
func (f *File) SetLinesForContent(content []byte) {
var lines []int
line := 0
for offset, b := range content {
if line >= 0 {
lines = append(lines, line)
}
line = -1
if b == '\n' {
line = offset + 1
}
}
// set lines table
f.set.mutex.Lock()
f.lines = lines
f.set.mutex.Unlock()
}
// A lineInfo object describes alternative file and line number
// information (such as provided via a //line comment in a .go
// file) for a given file offset.
type lineInfo struct {
// fields are exported to make them accessible to gob
Offset int
Filename string
Line int
}
// AddLineInfo adds alternative file and line number information for
// a given file offset. The offset must be larger than the offset for
// the previously added alternative line info and smaller than the
// file size; otherwise the information is ignored.
//
// AddLineInfo is typically used to register alternative position
// information for //line filename:line comments in source files.
//
func (f *File) AddLineInfo(offset int, filename string, line int) {
f.set.mutex.Lock()
if i := len(f.infos); i == 0 || f.infos[i-1].Offset < offset && offset < f.size {
f.infos = append(f.infos, lineInfo{offset, filename, line})
}
f.set.mutex.Unlock()
}
// Pos returns the Pos value for the given file offset;
// the offset must be <= f.Size().
// f.Pos(f.Offset(p)) == p.
//
func (f *File) Pos(offset int) Pos {
if offset > f.size {
panic("illegal file offset")
}
return Pos(f.base + offset)
}
// Offset returns the offset for the given file position p;
// p must be a valid Pos value in that file.
// f.Offset(f.Pos(offset)) == offset.
//
func (f *File) Offset(p Pos) int {
if int(p) < f.base || int(p) > f.base+f.size {
panic("illegal Pos value")
}
return int(p) - f.base
}
// Line returns the line number for the given file position p;
// p must be a Pos value in that file or NoPos.
//
func (f *File) Line(p Pos) int {
// TODO(gri) this can be implemented much more efficiently
return f.Position(p).Line
}
func searchLineInfos(a []lineInfo, x int) int {
return sort.Search(len(a), func(i int) bool { return a[i].Offset > x }) - 1
}
// info returns the file name, line, and column number for a file offset.
func (f *File) info(offset int) (filename string, line, column int) {
filename = f.name
if i := searchInts(f.lines, offset); i >= 0 {
line, column = i+1, offset-f.lines[i]+1
}
if len(f.infos) > 0 {
// almost no files have extra line infos
if i := searchLineInfos(f.infos, offset); i >= 0 {
alt := &f.infos[i]
filename = alt.Filename
if i := searchInts(f.lines, alt.Offset); i >= 0 {
line += alt.Line - i - 1
}
}
}
return
}
func (f *File) position(p Pos) (pos Position) {
offset := int(p) - f.base
pos.Offset = offset
pos.Filename, pos.Line, pos.Column = f.info(offset)
return
}
// Position returns the Position value for the given file position p;
// p must be a Pos value in that file or NoPos.
//
func (f *File) Position(p Pos) (pos Position) {
if p != NoPos {
if int(p) < f.base || int(p) > f.base+f.size {
panic("illegal Pos value")
}
pos = f.position(p)
}
return
}
// -----------------------------------------------------------------------------
// FileSet
// A FileSet represents a set of source files.
// Methods of file sets are synchronized; multiple goroutines
// may invoke them concurrently.
//
type FileSet struct {
mutex sync.RWMutex // protects the file set
base int // base offset for the next file
files []*File // list of files in the order added to the set
last *File // cache of last file looked up
}
// NewFileSet creates a new file set.
func NewFileSet() *FileSet {
s := new(FileSet)
s.base = 1 // 0 == NoPos
return s
}
// Base returns the minimum base offset that must be provided to
// AddFile when adding the next file.
//
func (s *FileSet) Base() int {
s.mutex.RLock()
b := s.base
s.mutex.RUnlock()
return b
}
// AddFile adds a new file with a given filename, base offset, and file size
// to the file set s and returns the file. Multiple files may have the same
// name. The base offset must not be smaller than the FileSet's Base(), and
// size must not be negative.
//
// Adding the file will set the file set's Base() value to base + size + 1
// as the minimum base value for the next file. The following relationship
// exists between a Pos value p for a given file offset offs:
//
// int(p) = base + offs
//
// with offs in the range [0, size] and thus p in the range [base, base+size].
// For convenience, File.Pos may be used to create file-specific position
// values from a file offset.
//
func (s *FileSet) AddFile(filename string, base, size int) *File {
s.mutex.Lock()
defer s.mutex.Unlock()
if base < s.base || size < 0 {
panic("illegal base or size")
}
// base >= s.base && size >= 0
f := &File{s, filename, base, size, []int{0}, nil}
base += size + 1 // +1 because EOF also has a position
if base < 0 {
panic("token.Pos offset overflow (> 2G of source code in file set)")
}
// add the file to the file set
s.base = base
s.files = append(s.files, f)
s.last = f
return f
}
// Iterate calls f for the files in the file set in the order they were added
// until f returns false.
//
func (s *FileSet) Iterate(f func(*File) bool) {
for i := 0; ; i++ {
var file *File
s.mutex.RLock()
if i < len(s.files) {
file = s.files[i]
}
s.mutex.RUnlock()
if file == nil || !f(file) {
break
}
}
}
func searchFiles(a []*File, x int) int {
return sort.Search(len(a), func(i int) bool { return a[i].base > x }) - 1
}
func (s *FileSet) file(p Pos) *File {
// common case: p is in last file
if f := s.last; f != nil && f.base <= int(p) && int(p) <= f.base+f.size {
return f
}
// p is not in last file - search all files
if i := searchFiles(s.files, int(p)); i >= 0 {
f := s.files[i]
// f.base <= int(p) by definition of searchFiles
if int(p) <= f.base+f.size {
s.last = f
return f
}
}
return nil
}
// File returns the file that contains the position p.
// If no such file is found (for instance for p == NoPos),
// the result is nil.
//
func (s *FileSet) File(p Pos) (f *File) {
if p != NoPos {
s.mutex.RLock()
f = s.file(p)
s.mutex.RUnlock()
}
return
}
// Position converts a Pos in the fileset into a general Position.
func (s *FileSet) Position(p Pos) (pos Position) {
if p != NoPos {
s.mutex.RLock()
if f := s.file(p); f != nil {
pos = f.position(p)
}
s.mutex.RUnlock()
}
return
}
// -----------------------------------------------------------------------------
// Helper functions
func searchInts(a []int, x int) int {
// This function body is a manually inlined version of:
//
// return sort.Search(len(a), func(i int) bool { return a[i] > x }) - 1
//
// With better compiler optimizations, this may not be needed in the
// future, but at the moment this change improves the go/printer
// benchmark performance by ~30%. This has a direct impact on the
// speed of gofmt and thus seems worthwhile (2011-04-29).
// TODO(gri): Remove this when compilers have caught up.
i, j := 0, len(a)
for i < j {
h := i + (j-i)/2 // avoid overflow when computing h
// i ≤ h < j
if a[h] <= x {
i = h + 1
} else {
j = h
}
}
return i - 1
}

View File

@ -0,0 +1,181 @@
// Copyright 2010 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 token
import (
"fmt"
"testing"
)
func checkPos(t *testing.T, msg string, p, q Position) {
if p.Filename != q.Filename {
t.Errorf("%s: expected filename = %q; got %q", msg, q.Filename, p.Filename)
}
if p.Offset != q.Offset {
t.Errorf("%s: expected offset = %d; got %d", msg, q.Offset, p.Offset)
}
if p.Line != q.Line {
t.Errorf("%s: expected line = %d; got %d", msg, q.Line, p.Line)
}
if p.Column != q.Column {
t.Errorf("%s: expected column = %d; got %d", msg, q.Column, p.Column)
}
}
func TestNoPos(t *testing.T) {
if NoPos.IsValid() {
t.Errorf("NoPos should not be valid")
}
var fset *FileSet
checkPos(t, "nil NoPos", fset.Position(NoPos), Position{})
fset = NewFileSet()
checkPos(t, "fset NoPos", fset.Position(NoPos), Position{})
}
var tests = []struct {
filename string
source []byte // may be nil
size int
lines []int
}{
{"a", []byte{}, 0, []int{}},
{"b", []byte("01234"), 5, []int{0}},
{"c", []byte("\n\n\n\n\n\n\n\n\n"), 9, []int{0, 1, 2, 3, 4, 5, 6, 7, 8}},
{"d", nil, 100, []int{0, 5, 10, 20, 30, 70, 71, 72, 80, 85, 90, 99}},
{"e", nil, 777, []int{0, 80, 100, 120, 130, 180, 267, 455, 500, 567, 620}},
{"f", []byte("package p\n\nimport \"fmt\""), 23, []int{0, 10, 11}},
{"g", []byte("package p\n\nimport \"fmt\"\n"), 24, []int{0, 10, 11}},
{"h", []byte("package p\n\nimport \"fmt\"\n "), 25, []int{0, 10, 11, 24}},
}
func linecol(lines []int, offs int) (int, int) {
prevLineOffs := 0
for line, lineOffs := range lines {
if offs < lineOffs {
return line, offs - prevLineOffs + 1
}
prevLineOffs = lineOffs
}
return len(lines), offs - prevLineOffs + 1
}
func verifyPositions(t *testing.T, fset *FileSet, f *File, lines []int) {
for offs := 0; offs < f.Size(); offs++ {
p := f.Pos(offs)
offs2 := f.Offset(p)
if offs2 != offs {
t.Errorf("%s, Offset: expected offset %d; got %d", f.Name(), offs, offs2)
}
line, col := linecol(lines, offs)
msg := fmt.Sprintf("%s (offs = %d, p = %d)", f.Name(), offs, p)
checkPos(t, msg, f.Position(f.Pos(offs)), Position{f.Name(), offs, line, col})
checkPos(t, msg, fset.Position(p), Position{f.Name(), offs, line, col})
}
}
func makeTestSource(size int, lines []int) []byte {
src := make([]byte, size)
for _, offs := range lines {
if offs > 0 {
src[offs-1] = '\n'
}
}
return src
}
func TestPositions(t *testing.T) {
const delta = 7 // a non-zero base offset increment
fset := NewFileSet()
for _, test := range tests {
// verify consistency of test case
if test.source != nil && len(test.source) != test.size {
t.Errorf("%s: inconsistent test case: expected file size %d; got %d", test.filename, test.size, len(test.source))
}
// add file and verify name and size
f := fset.AddFile(test.filename, fset.Base()+delta, test.size)
if f.Name() != test.filename {
t.Errorf("expected filename %q; got %q", test.filename, f.Name())
}
if f.Size() != test.size {
t.Errorf("%s: expected file size %d; got %d", f.Name(), test.size, f.Size())
}
if fset.File(f.Pos(0)) != f {
t.Errorf("%s: f.Pos(0) was not found in f", f.Name())
}
// add lines individually and verify all positions
for i, offset := range test.lines {
f.AddLine(offset)
if f.LineCount() != i+1 {
t.Errorf("%s, AddLine: expected line count %d; got %d", f.Name(), i+1, f.LineCount())
}
// adding the same offset again should be ignored
f.AddLine(offset)
if f.LineCount() != i+1 {
t.Errorf("%s, AddLine: expected unchanged line count %d; got %d", f.Name(), i+1, f.LineCount())
}
verifyPositions(t, fset, f, test.lines[0:i+1])
}
// add lines with SetLines and verify all positions
if ok := f.SetLines(test.lines); !ok {
t.Errorf("%s: SetLines failed", f.Name())
}
if f.LineCount() != len(test.lines) {
t.Errorf("%s, SetLines: expected line count %d; got %d", f.Name(), len(test.lines), f.LineCount())
}
verifyPositions(t, fset, f, test.lines)
// add lines with SetLinesForContent and verify all positions
src := test.source
if src == nil {
// no test source available - create one from scratch
src = makeTestSource(test.size, test.lines)
}
f.SetLinesForContent(src)
if f.LineCount() != len(test.lines) {
t.Errorf("%s, SetLinesForContent: expected line count %d; got %d", f.Name(), len(test.lines), f.LineCount())
}
verifyPositions(t, fset, f, test.lines)
}
}
func TestLineInfo(t *testing.T) {
fset := NewFileSet()
f := fset.AddFile("foo", fset.Base(), 500)
lines := []int{0, 42, 77, 100, 210, 220, 277, 300, 333, 401}
// add lines individually and provide alternative line information
for _, offs := range lines {
f.AddLine(offs)
f.AddLineInfo(offs, "bar", 42)
}
// verify positions for all offsets
for offs := 0; offs <= f.Size(); offs++ {
p := f.Pos(offs)
_, col := linecol(lines, offs)
msg := fmt.Sprintf("%s (offs = %d, p = %d)", f.Name(), offs, p)
checkPos(t, msg, f.Position(f.Pos(offs)), Position{"bar", offs, 42, col})
checkPos(t, msg, fset.Position(p), Position{"bar", offs, 42, col})
}
}
func TestFiles(t *testing.T) {
fset := NewFileSet()
for i, test := range tests {
fset.AddFile(test.filename, fset.Base(), test.size)
j := 0
fset.Iterate(func(f *File) bool {
if f.Name() != tests[j].filename {
t.Errorf("expected filename = %s; got %s", tests[j].filename, f.Name())
}
j++
return true
})
if j != i+1 {
t.Errorf("expected %d files; got %d", i+1, j)
}
}
}

View File

@ -0,0 +1,56 @@
// Copyright 2011 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 token
type serializedFile struct {
// fields correspond 1:1 to fields with same (lower-case) name in File
Name string
Base int
Size int
Lines []int
Infos []lineInfo
}
type serializedFileSet struct {
Base int
Files []serializedFile
}
// Read calls decode to deserialize a file set into s; s must not be nil.
func (s *FileSet) Read(decode func(interface{}) error) error {
var ss serializedFileSet
if err := decode(&ss); err != nil {
return err
}
s.mutex.Lock()
s.base = ss.Base
files := make([]*File, len(ss.Files))
for i := 0; i < len(ss.Files); i++ {
f := &ss.Files[i]
files[i] = &File{s, f.Name, f.Base, f.Size, f.Lines, f.Infos}
}
s.files = files
s.last = nil
s.mutex.Unlock()
return nil
}
// Write calls encode to serialize the file set s.
func (s *FileSet) Write(encode func(interface{}) error) error {
var ss serializedFileSet
s.mutex.Lock()
ss.Base = s.base
files := make([]serializedFile, len(s.files))
for i, f := range s.files {
files[i] = serializedFile{f.name, f.base, f.size, f.lines, f.infos}
}
ss.Files = files
s.mutex.Unlock()
return encode(ss)
}

View File

@ -0,0 +1,111 @@
// Copyright 2011 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 token
import (
"bytes"
"encoding/gob"
"fmt"
"testing"
)
// equal returns nil if p and q describe the same file set;
// otherwise it returns an error describing the discrepancy.
func equal(p, q *FileSet) error {
if p == q {
// avoid deadlock if p == q
return nil
}
// not strictly needed for the test
p.mutex.Lock()
q.mutex.Lock()
defer q.mutex.Unlock()
defer p.mutex.Unlock()
if p.base != q.base {
return fmt.Errorf("different bases: %d != %d", p.base, q.base)
}
if len(p.files) != len(q.files) {
return fmt.Errorf("different number of files: %d != %d", len(p.files), len(q.files))
}
for i, f := range p.files {
g := q.files[i]
if f.set != p {
return fmt.Errorf("wrong fileset for %q", f.name)
}
if g.set != q {
return fmt.Errorf("wrong fileset for %q", g.name)
}
if f.name != g.name {
return fmt.Errorf("different filenames: %q != %q", f.name, g.name)
}
if f.base != g.base {
return fmt.Errorf("different base for %q: %d != %d", f.name, f.base, g.base)
}
if f.size != g.size {
return fmt.Errorf("different size for %q: %d != %d", f.name, f.size, g.size)
}
for j, l := range f.lines {
m := g.lines[j]
if l != m {
return fmt.Errorf("different offsets for %q", f.name)
}
}
for j, l := range f.infos {
m := g.infos[j]
if l.Offset != m.Offset || l.Filename != m.Filename || l.Line != m.Line {
return fmt.Errorf("different infos for %q", f.name)
}
}
}
// we don't care about .last - it's just a cache
return nil
}
func checkSerialize(t *testing.T, p *FileSet) {
var buf bytes.Buffer
encode := func(x interface{}) error {
return gob.NewEncoder(&buf).Encode(x)
}
if err := p.Write(encode); err != nil {
t.Errorf("writing fileset failed: %s", err)
return
}
q := NewFileSet()
decode := func(x interface{}) error {
return gob.NewDecoder(&buf).Decode(x)
}
if err := q.Read(decode); err != nil {
t.Errorf("reading fileset failed: %s", err)
return
}
if err := equal(p, q); err != nil {
t.Errorf("filesets not identical: %s", err)
}
}
func TestSerialization(t *testing.T) {
p := NewFileSet()
checkSerialize(t, p)
// add some files
for i := 0; i < 10; i++ {
f := p.AddFile(fmt.Sprintf("file%d", i), p.Base()+i, i*100)
checkSerialize(t, p)
// add some lines and alternative file infos
line := 1000
for offs := 0; offs < f.Size(); offs += 40 + i {
f.AddLine(offs)
if offs%7 == 0 {
f.AddLineInfo(offs, fmt.Sprintf("file%d", offs), line)
line += 33
}
}
checkSerialize(t, p)
}
}

View File

@ -0,0 +1,83 @@
// 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.
// Package token defines constants representing the lexical tokens of the gcfg
// configuration syntax and basic operations on tokens (printing, predicates).
//
// Note that the API for the token package may change to accommodate new
// features or implementation changes in gcfg.
//
package token
import "strconv"
// Token is the set of lexical tokens of the gcfg configuration syntax.
type Token int
// The list of tokens.
const (
// Special tokens
ILLEGAL Token = iota
EOF
COMMENT
literal_beg
// Identifiers and basic type literals
// (these tokens stand for classes of literals)
IDENT // section-name, variable-name
STRING // "subsection-name", variable value
literal_end
operator_beg
// Operators and delimiters
ASSIGN // =
LBRACK // [
RBRACK // ]
EOL // \n
operator_end
)
var tokens = [...]string{
ILLEGAL: "ILLEGAL",
EOF: "EOF",
COMMENT: "COMMENT",
IDENT: "IDENT",
STRING: "STRING",
ASSIGN: "=",
LBRACK: "[",
RBRACK: "]",
EOL: "\n",
}
// String returns the string corresponding to the token tok.
// For operators and delimiters, the string is the actual token character
// sequence (e.g., for the token ASSIGN, the string is "="). For all other
// tokens the string corresponds to the token constant name (e.g. for the
// token IDENT, the string is "IDENT").
//
func (tok Token) String() string {
s := ""
if 0 <= tok && tok < Token(len(tokens)) {
s = tokens[tok]
}
if s == "" {
s = "token(" + strconv.Itoa(int(tok)) + ")"
}
return s
}
// Predicates
// IsLiteral returns true for tokens corresponding to identifiers
// and basic type literals; it returns false otherwise.
//
func (tok Token) IsLiteral() bool { return literal_beg < tok && tok < literal_end }
// IsOperator returns true for tokens corresponding to operators and
// delimiters; it returns false otherwise.
//
func (tok Token) IsOperator() bool { return operator_beg < tok && tok < operator_end }

View File

@ -0,0 +1,23 @@
package types
// BoolValues defines the name and value mappings for ParseBool.
var BoolValues = map[string]interface{}{
"true": true, "yes": true, "on": true, "1": true,
"false": false, "no": false, "off": false, "0": false,
}
var boolParser = func() *EnumParser {
ep := &EnumParser{}
ep.AddVals(BoolValues)
return ep
}()
// ParseBool parses bool values according to the definitions in BoolValues.
// Parsing is case-insensitive.
func ParseBool(s string) (bool, error) {
v, err := boolParser.Parse(s)
if err != nil {
return false, err
}
return v.(bool), nil
}

View File

@ -0,0 +1,4 @@
// Package types defines helpers for type conversions.
//
// The API for this package is not finalized yet.
package types

View File

@ -0,0 +1,44 @@
package types
import (
"fmt"
"reflect"
"strings"
)
// EnumParser parses "enum" values; i.e. a predefined set of strings to
// predefined values.
type EnumParser struct {
Type string // type name; if not set, use type of first value added
CaseMatch bool // if true, matching of strings is case-sensitive
// PrefixMatch bool
vals map[string]interface{}
}
// AddVals adds strings and values to an EnumParser.
func (ep *EnumParser) AddVals(vals map[string]interface{}) {
if ep.vals == nil {
ep.vals = make(map[string]interface{})
}
for k, v := range vals {
if ep.Type == "" {
ep.Type = reflect.TypeOf(v).Name()
}
if !ep.CaseMatch {
k = strings.ToLower(k)
}
ep.vals[k] = v
}
}
// Parse parses the string and returns the value or an error.
func (ep EnumParser) Parse(s string) (interface{}, error) {
if !ep.CaseMatch {
s = strings.ToLower(s)
}
v, ok := ep.vals[s]
if !ok {
return false, fmt.Errorf("failed to parse %s %#q", ep.Type, s)
}
return v, nil
}

View File

@ -0,0 +1,29 @@
package types
import (
"testing"
)
func TestEnumParserBool(t *testing.T) {
for _, tt := range []struct {
val string
res bool
ok bool
}{
{val: "tRuE", res: true, ok: true},
{val: "False", res: false, ok: true},
{val: "t", ok: false},
} {
b, err := ParseBool(tt.val)
switch {
case tt.ok && err != nil:
t.Errorf("%q: got error %v, want %v", tt.val, err, tt.res)
case !tt.ok && err == nil:
t.Errorf("%q: got %v, want error", tt.val, b)
case tt.ok && b != tt.res:
t.Errorf("%q: got %v, want %v", tt.val, b, tt.res)
default:
t.Logf("%q: got %v, %v", tt.val, b, err)
}
}
}

View File

@ -0,0 +1,86 @@
package types
import (
"fmt"
"strings"
)
// An IntMode is a mode for parsing integer values, representing a set of
// accepted bases.
type IntMode uint8
// IntMode values for ParseInt; can be combined using binary or.
const (
Dec IntMode = 1 << iota
Hex
Oct
)
// String returns a string representation of IntMode; e.g. `IntMode(Dec|Hex)`.
func (m IntMode) String() string {
var modes []string
if m&Dec != 0 {
modes = append(modes, "Dec")
}
if m&Hex != 0 {
modes = append(modes, "Hex")
}
if m&Oct != 0 {
modes = append(modes, "Oct")
}
return "IntMode(" + strings.Join(modes, "|") + ")"
}
var errIntAmbig = fmt.Errorf("ambiguous integer value; must include '0' prefix")
func prefix0(val string) bool {
return strings.HasPrefix(val, "0") || strings.HasPrefix(val, "-0")
}
func prefix0x(val string) bool {
return strings.HasPrefix(val, "0x") || strings.HasPrefix(val, "-0x")
}
// ParseInt parses val using mode into intptr, which must be a pointer to an
// integer kind type. Non-decimal value require prefix `0` or `0x` in the cases
// when mode permits ambiguity of base; otherwise the prefix can be omitted.
func ParseInt(intptr interface{}, val string, mode IntMode) error {
val = strings.TrimSpace(val)
verb := byte(0)
switch mode {
case Dec:
verb = 'd'
case Dec + Hex:
if prefix0x(val) {
verb = 'v'
} else {
verb = 'd'
}
case Dec + Oct:
if prefix0(val) && !prefix0x(val) {
verb = 'v'
} else {
verb = 'd'
}
case Dec + Hex + Oct:
verb = 'v'
case Hex:
if prefix0x(val) {
verb = 'v'
} else {
verb = 'x'
}
case Oct:
verb = 'o'
case Hex + Oct:
if prefix0(val) {
verb = 'v'
} else {
return errIntAmbig
}
}
if verb == 0 {
panic("unsupported mode")
}
return ScanFully(intptr, val, verb)
}

View File

@ -0,0 +1,67 @@
package types
import (
"reflect"
"testing"
)
func elem(p interface{}) interface{} {
return reflect.ValueOf(p).Elem().Interface()
}
func TestParseInt(t *testing.T) {
for _, tt := range []struct {
val string
mode IntMode
exp interface{}
ok bool
}{
{"0", Dec, int(0), true},
{"10", Dec, int(10), true},
{"-10", Dec, int(-10), true},
{"x", Dec, int(0), false},
{"0xa", Hex, int(0xa), true},
{"a", Hex, int(0xa), true},
{"10", Hex, int(0x10), true},
{"-0xa", Hex, int(-0xa), true},
{"0x", Hex, int(0x0), true}, // Scanf doesn't require digit behind 0x
{"-0x", Hex, int(0x0), true}, // Scanf doesn't require digit behind 0x
{"-a", Hex, int(-0xa), true},
{"-10", Hex, int(-0x10), true},
{"x", Hex, int(0), false},
{"10", Oct, int(010), true},
{"010", Oct, int(010), true},
{"-10", Oct, int(-010), true},
{"-010", Oct, int(-010), true},
{"10", Dec | Hex, int(10), true},
{"010", Dec | Hex, int(10), true},
{"0x10", Dec | Hex, int(0x10), true},
{"10", Dec | Oct, int(10), true},
{"010", Dec | Oct, int(010), true},
{"0x10", Dec | Oct, int(0), false},
{"10", Hex | Oct, int(0), false}, // need prefix to distinguish Hex/Oct
{"010", Hex | Oct, int(010), true},
{"0x10", Hex | Oct, int(0x10), true},
{"10", Dec | Hex | Oct, int(10), true},
{"010", Dec | Hex | Oct, int(010), true},
{"0x10", Dec | Hex | Oct, int(0x10), true},
} {
typ := reflect.TypeOf(tt.exp)
res := reflect.New(typ).Interface()
err := ParseInt(res, tt.val, tt.mode)
switch {
case tt.ok && err != nil:
t.Errorf("ParseInt(%v, %#v, %v): fail; got error %v, want ok",
typ, tt.val, tt.mode, err)
case !tt.ok && err == nil:
t.Errorf("ParseInt(%v, %#v, %v): fail; got %v, want error",
typ, tt.val, tt.mode, elem(res))
case tt.ok && !reflect.DeepEqual(elem(res), tt.exp):
t.Errorf("ParseInt(%v, %#v, %v): fail; got %v, want %v",
typ, tt.val, tt.mode, elem(res), tt.exp)
default:
t.Logf("ParseInt(%v, %#v, %s): pass; got %v, error %v",
typ, tt.val, tt.mode, elem(res), err)
}
}
}

View File

@ -0,0 +1,23 @@
package types
import (
"fmt"
"io"
"reflect"
)
// ScanFully uses fmt.Sscanf with verb to fully scan val into ptr.
func ScanFully(ptr interface{}, val string, verb byte) error {
t := reflect.ValueOf(ptr).Elem().Type()
// attempt to read extra bytes to make sure the value is consumed
var b []byte
n, err := fmt.Sscanf(val, "%"+string(verb)+"%s", ptr, &b)
switch {
case n < 1 || n == 1 && err != io.EOF:
return fmt.Errorf("failed to parse %q as %v: %v", val, t, err)
case n > 1:
return fmt.Errorf("failed to parse %q as %v: extra characters %q", val, t, string(b))
}
// n == 1 && err == io.EOF
return nil
}

View File

@ -0,0 +1,36 @@
package types
import (
"reflect"
"testing"
)
func TestScanFully(t *testing.T) {
for _, tt := range []struct {
val string
verb byte
res interface{}
ok bool
}{
{"a", 'v', int(0), false},
{"0x", 'v', int(0), true},
{"0x", 'd', int(0), false},
} {
d := reflect.New(reflect.TypeOf(tt.res)).Interface()
err := ScanFully(d, tt.val, tt.verb)
switch {
case tt.ok && err != nil:
t.Errorf("ScanFully(%T, %q, '%c'): want ok, got error %v",
d, tt.val, tt.verb, err)
case !tt.ok && err == nil:
t.Errorf("ScanFully(%T, %q, '%c'): want error, got %v",
d, tt.val, tt.verb, elem(d))
case tt.ok && err == nil && !reflect.DeepEqual(tt.res, elem(d)):
t.Errorf("ScanFully(%T, %q, '%c'): want %v, got %v",
d, tt.val, tt.verb, tt.res, elem(d))
default:
t.Logf("ScanFully(%T, %q, '%c') = %v; *ptr==%v",
d, tt.val, tt.verb, err, elem(d))
}
}
}

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Yusuke Inuzuka
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,10 @@
.PHONY: build test
build:
./_tools/go-inline *.go && go fmt . && go build
glua: *.go pm/*.go cmd/glua/glua.go
./_tools/go-inline *.go && go fmt . && go build cmd/glua/glua.go
test:
./_tools/go-inline *.go && go fmt . && go test

View File

@ -0,0 +1,708 @@
===============================================================================
GopherLua: VM and compiler for Lua in Go.
===============================================================================
.. image:: https://godoc.org/github.com/yuin/gopher-lua?status.svg
:target: http://godoc.org/github.com/yuin/gopher-lua
.. image:: https://travis-ci.org/yuin/gopher-lua.svg
:target: https://travis-ci.org/yuin/gopher-lua
.. image:: https://coveralls.io/repos/yuin/gopher-lua/badge.svg
:target: https://coveralls.io/r/yuin/gopher-lua
.. image:: https://badges.gitter.im/Join%20Chat.svg
:alt: Join the chat at https://gitter.im/yuin/gopher-lua
:target: https://gitter.im/yuin/gopher-lua?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
GopherLua is a Lua5.1 VM and compiler written in Go. GopherLua has a same goal
with Lua: **Be a scripting language with extensible semantics** . It provides
Go APIs that allow you to easily embed a scripting language to your Go host
programs.
.. contents::
:depth: 1
----------------------------------------------------------------
Design principle
----------------------------------------------------------------
- Be a scripting language with extensible semantics.
- User-friendly Go API
- The stack based API like the one used in the original Lua
implementation will cause a performance improvements in GopherLua
(It will reduce memory allocations and concrete type <-> interface conversions).
GopherLua API is **not** the stack based API.
GopherLua give preference to the user-friendliness over the performance.
----------------------------------------------------------------
How about performance?
----------------------------------------------------------------
GopherLua is not fast but not too slow, I think.
There are some benchmarks on the `wiki page <https://github.com/yuin/gopher-lua/wiki/Benchmarks>`_ .
----------------------------------------------------------------
Installation
----------------------------------------------------------------
.. code-block:: bash
go get github.com/yuin/gopher-lua
GopherLua supports >= Go1.4.
----------------------------------------------------------------
Usage
----------------------------------------------------------------
GopherLua APIs perform in much the same way as Lua, **but the stack is used only
for passing arguments and receiving returned values.**
GopherLua supports channel operations. See **"Goroutines"** section.
Import a package.
.. code-block:: go
import (
"github.com/yuin/gopher-lua"
)
Run scripts in the VM.
.. code-block:: go
L := lua.NewState()
defer L.Close()
if err := L.DoString(`print("hello")`); err != nil {
panic(err)
}
.. code-block:: go
L := lua.NewState()
defer L.Close()
if err := L.DoFile("hello.lua"); err != nil {
panic(err)
}
Refer to `Lua Reference Manual <http://www.lua.org/manual/5.1/>`_ and `Go doc <http://godoc.org/github.com/yuin/gopher-lua>`_ for further information.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Data model
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
All data in a GopherLua program is an ``LValue`` . ``LValue`` is an interface
type that has following methods.
- ``String() string``
- ``Type() LValueType``
Objects implement an LValue interface are
================ ========================= ================== =======================
Type name Go type Type() value Constants
================ ========================= ================== =======================
``LNilType`` (constants) ``LTNil`` ``LNil``
``LBool`` (constants) ``LTBool`` ``LTrue``, ``LFalse``
``LNumber`` float64 ``LTNumber`` ``-``
``LString`` string ``LTString`` ``-``
``LFunction`` struct pointer ``LTFunction`` ``-``
``LUserData`` struct pointer ``LTUserData`` ``-``
``LState`` struct pointer ``LTThread`` ``-``
``LTable`` struct pointer ``LTTable`` ``-``
``LChannel`` chan LValue ``LTChannel`` ``-``
================ ========================= ================== =======================
You can test an object type in Go way(type assertion) or using a ``Type()`` value.
.. code-block:: go
lv := L.Get(-1) // get the value at the top of the stack
if str, ok := lv.(lua.LString); ok {
// lv is LString
fmt.Println(string(str))
}
if lv.Type() != lua.LTString {
panic("string required.")
}
.. code-block:: go
lv := L.Get(-1) // get the value at the top of the stack
if tbl, ok := lv.(*lua.LTable); ok {
// lv is LTable
fmt.Println(L.ObjLen(tbl))
}
Note that ``LBool`` , ``LNumber`` , ``LString`` is not a pointer.
To test ``LNilType`` and ``LBool``, You **must** use pre-defined constants.
.. code-block:: go
lv := L.Get(-1) // get the value at the top of the stack
if lv == LTrue { // correct
}
if bl, ok == lv.(lua.LBool); ok && bool(bl) { // wrong
}
In Lua, both ``nil`` and ``false`` make a condition false. ``LVIsFalse`` and ``LVAsBool`` implement this specification.
.. code-block:: go
lv := L.Get(-1) // get the value at the top of the stack
if LVIsFalse(lv) { // lv is nil or false
}
if LVAsBool(lv) { // lv is neither nil nor false
}
Objects that based on go structs(``LFunction``. ``LUserData``, ``LTable``)
have some public methods and fields. You can use these methods and fields for
performance and debugging, but there are some limitations.
- Metatable does not work.
- No error handlings.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Callstack & Registry size
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Size of the callstack & registry is **fixed** for mainly performance.
You can change the default size of the callstack & registry.
.. code-block:: go
lua.RegistrySize = 1024 * 20
lua.CallStackSize = 1024
L := lua.NewState()
defer L.Close()
You can also create an LState object that has the callstack & registry size specified by ``Options`` .
.. code-block:: go
L := lua.NewState(lua.Options{
CallStackSize: 120,
RegistrySize: 120*20,
})
An LState object that has been created by ``*LState#NewThread()`` inherits the callstack & registry size from the parent LState object.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
API
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Refer to `Lua Reference Manual <http://www.lua.org/manual/5.1/>`_ and `Go doc(LState methods) <http://godoc.org/github.com/yuin/gopher-lua>`_ for further information.
+++++++++++++++++++++++++++++++++++++++++
Calling Go from Lua
+++++++++++++++++++++++++++++++++++++++++
.. code-block:: go
func Double(L *lua.LState) int {
lv := L.ToInt(1) /* get argument */
L.Push(lua.LNumber(lv * 2)) /* push result */
return 1 /* number of results */
}
func main() {
L := lua.NewState()
defer L.Close()
L.SetGlobal("double", L.NewFunction(Double)) /* Original lua_setglobal uses stack... */
}
.. code-block:: lua
print(double(20)) -- > "40"
Any function registered with GopherLua is a ``lua.LGFunction``, defined in ``value.go``
.. code-block:: go
type LGFunction func(*LState) int
Working with coroutines.
.. code-block:: go
co := L.NewThread() /* create a new thread */
fn := L.GetGlobal("coro").(*lua.LFunction) /* get function from lua */
for {
st, err, values := L.Resume(co, fn)
if st == lua.ResumeError {
fmt.Println("yield break(error)")
fmt.Println(err.Error())
break
}
for i, lv := range values {
fmt.Printf("%v : %v\n", i, lv)
}
if st == lua.ResumeOK {
fmt.Println("yield break(ok)")
break
}
}
+++++++++++++++++++++++++++++++++++++++++
Creating a module by Go
+++++++++++++++++++++++++++++++++++++++++
mymodule.go
.. code-block:: go
package mymodule
import (
"github.com/yuin/gopher-lua"
)
func Loader(L *lua.LState) int {
// register functions to the table
mod := L.SetFuncs(L.NewTable(), exports)
// register other stuff
L.SetField(mod, "name", lua.LString("value"))
// returns the module
L.Push(mod)
return 1
}
var exports = map[string]lua.LGFunction{
"myfunc": myfunc,
}
func myfunc(L *lua.LState) int {
return 0
}
mymain.go
.. code-block:: go
package main
import (
"./mymodule"
"github.com/yuin/gopher-lua"
)
func main() {
L := lua.NewState()
defer L.Close()
L.PreloadModule("mymodule", mymodule.Loader)
if err := L.DoFile("main.lua"); err != nil {
panic(err)
}
}
main.lua
.. code-block:: lua
local m = require("mymodule")
m.myfunc()
print(m.name)
+++++++++++++++++++++++++++++++++++++++++
Calling Lua from Go
+++++++++++++++++++++++++++++++++++++++++
.. code-block:: go
L := lua.NewState()
defer L.Close()
if err := L.DoFile("double.lua"); err != nil {
panic(err)
}
if err := L.CallByParam(lua.P{
Fn: L.GetGlobal("double"),
NRet: 1,
Protect: true,
}, lua.LNumber(10)); err != nil {
panic(err)
}
ret := L.Get(-1) // returned value
L.Pop(1) // remove received value
If ``Protect`` is false, GopherLua will panic instead of returning an ``error`` value.
+++++++++++++++++++++++++++++++++++++++++
User-Defined types
+++++++++++++++++++++++++++++++++++++++++
You can extend GopherLua with new types written in Go.
``LUserData`` is provided for this purpose.
.. code-block:: go
type Person struct {
Name string
}
const luaPersonTypeName = "person"
// Registers my person type to given L.
func registerPersonType(L *lua.LState) {
mt := L.NewTypeMetatable(luaPersonTypeName)
L.SetGlobal("person", mt)
// static attributes
L.SetField(mt, "new", L.NewFunction(newPerson))
// methods
L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), personMethods))
}
// Constructor
func newPerson(L *lua.LState) int {
person := &Person{L.CheckString(1)}
ud := L.NewUserData()
ud.Value = person
L.SetMetatable(ud, L.GetTypeMetatable(luaPersonTypeName))
L.Push(ud)
return 1
}
// Checks whether the first lua argument is a *LUserData with *Person and returns this *Person.
func checkPerson(L *lua.LState) *Person {
ud := L.CheckUserData(1)
if v, ok := ud.Value.(*Person); ok {
return v
}
L.ArgError(1, "person expected")
return nil
}
var personMethods = map[string]lua.LGFunction{
"name": personGetSetName,
}
// Getter and setter for the Person#Name
func personGetSetName(L *lua.LState) int {
p := checkPerson(L)
if L.GetTop() == 2 {
p.Name = L.CheckString(2)
return 0
}
L.Push(lua.LString(p.Name))
return 1
}
func main() {
L := lua.NewState()
defer L.Close()
registerPersonType(L)
if err := L.DoString(`
p = person.new("Steeve")
print(p:name()) -- "Steeve"
p:name("Alice")
print(p:name()) -- "Alice"
`); err != nil {
panic(err)
}
}
+++++++++++++++++++++++++++++++++++++++++
Goroutines
+++++++++++++++++++++++++++++++++++++++++
The ``LState`` is not goroutine-safe. It is recommended to use one LState per goroutine and communicate between goroutines by using channels.
Channels are represented by ``channel`` objects in GopherLua. And a ``channel`` table provides functions for performing channel operations.
Some objects can not be sent over channels due to having non-goroutine-safe objects inside itself.
- a thread(state)
- a function
- an userdata
- a table with a metatable
You **must not** send these objects from Go APIs to channels.
.. code-block:: go
func receiver(ch, quit chan lua.LValue) {
L := lua.NewState()
defer L.Close()
L.SetGlobal("ch", lua.LChannel(ch))
L.SetGlobal("quit", lua.LChannel(quit))
if err := L.DoString(`
local exit = false
while not exit do
channel.select(
{"|<-", ch, function(ok, v)
if not ok then
print("channel closed")
exit = true
else
print("received:", v)
end
end},
{"|<-", quit, function(ok, v)
print("quit")
exit = true
end}
)
end
`); err != nil {
panic(err)
}
}
func sender(ch, quit chan lua.LValue) {
L := lua.NewState()
defer L.Close()
L.SetGlobal("ch", lua.LChannel(ch))
L.SetGlobal("quit", lua.LChannel(quit))
if err := L.DoString(`
ch:send("1")
ch:send("2")
`); err != nil {
panic(err)
}
ch <- lua.LString("3")
quit <- lua.LTrue
}
func main() {
ch := make(chan lua.LValue)
quit := make(chan lua.LValue)
go receiver(ch, quit)
go sender(ch, quit)
time.Sleep(3 * time.Second)
}
'''''''''''''''
Go API
'''''''''''''''
``ToChannel``, ``CheckChannel``, ``OptChannel`` are available.
Refer to `Go doc(LState methods) <http://godoc.org/github.com/yuin/gopher-lua>`_ for further information.
'''''''''''''''
Lua API
'''''''''''''''
- **channel.make([buf:int]) -> ch:channel**
- Create new channel that has a buffer size of ``buf``. By default, ``buf`` is 0.
- **channel.select(case:table [, case:table, case:table ...]) -> {index:int, recv:any, ok}**
- Same as the ``select`` statement in Go. It returns the index of the chosen case and, if that
case was a receive operation, the value received and a boolean indicating whether the channel has been closed.
- ``case`` is a table that outlined below.
- receiving: `{"|<-", ch:channel [, handler:func(ok, data:any)]}`
- sending: `{"<-|", ch:channel, data:any [, handler:func(data:any)]}`
- default: `{"default" [, handler:func()]}`
``channel.select`` examples:
.. code-block:: lua
local idx, recv, ok = channel.select(
{"|<-", ch1},
{"|<-", ch2}
)
if not ok then
print("closed")
elseif idx == 1 then -- received from ch1
print(recv)
elseif idx == 2 then -- received from ch2
print(recv)
end
.. code-block:: lua
channel.select(
{"|<-", ch1, function(ok, data)
print(ok, data)
end},
{"<-|", ch2, "value", function(data)
print(data)
end},
{"default", function()
print("default action")
end}
)
- **channel:send(data:any)**
- Send ``data`` over the channel.
- **channel:receive() -> ok:bool, data:any**
- Receive some data over the channel.
- **channel:close()**
- Close the channel.
''''''''''''''''''''''''''''''
The LState pool pattern
''''''''''''''''''''''''''''''
To create per-thread LState instances, You can use the ``sync.Pool`` like mechanism.
.. code-block:: go
type lStatePool struct {
m sync.Mutex
saved []*lua.LState
}
func (pl *lStatePool) Get() *lua.LState {
pl.m.Lock()
defer pl.m.Unlock()
n := len(pl.saved)
if n == 0 {
return pl.New()
}
x := pl.saved[n-1]
pl.saved = pl.saved[0 : n-1]
return x
}
func (pl *lStatePool) New() *lua.LState {
L := lua.NewState()
// setting the L up here.
// load scripts, set global variables, share channels, etc...
return L
}
func (pl *lStatePool) Put(L *lua.LState) {
pl.m.Lock()
defer pl.m.Unlock()
pl.saved = append(pl.saved, L)
}
func (pl *lStatePool) Shutdown() {
for _, L := range pl.saved {
L.Close()
}
}
// Global LState pool
var luaPool = &lStatePool{
saved: make([]*lua.LState, 0, 4),
}
Now, you can get per-thread LState objects from the ``luaPool`` .
.. code-block:: go
func MyWorker() {
L := luaPool.Get()
defer luaPool.Put(L)
/* your code here */
}
func main() {
defer luaPool.Shutdown()
go MyWorker()
go MyWorker()
/* etc... */
}
----------------------------------------------------------------
Differences between Lua and GopherLua
----------------------------------------------------------------
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Goroutines
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- GopherLua supports channel operations.
- GopherLua has a type named ``channel``.
- The ``channel`` table provides functions for performing channel operations.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Unsupported functions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- ``string.dump``
- ``os.setlocale``
- ``collectgarbage``
- ``lua_Debug.namewhat``
- ``package.loadlib``
- debug hooks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Miscellaneous notes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- ``file:setvbuf`` does not support a line buffering.
- Daylight saving time is not supported.
- GopherLua has a function to set an environment variable : ``os.setenv(name, value)``
----------------------------------------------------------------
Standalone interpreter
----------------------------------------------------------------
Lua has an interpreter called ``lua`` . GopherLua has an interpreter called ``glua`` .
.. code-block:: bash
go get github.com/yuin/gopher-lua/cmd/glua
``glua`` has same options as ``lua`` .
----------------------------------------------------------------
How to Contribute
----------------------------------------------------------------
Any kind of contributions are welcome.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Pull requests
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Our workflow is based on the `github-flow <https://guides.github.com/introduction/flow/>`_ .
1. Create a new issue.
2. Fork the project.
3. Clone your fork and add the upstream.
::
git remote add upstream https://github.com/yuin/gopher-lua.git
4. Pull new changes from the upstream.
::
git checkout master
git fetch upstream
git merge upstream/master
5. Create a feature branch
::
git checkout -b <branch-name>
6. Commit your changes and reference the issue number in your comment.
::
git commit -m "Issue #<issue-ref> : <your message>"
7. Push the feature branch to your remote repository.
::
git push origin <branch-name>
8. Open new pull request.
----------------------------------------------------------------
Libraries for GopherLua
----------------------------------------------------------------
- `gopher-luar <https://github.com/layeh/gopher-luar>`_ : Custom type reflection for gopher-lua
- `gluamapper <https://github.com/yuin/gluamapper>`_ : Mapping a Lua table to a Go struct
- `gluahttp <https://github.com/cjoudrey/gluahttp>`_ : HTTP request module for gopher-lua
- `gopher-json <https://github.com/layeh/gopher-json>`_ : a simple JSON encoder/decoder for gopher-lua
----------------------------------------------------------------
License
----------------------------------------------------------------
MIT
----------------------------------------------------------------
Author
----------------------------------------------------------------
Yusuke Inuzuka

View File

@ -0,0 +1,48 @@
local ok, msg = pcall(function()
dofile("notexist")
end)
assert(not ok and string.find(msg, ".*notexist.*"))
local ok, msg = pcall(function()
assert(getfenv(2) == _G)
end)
assert(ok)
local i = 1
local fn = assert(load(function()
local tbl = {"return ", "1", "+", "1"}
local v = tbl[i]
i = i + 1
return v
end))
assert(fn() == 2)
local fn, msg = load(function()
return {}
end)
assert(not fn and string.find(msg, "must return a string"))
local i = 1
local fn, msg = load(function()
if i == 1 then
i = i + 1
return "returna"
end
end)
assert(not fn and string.find(string.lower(msg), "eof"))
local ok, a, b = xpcall(function()
return "a", "b"
end,
function(err)
assert(nil)
end)
assert(ok and a == "a" and b == "b")
local ok, a, b = xpcall(function()
error("error!")
end,
function(err)
return err .. "!", "b"
end)
assert(not ok and string.find(a, "error!!") and b == nil)

View File

@ -0,0 +1,17 @@
co = coroutine.wrap(function()
co()
end)
local ok, msg = pcall(function()
co()
end)
assert(not ok and string.find(msg, "can not resume a running thread"))
co = coroutine.wrap(function()
return 1
end)
assert(co() == 1)
local ok, msg = pcall(function()
co()
end)
assert(not ok and string.find(msg, "can not resume a dead thread"))

View File

@ -0,0 +1,83 @@
-- debug lib tests
-- debug stuff are partially implemented; hooks are not supported.
local function f1()
end
local env = {}
local mt = {}
debug.setfenv(f1, env)
assert(debug.getfenv(f1) == env)
debug.setmetatable(f1, mt)
assert(debug.getmetatable(f1) == mt)
local function f2()
local info = debug.getinfo(1, "Slunf")
assert(info.currentline == 14)
assert(info.linedefined == 13)
assert(info.func == f2)
assert(info.lastlinedefined == 25)
assert(info.nups == 1)
assert(info.name == "f2")
assert(info.what == "Lua")
if string.find(_VERSION, "GopherLua") then
assert(info.source == "db.lua")
end
end
f2()
local function f3()
end
local info = debug.getinfo(f3)
assert(info.currentline == -1)
assert(info.linedefined == 28)
assert(info.func == f3)
assert(info.lastlinedefined == 29)
assert(info.nups == 0)
assert(info.name == nil)
assert(info.what == "Lua")
if string.find(_VERSION, "GopherLua") then
assert(info.source == "db.lua")
end
local function f4()
local a,b,c = 1,2,3
local function f5()
local name, value = debug.getlocal(2, 2)
assert(debug.getlocal(2, 10) == nil)
assert(name == "b")
assert(value == 2)
name = debug.setlocal(2, 2, 10)
assert(debug.setlocal(2, 10, 10) == nil)
assert(name == "b")
local d = a
local e = c
local tb = debug.traceback("--msg--")
assert(string.find(tb, "\\-\\-msg\\-\\-"))
assert(string.find(tb, "in.*f5"))
assert(string.find(tb, "in.*f4"))
end
f5()
local name, value = debug.getupvalue(f5, 1)
assert(debug.getupvalue(f5, 10) == nil)
assert(name == "a")
assert(value == 1)
name = debug.setupvalue(f5, 1, 11)
assert(debug.setupvalue(f5, 10, 11) == nil)
assert(name == "a")
assert(a == 11)
assert(b == 10) -- changed by debug.setlocal in f4
end
f4()
local ok, msg = pcall(function()
debug.getlocal(10, 1)
end)
assert(not ok and string.find(msg, "level out of range"))
local ok, msg = pcall(function()
debug.setlocal(10, 1, 1)
end)
assert(not ok and string.find(msg, "level out of range"))

View File

@ -0,0 +1,68 @@
-- issue #10
local function inspect(options)
options = options or {}
return type(options)
end
assert(inspect(nil) == "table")
local function inspect(options)
options = options or setmetatable({}, {__mode = "test"})
return type(options)
end
assert(inspect(nil) == "table")
-- issue #16
local ok, msg = pcall(function()
local a = {}
a[nil] = 1
end)
assert(not ok and string.find(msg, "table index is nil", 1, true))
-- issue #19
local tbl = {1,2,3,4,5}
assert(#tbl == 5)
assert(table.remove(tbl) == 5)
assert(#tbl == 4)
assert(table.remove(tbl, 3) == 3)
assert(#tbl == 3)
-- issue #24
local tbl = {string.find('hello.world', '.', 0)}
assert(tbl[1] == 1 and tbl[2] == 1)
assert(string.sub('hello.world', 0, 2) == "he")
-- issue 33
local a,b
a = function ()
pcall(function()
end)
coroutine.yield("a")
return b()
end
b = function ()
return "b"
end
local co = coroutine.create(a)
assert(select(2, coroutine.resume(co)) == "a")
assert(select(2, coroutine.resume(co)) == "b")
assert(coroutine.status(co) == "dead")
-- issue 37
function test(a, b, c)
b = b or string.format("b%s", a)
c = c or string.format("c%s", a)
assert(a == "test")
assert(b == "btest")
assert(c == "ctest")
end
test("test")
-- issue 39
assert(string.match("あいうえお", ".*あ.*") == "あいうえお")
assert(string.match("あいうえお", "あいうえお") == "あいうえお")
-- issue 47
assert(string.gsub("A\nA", ".", "A") == "AAA")

View File

@ -0,0 +1,18 @@
local osname = "linux"
if string.find(os.getenv("OS") or "", "Windows") then
osname = "windows"
end
if osname == "linux" then
-- travis ci failed to start date command?
-- assert(os.execute("date") == 0)
assert(os.execute("date -a") == 1)
else
assert(os.execute("date /T") == 0)
assert(os.execute("md") == 1)
end
assert(os.getenv("PATH") ~= "")
assert(os.getenv("_____GLUATEST______") == nil)
assert(os.setenv("_____GLUATEST______", "1"))
assert(os.getenv("_____GLUATEST______") == "1")

View File

@ -0,0 +1,12 @@
local a = {}
assert(table.maxn(a) == 0)
a["key"] = 1
assert(table.maxn(a) == 0)
table.insert(a, 10)
table.insert(a, 3, 10)
assert(table.maxn(a) == 3)
local ok, msg = pcall(function()
table.insert(a)
end)
assert(not ok and string.find(msg, "wrong number of arguments"))

View File

@ -0,0 +1,82 @@
for i, v in ipairs({"hoge", {}, function() end, true, nil}) do
local ok, msg = pcall(function()
print(-v)
end)
assert(not ok and string.find(msg, "__unm undefined"))
end
assert(#"abc" == 3)
local tbl = {1,2,3}
setmetatable(tbl, {__len = function(self)
return 10
end})
assert(#tbl == 10)
setmetatable(tbl, nil)
assert(#tbl == 3)
local ok, msg = pcall(function()
return 1 < "hoge"
end)
assert(not ok and string.find(msg, "attempt to compare number with string"))
local ok, msg = pcall(function()
return {} < (function() end)
end)
assert(not ok and string.find(msg, "attempt to compare table with function"))
local ok, msg = pcall(function()
for n = nil,1 do
print(1)
end
end)
assert(not ok and string.find(msg, "for statement init must be a number"))
local ok, msg = pcall(function()
for n = 1,nil do
print(1)
end
end)
assert(not ok and string.find(msg, "for statement limit must be a number"))
local ok, msg = pcall(function()
for n = 1,10,nil do
print(1)
end
end)
assert(not ok and string.find(msg, "for statement step must be a number"))
local ok, msg = pcall(function()
return {} + (function() end)
end)
assert(not ok and string.find(msg, "cannot perform add operation between table and function"))
local ok, msg = pcall(function()
return {} .. (function() end)
end)
assert(not ok and string.find(msg, "cannot perform concat operation between table and function"))
-- test table with initial elements over 511
local bigtable = {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,10}
assert(bigtable[601] == 10)

Some files were not shown because too many files have changed in this diff Show More