mirror of https://github.com/mindoc-org/mindoc.git
添加依赖包
parent
6f1d7eb2dc
commit
6cb3dbede5
|
@ -29,7 +29,6 @@ install:
|
||||||
Start-FileDownload "http://libgd.blob.core.windows.net/mingw/i686-4.9.1-release-posix-dwarf-rt_v3-rev1.7z" -FileName mingw_x86.7z;
|
Start-FileDownload "http://libgd.blob.core.windows.net/mingw/i686-4.9.1-release-posix-dwarf-rt_v3-rev1.7z" -FileName mingw_x86.7z;
|
||||||
7z x -oC:\ mingw_x86.7z | out-null;
|
7z x -oC:\ mingw_x86.7z | out-null;
|
||||||
}
|
}
|
||||||
- set CGO_ENABLED=1
|
|
||||||
- echo %PATH%
|
- echo %PATH%
|
||||||
- echo %GOPATH%
|
- echo %GOPATH%
|
||||||
- go version
|
- go version
|
||||||
|
@ -39,6 +38,7 @@ install:
|
||||||
build_script:
|
build_script:
|
||||||
- if [%tbs_tools%]==[mingw] if [%tbs_arch%]==[x86] SET PATH=C:\mingw32\bin;%PATH%
|
- if [%tbs_tools%]==[mingw] if [%tbs_arch%]==[x86] SET PATH=C:\mingw32\bin;%PATH%
|
||||||
- if [%tbs_tools%]==[mingw] if [%tbs_arch%]==[x64] SET PATH=C:\mingw64\bin;%PATH%
|
- if [%tbs_tools%]==[mingw] if [%tbs_arch%]==[x64] SET PATH=C:\mingw64\bin;%PATH%
|
||||||
|
- set CGO_ENABLED=1
|
||||||
- go build -v -o "godoc_windows_%GOARCH%.exe" -ldflags="-w -X github.com/lifei6671/godoc/conf.VERSION=%APPVEYOR_REPO_TAG_NAME% -X 'github.com/lifei6671/godoc/conf.BUILD_TIME=`date`' -X 'conf.GO_VERSION=`github.com/lifei6671/godoc/go version`'"
|
- go build -v -o "godoc_windows_%GOARCH%.exe" -ldflags="-w -X github.com/lifei6671/godoc/conf.VERSION=%APPVEYOR_REPO_TAG_NAME% -X 'github.com/lifei6671/godoc/conf.BUILD_TIME=`date`' -X 'conf.GO_VERSION=`github.com/lifei6671/godoc/go version`'"
|
||||||
- 7z a -t7z -r godoc_windows_%GOARCH%.7z conf/app.conf static/* godoc_windows_%GOARCH%.exe views/* database/* logs/* uploads/*
|
- 7z a -t7z -r godoc_windows_%GOARCH%.7z conf/app.conf static/* godoc_windows_%GOARCH%.exe views/* database/* logs/* uploads/*
|
||||||
test_script:
|
test_script:
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
github.com/astaxie/beego/*/*:S1012
|
||||||
|
github.com/astaxie/beego/*:S1012
|
||||||
|
github.com/astaxie/beego/*/*:S1007
|
||||||
|
github.com/astaxie/beego/*:S1007
|
|
@ -0,0 +1,78 @@
|
||||||
|
package param
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
beecontext "github.com/astaxie/beego/context"
|
||||||
|
"github.com/astaxie/beego/logs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConvertParams converts http method params to values that will be passed to the method controller as arguments
|
||||||
|
func ConvertParams(methodParams []*MethodParam, methodType reflect.Type, ctx *beecontext.Context) (result []reflect.Value) {
|
||||||
|
result = make([]reflect.Value, 0, len(methodParams))
|
||||||
|
for i := 0; i < len(methodParams); i++ {
|
||||||
|
reflectValue := convertParam(methodParams[i], methodType.In(i), ctx)
|
||||||
|
result = append(result, reflectValue)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertParam(param *MethodParam, paramType reflect.Type, ctx *beecontext.Context) (result reflect.Value) {
|
||||||
|
paramValue := getParamValue(param, ctx)
|
||||||
|
if paramValue == "" {
|
||||||
|
if param.required {
|
||||||
|
ctx.Abort(400, fmt.Sprintf("Missing parameter %s", param.name))
|
||||||
|
} else {
|
||||||
|
paramValue = param.defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reflectValue, err := parseValue(param, paramValue, paramType)
|
||||||
|
if err != nil {
|
||||||
|
logs.Debug(fmt.Sprintf("Error converting param %s to type %s. Value: %v, Error: %s", param.name, paramType, paramValue, err))
|
||||||
|
ctx.Abort(400, fmt.Sprintf("Invalid parameter %s. Can not convert %v to type %s", param.name, paramValue, paramType))
|
||||||
|
}
|
||||||
|
|
||||||
|
return reflectValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func getParamValue(param *MethodParam, ctx *beecontext.Context) string {
|
||||||
|
switch param.in {
|
||||||
|
case body:
|
||||||
|
return string(ctx.Input.RequestBody)
|
||||||
|
case header:
|
||||||
|
return ctx.Input.Header(param.name)
|
||||||
|
case path:
|
||||||
|
return ctx.Input.Query(":" + param.name)
|
||||||
|
default:
|
||||||
|
return ctx.Input.Query(param.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseValue(param *MethodParam, paramValue string, paramType reflect.Type) (result reflect.Value, err error) {
|
||||||
|
if paramValue == "" {
|
||||||
|
return reflect.Zero(paramType), nil
|
||||||
|
}
|
||||||
|
parser := getParser(param, paramType)
|
||||||
|
value, err := parser.parse(paramValue, paramType)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return safeConvert(reflect.ValueOf(value), paramType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func safeConvert(value reflect.Value, t reflect.Type) (result reflect.Value, err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
var ok bool
|
||||||
|
err, ok = r.(error)
|
||||||
|
if !ok {
|
||||||
|
err = fmt.Errorf("%v", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
result = value.Convert(t)
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
package param
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
//MethodParam keeps param information to be auto passed to controller methods
|
||||||
|
type MethodParam struct {
|
||||||
|
name string
|
||||||
|
in paramType
|
||||||
|
required bool
|
||||||
|
defaultValue string
|
||||||
|
}
|
||||||
|
|
||||||
|
type paramType byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
param paramType = iota
|
||||||
|
path
|
||||||
|
body
|
||||||
|
header
|
||||||
|
)
|
||||||
|
|
||||||
|
//New creates a new MethodParam with name and specific options
|
||||||
|
func New(name string, opts ...MethodParamOption) *MethodParam {
|
||||||
|
return newParam(name, nil, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newParam(name string, parser paramParser, opts []MethodParamOption) (param *MethodParam) {
|
||||||
|
param = &MethodParam{name: name}
|
||||||
|
for _, option := range opts {
|
||||||
|
option(param)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Make creates an array of MethodParmas or an empty array
|
||||||
|
func Make(list ...*MethodParam) []*MethodParam {
|
||||||
|
if len(list) > 0 {
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mp *MethodParam) String() string {
|
||||||
|
options := []string{}
|
||||||
|
result := "param.New(\"" + mp.name + "\""
|
||||||
|
if mp.required {
|
||||||
|
options = append(options, "param.IsRequired")
|
||||||
|
}
|
||||||
|
switch mp.in {
|
||||||
|
case path:
|
||||||
|
options = append(options, "param.InPath")
|
||||||
|
case body:
|
||||||
|
options = append(options, "param.InBody")
|
||||||
|
case header:
|
||||||
|
options = append(options, "param.InHeader")
|
||||||
|
}
|
||||||
|
if mp.defaultValue != "" {
|
||||||
|
options = append(options, fmt.Sprintf(`param.Default("%s")`, mp.defaultValue))
|
||||||
|
}
|
||||||
|
if len(options) > 0 {
|
||||||
|
result += ", "
|
||||||
|
}
|
||||||
|
result += strings.Join(options, ", ")
|
||||||
|
result += ")"
|
||||||
|
return result
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package param
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MethodParamOption defines a func which apply options on a MethodParam
|
||||||
|
type MethodParamOption func(*MethodParam)
|
||||||
|
|
||||||
|
// IsRequired indicates that this param is required and can not be ommited from the http request
|
||||||
|
var IsRequired MethodParamOption = func(p *MethodParam) {
|
||||||
|
p.required = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// InHeader indicates that this param is passed via an http header
|
||||||
|
var InHeader MethodParamOption = func(p *MethodParam) {
|
||||||
|
p.in = header
|
||||||
|
}
|
||||||
|
|
||||||
|
// InPath indicates that this param is part of the URL path
|
||||||
|
var InPath MethodParamOption = func(p *MethodParam) {
|
||||||
|
p.in = path
|
||||||
|
}
|
||||||
|
|
||||||
|
// InBody indicates that this param is passed as an http request body
|
||||||
|
var InBody MethodParamOption = func(p *MethodParam) {
|
||||||
|
p.in = body
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default provides a default value for the http param
|
||||||
|
func Default(defaultValue interface{}) MethodParamOption {
|
||||||
|
return func(p *MethodParam) {
|
||||||
|
if defaultValue != nil {
|
||||||
|
p.defaultValue = fmt.Sprint(defaultValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,149 @@
|
||||||
|
package param
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type paramParser interface {
|
||||||
|
parse(value string, toType reflect.Type) (interface{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getParser(param *MethodParam, t reflect.Type) paramParser {
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||||
|
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
return intParser{}
|
||||||
|
case reflect.Slice:
|
||||||
|
if t.Elem().Kind() == reflect.Uint8 { //treat []byte as string
|
||||||
|
return stringParser{}
|
||||||
|
}
|
||||||
|
if param.in == body {
|
||||||
|
return jsonParser{}
|
||||||
|
}
|
||||||
|
elemParser := getParser(param, t.Elem())
|
||||||
|
if elemParser == (jsonParser{}) {
|
||||||
|
return elemParser
|
||||||
|
}
|
||||||
|
return sliceParser(elemParser)
|
||||||
|
case reflect.Bool:
|
||||||
|
return boolParser{}
|
||||||
|
case reflect.String:
|
||||||
|
return stringParser{}
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return floatParser{}
|
||||||
|
case reflect.Ptr:
|
||||||
|
elemParser := getParser(param, t.Elem())
|
||||||
|
if elemParser == (jsonParser{}) {
|
||||||
|
return elemParser
|
||||||
|
}
|
||||||
|
return ptrParser(elemParser)
|
||||||
|
default:
|
||||||
|
if t.PkgPath() == "time" && t.Name() == "Time" {
|
||||||
|
return timeParser{}
|
||||||
|
}
|
||||||
|
return jsonParser{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type parserFunc func(value string, toType reflect.Type) (interface{}, error)
|
||||||
|
|
||||||
|
func (f parserFunc) parse(value string, toType reflect.Type) (interface{}, error) {
|
||||||
|
return f(value, toType)
|
||||||
|
}
|
||||||
|
|
||||||
|
type boolParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p boolParser) parse(value string, toType reflect.Type) (interface{}, error) {
|
||||||
|
return strconv.ParseBool(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
type stringParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p stringParser) parse(value string, toType reflect.Type) (interface{}, error) {
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type intParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p intParser) parse(value string, toType reflect.Type) (interface{}, error) {
|
||||||
|
return strconv.Atoi(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
type floatParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p floatParser) parse(value string, toType reflect.Type) (interface{}, error) {
|
||||||
|
if toType.Kind() == reflect.Float32 {
|
||||||
|
res, err := strconv.ParseFloat(value, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return float32(res), nil
|
||||||
|
}
|
||||||
|
return strconv.ParseFloat(value, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
type timeParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p timeParser) parse(value string, toType reflect.Type) (result interface{}, err error) {
|
||||||
|
result, err = time.Parse(time.RFC3339, value)
|
||||||
|
if err != nil {
|
||||||
|
result, err = time.Parse("2006-01-02", value)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p jsonParser) parse(value string, toType reflect.Type) (interface{}, error) {
|
||||||
|
pResult := reflect.New(toType)
|
||||||
|
v := pResult.Interface()
|
||||||
|
err := json.Unmarshal([]byte(value), v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pResult.Elem().Interface(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sliceParser(elemParser paramParser) paramParser {
|
||||||
|
return parserFunc(func(value string, toType reflect.Type) (interface{}, error) {
|
||||||
|
values := strings.Split(value, ",")
|
||||||
|
result := reflect.MakeSlice(toType, 0, len(values))
|
||||||
|
elemType := toType.Elem()
|
||||||
|
for _, v := range values {
|
||||||
|
parsedValue, err := elemParser.parse(v, elemType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result = reflect.Append(result, reflect.ValueOf(parsedValue))
|
||||||
|
}
|
||||||
|
return result.Interface(), nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ptrParser(elemParser paramParser) paramParser {
|
||||||
|
return parserFunc(func(value string, toType reflect.Type) (interface{}, error) {
|
||||||
|
parsedValue, err := elemParser.parse(value, toType.Elem())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
newValPtr := reflect.New(toType.Elem())
|
||||||
|
newVal := reflect.Indirect(newValPtr)
|
||||||
|
convertedVal, err := safeConvert(reflect.ValueOf(parsedValue), toType.Elem())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
newVal.Set(convertedVal)
|
||||||
|
return newValPtr.Interface(), nil
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package context
|
||||||
|
|
||||||
|
// Renderer defines an http response renderer
|
||||||
|
type Renderer interface {
|
||||||
|
Render(ctx *Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
type rendererFunc func(ctx *Context)
|
||||||
|
|
||||||
|
func (f rendererFunc) Render(ctx *Context) {
|
||||||
|
f(ctx)
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
//BadRequest indicates http error 400
|
||||||
|
BadRequest StatusCode = http.StatusBadRequest
|
||||||
|
|
||||||
|
//NotFound indicates http error 404
|
||||||
|
NotFound StatusCode = http.StatusNotFound
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatusCode sets the http response status code
|
||||||
|
type StatusCode int
|
||||||
|
|
||||||
|
func (s StatusCode) Error() string {
|
||||||
|
return strconv.Itoa(int(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render sets the http status code
|
||||||
|
func (s StatusCode) Render(ctx *Context) {
|
||||||
|
ctx.Output.SetStatus(int(s))
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetGOPATHs returns all paths in GOPATH variable.
|
||||||
|
func GetGOPATHs() []string {
|
||||||
|
gopath := os.Getenv("GOPATH")
|
||||||
|
if gopath == "" && strings.Compare(runtime.Version(), "go1.8") >= 0 {
|
||||||
|
gopath = defaultGOPATH()
|
||||||
|
}
|
||||||
|
return filepath.SplitList(gopath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultGOPATH() string {
|
||||||
|
env := "HOME"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
env = "USERPROFILE"
|
||||||
|
} else if runtime.GOOS == "plan9" {
|
||||||
|
env = "home"
|
||||||
|
}
|
||||||
|
if home := os.Getenv(env); home != "" {
|
||||||
|
return filepath.Join(home, "go")
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Florian Sundermann
|
||||||
|
|
||||||
|
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.
|
|
@ -0,0 +1,18 @@
|
||||||
|
## Introduction ##
|
||||||
|
This is a package for GO which can be used to create different types of barcodes.
|
||||||
|
|
||||||
|
## Supported Barcode Types ##
|
||||||
|
* Aztec Code
|
||||||
|
* Codabar
|
||||||
|
* Code 128
|
||||||
|
* Code 39
|
||||||
|
* EAN 8
|
||||||
|
* EAN 13
|
||||||
|
* Datamatrix
|
||||||
|
* QR Codes
|
||||||
|
* 2 of 5
|
||||||
|
|
||||||
|
## Documentation ##
|
||||||
|
See [GoDoc](https://godoc.org/github.com/boombuler/barcode)
|
||||||
|
|
||||||
|
To create a barcode use the Encode function from one of the subpackages.
|
|
@ -0,0 +1,27 @@
|
||||||
|
package barcode
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
|
||||||
|
// Contains some meta information about a barcode
|
||||||
|
type Metadata struct {
|
||||||
|
// the name of the barcode kind
|
||||||
|
CodeKind string
|
||||||
|
// contains 1 for 1D barcodes or 2 for 2D barcodes
|
||||||
|
Dimensions byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// a rendered and encoded barcode
|
||||||
|
type Barcode interface {
|
||||||
|
image.Image
|
||||||
|
// returns some meta information about the barcode
|
||||||
|
Metadata() Metadata
|
||||||
|
// the data that was encoded in this barcode
|
||||||
|
Content() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional interface that some barcodes might implement to provide
|
||||||
|
// the value of its checksum.
|
||||||
|
type BarcodeIntCS interface {
|
||||||
|
Barcode
|
||||||
|
CheckSum() int
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
package qr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/boombuler/barcode/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const charSet string = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"
|
||||||
|
|
||||||
|
func stringToAlphaIdx(content string) <-chan int {
|
||||||
|
result := make(chan int)
|
||||||
|
go func() {
|
||||||
|
for _, r := range content {
|
||||||
|
idx := strings.IndexRune(charSet, r)
|
||||||
|
result <- idx
|
||||||
|
if idx < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(result)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeAlphaNumeric(content string, ecl ErrorCorrectionLevel) (*utils.BitList, *versionInfo, error) {
|
||||||
|
|
||||||
|
contentLenIsOdd := len(content)%2 == 1
|
||||||
|
contentBitCount := (len(content) / 2) * 11
|
||||||
|
if contentLenIsOdd {
|
||||||
|
contentBitCount += 6
|
||||||
|
}
|
||||||
|
vi := findSmallestVersionInfo(ecl, alphaNumericMode, contentBitCount)
|
||||||
|
if vi == nil {
|
||||||
|
return nil, nil, errors.New("To much data to encode")
|
||||||
|
}
|
||||||
|
|
||||||
|
res := new(utils.BitList)
|
||||||
|
res.AddBits(int(alphaNumericMode), 4)
|
||||||
|
res.AddBits(len(content), vi.charCountBits(alphaNumericMode))
|
||||||
|
|
||||||
|
encoder := stringToAlphaIdx(content)
|
||||||
|
|
||||||
|
for idx := 0; idx < len(content)/2; idx++ {
|
||||||
|
c1 := <-encoder
|
||||||
|
c2 := <-encoder
|
||||||
|
if c1 < 0 || c2 < 0 {
|
||||||
|
return nil, nil, fmt.Errorf("\"%s\" can not be encoded as %s", content, AlphaNumeric)
|
||||||
|
}
|
||||||
|
res.AddBits(c1*45+c2, 11)
|
||||||
|
}
|
||||||
|
if contentLenIsOdd {
|
||||||
|
c := <-encoder
|
||||||
|
if c < 0 {
|
||||||
|
return nil, nil, fmt.Errorf("\"%s\" can not be encoded as %s", content, AlphaNumeric)
|
||||||
|
}
|
||||||
|
res.AddBits(c, 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
addPaddingAndTerminator(res, vi)
|
||||||
|
|
||||||
|
return res, vi, nil
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package qr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/boombuler/barcode/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func encodeAuto(content string, ecl ErrorCorrectionLevel) (*utils.BitList, *versionInfo, error) {
|
||||||
|
bits, vi, _ := Numeric.getEncoder()(content, ecl)
|
||||||
|
if bits != nil && vi != nil {
|
||||||
|
return bits, vi, nil
|
||||||
|
}
|
||||||
|
bits, vi, _ = AlphaNumeric.getEncoder()(content, ecl)
|
||||||
|
if bits != nil && vi != nil {
|
||||||
|
return bits, vi, nil
|
||||||
|
}
|
||||||
|
bits, vi, _ = Unicode.getEncoder()(content, ecl)
|
||||||
|
if bits != nil && vi != nil {
|
||||||
|
return bits, vi, nil
|
||||||
|
}
|
||||||
|
return nil, nil, fmt.Errorf("No encoding found to encode \"%s\"", content)
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package qr
|
||||||
|
|
||||||
|
type block struct {
|
||||||
|
data []byte
|
||||||
|
ecc []byte
|
||||||
|
}
|
||||||
|
type blockList []*block
|
||||||
|
|
||||||
|
func splitToBlocks(data <-chan byte, vi *versionInfo) blockList {
|
||||||
|
result := make(blockList, vi.NumberOfBlocksInGroup1+vi.NumberOfBlocksInGroup2)
|
||||||
|
|
||||||
|
for b := 0; b < int(vi.NumberOfBlocksInGroup1); b++ {
|
||||||
|
blk := new(block)
|
||||||
|
blk.data = make([]byte, vi.DataCodeWordsPerBlockInGroup1)
|
||||||
|
for cw := 0; cw < int(vi.DataCodeWordsPerBlockInGroup1); cw++ {
|
||||||
|
blk.data[cw] = <-data
|
||||||
|
}
|
||||||
|
blk.ecc = ec.calcECC(blk.data, vi.ErrorCorrectionCodewordsPerBlock)
|
||||||
|
result[b] = blk
|
||||||
|
}
|
||||||
|
|
||||||
|
for b := 0; b < int(vi.NumberOfBlocksInGroup2); b++ {
|
||||||
|
blk := new(block)
|
||||||
|
blk.data = make([]byte, vi.DataCodeWordsPerBlockInGroup2)
|
||||||
|
for cw := 0; cw < int(vi.DataCodeWordsPerBlockInGroup2); cw++ {
|
||||||
|
blk.data[cw] = <-data
|
||||||
|
}
|
||||||
|
blk.ecc = ec.calcECC(blk.data, vi.ErrorCorrectionCodewordsPerBlock)
|
||||||
|
result[int(vi.NumberOfBlocksInGroup1)+b] = blk
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bl blockList) interleave(vi *versionInfo) []byte {
|
||||||
|
var maxCodewordCount int
|
||||||
|
if vi.DataCodeWordsPerBlockInGroup1 > vi.DataCodeWordsPerBlockInGroup2 {
|
||||||
|
maxCodewordCount = int(vi.DataCodeWordsPerBlockInGroup1)
|
||||||
|
} else {
|
||||||
|
maxCodewordCount = int(vi.DataCodeWordsPerBlockInGroup2)
|
||||||
|
}
|
||||||
|
resultLen := (vi.DataCodeWordsPerBlockInGroup1+vi.ErrorCorrectionCodewordsPerBlock)*vi.NumberOfBlocksInGroup1 +
|
||||||
|
(vi.DataCodeWordsPerBlockInGroup2+vi.ErrorCorrectionCodewordsPerBlock)*vi.NumberOfBlocksInGroup2
|
||||||
|
|
||||||
|
result := make([]byte, 0, resultLen)
|
||||||
|
for i := 0; i < maxCodewordCount; i++ {
|
||||||
|
for b := 0; b < len(bl); b++ {
|
||||||
|
if len(bl[b].data) > i {
|
||||||
|
result = append(result, bl[b].data[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := 0; i < int(vi.ErrorCorrectionCodewordsPerBlock); i++ {
|
||||||
|
for b := 0; b < len(bl); b++ {
|
||||||
|
result = append(result, bl[b].ecc[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
|
@ -0,0 +1,416 @@
|
||||||
|
// Package qr can be used to create QR barcodes.
|
||||||
|
package qr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
|
||||||
|
"github.com/boombuler/barcode"
|
||||||
|
"github.com/boombuler/barcode/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type encodeFn func(content string, eccLevel ErrorCorrectionLevel) (*utils.BitList, *versionInfo, error)
|
||||||
|
|
||||||
|
// Encoding mode for QR Codes.
|
||||||
|
type Encoding byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Auto will choose ths best matching encoding
|
||||||
|
Auto Encoding = iota
|
||||||
|
// Numeric encoding only encodes numbers [0-9]
|
||||||
|
Numeric
|
||||||
|
// AlphaNumeric encoding only encodes uppercase letters, numbers and [Space], $, %, *, +, -, ., /, :
|
||||||
|
AlphaNumeric
|
||||||
|
// Unicode encoding encodes the string as utf-8
|
||||||
|
Unicode
|
||||||
|
// only for testing purpose
|
||||||
|
unknownEncoding
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e Encoding) getEncoder() encodeFn {
|
||||||
|
switch e {
|
||||||
|
case Auto:
|
||||||
|
return encodeAuto
|
||||||
|
case Numeric:
|
||||||
|
return encodeNumeric
|
||||||
|
case AlphaNumeric:
|
||||||
|
return encodeAlphaNumeric
|
||||||
|
case Unicode:
|
||||||
|
return encodeUnicode
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Encoding) String() string {
|
||||||
|
switch e {
|
||||||
|
case Auto:
|
||||||
|
return "Auto"
|
||||||
|
case Numeric:
|
||||||
|
return "Numeric"
|
||||||
|
case AlphaNumeric:
|
||||||
|
return "AlphaNumeric"
|
||||||
|
case Unicode:
|
||||||
|
return "Unicode"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode returns a QR barcode with the given content, error correction level and uses the given encoding
|
||||||
|
func Encode(content string, level ErrorCorrectionLevel, mode Encoding) (barcode.Barcode, error) {
|
||||||
|
bits, vi, err := mode.getEncoder()(content, level)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks := splitToBlocks(bits.IterateBytes(), vi)
|
||||||
|
data := blocks.interleave(vi)
|
||||||
|
result := render(data, vi)
|
||||||
|
result.content = content
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func render(data []byte, vi *versionInfo) *qrcode {
|
||||||
|
dim := vi.modulWidth()
|
||||||
|
results := make([]*qrcode, 8)
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
results[i] = newBarcode(dim)
|
||||||
|
}
|
||||||
|
|
||||||
|
occupied := newBarcode(dim)
|
||||||
|
|
||||||
|
setAll := func(x int, y int, val bool) {
|
||||||
|
occupied.Set(x, y, true)
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
results[i].Set(x, y, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawFinderPatterns(vi, setAll)
|
||||||
|
drawAlignmentPatterns(occupied, vi, setAll)
|
||||||
|
|
||||||
|
//Timing Pattern:
|
||||||
|
var i int
|
||||||
|
for i = 0; i < dim; i++ {
|
||||||
|
if !occupied.Get(i, 6) {
|
||||||
|
setAll(i, 6, i%2 == 0)
|
||||||
|
}
|
||||||
|
if !occupied.Get(6, i) {
|
||||||
|
setAll(6, i, i%2 == 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Dark Module
|
||||||
|
setAll(8, dim-8, true)
|
||||||
|
|
||||||
|
drawVersionInfo(vi, setAll)
|
||||||
|
drawFormatInfo(vi, -1, occupied.Set)
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
drawFormatInfo(vi, i, results[i].Set)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the data
|
||||||
|
var curBitNo int
|
||||||
|
|
||||||
|
for pos := range iterateModules(occupied) {
|
||||||
|
var curBit bool
|
||||||
|
if curBitNo < len(data)*8 {
|
||||||
|
curBit = ((data[curBitNo/8] >> uint(7-(curBitNo%8))) & 1) == 1
|
||||||
|
} else {
|
||||||
|
curBit = false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
setMasked(pos.X, pos.Y, curBit, i, results[i].Set)
|
||||||
|
}
|
||||||
|
curBitNo++
|
||||||
|
}
|
||||||
|
|
||||||
|
lowestPenalty := ^uint(0)
|
||||||
|
lowestPenaltyIdx := -1
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
p := results[i].calcPenalty()
|
||||||
|
if p < lowestPenalty {
|
||||||
|
lowestPenalty = p
|
||||||
|
lowestPenaltyIdx = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results[lowestPenaltyIdx]
|
||||||
|
}
|
||||||
|
|
||||||
|
func setMasked(x, y int, val bool, mask int, set func(int, int, bool)) {
|
||||||
|
switch mask {
|
||||||
|
case 0:
|
||||||
|
val = val != (((y + x) % 2) == 0)
|
||||||
|
break
|
||||||
|
case 1:
|
||||||
|
val = val != ((y % 2) == 0)
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
val = val != ((x % 3) == 0)
|
||||||
|
break
|
||||||
|
case 3:
|
||||||
|
val = val != (((y + x) % 3) == 0)
|
||||||
|
break
|
||||||
|
case 4:
|
||||||
|
val = val != (((y/2 + x/3) % 2) == 0)
|
||||||
|
break
|
||||||
|
case 5:
|
||||||
|
val = val != (((y*x)%2)+((y*x)%3) == 0)
|
||||||
|
break
|
||||||
|
case 6:
|
||||||
|
val = val != ((((y*x)%2)+((y*x)%3))%2 == 0)
|
||||||
|
break
|
||||||
|
case 7:
|
||||||
|
val = val != ((((y+x)%2)+((y*x)%3))%2 == 0)
|
||||||
|
}
|
||||||
|
set(x, y, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func iterateModules(occupied *qrcode) <-chan image.Point {
|
||||||
|
result := make(chan image.Point)
|
||||||
|
allPoints := make(chan image.Point)
|
||||||
|
go func() {
|
||||||
|
curX := occupied.dimension - 1
|
||||||
|
curY := occupied.dimension - 1
|
||||||
|
isUpward := true
|
||||||
|
|
||||||
|
for true {
|
||||||
|
if isUpward {
|
||||||
|
allPoints <- image.Pt(curX, curY)
|
||||||
|
allPoints <- image.Pt(curX-1, curY)
|
||||||
|
curY--
|
||||||
|
if curY < 0 {
|
||||||
|
curY = 0
|
||||||
|
curX -= 2
|
||||||
|
if curX == 6 {
|
||||||
|
curX--
|
||||||
|
}
|
||||||
|
if curX < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
isUpward = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
allPoints <- image.Pt(curX, curY)
|
||||||
|
allPoints <- image.Pt(curX-1, curY)
|
||||||
|
curY++
|
||||||
|
if curY >= occupied.dimension {
|
||||||
|
curY = occupied.dimension - 1
|
||||||
|
curX -= 2
|
||||||
|
if curX == 6 {
|
||||||
|
curX--
|
||||||
|
}
|
||||||
|
isUpward = true
|
||||||
|
if curX < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(allPoints)
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
for pt := range allPoints {
|
||||||
|
if !occupied.Get(pt.X, pt.Y) {
|
||||||
|
result <- pt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(result)
|
||||||
|
}()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawFinderPatterns(vi *versionInfo, set func(int, int, bool)) {
|
||||||
|
dim := vi.modulWidth()
|
||||||
|
drawPattern := func(xoff int, yoff int) {
|
||||||
|
for x := -1; x < 8; x++ {
|
||||||
|
for y := -1; y < 8; y++ {
|
||||||
|
val := (x == 0 || x == 6 || y == 0 || y == 6 || (x > 1 && x < 5 && y > 1 && y < 5)) && (x <= 6 && y <= 6 && x >= 0 && y >= 0)
|
||||||
|
|
||||||
|
if x+xoff >= 0 && x+xoff < dim && y+yoff >= 0 && y+yoff < dim {
|
||||||
|
set(x+xoff, y+yoff, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drawPattern(0, 0)
|
||||||
|
drawPattern(0, dim-7)
|
||||||
|
drawPattern(dim-7, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawAlignmentPatterns(occupied *qrcode, vi *versionInfo, set func(int, int, bool)) {
|
||||||
|
drawPattern := func(xoff int, yoff int) {
|
||||||
|
for x := -2; x <= 2; x++ {
|
||||||
|
for y := -2; y <= 2; y++ {
|
||||||
|
val := x == -2 || x == 2 || y == -2 || y == 2 || (x == 0 && y == 0)
|
||||||
|
set(x+xoff, y+yoff, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
positions := vi.alignmentPatternPlacements()
|
||||||
|
|
||||||
|
for _, x := range positions {
|
||||||
|
for _, y := range positions {
|
||||||
|
if occupied.Get(x, y) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
drawPattern(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var formatInfos = map[ErrorCorrectionLevel]map[int][]bool{
|
||||||
|
L: {
|
||||||
|
0: []bool{true, true, true, false, true, true, true, true, true, false, false, false, true, false, false},
|
||||||
|
1: []bool{true, true, true, false, false, true, false, true, true, true, true, false, false, true, true},
|
||||||
|
2: []bool{true, true, true, true, true, false, true, true, false, true, false, true, false, true, false},
|
||||||
|
3: []bool{true, true, true, true, false, false, false, true, false, false, true, true, true, false, true},
|
||||||
|
4: []bool{true, true, false, false, true, true, false, false, false, true, false, true, true, true, true},
|
||||||
|
5: []bool{true, true, false, false, false, true, true, false, false, false, true, true, false, false, false},
|
||||||
|
6: []bool{true, true, false, true, true, false, false, false, true, false, false, false, false, false, true},
|
||||||
|
7: []bool{true, true, false, true, false, false, true, false, true, true, true, false, true, true, false},
|
||||||
|
},
|
||||||
|
M: {
|
||||||
|
0: []bool{true, false, true, false, true, false, false, false, false, false, true, false, false, true, false},
|
||||||
|
1: []bool{true, false, true, false, false, false, true, false, false, true, false, false, true, false, true},
|
||||||
|
2: []bool{true, false, true, true, true, true, false, false, true, true, true, true, true, false, false},
|
||||||
|
3: []bool{true, false, true, true, false, true, true, false, true, false, false, true, false, true, true},
|
||||||
|
4: []bool{true, false, false, false, true, false, true, true, true, true, true, true, false, false, true},
|
||||||
|
5: []bool{true, false, false, false, false, false, false, true, true, false, false, true, true, true, false},
|
||||||
|
6: []bool{true, false, false, true, true, true, true, true, false, false, true, false, true, true, true},
|
||||||
|
7: []bool{true, false, false, true, false, true, false, true, false, true, false, false, false, false, false},
|
||||||
|
},
|
||||||
|
Q: {
|
||||||
|
0: []bool{false, true, true, false, true, false, true, false, true, false, true, true, true, true, true},
|
||||||
|
1: []bool{false, true, true, false, false, false, false, false, true, true, false, true, false, false, false},
|
||||||
|
2: []bool{false, true, true, true, true, true, true, false, false, true, true, false, false, false, true},
|
||||||
|
3: []bool{false, true, true, true, false, true, false, false, false, false, false, false, true, true, false},
|
||||||
|
4: []bool{false, true, false, false, true, false, false, true, false, true, true, false, true, false, false},
|
||||||
|
5: []bool{false, true, false, false, false, false, true, true, false, false, false, false, false, true, true},
|
||||||
|
6: []bool{false, true, false, true, true, true, false, true, true, false, true, true, false, true, false},
|
||||||
|
7: []bool{false, true, false, true, false, true, true, true, true, true, false, true, true, false, true},
|
||||||
|
},
|
||||||
|
H: {
|
||||||
|
0: []bool{false, false, true, false, true, true, false, true, false, false, false, true, false, false, true},
|
||||||
|
1: []bool{false, false, true, false, false, true, true, true, false, true, true, true, true, true, false},
|
||||||
|
2: []bool{false, false, true, true, true, false, false, true, true, true, false, false, true, true, true},
|
||||||
|
3: []bool{false, false, true, true, false, false, true, true, true, false, true, false, false, false, false},
|
||||||
|
4: []bool{false, false, false, false, true, true, true, false, true, true, false, false, false, true, false},
|
||||||
|
5: []bool{false, false, false, false, false, true, false, false, true, false, true, false, true, false, true},
|
||||||
|
6: []bool{false, false, false, true, true, false, true, false, false, false, false, true, true, false, false},
|
||||||
|
7: []bool{false, false, false, true, false, false, false, false, false, true, true, true, false, true, true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawFormatInfo(vi *versionInfo, usedMask int, set func(int, int, bool)) {
|
||||||
|
var formatInfo []bool
|
||||||
|
|
||||||
|
if usedMask == -1 {
|
||||||
|
formatInfo = []bool{true, true, true, true, true, true, true, true, true, true, true, true, true, true, true} // Set all to true cause -1 --> occupied mask.
|
||||||
|
} else {
|
||||||
|
formatInfo = formatInfos[vi.Level][usedMask]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(formatInfo) == 15 {
|
||||||
|
dim := vi.modulWidth()
|
||||||
|
set(0, 8, formatInfo[0])
|
||||||
|
set(1, 8, formatInfo[1])
|
||||||
|
set(2, 8, formatInfo[2])
|
||||||
|
set(3, 8, formatInfo[3])
|
||||||
|
set(4, 8, formatInfo[4])
|
||||||
|
set(5, 8, formatInfo[5])
|
||||||
|
set(7, 8, formatInfo[6])
|
||||||
|
set(8, 8, formatInfo[7])
|
||||||
|
set(8, 7, formatInfo[8])
|
||||||
|
set(8, 5, formatInfo[9])
|
||||||
|
set(8, 4, formatInfo[10])
|
||||||
|
set(8, 3, formatInfo[11])
|
||||||
|
set(8, 2, formatInfo[12])
|
||||||
|
set(8, 1, formatInfo[13])
|
||||||
|
set(8, 0, formatInfo[14])
|
||||||
|
|
||||||
|
set(8, dim-1, formatInfo[0])
|
||||||
|
set(8, dim-2, formatInfo[1])
|
||||||
|
set(8, dim-3, formatInfo[2])
|
||||||
|
set(8, dim-4, formatInfo[3])
|
||||||
|
set(8, dim-5, formatInfo[4])
|
||||||
|
set(8, dim-6, formatInfo[5])
|
||||||
|
set(8, dim-7, formatInfo[6])
|
||||||
|
set(dim-8, 8, formatInfo[7])
|
||||||
|
set(dim-7, 8, formatInfo[8])
|
||||||
|
set(dim-6, 8, formatInfo[9])
|
||||||
|
set(dim-5, 8, formatInfo[10])
|
||||||
|
set(dim-4, 8, formatInfo[11])
|
||||||
|
set(dim-3, 8, formatInfo[12])
|
||||||
|
set(dim-2, 8, formatInfo[13])
|
||||||
|
set(dim-1, 8, formatInfo[14])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var versionInfoBitsByVersion = map[byte][]bool{
|
||||||
|
7: []bool{false, false, false, true, true, true, true, true, false, false, true, false, false, true, false, true, false, false},
|
||||||
|
8: []bool{false, false, true, false, false, false, false, true, false, true, true, false, true, true, true, true, false, false},
|
||||||
|
9: []bool{false, false, true, false, false, true, true, false, true, false, true, false, false, true, true, false, false, true},
|
||||||
|
10: []bool{false, false, true, false, true, false, false, true, false, false, true, true, false, true, false, false, true, true},
|
||||||
|
11: []bool{false, false, true, false, true, true, true, false, true, true, true, true, true, true, false, true, true, false},
|
||||||
|
12: []bool{false, false, true, true, false, false, false, true, true, true, false, true, true, false, false, false, true, false},
|
||||||
|
13: []bool{false, false, true, true, false, true, true, false, false, false, false, true, false, false, false, true, true, true},
|
||||||
|
14: []bool{false, false, true, true, true, false, false, true, true, false, false, false, false, false, true, true, false, true},
|
||||||
|
15: []bool{false, false, true, true, true, true, true, false, false, true, false, false, true, false, true, false, false, false},
|
||||||
|
16: []bool{false, true, false, false, false, false, true, false, true, true, false, true, true, true, true, false, false, false},
|
||||||
|
17: []bool{false, true, false, false, false, true, false, true, false, false, false, true, false, true, true, true, false, true},
|
||||||
|
18: []bool{false, true, false, false, true, false, true, false, true, false, false, false, false, true, false, true, true, true},
|
||||||
|
19: []bool{false, true, false, false, true, true, false, true, false, true, false, false, true, true, false, false, true, false},
|
||||||
|
20: []bool{false, true, false, true, false, false, true, false, false, true, true, false, true, false, false, true, true, false},
|
||||||
|
21: []bool{false, true, false, true, false, true, false, true, true, false, true, false, false, false, false, false, true, true},
|
||||||
|
22: []bool{false, true, false, true, true, false, true, false, false, false, true, true, false, false, true, false, false, true},
|
||||||
|
23: []bool{false, true, false, true, true, true, false, true, true, true, true, true, true, false, true, true, false, false},
|
||||||
|
24: []bool{false, true, true, false, false, false, true, true, true, false, true, true, false, false, false, true, false, false},
|
||||||
|
25: []bool{false, true, true, false, false, true, false, false, false, true, true, true, true, false, false, false, false, true},
|
||||||
|
26: []bool{false, true, true, false, true, false, true, true, true, true, true, false, true, false, true, false, true, true},
|
||||||
|
27: []bool{false, true, true, false, true, true, false, false, false, false, true, false, false, false, true, true, true, false},
|
||||||
|
28: []bool{false, true, true, true, false, false, true, true, false, false, false, false, false, true, true, false, true, false},
|
||||||
|
29: []bool{false, true, true, true, false, true, false, false, true, true, false, false, true, true, true, true, true, true},
|
||||||
|
30: []bool{false, true, true, true, true, false, true, true, false, true, false, true, true, true, false, true, false, true},
|
||||||
|
31: []bool{false, true, true, true, true, true, false, false, true, false, false, true, false, true, false, false, false, false},
|
||||||
|
32: []bool{true, false, false, false, false, false, true, false, false, true, true, true, false, true, false, true, false, true},
|
||||||
|
33: []bool{true, false, false, false, false, true, false, true, true, false, true, true, true, true, false, false, false, false},
|
||||||
|
34: []bool{true, false, false, false, true, false, true, false, false, false, true, false, true, true, true, false, true, false},
|
||||||
|
35: []bool{true, false, false, false, true, true, false, true, true, true, true, false, false, true, true, true, true, true},
|
||||||
|
36: []bool{true, false, false, true, false, false, true, false, true, true, false, false, false, false, true, false, true, true},
|
||||||
|
37: []bool{true, false, false, true, false, true, false, true, false, false, false, false, true, false, true, true, true, false},
|
||||||
|
38: []bool{true, false, false, true, true, false, true, false, true, false, false, true, true, false, false, true, false, false},
|
||||||
|
39: []bool{true, false, false, true, true, true, false, true, false, true, false, true, false, false, false, false, false, true},
|
||||||
|
40: []bool{true, false, true, false, false, false, true, true, false, false, false, true, true, false, true, false, false, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawVersionInfo(vi *versionInfo, set func(int, int, bool)) {
|
||||||
|
versionInfoBits, ok := versionInfoBitsByVersion[vi.Version]
|
||||||
|
|
||||||
|
if ok && len(versionInfoBits) > 0 {
|
||||||
|
for i := 0; i < len(versionInfoBits); i++ {
|
||||||
|
x := (vi.modulWidth() - 11) + i%3
|
||||||
|
y := i / 3
|
||||||
|
set(x, y, versionInfoBits[len(versionInfoBits)-i-1])
|
||||||
|
set(y, x, versionInfoBits[len(versionInfoBits)-i-1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func addPaddingAndTerminator(bl *utils.BitList, vi *versionInfo) {
|
||||||
|
for i := 0; i < 4 && bl.Len() < vi.totalDataBytes()*8; i++ {
|
||||||
|
bl.AddBit(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
for bl.Len()%8 != 0 {
|
||||||
|
bl.AddBit(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; bl.Len() < vi.totalDataBytes()*8; i++ {
|
||||||
|
if i%2 == 0 {
|
||||||
|
bl.AddByte(236)
|
||||||
|
} else {
|
||||||
|
bl.AddByte(17)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package qr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/boombuler/barcode/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type errorCorrection struct {
|
||||||
|
rs *utils.ReedSolomonEncoder
|
||||||
|
}
|
||||||
|
|
||||||
|
var ec = newErrorCorrection()
|
||||||
|
|
||||||
|
func newErrorCorrection() *errorCorrection {
|
||||||
|
fld := utils.NewGaloisField(285, 256, 0)
|
||||||
|
return &errorCorrection{utils.NewReedSolomonEncoder(fld)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *errorCorrection) calcECC(data []byte, eccCount byte) []byte {
|
||||||
|
dataInts := make([]int, len(data))
|
||||||
|
for i := 0; i < len(data); i++ {
|
||||||
|
dataInts[i] = int(data[i])
|
||||||
|
}
|
||||||
|
res := ec.rs.Encode(dataInts, int(eccCount))
|
||||||
|
result := make([]byte, len(res))
|
||||||
|
for i := 0; i < len(res); i++ {
|
||||||
|
result[i] = byte(res[i])
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package qr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/boombuler/barcode/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func encodeNumeric(content string, ecl ErrorCorrectionLevel) (*utils.BitList, *versionInfo, error) {
|
||||||
|
contentBitCount := (len(content) / 3) * 10
|
||||||
|
switch len(content) % 3 {
|
||||||
|
case 1:
|
||||||
|
contentBitCount += 4
|
||||||
|
case 2:
|
||||||
|
contentBitCount += 7
|
||||||
|
}
|
||||||
|
vi := findSmallestVersionInfo(ecl, numericMode, contentBitCount)
|
||||||
|
if vi == nil {
|
||||||
|
return nil, nil, errors.New("To much data to encode")
|
||||||
|
}
|
||||||
|
res := new(utils.BitList)
|
||||||
|
res.AddBits(int(numericMode), 4)
|
||||||
|
res.AddBits(len(content), vi.charCountBits(numericMode))
|
||||||
|
|
||||||
|
for pos := 0; pos < len(content); pos += 3 {
|
||||||
|
var curStr string
|
||||||
|
if pos+3 <= len(content) {
|
||||||
|
curStr = content[pos : pos+3]
|
||||||
|
} else {
|
||||||
|
curStr = content[pos:]
|
||||||
|
}
|
||||||
|
|
||||||
|
i, err := strconv.Atoi(curStr)
|
||||||
|
if err != nil || i < 0 {
|
||||||
|
return nil, nil, fmt.Errorf("\"%s\" can not be encoded as %s", content, Numeric)
|
||||||
|
}
|
||||||
|
var bitCnt byte
|
||||||
|
switch len(curStr) % 3 {
|
||||||
|
case 0:
|
||||||
|
bitCnt = 10
|
||||||
|
case 1:
|
||||||
|
bitCnt = 4
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
bitCnt = 7
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
res.AddBits(i, bitCnt)
|
||||||
|
}
|
||||||
|
|
||||||
|
addPaddingAndTerminator(res, vi)
|
||||||
|
return res, vi, nil
|
||||||
|
}
|
|
@ -0,0 +1,166 @@
|
||||||
|
package qr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/boombuler/barcode"
|
||||||
|
"github.com/boombuler/barcode/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type qrcode struct {
|
||||||
|
dimension int
|
||||||
|
data *utils.BitList
|
||||||
|
content string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) Content() string {
|
||||||
|
return qr.content
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) Metadata() barcode.Metadata {
|
||||||
|
return barcode.Metadata{"QR Code", 2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) ColorModel() color.Model {
|
||||||
|
return color.Gray16Model
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) Bounds() image.Rectangle {
|
||||||
|
return image.Rect(0, 0, qr.dimension, qr.dimension)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) At(x, y int) color.Color {
|
||||||
|
if qr.Get(x, y) {
|
||||||
|
return color.Black
|
||||||
|
}
|
||||||
|
return color.White
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) Get(x, y int) bool {
|
||||||
|
return qr.data.GetBit(x*qr.dimension + y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) Set(x, y int, val bool) {
|
||||||
|
qr.data.SetBit(x*qr.dimension+y, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) calcPenalty() uint {
|
||||||
|
return qr.calcPenaltyRule1() + qr.calcPenaltyRule2() + qr.calcPenaltyRule3() + qr.calcPenaltyRule4()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) calcPenaltyRule1() uint {
|
||||||
|
var result uint
|
||||||
|
for x := 0; x < qr.dimension; x++ {
|
||||||
|
checkForX := false
|
||||||
|
var cntX uint
|
||||||
|
checkForY := false
|
||||||
|
var cntY uint
|
||||||
|
|
||||||
|
for y := 0; y < qr.dimension; y++ {
|
||||||
|
if qr.Get(x, y) == checkForX {
|
||||||
|
cntX++
|
||||||
|
} else {
|
||||||
|
checkForX = !checkForX
|
||||||
|
if cntX >= 5 {
|
||||||
|
result += cntX - 2
|
||||||
|
}
|
||||||
|
cntX = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if qr.Get(y, x) == checkForY {
|
||||||
|
cntY++
|
||||||
|
} else {
|
||||||
|
checkForY = !checkForY
|
||||||
|
if cntY >= 5 {
|
||||||
|
result += cntY - 2
|
||||||
|
}
|
||||||
|
cntY = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cntX >= 5 {
|
||||||
|
result += cntX - 2
|
||||||
|
}
|
||||||
|
if cntY >= 5 {
|
||||||
|
result += cntY - 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) calcPenaltyRule2() uint {
|
||||||
|
var result uint
|
||||||
|
for x := 0; x < qr.dimension-1; x++ {
|
||||||
|
for y := 0; y < qr.dimension-1; y++ {
|
||||||
|
check := qr.Get(x, y)
|
||||||
|
if qr.Get(x, y+1) == check && qr.Get(x+1, y) == check && qr.Get(x+1, y+1) == check {
|
||||||
|
result += 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) calcPenaltyRule3() uint {
|
||||||
|
pattern1 := []bool{true, false, true, true, true, false, true, false, false, false, false}
|
||||||
|
pattern2 := []bool{false, false, false, false, true, false, true, true, true, false, true}
|
||||||
|
|
||||||
|
var result uint
|
||||||
|
for x := 0; x <= qr.dimension-len(pattern1); x++ {
|
||||||
|
for y := 0; y < qr.dimension; y++ {
|
||||||
|
pattern1XFound := true
|
||||||
|
pattern2XFound := true
|
||||||
|
pattern1YFound := true
|
||||||
|
pattern2YFound := true
|
||||||
|
|
||||||
|
for i := 0; i < len(pattern1); i++ {
|
||||||
|
iv := qr.Get(x+i, y)
|
||||||
|
if iv != pattern1[i] {
|
||||||
|
pattern1XFound = false
|
||||||
|
}
|
||||||
|
if iv != pattern2[i] {
|
||||||
|
pattern2XFound = false
|
||||||
|
}
|
||||||
|
iv = qr.Get(y, x+i)
|
||||||
|
if iv != pattern1[i] {
|
||||||
|
pattern1YFound = false
|
||||||
|
}
|
||||||
|
if iv != pattern2[i] {
|
||||||
|
pattern2YFound = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pattern1XFound || pattern2XFound {
|
||||||
|
result += 40
|
||||||
|
}
|
||||||
|
if pattern1YFound || pattern2YFound {
|
||||||
|
result += 40
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) calcPenaltyRule4() uint {
|
||||||
|
totalNum := qr.data.Len()
|
||||||
|
trueCnt := 0
|
||||||
|
for i := 0; i < totalNum; i++ {
|
||||||
|
if qr.data.GetBit(i) {
|
||||||
|
trueCnt++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
percDark := float64(trueCnt) * 100 / float64(totalNum)
|
||||||
|
floor := math.Abs(math.Floor(percDark/5) - 10)
|
||||||
|
ceil := math.Abs(math.Ceil(percDark/5) - 10)
|
||||||
|
return uint(math.Min(floor, ceil) * 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBarcode(dim int) *qrcode {
|
||||||
|
res := new(qrcode)
|
||||||
|
res.dimension = dim
|
||||||
|
res.data = utils.NewBitList(dim * dim)
|
||||||
|
return res
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package qr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/boombuler/barcode/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func encodeUnicode(content string, ecl ErrorCorrectionLevel) (*utils.BitList, *versionInfo, error) {
|
||||||
|
data := []byte(content)
|
||||||
|
|
||||||
|
vi := findSmallestVersionInfo(ecl, byteMode, len(data)*8)
|
||||||
|
if vi == nil {
|
||||||
|
return nil, nil, errors.New("To much data to encode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's not correct to add the unicode bytes to the result directly but most readers can't handle the
|
||||||
|
// required ECI header...
|
||||||
|
res := new(utils.BitList)
|
||||||
|
res.AddBits(int(byteMode), 4)
|
||||||
|
res.AddBits(len(content), vi.charCountBits(byteMode))
|
||||||
|
for _, b := range data {
|
||||||
|
res.AddByte(b)
|
||||||
|
}
|
||||||
|
addPaddingAndTerminator(res, vi)
|
||||||
|
return res, vi, nil
|
||||||
|
}
|
|
@ -0,0 +1,310 @@
|
||||||
|
package qr
|
||||||
|
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
// ErrorCorrectionLevel indicates the amount of "backup data" stored in the QR code
|
||||||
|
type ErrorCorrectionLevel byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
// L recovers 7% of data
|
||||||
|
L ErrorCorrectionLevel = iota
|
||||||
|
// M recovers 15% of data
|
||||||
|
M
|
||||||
|
// Q recovers 25% of data
|
||||||
|
Q
|
||||||
|
// H recovers 30% of data
|
||||||
|
H
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ecl ErrorCorrectionLevel) String() string {
|
||||||
|
switch ecl {
|
||||||
|
case L:
|
||||||
|
return "L"
|
||||||
|
case M:
|
||||||
|
return "M"
|
||||||
|
case Q:
|
||||||
|
return "Q"
|
||||||
|
case H:
|
||||||
|
return "H"
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
type encodingMode byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
numericMode encodingMode = 1
|
||||||
|
alphaNumericMode encodingMode = 2
|
||||||
|
byteMode encodingMode = 4
|
||||||
|
kanjiMode encodingMode = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
type versionInfo struct {
|
||||||
|
Version byte
|
||||||
|
Level ErrorCorrectionLevel
|
||||||
|
ErrorCorrectionCodewordsPerBlock byte
|
||||||
|
NumberOfBlocksInGroup1 byte
|
||||||
|
DataCodeWordsPerBlockInGroup1 byte
|
||||||
|
NumberOfBlocksInGroup2 byte
|
||||||
|
DataCodeWordsPerBlockInGroup2 byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var versionInfos = []*versionInfo{
|
||||||
|
&versionInfo{1, L, 7, 1, 19, 0, 0},
|
||||||
|
&versionInfo{1, M, 10, 1, 16, 0, 0},
|
||||||
|
&versionInfo{1, Q, 13, 1, 13, 0, 0},
|
||||||
|
&versionInfo{1, H, 17, 1, 9, 0, 0},
|
||||||
|
&versionInfo{2, L, 10, 1, 34, 0, 0},
|
||||||
|
&versionInfo{2, M, 16, 1, 28, 0, 0},
|
||||||
|
&versionInfo{2, Q, 22, 1, 22, 0, 0},
|
||||||
|
&versionInfo{2, H, 28, 1, 16, 0, 0},
|
||||||
|
&versionInfo{3, L, 15, 1, 55, 0, 0},
|
||||||
|
&versionInfo{3, M, 26, 1, 44, 0, 0},
|
||||||
|
&versionInfo{3, Q, 18, 2, 17, 0, 0},
|
||||||
|
&versionInfo{3, H, 22, 2, 13, 0, 0},
|
||||||
|
&versionInfo{4, L, 20, 1, 80, 0, 0},
|
||||||
|
&versionInfo{4, M, 18, 2, 32, 0, 0},
|
||||||
|
&versionInfo{4, Q, 26, 2, 24, 0, 0},
|
||||||
|
&versionInfo{4, H, 16, 4, 9, 0, 0},
|
||||||
|
&versionInfo{5, L, 26, 1, 108, 0, 0},
|
||||||
|
&versionInfo{5, M, 24, 2, 43, 0, 0},
|
||||||
|
&versionInfo{5, Q, 18, 2, 15, 2, 16},
|
||||||
|
&versionInfo{5, H, 22, 2, 11, 2, 12},
|
||||||
|
&versionInfo{6, L, 18, 2, 68, 0, 0},
|
||||||
|
&versionInfo{6, M, 16, 4, 27, 0, 0},
|
||||||
|
&versionInfo{6, Q, 24, 4, 19, 0, 0},
|
||||||
|
&versionInfo{6, H, 28, 4, 15, 0, 0},
|
||||||
|
&versionInfo{7, L, 20, 2, 78, 0, 0},
|
||||||
|
&versionInfo{7, M, 18, 4, 31, 0, 0},
|
||||||
|
&versionInfo{7, Q, 18, 2, 14, 4, 15},
|
||||||
|
&versionInfo{7, H, 26, 4, 13, 1, 14},
|
||||||
|
&versionInfo{8, L, 24, 2, 97, 0, 0},
|
||||||
|
&versionInfo{8, M, 22, 2, 38, 2, 39},
|
||||||
|
&versionInfo{8, Q, 22, 4, 18, 2, 19},
|
||||||
|
&versionInfo{8, H, 26, 4, 14, 2, 15},
|
||||||
|
&versionInfo{9, L, 30, 2, 116, 0, 0},
|
||||||
|
&versionInfo{9, M, 22, 3, 36, 2, 37},
|
||||||
|
&versionInfo{9, Q, 20, 4, 16, 4, 17},
|
||||||
|
&versionInfo{9, H, 24, 4, 12, 4, 13},
|
||||||
|
&versionInfo{10, L, 18, 2, 68, 2, 69},
|
||||||
|
&versionInfo{10, M, 26, 4, 43, 1, 44},
|
||||||
|
&versionInfo{10, Q, 24, 6, 19, 2, 20},
|
||||||
|
&versionInfo{10, H, 28, 6, 15, 2, 16},
|
||||||
|
&versionInfo{11, L, 20, 4, 81, 0, 0},
|
||||||
|
&versionInfo{11, M, 30, 1, 50, 4, 51},
|
||||||
|
&versionInfo{11, Q, 28, 4, 22, 4, 23},
|
||||||
|
&versionInfo{11, H, 24, 3, 12, 8, 13},
|
||||||
|
&versionInfo{12, L, 24, 2, 92, 2, 93},
|
||||||
|
&versionInfo{12, M, 22, 6, 36, 2, 37},
|
||||||
|
&versionInfo{12, Q, 26, 4, 20, 6, 21},
|
||||||
|
&versionInfo{12, H, 28, 7, 14, 4, 15},
|
||||||
|
&versionInfo{13, L, 26, 4, 107, 0, 0},
|
||||||
|
&versionInfo{13, M, 22, 8, 37, 1, 38},
|
||||||
|
&versionInfo{13, Q, 24, 8, 20, 4, 21},
|
||||||
|
&versionInfo{13, H, 22, 12, 11, 4, 12},
|
||||||
|
&versionInfo{14, L, 30, 3, 115, 1, 116},
|
||||||
|
&versionInfo{14, M, 24, 4, 40, 5, 41},
|
||||||
|
&versionInfo{14, Q, 20, 11, 16, 5, 17},
|
||||||
|
&versionInfo{14, H, 24, 11, 12, 5, 13},
|
||||||
|
&versionInfo{15, L, 22, 5, 87, 1, 88},
|
||||||
|
&versionInfo{15, M, 24, 5, 41, 5, 42},
|
||||||
|
&versionInfo{15, Q, 30, 5, 24, 7, 25},
|
||||||
|
&versionInfo{15, H, 24, 11, 12, 7, 13},
|
||||||
|
&versionInfo{16, L, 24, 5, 98, 1, 99},
|
||||||
|
&versionInfo{16, M, 28, 7, 45, 3, 46},
|
||||||
|
&versionInfo{16, Q, 24, 15, 19, 2, 20},
|
||||||
|
&versionInfo{16, H, 30, 3, 15, 13, 16},
|
||||||
|
&versionInfo{17, L, 28, 1, 107, 5, 108},
|
||||||
|
&versionInfo{17, M, 28, 10, 46, 1, 47},
|
||||||
|
&versionInfo{17, Q, 28, 1, 22, 15, 23},
|
||||||
|
&versionInfo{17, H, 28, 2, 14, 17, 15},
|
||||||
|
&versionInfo{18, L, 30, 5, 120, 1, 121},
|
||||||
|
&versionInfo{18, M, 26, 9, 43, 4, 44},
|
||||||
|
&versionInfo{18, Q, 28, 17, 22, 1, 23},
|
||||||
|
&versionInfo{18, H, 28, 2, 14, 19, 15},
|
||||||
|
&versionInfo{19, L, 28, 3, 113, 4, 114},
|
||||||
|
&versionInfo{19, M, 26, 3, 44, 11, 45},
|
||||||
|
&versionInfo{19, Q, 26, 17, 21, 4, 22},
|
||||||
|
&versionInfo{19, H, 26, 9, 13, 16, 14},
|
||||||
|
&versionInfo{20, L, 28, 3, 107, 5, 108},
|
||||||
|
&versionInfo{20, M, 26, 3, 41, 13, 42},
|
||||||
|
&versionInfo{20, Q, 30, 15, 24, 5, 25},
|
||||||
|
&versionInfo{20, H, 28, 15, 15, 10, 16},
|
||||||
|
&versionInfo{21, L, 28, 4, 116, 4, 117},
|
||||||
|
&versionInfo{21, M, 26, 17, 42, 0, 0},
|
||||||
|
&versionInfo{21, Q, 28, 17, 22, 6, 23},
|
||||||
|
&versionInfo{21, H, 30, 19, 16, 6, 17},
|
||||||
|
&versionInfo{22, L, 28, 2, 111, 7, 112},
|
||||||
|
&versionInfo{22, M, 28, 17, 46, 0, 0},
|
||||||
|
&versionInfo{22, Q, 30, 7, 24, 16, 25},
|
||||||
|
&versionInfo{22, H, 24, 34, 13, 0, 0},
|
||||||
|
&versionInfo{23, L, 30, 4, 121, 5, 122},
|
||||||
|
&versionInfo{23, M, 28, 4, 47, 14, 48},
|
||||||
|
&versionInfo{23, Q, 30, 11, 24, 14, 25},
|
||||||
|
&versionInfo{23, H, 30, 16, 15, 14, 16},
|
||||||
|
&versionInfo{24, L, 30, 6, 117, 4, 118},
|
||||||
|
&versionInfo{24, M, 28, 6, 45, 14, 46},
|
||||||
|
&versionInfo{24, Q, 30, 11, 24, 16, 25},
|
||||||
|
&versionInfo{24, H, 30, 30, 16, 2, 17},
|
||||||
|
&versionInfo{25, L, 26, 8, 106, 4, 107},
|
||||||
|
&versionInfo{25, M, 28, 8, 47, 13, 48},
|
||||||
|
&versionInfo{25, Q, 30, 7, 24, 22, 25},
|
||||||
|
&versionInfo{25, H, 30, 22, 15, 13, 16},
|
||||||
|
&versionInfo{26, L, 28, 10, 114, 2, 115},
|
||||||
|
&versionInfo{26, M, 28, 19, 46, 4, 47},
|
||||||
|
&versionInfo{26, Q, 28, 28, 22, 6, 23},
|
||||||
|
&versionInfo{26, H, 30, 33, 16, 4, 17},
|
||||||
|
&versionInfo{27, L, 30, 8, 122, 4, 123},
|
||||||
|
&versionInfo{27, M, 28, 22, 45, 3, 46},
|
||||||
|
&versionInfo{27, Q, 30, 8, 23, 26, 24},
|
||||||
|
&versionInfo{27, H, 30, 12, 15, 28, 16},
|
||||||
|
&versionInfo{28, L, 30, 3, 117, 10, 118},
|
||||||
|
&versionInfo{28, M, 28, 3, 45, 23, 46},
|
||||||
|
&versionInfo{28, Q, 30, 4, 24, 31, 25},
|
||||||
|
&versionInfo{28, H, 30, 11, 15, 31, 16},
|
||||||
|
&versionInfo{29, L, 30, 7, 116, 7, 117},
|
||||||
|
&versionInfo{29, M, 28, 21, 45, 7, 46},
|
||||||
|
&versionInfo{29, Q, 30, 1, 23, 37, 24},
|
||||||
|
&versionInfo{29, H, 30, 19, 15, 26, 16},
|
||||||
|
&versionInfo{30, L, 30, 5, 115, 10, 116},
|
||||||
|
&versionInfo{30, M, 28, 19, 47, 10, 48},
|
||||||
|
&versionInfo{30, Q, 30, 15, 24, 25, 25},
|
||||||
|
&versionInfo{30, H, 30, 23, 15, 25, 16},
|
||||||
|
&versionInfo{31, L, 30, 13, 115, 3, 116},
|
||||||
|
&versionInfo{31, M, 28, 2, 46, 29, 47},
|
||||||
|
&versionInfo{31, Q, 30, 42, 24, 1, 25},
|
||||||
|
&versionInfo{31, H, 30, 23, 15, 28, 16},
|
||||||
|
&versionInfo{32, L, 30, 17, 115, 0, 0},
|
||||||
|
&versionInfo{32, M, 28, 10, 46, 23, 47},
|
||||||
|
&versionInfo{32, Q, 30, 10, 24, 35, 25},
|
||||||
|
&versionInfo{32, H, 30, 19, 15, 35, 16},
|
||||||
|
&versionInfo{33, L, 30, 17, 115, 1, 116},
|
||||||
|
&versionInfo{33, M, 28, 14, 46, 21, 47},
|
||||||
|
&versionInfo{33, Q, 30, 29, 24, 19, 25},
|
||||||
|
&versionInfo{33, H, 30, 11, 15, 46, 16},
|
||||||
|
&versionInfo{34, L, 30, 13, 115, 6, 116},
|
||||||
|
&versionInfo{34, M, 28, 14, 46, 23, 47},
|
||||||
|
&versionInfo{34, Q, 30, 44, 24, 7, 25},
|
||||||
|
&versionInfo{34, H, 30, 59, 16, 1, 17},
|
||||||
|
&versionInfo{35, L, 30, 12, 121, 7, 122},
|
||||||
|
&versionInfo{35, M, 28, 12, 47, 26, 48},
|
||||||
|
&versionInfo{35, Q, 30, 39, 24, 14, 25},
|
||||||
|
&versionInfo{35, H, 30, 22, 15, 41, 16},
|
||||||
|
&versionInfo{36, L, 30, 6, 121, 14, 122},
|
||||||
|
&versionInfo{36, M, 28, 6, 47, 34, 48},
|
||||||
|
&versionInfo{36, Q, 30, 46, 24, 10, 25},
|
||||||
|
&versionInfo{36, H, 30, 2, 15, 64, 16},
|
||||||
|
&versionInfo{37, L, 30, 17, 122, 4, 123},
|
||||||
|
&versionInfo{37, M, 28, 29, 46, 14, 47},
|
||||||
|
&versionInfo{37, Q, 30, 49, 24, 10, 25},
|
||||||
|
&versionInfo{37, H, 30, 24, 15, 46, 16},
|
||||||
|
&versionInfo{38, L, 30, 4, 122, 18, 123},
|
||||||
|
&versionInfo{38, M, 28, 13, 46, 32, 47},
|
||||||
|
&versionInfo{38, Q, 30, 48, 24, 14, 25},
|
||||||
|
&versionInfo{38, H, 30, 42, 15, 32, 16},
|
||||||
|
&versionInfo{39, L, 30, 20, 117, 4, 118},
|
||||||
|
&versionInfo{39, M, 28, 40, 47, 7, 48},
|
||||||
|
&versionInfo{39, Q, 30, 43, 24, 22, 25},
|
||||||
|
&versionInfo{39, H, 30, 10, 15, 67, 16},
|
||||||
|
&versionInfo{40, L, 30, 19, 118, 6, 119},
|
||||||
|
&versionInfo{40, M, 28, 18, 47, 31, 48},
|
||||||
|
&versionInfo{40, Q, 30, 34, 24, 34, 25},
|
||||||
|
&versionInfo{40, H, 30, 20, 15, 61, 16},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vi *versionInfo) totalDataBytes() int {
|
||||||
|
g1Data := int(vi.NumberOfBlocksInGroup1) * int(vi.DataCodeWordsPerBlockInGroup1)
|
||||||
|
g2Data := int(vi.NumberOfBlocksInGroup2) * int(vi.DataCodeWordsPerBlockInGroup2)
|
||||||
|
return (g1Data + g2Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vi *versionInfo) charCountBits(m encodingMode) byte {
|
||||||
|
switch m {
|
||||||
|
case numericMode:
|
||||||
|
if vi.Version < 10 {
|
||||||
|
return 10
|
||||||
|
} else if vi.Version < 27 {
|
||||||
|
return 12
|
||||||
|
}
|
||||||
|
return 14
|
||||||
|
|
||||||
|
case alphaNumericMode:
|
||||||
|
if vi.Version < 10 {
|
||||||
|
return 9
|
||||||
|
} else if vi.Version < 27 {
|
||||||
|
return 11
|
||||||
|
}
|
||||||
|
return 13
|
||||||
|
|
||||||
|
case byteMode:
|
||||||
|
if vi.Version < 10 {
|
||||||
|
return 8
|
||||||
|
}
|
||||||
|
return 16
|
||||||
|
|
||||||
|
case kanjiMode:
|
||||||
|
if vi.Version < 10 {
|
||||||
|
return 8
|
||||||
|
} else if vi.Version < 27 {
|
||||||
|
return 10
|
||||||
|
}
|
||||||
|
return 12
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vi *versionInfo) modulWidth() int {
|
||||||
|
return ((int(vi.Version) - 1) * 4) + 21
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vi *versionInfo) alignmentPatternPlacements() []int {
|
||||||
|
if vi.Version == 1 {
|
||||||
|
return make([]int, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
first := 6
|
||||||
|
last := vi.modulWidth() - 7
|
||||||
|
space := float64(last - first)
|
||||||
|
count := int(math.Ceil(space/28)) + 1
|
||||||
|
|
||||||
|
result := make([]int, count)
|
||||||
|
result[0] = first
|
||||||
|
result[len(result)-1] = last
|
||||||
|
if count > 2 {
|
||||||
|
step := int(math.Ceil(float64(last-first) / float64(count-1)))
|
||||||
|
if step%2 == 1 {
|
||||||
|
frac := float64(last-first) / float64(count-1)
|
||||||
|
_, x := math.Modf(frac)
|
||||||
|
if x >= 0.5 {
|
||||||
|
frac = math.Ceil(frac)
|
||||||
|
} else {
|
||||||
|
frac = math.Floor(frac)
|
||||||
|
}
|
||||||
|
|
||||||
|
if int(frac)%2 == 0 {
|
||||||
|
step--
|
||||||
|
} else {
|
||||||
|
step++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i <= count-2; i++ {
|
||||||
|
result[i] = last - (step * (count - 1 - i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func findSmallestVersionInfo(ecl ErrorCorrectionLevel, mode encodingMode, dataBits int) *versionInfo {
|
||||||
|
dataBits = dataBits + 4 // mode indicator
|
||||||
|
for _, vi := range versionInfos {
|
||||||
|
if vi.Level == ecl {
|
||||||
|
if (vi.totalDataBytes() * 8) >= (dataBits + int(vi.charCountBits(mode))) {
|
||||||
|
return vi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,134 @@
|
||||||
|
package barcode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
type wrapFunc func(x, y int) color.Color
|
||||||
|
|
||||||
|
type scaledBarcode struct {
|
||||||
|
wrapped Barcode
|
||||||
|
wrapperFunc wrapFunc
|
||||||
|
rect image.Rectangle
|
||||||
|
}
|
||||||
|
|
||||||
|
type intCSscaledBC struct {
|
||||||
|
scaledBarcode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *scaledBarcode) Content() string {
|
||||||
|
return bc.wrapped.Content()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *scaledBarcode) Metadata() Metadata {
|
||||||
|
return bc.wrapped.Metadata()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *scaledBarcode) ColorModel() color.Model {
|
||||||
|
return bc.wrapped.ColorModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *scaledBarcode) Bounds() image.Rectangle {
|
||||||
|
return bc.rect
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *scaledBarcode) At(x, y int) color.Color {
|
||||||
|
return bc.wrapperFunc(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *intCSscaledBC) CheckSum() int {
|
||||||
|
if cs, ok := bc.wrapped.(BarcodeIntCS); ok {
|
||||||
|
return cs.CheckSum()
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale returns a resized barcode with the given width and height.
|
||||||
|
func Scale(bc Barcode, width, height int) (Barcode, error) {
|
||||||
|
switch bc.Metadata().Dimensions {
|
||||||
|
case 1:
|
||||||
|
return scale1DCode(bc, width, height)
|
||||||
|
case 2:
|
||||||
|
return scale2DCode(bc, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("unsupported barcode format")
|
||||||
|
}
|
||||||
|
|
||||||
|
func newScaledBC(wrapped Barcode, wrapperFunc wrapFunc, rect image.Rectangle) Barcode {
|
||||||
|
result := &scaledBarcode{
|
||||||
|
wrapped: wrapped,
|
||||||
|
wrapperFunc: wrapperFunc,
|
||||||
|
rect: rect,
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := wrapped.(BarcodeIntCS); ok {
|
||||||
|
return &intCSscaledBC{*result}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func scale2DCode(bc Barcode, width, height int) (Barcode, error) {
|
||||||
|
orgBounds := bc.Bounds()
|
||||||
|
orgWidth := orgBounds.Max.X - orgBounds.Min.X
|
||||||
|
orgHeight := orgBounds.Max.Y - orgBounds.Min.Y
|
||||||
|
|
||||||
|
factor := int(math.Min(float64(width)/float64(orgWidth), float64(height)/float64(orgHeight)))
|
||||||
|
if factor <= 0 {
|
||||||
|
return nil, fmt.Errorf("can not scale barcode to an image smaller than %dx%d", orgWidth, orgHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
offsetX := (width - (orgWidth * factor)) / 2
|
||||||
|
offsetY := (height - (orgHeight * factor)) / 2
|
||||||
|
|
||||||
|
wrap := func(x, y int) color.Color {
|
||||||
|
if x < offsetX || y < offsetY {
|
||||||
|
return color.White
|
||||||
|
}
|
||||||
|
x = (x - offsetX) / factor
|
||||||
|
y = (y - offsetY) / factor
|
||||||
|
if x >= orgWidth || y >= orgHeight {
|
||||||
|
return color.White
|
||||||
|
}
|
||||||
|
return bc.At(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newScaledBC(
|
||||||
|
bc,
|
||||||
|
wrap,
|
||||||
|
image.Rect(0, 0, width, height),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func scale1DCode(bc Barcode, width, height int) (Barcode, error) {
|
||||||
|
orgBounds := bc.Bounds()
|
||||||
|
orgWidth := orgBounds.Max.X - orgBounds.Min.X
|
||||||
|
factor := int(float64(width) / float64(orgWidth))
|
||||||
|
|
||||||
|
if factor <= 0 {
|
||||||
|
return nil, fmt.Errorf("can not scale barcode to an image smaller than %dx1", orgWidth)
|
||||||
|
}
|
||||||
|
offsetX := (width - (orgWidth * factor)) / 2
|
||||||
|
|
||||||
|
wrap := func(x, y int) color.Color {
|
||||||
|
if x < offsetX {
|
||||||
|
return color.White
|
||||||
|
}
|
||||||
|
x = (x - offsetX) / factor
|
||||||
|
|
||||||
|
if x >= orgWidth {
|
||||||
|
return color.White
|
||||||
|
}
|
||||||
|
return bc.At(x, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newScaledBC(
|
||||||
|
bc,
|
||||||
|
wrap,
|
||||||
|
image.Rect(0, 0, width, height),
|
||||||
|
), nil
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
// Package utils contain some utilities which are needed to create barcodes
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/boombuler/barcode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type base1DCode struct {
|
||||||
|
*BitList
|
||||||
|
kind string
|
||||||
|
content string
|
||||||
|
}
|
||||||
|
|
||||||
|
type base1DCodeIntCS struct {
|
||||||
|
base1DCode
|
||||||
|
checksum int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *base1DCode) Content() string {
|
||||||
|
return c.content
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *base1DCode) Metadata() barcode.Metadata {
|
||||||
|
return barcode.Metadata{c.kind, 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *base1DCode) ColorModel() color.Model {
|
||||||
|
return color.Gray16Model
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *base1DCode) Bounds() image.Rectangle {
|
||||||
|
return image.Rect(0, 0, c.Len(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *base1DCode) At(x, y int) color.Color {
|
||||||
|
if c.GetBit(x) {
|
||||||
|
return color.Black
|
||||||
|
}
|
||||||
|
return color.White
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *base1DCodeIntCS) CheckSum() int {
|
||||||
|
return c.checksum
|
||||||
|
}
|
||||||
|
|
||||||
|
// New1DCode creates a new 1D barcode where the bars are represented by the bits in the bars BitList
|
||||||
|
func New1DCodeIntCheckSum(codeKind, content string, bars *BitList, checksum int) barcode.BarcodeIntCS {
|
||||||
|
return &base1DCodeIntCS{base1DCode{bars, codeKind, content}, checksum}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New1DCode creates a new 1D barcode where the bars are represented by the bits in the bars BitList
|
||||||
|
func New1DCode(codeKind, content string, bars *BitList) barcode.Barcode {
|
||||||
|
return &base1DCode{bars, codeKind, content}
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
// BitList is a list that contains bits
|
||||||
|
type BitList struct {
|
||||||
|
count int
|
||||||
|
data []int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBitList returns a new BitList with the given length
|
||||||
|
// all bits are initialize with false
|
||||||
|
func NewBitList(capacity int) *BitList {
|
||||||
|
bl := new(BitList)
|
||||||
|
bl.count = capacity
|
||||||
|
x := 0
|
||||||
|
if capacity%32 != 0 {
|
||||||
|
x = 1
|
||||||
|
}
|
||||||
|
bl.data = make([]int32, capacity/32+x)
|
||||||
|
return bl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of contained bits
|
||||||
|
func (bl *BitList) Len() int {
|
||||||
|
return bl.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bl *BitList) grow() {
|
||||||
|
growBy := len(bl.data)
|
||||||
|
if growBy < 128 {
|
||||||
|
growBy = 128
|
||||||
|
} else if growBy >= 1024 {
|
||||||
|
growBy = 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
nd := make([]int32, len(bl.data)+growBy)
|
||||||
|
copy(nd, bl.data)
|
||||||
|
bl.data = nd
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddBit appends the given bits to the end of the list
|
||||||
|
func (bl *BitList) AddBit(bits ...bool) {
|
||||||
|
for _, bit := range bits {
|
||||||
|
itmIndex := bl.count / 32
|
||||||
|
for itmIndex >= len(bl.data) {
|
||||||
|
bl.grow()
|
||||||
|
}
|
||||||
|
bl.SetBit(bl.count, bit)
|
||||||
|
bl.count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBit sets the bit at the given index to the given value
|
||||||
|
func (bl *BitList) SetBit(index int, value bool) {
|
||||||
|
itmIndex := index / 32
|
||||||
|
itmBitShift := 31 - (index % 32)
|
||||||
|
if value {
|
||||||
|
bl.data[itmIndex] = bl.data[itmIndex] | 1<<uint(itmBitShift)
|
||||||
|
} else {
|
||||||
|
bl.data[itmIndex] = bl.data[itmIndex] & ^(1 << uint(itmBitShift))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBit returns the bit at the given index
|
||||||
|
func (bl *BitList) GetBit(index int) bool {
|
||||||
|
itmIndex := index / 32
|
||||||
|
itmBitShift := 31 - (index % 32)
|
||||||
|
return ((bl.data[itmIndex] >> uint(itmBitShift)) & 1) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddByte appends all 8 bits of the given byte to the end of the list
|
||||||
|
func (bl *BitList) AddByte(b byte) {
|
||||||
|
for i := 7; i >= 0; i-- {
|
||||||
|
bl.AddBit(((b >> uint(i)) & 1) == 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddBits appends the last (LSB) 'count' bits of 'b' the the end of the list
|
||||||
|
func (bl *BitList) AddBits(b int, count byte) {
|
||||||
|
for i := int(count) - 1; i >= 0; i-- {
|
||||||
|
bl.AddBit(((b >> uint(i)) & 1) == 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBytes returns all bits of the BitList as a []byte
|
||||||
|
func (bl *BitList) GetBytes() []byte {
|
||||||
|
len := bl.count >> 3
|
||||||
|
if (bl.count % 8) != 0 {
|
||||||
|
len++
|
||||||
|
}
|
||||||
|
result := make([]byte, len)
|
||||||
|
for i := 0; i < len; i++ {
|
||||||
|
shift := (3 - (i % 4)) * 8
|
||||||
|
result[i] = (byte)((bl.data[i/4] >> uint(shift)) & 0xFF)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// IterateBytes iterates through all bytes contained in the BitList
|
||||||
|
func (bl *BitList) IterateBytes() <-chan byte {
|
||||||
|
res := make(chan byte)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
c := bl.count
|
||||||
|
shift := 24
|
||||||
|
i := 0
|
||||||
|
for c > 0 {
|
||||||
|
res <- byte((bl.data[i] >> uint(shift)) & 0xFF)
|
||||||
|
shift -= 8
|
||||||
|
if shift < 0 {
|
||||||
|
shift = 24
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
c -= 8
|
||||||
|
}
|
||||||
|
close(res)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
// GaloisField encapsulates galois field arithmetics
|
||||||
|
type GaloisField struct {
|
||||||
|
Size int
|
||||||
|
Base int
|
||||||
|
ALogTbl []int
|
||||||
|
LogTbl []int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGaloisField creates a new galois field
|
||||||
|
func NewGaloisField(pp, fieldSize, b int) *GaloisField {
|
||||||
|
result := new(GaloisField)
|
||||||
|
|
||||||
|
result.Size = fieldSize
|
||||||
|
result.Base = b
|
||||||
|
result.ALogTbl = make([]int, fieldSize)
|
||||||
|
result.LogTbl = make([]int, fieldSize)
|
||||||
|
|
||||||
|
x := 1
|
||||||
|
for i := 0; i < fieldSize; i++ {
|
||||||
|
result.ALogTbl[i] = x
|
||||||
|
x = x * 2
|
||||||
|
if x >= fieldSize {
|
||||||
|
x = (x ^ pp) & (fieldSize - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < fieldSize; i++ {
|
||||||
|
result.LogTbl[result.ALogTbl[i]] = int(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gf *GaloisField) Zero() *GFPoly {
|
||||||
|
return NewGFPoly(gf, []int{0})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddOrSub add or substract two numbers
|
||||||
|
func (gf *GaloisField) AddOrSub(a, b int) int {
|
||||||
|
return a ^ b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiply multiplys two numbers
|
||||||
|
func (gf *GaloisField) Multiply(a, b int) int {
|
||||||
|
if a == 0 || b == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return gf.ALogTbl[(gf.LogTbl[a]+gf.LogTbl[b])%(gf.Size-1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Divide divides two numbers
|
||||||
|
func (gf *GaloisField) Divide(a, b int) int {
|
||||||
|
if b == 0 {
|
||||||
|
panic("divide by zero")
|
||||||
|
} else if a == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return gf.ALogTbl[(gf.LogTbl[a]-gf.LogTbl[b])%(gf.Size-1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gf *GaloisField) Invers(num int) int {
|
||||||
|
return gf.ALogTbl[(gf.Size-1)-gf.LogTbl[num]]
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
type GFPoly struct {
|
||||||
|
gf *GaloisField
|
||||||
|
Coefficients []int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gp *GFPoly) Degree() int {
|
||||||
|
return len(gp.Coefficients) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gp *GFPoly) Zero() bool {
|
||||||
|
return gp.Coefficients[0] == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCoefficient returns the coefficient of x ^ degree
|
||||||
|
func (gp *GFPoly) GetCoefficient(degree int) int {
|
||||||
|
return gp.Coefficients[gp.Degree()-degree]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gp *GFPoly) AddOrSubstract(other *GFPoly) *GFPoly {
|
||||||
|
if gp.Zero() {
|
||||||
|
return other
|
||||||
|
} else if other.Zero() {
|
||||||
|
return gp
|
||||||
|
}
|
||||||
|
smallCoeff := gp.Coefficients
|
||||||
|
largeCoeff := other.Coefficients
|
||||||
|
if len(smallCoeff) > len(largeCoeff) {
|
||||||
|
largeCoeff, smallCoeff = smallCoeff, largeCoeff
|
||||||
|
}
|
||||||
|
sumDiff := make([]int, len(largeCoeff))
|
||||||
|
lenDiff := len(largeCoeff) - len(smallCoeff)
|
||||||
|
copy(sumDiff, largeCoeff[:lenDiff])
|
||||||
|
for i := lenDiff; i < len(largeCoeff); i++ {
|
||||||
|
sumDiff[i] = int(gp.gf.AddOrSub(int(smallCoeff[i-lenDiff]), int(largeCoeff[i])))
|
||||||
|
}
|
||||||
|
return NewGFPoly(gp.gf, sumDiff)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gp *GFPoly) MultByMonominal(degree int, coeff int) *GFPoly {
|
||||||
|
if coeff == 0 {
|
||||||
|
return gp.gf.Zero()
|
||||||
|
}
|
||||||
|
size := len(gp.Coefficients)
|
||||||
|
result := make([]int, size+degree)
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
result[i] = int(gp.gf.Multiply(int(gp.Coefficients[i]), int(coeff)))
|
||||||
|
}
|
||||||
|
return NewGFPoly(gp.gf, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gp *GFPoly) Multiply(other *GFPoly) *GFPoly {
|
||||||
|
if gp.Zero() || other.Zero() {
|
||||||
|
return gp.gf.Zero()
|
||||||
|
}
|
||||||
|
aCoeff := gp.Coefficients
|
||||||
|
aLen := len(aCoeff)
|
||||||
|
bCoeff := other.Coefficients
|
||||||
|
bLen := len(bCoeff)
|
||||||
|
product := make([]int, aLen+bLen-1)
|
||||||
|
for i := 0; i < aLen; i++ {
|
||||||
|
ac := int(aCoeff[i])
|
||||||
|
for j := 0; j < bLen; j++ {
|
||||||
|
bc := int(bCoeff[j])
|
||||||
|
product[i+j] = int(gp.gf.AddOrSub(int(product[i+j]), gp.gf.Multiply(ac, bc)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NewGFPoly(gp.gf, product)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gp *GFPoly) Divide(other *GFPoly) (quotient *GFPoly, remainder *GFPoly) {
|
||||||
|
quotient = gp.gf.Zero()
|
||||||
|
remainder = gp
|
||||||
|
fld := gp.gf
|
||||||
|
denomLeadTerm := other.GetCoefficient(other.Degree())
|
||||||
|
inversDenomLeadTerm := fld.Invers(int(denomLeadTerm))
|
||||||
|
for remainder.Degree() >= other.Degree() && !remainder.Zero() {
|
||||||
|
degreeDiff := remainder.Degree() - other.Degree()
|
||||||
|
scale := int(fld.Multiply(int(remainder.GetCoefficient(remainder.Degree())), inversDenomLeadTerm))
|
||||||
|
term := other.MultByMonominal(degreeDiff, scale)
|
||||||
|
itQuot := NewMonominalPoly(fld, degreeDiff, scale)
|
||||||
|
quotient = quotient.AddOrSubstract(itQuot)
|
||||||
|
remainder = remainder.AddOrSubstract(term)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMonominalPoly(field *GaloisField, degree int, coeff int) *GFPoly {
|
||||||
|
if coeff == 0 {
|
||||||
|
return field.Zero()
|
||||||
|
}
|
||||||
|
result := make([]int, degree+1)
|
||||||
|
result[0] = coeff
|
||||||
|
return NewGFPoly(field, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGFPoly(field *GaloisField, coefficients []int) *GFPoly {
|
||||||
|
for len(coefficients) > 1 && coefficients[0] == 0 {
|
||||||
|
coefficients = coefficients[1:]
|
||||||
|
}
|
||||||
|
return &GFPoly{field, coefficients}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReedSolomonEncoder struct {
|
||||||
|
gf *GaloisField
|
||||||
|
polynomes []*GFPoly
|
||||||
|
m *sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReedSolomonEncoder(gf *GaloisField) *ReedSolomonEncoder {
|
||||||
|
return &ReedSolomonEncoder{
|
||||||
|
gf, []*GFPoly{NewGFPoly(gf, []int{1})}, new(sync.Mutex),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *ReedSolomonEncoder) getPolynomial(degree int) *GFPoly {
|
||||||
|
rs.m.Lock()
|
||||||
|
defer rs.m.Unlock()
|
||||||
|
|
||||||
|
if degree >= len(rs.polynomes) {
|
||||||
|
last := rs.polynomes[len(rs.polynomes)-1]
|
||||||
|
for d := len(rs.polynomes); d <= degree; d++ {
|
||||||
|
next := last.Multiply(NewGFPoly(rs.gf, []int{1, rs.gf.ALogTbl[d-1+rs.gf.Base]}))
|
||||||
|
rs.polynomes = append(rs.polynomes, next)
|
||||||
|
last = next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rs.polynomes[degree]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *ReedSolomonEncoder) Encode(data []int, eccCount int) []int {
|
||||||
|
generator := rs.getPolynomial(eccCount)
|
||||||
|
info := NewGFPoly(rs.gf, data)
|
||||||
|
info = info.MultByMonominal(eccCount, 1)
|
||||||
|
_, remainder := info.Divide(generator)
|
||||||
|
|
||||||
|
result := make([]int, eccCount)
|
||||||
|
numZero := int(eccCount) - len(remainder.Coefficients)
|
||||||
|
copy(result[numZero:], remainder.Coefficients)
|
||||||
|
return result
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
// RuneToInt converts a rune between '0' and '9' to an integer between 0 and 9
|
||||||
|
// If the rune is outside of this range -1 is returned.
|
||||||
|
func RuneToInt(r rune) int {
|
||||||
|
if r >= '0' && r <= '9' {
|
||||||
|
return int(r - '0')
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntToRune converts a digit 0 - 9 to the rune '0' - '9'. If the given int is outside
|
||||||
|
// of this range 'F' is returned!
|
||||||
|
func IntToRune(i int) rune {
|
||||||
|
if i >= 0 && i <= 9 {
|
||||||
|
return rune(i + '0')
|
||||||
|
}
|
||||||
|
return 'F'
|
||||||
|
}
|
|
@ -0,0 +1,202 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
|
@ -0,0 +1,684 @@
|
||||||
|
/*
|
||||||
|
Copyright 2011 Google Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package memcache provides a client for the memcached cache server.
|
||||||
|
package memcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Similar to:
|
||||||
|
// http://code.google.com/appengine/docs/go/memcache/reference.html
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrCacheMiss means that a Get failed because the item wasn't present.
|
||||||
|
ErrCacheMiss = errors.New("memcache: cache miss")
|
||||||
|
|
||||||
|
// ErrCASConflict means that a CompareAndSwap call failed due to the
|
||||||
|
// cached value being modified between the Get and the CompareAndSwap.
|
||||||
|
// If the cached value was simply evicted rather than replaced,
|
||||||
|
// ErrNotStored will be returned instead.
|
||||||
|
ErrCASConflict = errors.New("memcache: compare-and-swap conflict")
|
||||||
|
|
||||||
|
// ErrNotStored means that a conditional write operation (i.e. Add or
|
||||||
|
// CompareAndSwap) failed because the condition was not satisfied.
|
||||||
|
ErrNotStored = errors.New("memcache: item not stored")
|
||||||
|
|
||||||
|
// ErrServer means that a server error occurred.
|
||||||
|
ErrServerError = errors.New("memcache: server error")
|
||||||
|
|
||||||
|
// ErrNoStats means that no statistics were available.
|
||||||
|
ErrNoStats = errors.New("memcache: no statistics available")
|
||||||
|
|
||||||
|
// ErrMalformedKey is returned when an invalid key is used.
|
||||||
|
// Keys must be at maximum 250 bytes long and not
|
||||||
|
// contain whitespace or control characters.
|
||||||
|
ErrMalformedKey = errors.New("malformed: key is too long or contains invalid characters")
|
||||||
|
|
||||||
|
// ErrNoServers is returned when no servers are configured or available.
|
||||||
|
ErrNoServers = errors.New("memcache: no servers configured or available")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultTimeout is the default socket read/write timeout.
|
||||||
|
DefaultTimeout = 100 * time.Millisecond
|
||||||
|
|
||||||
|
// DefaultMaxIdleConns is the default maximum number of idle connections
|
||||||
|
// kept for any single address.
|
||||||
|
DefaultMaxIdleConns = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
const buffered = 8 // arbitrary buffered channel size, for readability
|
||||||
|
|
||||||
|
// resumableError returns true if err is only a protocol-level cache error.
|
||||||
|
// This is used to determine whether or not a server connection should
|
||||||
|
// be re-used or not. If an error occurs, by default we don't reuse the
|
||||||
|
// connection, unless it was just a cache error.
|
||||||
|
func resumableError(err error) bool {
|
||||||
|
switch err {
|
||||||
|
case ErrCacheMiss, ErrCASConflict, ErrNotStored, ErrMalformedKey:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func legalKey(key string) bool {
|
||||||
|
if len(key) > 250 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i < len(key); i++ {
|
||||||
|
if key[i] <= ' ' || key[i] == 0x7f {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
crlf = []byte("\r\n")
|
||||||
|
space = []byte(" ")
|
||||||
|
resultOK = []byte("OK\r\n")
|
||||||
|
resultStored = []byte("STORED\r\n")
|
||||||
|
resultNotStored = []byte("NOT_STORED\r\n")
|
||||||
|
resultExists = []byte("EXISTS\r\n")
|
||||||
|
resultNotFound = []byte("NOT_FOUND\r\n")
|
||||||
|
resultDeleted = []byte("DELETED\r\n")
|
||||||
|
resultEnd = []byte("END\r\n")
|
||||||
|
resultOk = []byte("OK\r\n")
|
||||||
|
resultTouched = []byte("TOUCHED\r\n")
|
||||||
|
|
||||||
|
resultClientErrorPrefix = []byte("CLIENT_ERROR ")
|
||||||
|
)
|
||||||
|
|
||||||
|
// New returns a memcache client using the provided server(s)
|
||||||
|
// with equal weight. If a server is listed multiple times,
|
||||||
|
// it gets a proportional amount of weight.
|
||||||
|
func New(server ...string) *Client {
|
||||||
|
ss := new(ServerList)
|
||||||
|
ss.SetServers(server...)
|
||||||
|
return NewFromSelector(ss)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFromSelector returns a new Client using the provided ServerSelector.
|
||||||
|
func NewFromSelector(ss ServerSelector) *Client {
|
||||||
|
return &Client{selector: ss}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client is a memcache client.
|
||||||
|
// It is safe for unlocked use by multiple concurrent goroutines.
|
||||||
|
type Client struct {
|
||||||
|
// Timeout specifies the socket read/write timeout.
|
||||||
|
// If zero, DefaultTimeout is used.
|
||||||
|
Timeout time.Duration
|
||||||
|
|
||||||
|
// MaxIdleConns specifies the maximum number of idle connections that will
|
||||||
|
// be maintained per address. If less than one, DefaultMaxIdleConns will be
|
||||||
|
// used.
|
||||||
|
//
|
||||||
|
// Consider your expected traffic rates and latency carefully. This should
|
||||||
|
// be set to a number higher than your peak parallel requests.
|
||||||
|
MaxIdleConns int
|
||||||
|
|
||||||
|
selector ServerSelector
|
||||||
|
|
||||||
|
lk sync.Mutex
|
||||||
|
freeconn map[string][]*conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item is an item to be got or stored in a memcached server.
|
||||||
|
type Item struct {
|
||||||
|
// Key is the Item's key (250 bytes maximum).
|
||||||
|
Key string
|
||||||
|
|
||||||
|
// Value is the Item's value.
|
||||||
|
Value []byte
|
||||||
|
|
||||||
|
// Flags are server-opaque flags whose semantics are entirely
|
||||||
|
// up to the app.
|
||||||
|
Flags uint32
|
||||||
|
|
||||||
|
// Expiration is the cache expiration time, in seconds: either a relative
|
||||||
|
// time from now (up to 1 month), or an absolute Unix epoch time.
|
||||||
|
// Zero means the Item has no expiration time.
|
||||||
|
Expiration int32
|
||||||
|
|
||||||
|
// Compare and swap ID.
|
||||||
|
casid uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// conn is a connection to a server.
|
||||||
|
type conn struct {
|
||||||
|
nc net.Conn
|
||||||
|
rw *bufio.ReadWriter
|
||||||
|
addr net.Addr
|
||||||
|
c *Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// release returns this connection back to the client's free pool
|
||||||
|
func (cn *conn) release() {
|
||||||
|
cn.c.putFreeConn(cn.addr, cn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cn *conn) extendDeadline() {
|
||||||
|
cn.nc.SetDeadline(time.Now().Add(cn.c.netTimeout()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// condRelease releases this connection if the error pointed to by err
|
||||||
|
// is nil (not an error) or is only a protocol level error (e.g. a
|
||||||
|
// cache miss). The purpose is to not recycle TCP connections that
|
||||||
|
// are bad.
|
||||||
|
func (cn *conn) condRelease(err *error) {
|
||||||
|
if *err == nil || resumableError(*err) {
|
||||||
|
cn.release()
|
||||||
|
} else {
|
||||||
|
cn.nc.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) putFreeConn(addr net.Addr, cn *conn) {
|
||||||
|
c.lk.Lock()
|
||||||
|
defer c.lk.Unlock()
|
||||||
|
if c.freeconn == nil {
|
||||||
|
c.freeconn = make(map[string][]*conn)
|
||||||
|
}
|
||||||
|
freelist := c.freeconn[addr.String()]
|
||||||
|
if len(freelist) >= c.maxIdleConns() {
|
||||||
|
cn.nc.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.freeconn[addr.String()] = append(freelist, cn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getFreeConn(addr net.Addr) (cn *conn, ok bool) {
|
||||||
|
c.lk.Lock()
|
||||||
|
defer c.lk.Unlock()
|
||||||
|
if c.freeconn == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
freelist, ok := c.freeconn[addr.String()]
|
||||||
|
if !ok || len(freelist) == 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
cn = freelist[len(freelist)-1]
|
||||||
|
c.freeconn[addr.String()] = freelist[:len(freelist)-1]
|
||||||
|
return cn, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) netTimeout() time.Duration {
|
||||||
|
if c.Timeout != 0 {
|
||||||
|
return c.Timeout
|
||||||
|
}
|
||||||
|
return DefaultTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) maxIdleConns() int {
|
||||||
|
if c.MaxIdleConns > 0 {
|
||||||
|
return c.MaxIdleConns
|
||||||
|
}
|
||||||
|
return DefaultMaxIdleConns
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectTimeoutError is the error type used when it takes
|
||||||
|
// too long to connect to the desired host. This level of
|
||||||
|
// detail can generally be ignored.
|
||||||
|
type ConnectTimeoutError struct {
|
||||||
|
Addr net.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cte *ConnectTimeoutError) Error() string {
|
||||||
|
return "memcache: connect timeout to " + cte.Addr.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) dial(addr net.Addr) (net.Conn, error) {
|
||||||
|
type connError struct {
|
||||||
|
cn net.Conn
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
nc, err := net.DialTimeout(addr.Network(), addr.String(), c.netTimeout())
|
||||||
|
if err == nil {
|
||||||
|
return nc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
||||||
|
return nil, &ConnectTimeoutError{addr}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getConn(addr net.Addr) (*conn, error) {
|
||||||
|
cn, ok := c.getFreeConn(addr)
|
||||||
|
if ok {
|
||||||
|
cn.extendDeadline()
|
||||||
|
return cn, nil
|
||||||
|
}
|
||||||
|
nc, err := c.dial(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cn = &conn{
|
||||||
|
nc: nc,
|
||||||
|
addr: addr,
|
||||||
|
rw: bufio.NewReadWriter(bufio.NewReader(nc), bufio.NewWriter(nc)),
|
||||||
|
c: c,
|
||||||
|
}
|
||||||
|
cn.extendDeadline()
|
||||||
|
return cn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) onItem(item *Item, fn func(*Client, *bufio.ReadWriter, *Item) error) error {
|
||||||
|
addr, err := c.selector.PickServer(item.Key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cn, err := c.getConn(addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer cn.condRelease(&err)
|
||||||
|
if err = fn(c, cn.rw, item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) FlushAll() error {
|
||||||
|
return c.selector.Each(c.flushAllFromAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get gets the item for the given key. ErrCacheMiss is returned for a
|
||||||
|
// memcache cache miss. The key must be at most 250 bytes in length.
|
||||||
|
func (c *Client) Get(key string) (item *Item, err error) {
|
||||||
|
err = c.withKeyAddr(key, func(addr net.Addr) error {
|
||||||
|
return c.getFromAddr(addr, []string{key}, func(it *Item) { item = it })
|
||||||
|
})
|
||||||
|
if err == nil && item == nil {
|
||||||
|
err = ErrCacheMiss
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Touch updates the expiry for the given key. The seconds parameter is either
|
||||||
|
// a Unix timestamp or, if seconds is less than 1 month, the number of seconds
|
||||||
|
// into the future at which time the item will expire. ErrCacheMiss is returned if the
|
||||||
|
// key is not in the cache. The key must be at most 250 bytes in length.
|
||||||
|
func (c *Client) Touch(key string, seconds int32) (err error) {
|
||||||
|
return c.withKeyAddr(key, func(addr net.Addr) error {
|
||||||
|
return c.touchFromAddr(addr, []string{key}, seconds)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) withKeyAddr(key string, fn func(net.Addr) error) (err error) {
|
||||||
|
if !legalKey(key) {
|
||||||
|
return ErrMalformedKey
|
||||||
|
}
|
||||||
|
addr, err := c.selector.PickServer(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fn(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) withAddrRw(addr net.Addr, fn func(*bufio.ReadWriter) error) (err error) {
|
||||||
|
cn, err := c.getConn(addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer cn.condRelease(&err)
|
||||||
|
return fn(cn.rw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) withKeyRw(key string, fn func(*bufio.ReadWriter) error) error {
|
||||||
|
return c.withKeyAddr(key, func(addr net.Addr) error {
|
||||||
|
return c.withAddrRw(addr, fn)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getFromAddr(addr net.Addr, keys []string, cb func(*Item)) error {
|
||||||
|
return c.withAddrRw(addr, func(rw *bufio.ReadWriter) error {
|
||||||
|
if _, err := fmt.Fprintf(rw, "gets %s\r\n", strings.Join(keys, " ")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := rw.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := parseGetResponse(rw.Reader, cb); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// flushAllFromAddr send the flush_all command to the given addr
|
||||||
|
func (c *Client) flushAllFromAddr(addr net.Addr) error {
|
||||||
|
return c.withAddrRw(addr, func(rw *bufio.ReadWriter) error {
|
||||||
|
if _, err := fmt.Fprintf(rw, "flush_all\r\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := rw.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
line, err := rw.ReadSlice('\n')
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(line, resultOk):
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("memcache: unexpected response line from flush_all: %q", string(line))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) touchFromAddr(addr net.Addr, keys []string, expiration int32) error {
|
||||||
|
return c.withAddrRw(addr, func(rw *bufio.ReadWriter) error {
|
||||||
|
for _, key := range keys {
|
||||||
|
if _, err := fmt.Fprintf(rw, "touch %s %d\r\n", key, expiration); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := rw.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
line, err := rw.ReadSlice('\n')
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(line, resultTouched):
|
||||||
|
break
|
||||||
|
case bytes.Equal(line, resultNotFound):
|
||||||
|
return ErrCacheMiss
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("memcache: unexpected response line from touch: %q", string(line))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMulti is a batch version of Get. The returned map from keys to
|
||||||
|
// items may have fewer elements than the input slice, due to memcache
|
||||||
|
// cache misses. Each key must be at most 250 bytes in length.
|
||||||
|
// If no error is returned, the returned map will also be non-nil.
|
||||||
|
func (c *Client) GetMulti(keys []string) (map[string]*Item, error) {
|
||||||
|
var lk sync.Mutex
|
||||||
|
m := make(map[string]*Item)
|
||||||
|
addItemToMap := func(it *Item) {
|
||||||
|
lk.Lock()
|
||||||
|
defer lk.Unlock()
|
||||||
|
m[it.Key] = it
|
||||||
|
}
|
||||||
|
|
||||||
|
keyMap := make(map[net.Addr][]string)
|
||||||
|
for _, key := range keys {
|
||||||
|
if !legalKey(key) {
|
||||||
|
return nil, ErrMalformedKey
|
||||||
|
}
|
||||||
|
addr, err := c.selector.PickServer(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
keyMap[addr] = append(keyMap[addr], key)
|
||||||
|
}
|
||||||
|
|
||||||
|
ch := make(chan error, buffered)
|
||||||
|
for addr, keys := range keyMap {
|
||||||
|
go func(addr net.Addr, keys []string) {
|
||||||
|
ch <- c.getFromAddr(addr, keys, addItemToMap)
|
||||||
|
}(addr, keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
for _ = range keyMap {
|
||||||
|
if ge := <-ch; ge != nil {
|
||||||
|
err = ge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseGetResponse reads a GET response from r and calls cb for each
|
||||||
|
// read and allocated Item
|
||||||
|
func parseGetResponse(r *bufio.Reader, cb func(*Item)) error {
|
||||||
|
for {
|
||||||
|
line, err := r.ReadSlice('\n')
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if bytes.Equal(line, resultEnd) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
it := new(Item)
|
||||||
|
size, err := scanGetResponseLine(line, it)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
it.Value, err = ioutil.ReadAll(io.LimitReader(r, int64(size)+2))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !bytes.HasSuffix(it.Value, crlf) {
|
||||||
|
return fmt.Errorf("memcache: corrupt get result read")
|
||||||
|
}
|
||||||
|
it.Value = it.Value[:size]
|
||||||
|
cb(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanGetResponseLine populates it and returns the declared size of the item.
|
||||||
|
// It does not read the bytes of the item.
|
||||||
|
func scanGetResponseLine(line []byte, it *Item) (size int, err error) {
|
||||||
|
pattern := "VALUE %s %d %d %d\r\n"
|
||||||
|
dest := []interface{}{&it.Key, &it.Flags, &size, &it.casid}
|
||||||
|
if bytes.Count(line, space) == 3 {
|
||||||
|
pattern = "VALUE %s %d %d\r\n"
|
||||||
|
dest = dest[:3]
|
||||||
|
}
|
||||||
|
n, err := fmt.Sscanf(string(line), pattern, dest...)
|
||||||
|
if err != nil || n != len(dest) {
|
||||||
|
return -1, fmt.Errorf("memcache: unexpected line in get response: %q", line)
|
||||||
|
}
|
||||||
|
return size, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set writes the given item, unconditionally.
|
||||||
|
func (c *Client) Set(item *Item) error {
|
||||||
|
return c.onItem(item, (*Client).set)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) set(rw *bufio.ReadWriter, item *Item) error {
|
||||||
|
return c.populateOne(rw, "set", item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add writes the given item, if no value already exists for its
|
||||||
|
// key. ErrNotStored is returned if that condition is not met.
|
||||||
|
func (c *Client) Add(item *Item) error {
|
||||||
|
return c.onItem(item, (*Client).add)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) add(rw *bufio.ReadWriter, item *Item) error {
|
||||||
|
return c.populateOne(rw, "add", item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace writes the given item, but only if the server *does*
|
||||||
|
// already hold data for this key
|
||||||
|
func (c *Client) Replace(item *Item) error {
|
||||||
|
return c.onItem(item, (*Client).replace)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) replace(rw *bufio.ReadWriter, item *Item) error {
|
||||||
|
return c.populateOne(rw, "replace", item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompareAndSwap writes the given item that was previously returned
|
||||||
|
// by Get, if the value was neither modified or evicted between the
|
||||||
|
// Get and the CompareAndSwap calls. The item's Key should not change
|
||||||
|
// between calls but all other item fields may differ. ErrCASConflict
|
||||||
|
// is returned if the value was modified in between the
|
||||||
|
// calls. ErrNotStored is returned if the value was evicted in between
|
||||||
|
// the calls.
|
||||||
|
func (c *Client) CompareAndSwap(item *Item) error {
|
||||||
|
return c.onItem(item, (*Client).cas)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) cas(rw *bufio.ReadWriter, item *Item) error {
|
||||||
|
return c.populateOne(rw, "cas", item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) populateOne(rw *bufio.ReadWriter, verb string, item *Item) error {
|
||||||
|
if !legalKey(item.Key) {
|
||||||
|
return ErrMalformedKey
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
if verb == "cas" {
|
||||||
|
_, err = fmt.Fprintf(rw, "%s %s %d %d %d %d\r\n",
|
||||||
|
verb, item.Key, item.Flags, item.Expiration, len(item.Value), item.casid)
|
||||||
|
} else {
|
||||||
|
_, err = fmt.Fprintf(rw, "%s %s %d %d %d\r\n",
|
||||||
|
verb, item.Key, item.Flags, item.Expiration, len(item.Value))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err = rw.Write(item.Value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := rw.Write(crlf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := rw.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
line, err := rw.ReadSlice('\n')
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(line, resultStored):
|
||||||
|
return nil
|
||||||
|
case bytes.Equal(line, resultNotStored):
|
||||||
|
return ErrNotStored
|
||||||
|
case bytes.Equal(line, resultExists):
|
||||||
|
return ErrCASConflict
|
||||||
|
case bytes.Equal(line, resultNotFound):
|
||||||
|
return ErrCacheMiss
|
||||||
|
}
|
||||||
|
return fmt.Errorf("memcache: unexpected response line from %q: %q", verb, string(line))
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeReadLine(rw *bufio.ReadWriter, format string, args ...interface{}) ([]byte, error) {
|
||||||
|
_, err := fmt.Fprintf(rw, format, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rw.Flush(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
line, err := rw.ReadSlice('\n')
|
||||||
|
return line, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeExpectf(rw *bufio.ReadWriter, expect []byte, format string, args ...interface{}) error {
|
||||||
|
line, err := writeReadLine(rw, format, args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(line, resultOK):
|
||||||
|
return nil
|
||||||
|
case bytes.Equal(line, expect):
|
||||||
|
return nil
|
||||||
|
case bytes.Equal(line, resultNotStored):
|
||||||
|
return ErrNotStored
|
||||||
|
case bytes.Equal(line, resultExists):
|
||||||
|
return ErrCASConflict
|
||||||
|
case bytes.Equal(line, resultNotFound):
|
||||||
|
return ErrCacheMiss
|
||||||
|
}
|
||||||
|
return fmt.Errorf("memcache: unexpected response line: %q", string(line))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes the item with the provided key. The error ErrCacheMiss is
|
||||||
|
// returned if the item didn't already exist in the cache.
|
||||||
|
func (c *Client) Delete(key string) error {
|
||||||
|
return c.withKeyRw(key, func(rw *bufio.ReadWriter) error {
|
||||||
|
return writeExpectf(rw, resultDeleted, "delete %s\r\n", key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAll deletes all items in the cache.
|
||||||
|
func (c *Client) DeleteAll() error {
|
||||||
|
return c.withKeyRw("", func(rw *bufio.ReadWriter) error {
|
||||||
|
return writeExpectf(rw, resultDeleted, "flush_all\r\n")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment atomically increments key by delta. The return value is
|
||||||
|
// the new value after being incremented or an error. If the value
|
||||||
|
// didn't exist in memcached the error is ErrCacheMiss. The value in
|
||||||
|
// memcached must be an decimal number, or an error will be returned.
|
||||||
|
// On 64-bit overflow, the new value wraps around.
|
||||||
|
func (c *Client) Increment(key string, delta uint64) (newValue uint64, err error) {
|
||||||
|
return c.incrDecr("incr", key, delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrement atomically decrements key by delta. The return value is
|
||||||
|
// the new value after being decremented or an error. If the value
|
||||||
|
// didn't exist in memcached the error is ErrCacheMiss. The value in
|
||||||
|
// memcached must be an decimal number, or an error will be returned.
|
||||||
|
// On underflow, the new value is capped at zero and does not wrap
|
||||||
|
// around.
|
||||||
|
func (c *Client) Decrement(key string, delta uint64) (newValue uint64, err error) {
|
||||||
|
return c.incrDecr("decr", key, delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) incrDecr(verb, key string, delta uint64) (uint64, error) {
|
||||||
|
var val uint64
|
||||||
|
err := c.withKeyRw(key, func(rw *bufio.ReadWriter) error {
|
||||||
|
line, err := writeReadLine(rw, "%s %s %d\r\n", verb, key, delta)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(line, resultNotFound):
|
||||||
|
return ErrCacheMiss
|
||||||
|
case bytes.HasPrefix(line, resultClientErrorPrefix):
|
||||||
|
errMsg := line[len(resultClientErrorPrefix) : len(line)-2]
|
||||||
|
return errors.New("memcache: client error: " + string(errMsg))
|
||||||
|
}
|
||||||
|
val, err = strconv.ParseUint(string(line[:len(line)-2]), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return val, err
|
||||||
|
}
|
|
@ -0,0 +1,129 @@
|
||||||
|
/*
|
||||||
|
Copyright 2011 Google Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package memcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"hash/crc32"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServerSelector is the interface that selects a memcache server
|
||||||
|
// as a function of the item's key.
|
||||||
|
//
|
||||||
|
// All ServerSelector implementations must be safe for concurrent use
|
||||||
|
// by multiple goroutines.
|
||||||
|
type ServerSelector interface {
|
||||||
|
// PickServer returns the server address that a given item
|
||||||
|
// should be shared onto.
|
||||||
|
PickServer(key string) (net.Addr, error)
|
||||||
|
Each(func(net.Addr) error) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerList is a simple ServerSelector. Its zero value is usable.
|
||||||
|
type ServerList struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
addrs []net.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// staticAddr caches the Network() and String() values from any net.Addr.
|
||||||
|
type staticAddr struct {
|
||||||
|
ntw, str string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStaticAddr(a net.Addr) net.Addr {
|
||||||
|
return &staticAddr{
|
||||||
|
ntw: a.Network(),
|
||||||
|
str: a.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *staticAddr) Network() string { return s.ntw }
|
||||||
|
func (s *staticAddr) String() string { return s.str }
|
||||||
|
|
||||||
|
// SetServers changes a ServerList's set of servers at runtime and is
|
||||||
|
// safe for concurrent use by multiple goroutines.
|
||||||
|
//
|
||||||
|
// Each server is given equal weight. A server is given more weight
|
||||||
|
// if it's listed multiple times.
|
||||||
|
//
|
||||||
|
// SetServers returns an error if any of the server names fail to
|
||||||
|
// resolve. No attempt is made to connect to the server. If any error
|
||||||
|
// is returned, no changes are made to the ServerList.
|
||||||
|
func (ss *ServerList) SetServers(servers ...string) error {
|
||||||
|
naddr := make([]net.Addr, len(servers))
|
||||||
|
for i, server := range servers {
|
||||||
|
if strings.Contains(server, "/") {
|
||||||
|
addr, err := net.ResolveUnixAddr("unix", server)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
naddr[i] = newStaticAddr(addr)
|
||||||
|
} else {
|
||||||
|
tcpaddr, err := net.ResolveTCPAddr("tcp", server)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
naddr[i] = newStaticAddr(tcpaddr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ss.mu.Lock()
|
||||||
|
defer ss.mu.Unlock()
|
||||||
|
ss.addrs = naddr
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each iterates over each server calling the given function
|
||||||
|
func (ss *ServerList) Each(f func(net.Addr) error) error {
|
||||||
|
ss.mu.RLock()
|
||||||
|
defer ss.mu.RUnlock()
|
||||||
|
for _, a := range ss.addrs {
|
||||||
|
if err := f(a); nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// keyBufPool returns []byte buffers for use by PickServer's call to
|
||||||
|
// crc32.ChecksumIEEE to avoid allocations. (but doesn't avoid the
|
||||||
|
// copies, which at least are bounded in size and small)
|
||||||
|
var keyBufPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
b := make([]byte, 256)
|
||||||
|
return &b
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *ServerList) PickServer(key string) (net.Addr, error) {
|
||||||
|
ss.mu.RLock()
|
||||||
|
defer ss.mu.RUnlock()
|
||||||
|
if len(ss.addrs) == 0 {
|
||||||
|
return nil, ErrNoServers
|
||||||
|
}
|
||||||
|
if len(ss.addrs) == 1 {
|
||||||
|
return ss.addrs[0], nil
|
||||||
|
}
|
||||||
|
bufp := keyBufPool.Get().(*[]byte)
|
||||||
|
n := copy(*bufp, key)
|
||||||
|
cs := crc32.ChecksumIEEE((*bufp)[:n])
|
||||||
|
keyBufPool.Put(bufp)
|
||||||
|
|
||||||
|
return ss.addrs[cs%uint32(len(ss.addrs))], nil
|
||||||
|
}
|
|
@ -0,0 +1,175 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
|
@ -0,0 +1,618 @@
|
||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// conn is the low-level implementation of Conn
|
||||||
|
type conn struct {
|
||||||
|
|
||||||
|
// Shared
|
||||||
|
mu sync.Mutex
|
||||||
|
pending int
|
||||||
|
err error
|
||||||
|
conn net.Conn
|
||||||
|
|
||||||
|
// Read
|
||||||
|
readTimeout time.Duration
|
||||||
|
br *bufio.Reader
|
||||||
|
|
||||||
|
// Write
|
||||||
|
writeTimeout time.Duration
|
||||||
|
bw *bufio.Writer
|
||||||
|
|
||||||
|
// Scratch space for formatting argument length.
|
||||||
|
// '*' or '$', length, "\r\n"
|
||||||
|
lenScratch [32]byte
|
||||||
|
|
||||||
|
// Scratch space for formatting integers and floats.
|
||||||
|
numScratch [40]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialTimeout acts like Dial but takes timeouts for establishing the
|
||||||
|
// connection to the server, writing a command and reading a reply.
|
||||||
|
//
|
||||||
|
// Deprecated: Use Dial with options instead.
|
||||||
|
func DialTimeout(network, address string, connectTimeout, readTimeout, writeTimeout time.Duration) (Conn, error) {
|
||||||
|
return Dial(network, address,
|
||||||
|
DialConnectTimeout(connectTimeout),
|
||||||
|
DialReadTimeout(readTimeout),
|
||||||
|
DialWriteTimeout(writeTimeout))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialOption specifies an option for dialing a Redis server.
|
||||||
|
type DialOption struct {
|
||||||
|
f func(*dialOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
type dialOptions struct {
|
||||||
|
readTimeout time.Duration
|
||||||
|
writeTimeout time.Duration
|
||||||
|
dial func(network, addr string) (net.Conn, error)
|
||||||
|
db int
|
||||||
|
password string
|
||||||
|
dialTLS bool
|
||||||
|
skipVerify bool
|
||||||
|
tlsConfig *tls.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialReadTimeout specifies the timeout for reading a single command reply.
|
||||||
|
func DialReadTimeout(d time.Duration) DialOption {
|
||||||
|
return DialOption{func(do *dialOptions) {
|
||||||
|
do.readTimeout = d
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialWriteTimeout specifies the timeout for writing a single command.
|
||||||
|
func DialWriteTimeout(d time.Duration) DialOption {
|
||||||
|
return DialOption{func(do *dialOptions) {
|
||||||
|
do.writeTimeout = d
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialConnectTimeout specifies the timeout for connecting to the Redis server.
|
||||||
|
func DialConnectTimeout(d time.Duration) DialOption {
|
||||||
|
return DialOption{func(do *dialOptions) {
|
||||||
|
dialer := net.Dialer{Timeout: d}
|
||||||
|
do.dial = dialer.Dial
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialNetDial specifies a custom dial function for creating TCP
|
||||||
|
// connections. If this option is left out, then net.Dial is
|
||||||
|
// used. DialNetDial overrides DialConnectTimeout.
|
||||||
|
func DialNetDial(dial func(network, addr string) (net.Conn, error)) DialOption {
|
||||||
|
return DialOption{func(do *dialOptions) {
|
||||||
|
do.dial = dial
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialDatabase specifies the database to select when dialing a connection.
|
||||||
|
func DialDatabase(db int) DialOption {
|
||||||
|
return DialOption{func(do *dialOptions) {
|
||||||
|
do.db = db
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialPassword specifies the password to use when connecting to
|
||||||
|
// the Redis server.
|
||||||
|
func DialPassword(password string) DialOption {
|
||||||
|
return DialOption{func(do *dialOptions) {
|
||||||
|
do.password = password
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialTLSConfig specifies the config to use when a TLS connection is dialed.
|
||||||
|
// Has no effect when not dialing a TLS connection.
|
||||||
|
func DialTLSConfig(c *tls.Config) DialOption {
|
||||||
|
return DialOption{func(do *dialOptions) {
|
||||||
|
do.tlsConfig = c
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialTLSSkipVerify to disable server name verification when connecting
|
||||||
|
// over TLS. Has no effect when not dialing a TLS connection.
|
||||||
|
func DialTLSSkipVerify(skip bool) DialOption {
|
||||||
|
return DialOption{func(do *dialOptions) {
|
||||||
|
do.skipVerify = skip
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial connects to the Redis server at the given network and
|
||||||
|
// address using the specified options.
|
||||||
|
func Dial(network, address string, options ...DialOption) (Conn, error) {
|
||||||
|
do := dialOptions{
|
||||||
|
dial: net.Dial,
|
||||||
|
}
|
||||||
|
for _, option := range options {
|
||||||
|
option.f(&do)
|
||||||
|
}
|
||||||
|
|
||||||
|
netConn, err := do.dial(network, address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if do.dialTLS {
|
||||||
|
tlsConfig := cloneTLSClientConfig(do.tlsConfig, do.skipVerify)
|
||||||
|
if tlsConfig.ServerName == "" {
|
||||||
|
host, _, err := net.SplitHostPort(address)
|
||||||
|
if err != nil {
|
||||||
|
netConn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tlsConfig.ServerName = host
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConn := tls.Client(netConn, tlsConfig)
|
||||||
|
if err := tlsConn.Handshake(); err != nil {
|
||||||
|
netConn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
netConn = tlsConn
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &conn{
|
||||||
|
conn: netConn,
|
||||||
|
bw: bufio.NewWriter(netConn),
|
||||||
|
br: bufio.NewReader(netConn),
|
||||||
|
readTimeout: do.readTimeout,
|
||||||
|
writeTimeout: do.writeTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
if do.password != "" {
|
||||||
|
if _, err := c.Do("AUTH", do.password); err != nil {
|
||||||
|
netConn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if do.db != 0 {
|
||||||
|
if _, err := c.Do("SELECT", do.db); err != nil {
|
||||||
|
netConn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dialTLS(do *dialOptions) {
|
||||||
|
do.dialTLS = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var pathDBRegexp = regexp.MustCompile(`/(\d*)\z`)
|
||||||
|
|
||||||
|
// DialURL connects to a Redis server at the given URL using the Redis
|
||||||
|
// URI scheme. URLs should follow the draft IANA specification for the
|
||||||
|
// scheme (https://www.iana.org/assignments/uri-schemes/prov/redis).
|
||||||
|
func DialURL(rawurl string, options ...DialOption) (Conn, error) {
|
||||||
|
u, err := url.Parse(rawurl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Scheme != "redis" && u.Scheme != "rediss" {
|
||||||
|
return nil, fmt.Errorf("invalid redis URL scheme: %s", u.Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
// As per the IANA draft spec, the host defaults to localhost and
|
||||||
|
// the port defaults to 6379.
|
||||||
|
host, port, err := net.SplitHostPort(u.Host)
|
||||||
|
if err != nil {
|
||||||
|
// assume port is missing
|
||||||
|
host = u.Host
|
||||||
|
port = "6379"
|
||||||
|
}
|
||||||
|
if host == "" {
|
||||||
|
host = "localhost"
|
||||||
|
}
|
||||||
|
address := net.JoinHostPort(host, port)
|
||||||
|
|
||||||
|
if u.User != nil {
|
||||||
|
password, isSet := u.User.Password()
|
||||||
|
if isSet {
|
||||||
|
options = append(options, DialPassword(password))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match := pathDBRegexp.FindStringSubmatch(u.Path)
|
||||||
|
if len(match) == 2 {
|
||||||
|
db := 0
|
||||||
|
if len(match[1]) > 0 {
|
||||||
|
db, err = strconv.Atoi(match[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid database: %s", u.Path[1:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if db != 0 {
|
||||||
|
options = append(options, DialDatabase(db))
|
||||||
|
}
|
||||||
|
} else if u.Path != "" {
|
||||||
|
return nil, fmt.Errorf("invalid database: %s", u.Path[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Scheme == "rediss" {
|
||||||
|
options = append([]DialOption{{dialTLS}}, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Dial("tcp", address, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConn returns a new Redigo connection for the given net connection.
|
||||||
|
func NewConn(netConn net.Conn, readTimeout, writeTimeout time.Duration) Conn {
|
||||||
|
return &conn{
|
||||||
|
conn: netConn,
|
||||||
|
bw: bufio.NewWriter(netConn),
|
||||||
|
br: bufio.NewReader(netConn),
|
||||||
|
readTimeout: readTimeout,
|
||||||
|
writeTimeout: writeTimeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) Close() error {
|
||||||
|
c.mu.Lock()
|
||||||
|
err := c.err
|
||||||
|
if c.err == nil {
|
||||||
|
c.err = errors.New("redigo: closed")
|
||||||
|
err = c.conn.Close()
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) fatal(err error) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
if c.err == nil {
|
||||||
|
c.err = err
|
||||||
|
// Close connection to force errors on subsequent calls and to unblock
|
||||||
|
// other reader or writer.
|
||||||
|
c.conn.Close()
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) Err() error {
|
||||||
|
c.mu.Lock()
|
||||||
|
err := c.err
|
||||||
|
c.mu.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) writeLen(prefix byte, n int) error {
|
||||||
|
c.lenScratch[len(c.lenScratch)-1] = '\n'
|
||||||
|
c.lenScratch[len(c.lenScratch)-2] = '\r'
|
||||||
|
i := len(c.lenScratch) - 3
|
||||||
|
for {
|
||||||
|
c.lenScratch[i] = byte('0' + n%10)
|
||||||
|
i -= 1
|
||||||
|
n = n / 10
|
||||||
|
if n == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.lenScratch[i] = prefix
|
||||||
|
_, err := c.bw.Write(c.lenScratch[i:])
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) writeString(s string) error {
|
||||||
|
c.writeLen('$', len(s))
|
||||||
|
c.bw.WriteString(s)
|
||||||
|
_, err := c.bw.WriteString("\r\n")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) writeBytes(p []byte) error {
|
||||||
|
c.writeLen('$', len(p))
|
||||||
|
c.bw.Write(p)
|
||||||
|
_, err := c.bw.WriteString("\r\n")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) writeInt64(n int64) error {
|
||||||
|
return c.writeBytes(strconv.AppendInt(c.numScratch[:0], n, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) writeFloat64(n float64) error {
|
||||||
|
return c.writeBytes(strconv.AppendFloat(c.numScratch[:0], n, 'g', -1, 64))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) writeCommand(cmd string, args []interface{}) (err error) {
|
||||||
|
c.writeLen('*', 1+len(args))
|
||||||
|
err = c.writeString(cmd)
|
||||||
|
for _, arg := range args {
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch arg := arg.(type) {
|
||||||
|
case string:
|
||||||
|
err = c.writeString(arg)
|
||||||
|
case []byte:
|
||||||
|
err = c.writeBytes(arg)
|
||||||
|
case int:
|
||||||
|
err = c.writeInt64(int64(arg))
|
||||||
|
case int64:
|
||||||
|
err = c.writeInt64(arg)
|
||||||
|
case float64:
|
||||||
|
err = c.writeFloat64(arg)
|
||||||
|
case bool:
|
||||||
|
if arg {
|
||||||
|
err = c.writeString("1")
|
||||||
|
} else {
|
||||||
|
err = c.writeString("0")
|
||||||
|
}
|
||||||
|
case nil:
|
||||||
|
err = c.writeString("")
|
||||||
|
default:
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fmt.Fprint(&buf, arg)
|
||||||
|
err = c.writeBytes(buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type protocolError string
|
||||||
|
|
||||||
|
func (pe protocolError) Error() string {
|
||||||
|
return fmt.Sprintf("redigo: %s (possible server error or unsupported concurrent read by application)", string(pe))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) readLine() ([]byte, error) {
|
||||||
|
p, err := c.br.ReadSlice('\n')
|
||||||
|
if err == bufio.ErrBufferFull {
|
||||||
|
return nil, protocolError("long response line")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
i := len(p) - 2
|
||||||
|
if i < 0 || p[i] != '\r' {
|
||||||
|
return nil, protocolError("bad response line terminator")
|
||||||
|
}
|
||||||
|
return p[:i], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLen parses bulk string and array lengths.
|
||||||
|
func parseLen(p []byte) (int, error) {
|
||||||
|
if len(p) == 0 {
|
||||||
|
return -1, protocolError("malformed length")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p[0] == '-' && len(p) == 2 && p[1] == '1' {
|
||||||
|
// handle $-1 and $-1 null replies.
|
||||||
|
return -1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var n int
|
||||||
|
for _, b := range p {
|
||||||
|
n *= 10
|
||||||
|
if b < '0' || b > '9' {
|
||||||
|
return -1, protocolError("illegal bytes in length")
|
||||||
|
}
|
||||||
|
n += int(b - '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseInt parses an integer reply.
|
||||||
|
func parseInt(p []byte) (interface{}, error) {
|
||||||
|
if len(p) == 0 {
|
||||||
|
return 0, protocolError("malformed integer")
|
||||||
|
}
|
||||||
|
|
||||||
|
var negate bool
|
||||||
|
if p[0] == '-' {
|
||||||
|
negate = true
|
||||||
|
p = p[1:]
|
||||||
|
if len(p) == 0 {
|
||||||
|
return 0, protocolError("malformed integer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var n int64
|
||||||
|
for _, b := range p {
|
||||||
|
n *= 10
|
||||||
|
if b < '0' || b > '9' {
|
||||||
|
return 0, protocolError("illegal bytes in length")
|
||||||
|
}
|
||||||
|
n += int64(b - '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
if negate {
|
||||||
|
n = -n
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
okReply interface{} = "OK"
|
||||||
|
pongReply interface{} = "PONG"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *conn) readReply() (interface{}, error) {
|
||||||
|
line, err := c.readLine()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(line) == 0 {
|
||||||
|
return nil, protocolError("short response line")
|
||||||
|
}
|
||||||
|
switch line[0] {
|
||||||
|
case '+':
|
||||||
|
switch {
|
||||||
|
case len(line) == 3 && line[1] == 'O' && line[2] == 'K':
|
||||||
|
// Avoid allocation for frequent "+OK" response.
|
||||||
|
return okReply, nil
|
||||||
|
case len(line) == 5 && line[1] == 'P' && line[2] == 'O' && line[3] == 'N' && line[4] == 'G':
|
||||||
|
// Avoid allocation in PING command benchmarks :)
|
||||||
|
return pongReply, nil
|
||||||
|
default:
|
||||||
|
return string(line[1:]), nil
|
||||||
|
}
|
||||||
|
case '-':
|
||||||
|
return Error(string(line[1:])), nil
|
||||||
|
case ':':
|
||||||
|
return parseInt(line[1:])
|
||||||
|
case '$':
|
||||||
|
n, err := parseLen(line[1:])
|
||||||
|
if n < 0 || err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p := make([]byte, n)
|
||||||
|
_, err = io.ReadFull(c.br, p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if line, err := c.readLine(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(line) != 0 {
|
||||||
|
return nil, protocolError("bad bulk string format")
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
case '*':
|
||||||
|
n, err := parseLen(line[1:])
|
||||||
|
if n < 0 || err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r := make([]interface{}, n)
|
||||||
|
for i := range r {
|
||||||
|
r[i], err = c.readReply()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
return nil, protocolError("unexpected response line")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) Send(cmd string, args ...interface{}) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
c.pending += 1
|
||||||
|
c.mu.Unlock()
|
||||||
|
if c.writeTimeout != 0 {
|
||||||
|
c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
|
||||||
|
}
|
||||||
|
if err := c.writeCommand(cmd, args); err != nil {
|
||||||
|
return c.fatal(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) Flush() error {
|
||||||
|
if c.writeTimeout != 0 {
|
||||||
|
c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
|
||||||
|
}
|
||||||
|
if err := c.bw.Flush(); err != nil {
|
||||||
|
return c.fatal(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) Receive() (reply interface{}, err error) {
|
||||||
|
if c.readTimeout != 0 {
|
||||||
|
c.conn.SetReadDeadline(time.Now().Add(c.readTimeout))
|
||||||
|
}
|
||||||
|
if reply, err = c.readReply(); err != nil {
|
||||||
|
return nil, c.fatal(err)
|
||||||
|
}
|
||||||
|
// When using pub/sub, the number of receives can be greater than the
|
||||||
|
// number of sends. To enable normal use of the connection after
|
||||||
|
// unsubscribing from all channels, we do not decrement pending to a
|
||||||
|
// negative value.
|
||||||
|
//
|
||||||
|
// The pending field is decremented after the reply is read to handle the
|
||||||
|
// case where Receive is called before Send.
|
||||||
|
c.mu.Lock()
|
||||||
|
if c.pending > 0 {
|
||||||
|
c.pending -= 1
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
if err, ok := reply.(Error); ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) Do(cmd string, args ...interface{}) (interface{}, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
pending := c.pending
|
||||||
|
c.pending = 0
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
if cmd == "" && pending == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.writeTimeout != 0 {
|
||||||
|
c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd != "" {
|
||||||
|
if err := c.writeCommand(cmd, args); err != nil {
|
||||||
|
return nil, c.fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.bw.Flush(); err != nil {
|
||||||
|
return nil, c.fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.readTimeout != 0 {
|
||||||
|
c.conn.SetReadDeadline(time.Now().Add(c.readTimeout))
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd == "" {
|
||||||
|
reply := make([]interface{}, pending)
|
||||||
|
for i := range reply {
|
||||||
|
r, e := c.readReply()
|
||||||
|
if e != nil {
|
||||||
|
return nil, c.fatal(e)
|
||||||
|
}
|
||||||
|
reply[i] = r
|
||||||
|
}
|
||||||
|
return reply, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var reply interface{}
|
||||||
|
for i := 0; i <= pending; i++ {
|
||||||
|
var e error
|
||||||
|
if reply, e = c.readReply(); e != nil {
|
||||||
|
return nil, c.fatal(e)
|
||||||
|
}
|
||||||
|
if e, ok := reply.(Error); ok && err == nil {
|
||||||
|
err = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reply, err
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
// +build go1.7
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import "crypto/tls"
|
||||||
|
|
||||||
|
// similar cloneTLSClientConfig in the stdlib, but also honor skipVerify for the nil case
|
||||||
|
func cloneTLSClientConfig(cfg *tls.Config, skipVerify bool) *tls.Config {
|
||||||
|
if cfg == nil {
|
||||||
|
return &tls.Config{InsecureSkipVerify: skipVerify}
|
||||||
|
}
|
||||||
|
return &tls.Config{
|
||||||
|
Rand: cfg.Rand,
|
||||||
|
Time: cfg.Time,
|
||||||
|
Certificates: cfg.Certificates,
|
||||||
|
NameToCertificate: cfg.NameToCertificate,
|
||||||
|
GetCertificate: cfg.GetCertificate,
|
||||||
|
RootCAs: cfg.RootCAs,
|
||||||
|
NextProtos: cfg.NextProtos,
|
||||||
|
ServerName: cfg.ServerName,
|
||||||
|
ClientAuth: cfg.ClientAuth,
|
||||||
|
ClientCAs: cfg.ClientCAs,
|
||||||
|
InsecureSkipVerify: cfg.InsecureSkipVerify,
|
||||||
|
CipherSuites: cfg.CipherSuites,
|
||||||
|
PreferServerCipherSuites: cfg.PreferServerCipherSuites,
|
||||||
|
ClientSessionCache: cfg.ClientSessionCache,
|
||||||
|
MinVersion: cfg.MinVersion,
|
||||||
|
MaxVersion: cfg.MaxVersion,
|
||||||
|
CurvePreferences: cfg.CurvePreferences,
|
||||||
|
DynamicRecordSizingDisabled: cfg.DynamicRecordSizingDisabled,
|
||||||
|
Renegotiation: cfg.Renegotiation,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewLoggingConn returns a logging wrapper around a connection.
|
||||||
|
func NewLoggingConn(conn Conn, logger *log.Logger, prefix string) Conn {
|
||||||
|
if prefix != "" {
|
||||||
|
prefix = prefix + "."
|
||||||
|
}
|
||||||
|
return &loggingConn{conn, logger, prefix}
|
||||||
|
}
|
||||||
|
|
||||||
|
type loggingConn struct {
|
||||||
|
Conn
|
||||||
|
logger *log.Logger
|
||||||
|
prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *loggingConn) Close() error {
|
||||||
|
err := c.Conn.Close()
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fmt.Fprintf(&buf, "%sClose() -> (%v)", c.prefix, err)
|
||||||
|
c.logger.Output(2, buf.String())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *loggingConn) printValue(buf *bytes.Buffer, v interface{}) {
|
||||||
|
const chop = 32
|
||||||
|
switch v := v.(type) {
|
||||||
|
case []byte:
|
||||||
|
if len(v) > chop {
|
||||||
|
fmt.Fprintf(buf, "%q...", v[:chop])
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(buf, "%q", v)
|
||||||
|
}
|
||||||
|
case string:
|
||||||
|
if len(v) > chop {
|
||||||
|
fmt.Fprintf(buf, "%q...", v[:chop])
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(buf, "%q", v)
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
if len(v) == 0 {
|
||||||
|
buf.WriteString("[]")
|
||||||
|
} else {
|
||||||
|
sep := "["
|
||||||
|
fin := "]"
|
||||||
|
if len(v) > chop {
|
||||||
|
v = v[:chop]
|
||||||
|
fin = "...]"
|
||||||
|
}
|
||||||
|
for _, vv := range v {
|
||||||
|
buf.WriteString(sep)
|
||||||
|
c.printValue(buf, vv)
|
||||||
|
sep = ", "
|
||||||
|
}
|
||||||
|
buf.WriteString(fin)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
fmt.Fprint(buf, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *loggingConn) print(method, commandName string, args []interface{}, reply interface{}, err error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fmt.Fprintf(&buf, "%s%s(", c.prefix, method)
|
||||||
|
if method != "Receive" {
|
||||||
|
buf.WriteString(commandName)
|
||||||
|
for _, arg := range args {
|
||||||
|
buf.WriteString(", ")
|
||||||
|
c.printValue(&buf, arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.WriteString(") -> (")
|
||||||
|
if method != "Send" {
|
||||||
|
c.printValue(&buf, reply)
|
||||||
|
buf.WriteString(", ")
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&buf, "%v)", err)
|
||||||
|
c.logger.Output(3, buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *loggingConn) Do(commandName string, args ...interface{}) (interface{}, error) {
|
||||||
|
reply, err := c.Conn.Do(commandName, args...)
|
||||||
|
c.print("Do", commandName, args, reply, err)
|
||||||
|
return reply, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *loggingConn) Send(commandName string, args ...interface{}) error {
|
||||||
|
err := c.Conn.Send(commandName, args...)
|
||||||
|
c.print("Send", commandName, args, nil, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *loggingConn) Receive() (interface{}, error) {
|
||||||
|
reply, err := c.Conn.Receive()
|
||||||
|
c.print("Receive", "", nil, reply, err)
|
||||||
|
return reply, err
|
||||||
|
}
|
|
@ -0,0 +1,416 @@
|
||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"container/list"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha1"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/garyburd/redigo/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
var nowFunc = time.Now // for testing
|
||||||
|
|
||||||
|
// ErrPoolExhausted is returned from a pool connection method (Do, Send,
|
||||||
|
// Receive, Flush, Err) when the maximum number of database connections in the
|
||||||
|
// pool has been reached.
|
||||||
|
var ErrPoolExhausted = errors.New("redigo: connection pool exhausted")
|
||||||
|
|
||||||
|
var (
|
||||||
|
errPoolClosed = errors.New("redigo: connection pool closed")
|
||||||
|
errConnClosed = errors.New("redigo: connection closed")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pool maintains a pool of connections. The application calls the Get method
|
||||||
|
// to get a connection from the pool and the connection's Close method to
|
||||||
|
// return the connection's resources to the pool.
|
||||||
|
//
|
||||||
|
// The following example shows how to use a pool in a web application. The
|
||||||
|
// application creates a pool at application startup and makes it available to
|
||||||
|
// request handlers using a package level variable. The pool configuration used
|
||||||
|
// here is an example, not a recommendation.
|
||||||
|
//
|
||||||
|
// func newPool(addr string) *redis.Pool {
|
||||||
|
// return &redis.Pool{
|
||||||
|
// MaxIdle: 3,
|
||||||
|
// IdleTimeout: 240 * time.Second,
|
||||||
|
// Dial: func () (redis.Conn, error) { return redis.Dial("tcp", addr) },
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var (
|
||||||
|
// pool *redis.Pool
|
||||||
|
// redisServer = flag.String("redisServer", ":6379", "")
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// func main() {
|
||||||
|
// flag.Parse()
|
||||||
|
// pool = newPool(*redisServer)
|
||||||
|
// ...
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// A request handler gets a connection from the pool and closes the connection
|
||||||
|
// when the handler is done:
|
||||||
|
//
|
||||||
|
// func serveHome(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// conn := pool.Get()
|
||||||
|
// defer conn.Close()
|
||||||
|
// ...
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Use the Dial function to authenticate connections with the AUTH command or
|
||||||
|
// select a database with the SELECT command:
|
||||||
|
//
|
||||||
|
// pool := &redis.Pool{
|
||||||
|
// // Other pool configuration not shown in this example.
|
||||||
|
// Dial: func () (redis.Conn, error) {
|
||||||
|
// c, err := redis.Dial("tcp", server)
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
// if _, err := c.Do("AUTH", password); err != nil {
|
||||||
|
// c.Close()
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
// if _, err := c.Do("SELECT", db); err != nil {
|
||||||
|
// c.Close()
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
// return c, nil
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Use the TestOnBorrow function to check the health of an idle connection
|
||||||
|
// before the connection is returned to the application. This example PINGs
|
||||||
|
// connections that have been idle more than a minute:
|
||||||
|
//
|
||||||
|
// pool := &redis.Pool{
|
||||||
|
// // Other pool configuration not shown in this example.
|
||||||
|
// TestOnBorrow: func(c redis.Conn, t time.Time) error {
|
||||||
|
// if time.Since(t) < time.Minute {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
// _, err := c.Do("PING")
|
||||||
|
// return err
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
type Pool struct {
|
||||||
|
|
||||||
|
// Dial is an application supplied function for creating and configuring a
|
||||||
|
// connection.
|
||||||
|
//
|
||||||
|
// The connection returned from Dial must not be in a special state
|
||||||
|
// (subscribed to pubsub channel, transaction started, ...).
|
||||||
|
Dial func() (Conn, error)
|
||||||
|
|
||||||
|
// TestOnBorrow is an optional application supplied function for checking
|
||||||
|
// the health of an idle connection before the connection is used again by
|
||||||
|
// the application. Argument t is the time that the connection was returned
|
||||||
|
// to the pool. If the function returns an error, then the connection is
|
||||||
|
// closed.
|
||||||
|
TestOnBorrow func(c Conn, t time.Time) error
|
||||||
|
|
||||||
|
// Maximum number of idle connections in the pool.
|
||||||
|
MaxIdle int
|
||||||
|
|
||||||
|
// Maximum number of connections allocated by the pool at a given time.
|
||||||
|
// When zero, there is no limit on the number of connections in the pool.
|
||||||
|
MaxActive int
|
||||||
|
|
||||||
|
// Close connections after remaining idle for this duration. If the value
|
||||||
|
// is zero, then idle connections are not closed. Applications should set
|
||||||
|
// the timeout to a value less than the server's timeout.
|
||||||
|
IdleTimeout time.Duration
|
||||||
|
|
||||||
|
// If Wait is true and the pool is at the MaxActive limit, then Get() waits
|
||||||
|
// for a connection to be returned to the pool before returning.
|
||||||
|
Wait bool
|
||||||
|
|
||||||
|
// mu protects fields defined below.
|
||||||
|
mu sync.Mutex
|
||||||
|
cond *sync.Cond
|
||||||
|
closed bool
|
||||||
|
active int
|
||||||
|
|
||||||
|
// Stack of idleConn with most recently used at the front.
|
||||||
|
idle list.List
|
||||||
|
}
|
||||||
|
|
||||||
|
type idleConn struct {
|
||||||
|
c Conn
|
||||||
|
t time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPool creates a new pool.
|
||||||
|
//
|
||||||
|
// Deprecated: Initialize the Pool directory as shown in the example.
|
||||||
|
func NewPool(newFn func() (Conn, error), maxIdle int) *Pool {
|
||||||
|
return &Pool{Dial: newFn, MaxIdle: maxIdle}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get gets a connection. The application must close the returned connection.
|
||||||
|
// This method always returns a valid connection so that applications can defer
|
||||||
|
// error handling to the first use of the connection. If there is an error
|
||||||
|
// getting an underlying connection, then the connection Err, Do, Send, Flush
|
||||||
|
// and Receive methods return that error.
|
||||||
|
func (p *Pool) Get() Conn {
|
||||||
|
c, err := p.get()
|
||||||
|
if err != nil {
|
||||||
|
return errorConnection{err}
|
||||||
|
}
|
||||||
|
return &pooledConnection{p: p, c: c}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActiveCount returns the number of active connections in the pool.
|
||||||
|
func (p *Pool) ActiveCount() int {
|
||||||
|
p.mu.Lock()
|
||||||
|
active := p.active
|
||||||
|
p.mu.Unlock()
|
||||||
|
return active
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close releases the resources used by the pool.
|
||||||
|
func (p *Pool) Close() error {
|
||||||
|
p.mu.Lock()
|
||||||
|
idle := p.idle
|
||||||
|
p.idle.Init()
|
||||||
|
p.closed = true
|
||||||
|
p.active -= idle.Len()
|
||||||
|
if p.cond != nil {
|
||||||
|
p.cond.Broadcast()
|
||||||
|
}
|
||||||
|
p.mu.Unlock()
|
||||||
|
for e := idle.Front(); e != nil; e = e.Next() {
|
||||||
|
e.Value.(idleConn).c.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// release decrements the active count and signals waiters. The caller must
|
||||||
|
// hold p.mu during the call.
|
||||||
|
func (p *Pool) release() {
|
||||||
|
p.active -= 1
|
||||||
|
if p.cond != nil {
|
||||||
|
p.cond.Signal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get prunes stale connections and returns a connection from the idle list or
|
||||||
|
// creates a new connection.
|
||||||
|
func (p *Pool) get() (Conn, error) {
|
||||||
|
p.mu.Lock()
|
||||||
|
|
||||||
|
// Prune stale connections.
|
||||||
|
|
||||||
|
if timeout := p.IdleTimeout; timeout > 0 {
|
||||||
|
for i, n := 0, p.idle.Len(); i < n; i++ {
|
||||||
|
e := p.idle.Back()
|
||||||
|
if e == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ic := e.Value.(idleConn)
|
||||||
|
if ic.t.Add(timeout).After(nowFunc()) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
p.idle.Remove(e)
|
||||||
|
p.release()
|
||||||
|
p.mu.Unlock()
|
||||||
|
ic.c.Close()
|
||||||
|
p.mu.Lock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
|
||||||
|
// Get idle connection.
|
||||||
|
|
||||||
|
for i, n := 0, p.idle.Len(); i < n; i++ {
|
||||||
|
e := p.idle.Front()
|
||||||
|
if e == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ic := e.Value.(idleConn)
|
||||||
|
p.idle.Remove(e)
|
||||||
|
test := p.TestOnBorrow
|
||||||
|
p.mu.Unlock()
|
||||||
|
if test == nil || test(ic.c, ic.t) == nil {
|
||||||
|
return ic.c, nil
|
||||||
|
}
|
||||||
|
ic.c.Close()
|
||||||
|
p.mu.Lock()
|
||||||
|
p.release()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for pool closed before dialing a new connection.
|
||||||
|
|
||||||
|
if p.closed {
|
||||||
|
p.mu.Unlock()
|
||||||
|
return nil, errors.New("redigo: get on closed pool")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial new connection if under limit.
|
||||||
|
|
||||||
|
if p.MaxActive == 0 || p.active < p.MaxActive {
|
||||||
|
dial := p.Dial
|
||||||
|
p.active += 1
|
||||||
|
p.mu.Unlock()
|
||||||
|
c, err := dial()
|
||||||
|
if err != nil {
|
||||||
|
p.mu.Lock()
|
||||||
|
p.release()
|
||||||
|
p.mu.Unlock()
|
||||||
|
c = nil
|
||||||
|
}
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.Wait {
|
||||||
|
p.mu.Unlock()
|
||||||
|
return nil, ErrPoolExhausted
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.cond == nil {
|
||||||
|
p.cond = sync.NewCond(&p.mu)
|
||||||
|
}
|
||||||
|
p.cond.Wait()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pool) put(c Conn, forceClose bool) error {
|
||||||
|
err := c.Err()
|
||||||
|
p.mu.Lock()
|
||||||
|
if !p.closed && err == nil && !forceClose {
|
||||||
|
p.idle.PushFront(idleConn{t: nowFunc(), c: c})
|
||||||
|
if p.idle.Len() > p.MaxIdle {
|
||||||
|
c = p.idle.Remove(p.idle.Back()).(idleConn).c
|
||||||
|
} else {
|
||||||
|
c = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == nil {
|
||||||
|
if p.cond != nil {
|
||||||
|
p.cond.Signal()
|
||||||
|
}
|
||||||
|
p.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p.release()
|
||||||
|
p.mu.Unlock()
|
||||||
|
return c.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
type pooledConnection struct {
|
||||||
|
p *Pool
|
||||||
|
c Conn
|
||||||
|
state int
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
sentinel []byte
|
||||||
|
sentinelOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func initSentinel() {
|
||||||
|
p := make([]byte, 64)
|
||||||
|
if _, err := rand.Read(p); err == nil {
|
||||||
|
sentinel = p
|
||||||
|
} else {
|
||||||
|
h := sha1.New()
|
||||||
|
io.WriteString(h, "Oops, rand failed. Use time instead.")
|
||||||
|
io.WriteString(h, strconv.FormatInt(time.Now().UnixNano(), 10))
|
||||||
|
sentinel = h.Sum(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *pooledConnection) Close() error {
|
||||||
|
c := pc.c
|
||||||
|
if _, ok := c.(errorConnection); ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
pc.c = errorConnection{errConnClosed}
|
||||||
|
|
||||||
|
if pc.state&internal.MultiState != 0 {
|
||||||
|
c.Send("DISCARD")
|
||||||
|
pc.state &^= (internal.MultiState | internal.WatchState)
|
||||||
|
} else if pc.state&internal.WatchState != 0 {
|
||||||
|
c.Send("UNWATCH")
|
||||||
|
pc.state &^= internal.WatchState
|
||||||
|
}
|
||||||
|
if pc.state&internal.SubscribeState != 0 {
|
||||||
|
c.Send("UNSUBSCRIBE")
|
||||||
|
c.Send("PUNSUBSCRIBE")
|
||||||
|
// To detect the end of the message stream, ask the server to echo
|
||||||
|
// a sentinel value and read until we see that value.
|
||||||
|
sentinelOnce.Do(initSentinel)
|
||||||
|
c.Send("ECHO", sentinel)
|
||||||
|
c.Flush()
|
||||||
|
for {
|
||||||
|
p, err := c.Receive()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if p, ok := p.([]byte); ok && bytes.Equal(p, sentinel) {
|
||||||
|
pc.state &^= internal.SubscribeState
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Do("")
|
||||||
|
pc.p.put(c, pc.state != 0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *pooledConnection) Err() error {
|
||||||
|
return pc.c.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *pooledConnection) Do(commandName string, args ...interface{}) (reply interface{}, err error) {
|
||||||
|
ci := internal.LookupCommandInfo(commandName)
|
||||||
|
pc.state = (pc.state | ci.Set) &^ ci.Clear
|
||||||
|
return pc.c.Do(commandName, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *pooledConnection) Send(commandName string, args ...interface{}) error {
|
||||||
|
ci := internal.LookupCommandInfo(commandName)
|
||||||
|
pc.state = (pc.state | ci.Set) &^ ci.Clear
|
||||||
|
return pc.c.Send(commandName, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *pooledConnection) Flush() error {
|
||||||
|
return pc.c.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *pooledConnection) Receive() (reply interface{}, err error) {
|
||||||
|
return pc.c.Receive()
|
||||||
|
}
|
||||||
|
|
||||||
|
type errorConnection struct{ err error }
|
||||||
|
|
||||||
|
func (ec errorConnection) Do(string, ...interface{}) (interface{}, error) { return nil, ec.err }
|
||||||
|
func (ec errorConnection) Send(string, ...interface{}) error { return ec.err }
|
||||||
|
func (ec errorConnection) Err() error { return ec.err }
|
||||||
|
func (ec errorConnection) Close() error { return ec.err }
|
||||||
|
func (ec errorConnection) Flush() error { return ec.err }
|
||||||
|
func (ec errorConnection) Receive() (interface{}, error) { return nil, ec.err }
|
|
@ -0,0 +1,31 @@
|
||||||
|
// +build !go1.7
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import "crypto/tls"
|
||||||
|
|
||||||
|
// similar cloneTLSClientConfig in the stdlib, but also honor skipVerify for the nil case
|
||||||
|
func cloneTLSClientConfig(cfg *tls.Config, skipVerify bool) *tls.Config {
|
||||||
|
if cfg == nil {
|
||||||
|
return &tls.Config{InsecureSkipVerify: skipVerify}
|
||||||
|
}
|
||||||
|
return &tls.Config{
|
||||||
|
Rand: cfg.Rand,
|
||||||
|
Time: cfg.Time,
|
||||||
|
Certificates: cfg.Certificates,
|
||||||
|
NameToCertificate: cfg.NameToCertificate,
|
||||||
|
GetCertificate: cfg.GetCertificate,
|
||||||
|
RootCAs: cfg.RootCAs,
|
||||||
|
NextProtos: cfg.NextProtos,
|
||||||
|
ServerName: cfg.ServerName,
|
||||||
|
ClientAuth: cfg.ClientAuth,
|
||||||
|
ClientCAs: cfg.ClientCAs,
|
||||||
|
InsecureSkipVerify: cfg.InsecureSkipVerify,
|
||||||
|
CipherSuites: cfg.CipherSuites,
|
||||||
|
PreferServerCipherSuites: cfg.PreferServerCipherSuites,
|
||||||
|
ClientSessionCache: cfg.ClientSessionCache,
|
||||||
|
MinVersion: cfg.MinVersion,
|
||||||
|
MaxVersion: cfg.MaxVersion,
|
||||||
|
CurvePreferences: cfg.CurvePreferences,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// Subscription represents a subscribe or unsubscribe notification.
|
||||||
|
type Subscription struct {
|
||||||
|
|
||||||
|
// Kind is "subscribe", "unsubscribe", "psubscribe" or "punsubscribe"
|
||||||
|
Kind string
|
||||||
|
|
||||||
|
// The channel that was changed.
|
||||||
|
Channel string
|
||||||
|
|
||||||
|
// The current number of subscriptions for connection.
|
||||||
|
Count int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message represents a message notification.
|
||||||
|
type Message struct {
|
||||||
|
|
||||||
|
// The originating channel.
|
||||||
|
Channel string
|
||||||
|
|
||||||
|
// The message data.
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// PMessage represents a pmessage notification.
|
||||||
|
type PMessage struct {
|
||||||
|
|
||||||
|
// The matched pattern.
|
||||||
|
Pattern string
|
||||||
|
|
||||||
|
// The originating channel.
|
||||||
|
Channel string
|
||||||
|
|
||||||
|
// The message data.
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pong represents a pubsub pong notification.
|
||||||
|
type Pong struct {
|
||||||
|
Data string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PubSubConn wraps a Conn with convenience methods for subscribers.
|
||||||
|
type PubSubConn struct {
|
||||||
|
Conn Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the connection.
|
||||||
|
func (c PubSubConn) Close() error {
|
||||||
|
return c.Conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe subscribes the connection to the specified channels.
|
||||||
|
func (c PubSubConn) Subscribe(channel ...interface{}) error {
|
||||||
|
c.Conn.Send("SUBSCRIBE", channel...)
|
||||||
|
return c.Conn.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSubscribe subscribes the connection to the given patterns.
|
||||||
|
func (c PubSubConn) PSubscribe(channel ...interface{}) error {
|
||||||
|
c.Conn.Send("PSUBSCRIBE", channel...)
|
||||||
|
return c.Conn.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe unsubscribes the connection from the given channels, or from all
|
||||||
|
// of them if none is given.
|
||||||
|
func (c PubSubConn) Unsubscribe(channel ...interface{}) error {
|
||||||
|
c.Conn.Send("UNSUBSCRIBE", channel...)
|
||||||
|
return c.Conn.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUnsubscribe unsubscribes the connection from the given patterns, or from all
|
||||||
|
// of them if none is given.
|
||||||
|
func (c PubSubConn) PUnsubscribe(channel ...interface{}) error {
|
||||||
|
c.Conn.Send("PUNSUBSCRIBE", channel...)
|
||||||
|
return c.Conn.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping sends a PING to the server with the specified data.
|
||||||
|
func (c PubSubConn) Ping(data string) error {
|
||||||
|
c.Conn.Send("PING", data)
|
||||||
|
return c.Conn.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receive returns a pushed message as a Subscription, Message, PMessage, Pong
|
||||||
|
// or error. The return value is intended to be used directly in a type switch
|
||||||
|
// as illustrated in the PubSubConn example.
|
||||||
|
func (c PubSubConn) Receive() interface{} {
|
||||||
|
reply, err := Values(c.Conn.Receive())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var kind string
|
||||||
|
reply, err = Scan(reply, &kind)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch kind {
|
||||||
|
case "message":
|
||||||
|
var m Message
|
||||||
|
if _, err := Scan(reply, &m.Channel, &m.Data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
case "pmessage":
|
||||||
|
var pm PMessage
|
||||||
|
if _, err := Scan(reply, &pm.Pattern, &pm.Channel, &pm.Data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return pm
|
||||||
|
case "subscribe", "psubscribe", "unsubscribe", "punsubscribe":
|
||||||
|
s := Subscription{Kind: kind}
|
||||||
|
if _, err := Scan(reply, &s.Channel, &s.Count); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
case "pong":
|
||||||
|
var p Pong
|
||||||
|
if _, err := Scan(reply, &p.Data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
return errors.New("redigo: unknown pubsub notification")
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
// Error represents an error returned in a command reply.
|
||||||
|
type Error string
|
||||||
|
|
||||||
|
func (err Error) Error() string { return string(err) }
|
||||||
|
|
||||||
|
// Conn represents a connection to a Redis server.
|
||||||
|
type Conn interface {
|
||||||
|
// Close closes the connection.
|
||||||
|
Close() error
|
||||||
|
|
||||||
|
// Err returns a non-nil value when the connection is not usable.
|
||||||
|
Err() error
|
||||||
|
|
||||||
|
// Do sends a command to the server and returns the received reply.
|
||||||
|
Do(commandName string, args ...interface{}) (reply interface{}, err error)
|
||||||
|
|
||||||
|
// Send writes the command to the client's output buffer.
|
||||||
|
Send(commandName string, args ...interface{}) error
|
||||||
|
|
||||||
|
// Flush flushes the output buffer to the Redis server.
|
||||||
|
Flush() error
|
||||||
|
|
||||||
|
// Receive receives a single reply from the Redis server
|
||||||
|
Receive() (reply interface{}, err error)
|
||||||
|
}
|
|
@ -0,0 +1,425 @@
|
||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrNil indicates that a reply value is nil.
|
||||||
|
var ErrNil = errors.New("redigo: nil returned")
|
||||||
|
|
||||||
|
// Int is a helper that converts a command reply to an integer. If err is not
|
||||||
|
// equal to nil, then Int returns 0, err. Otherwise, Int converts the
|
||||||
|
// reply to an int as follows:
|
||||||
|
//
|
||||||
|
// Reply type Result
|
||||||
|
// integer int(reply), nil
|
||||||
|
// bulk string parsed reply, nil
|
||||||
|
// nil 0, ErrNil
|
||||||
|
// other 0, error
|
||||||
|
func Int(reply interface{}, err error) (int, error) {
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case int64:
|
||||||
|
x := int(reply)
|
||||||
|
if int64(x) != reply {
|
||||||
|
return 0, strconv.ErrRange
|
||||||
|
}
|
||||||
|
return x, nil
|
||||||
|
case []byte:
|
||||||
|
n, err := strconv.ParseInt(string(reply), 10, 0)
|
||||||
|
return int(n), err
|
||||||
|
case nil:
|
||||||
|
return 0, ErrNil
|
||||||
|
case Error:
|
||||||
|
return 0, reply
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("redigo: unexpected type for Int, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64 is a helper that converts a command reply to 64 bit integer. If err is
|
||||||
|
// not equal to nil, then Int returns 0, err. Otherwise, Int64 converts the
|
||||||
|
// reply to an int64 as follows:
|
||||||
|
//
|
||||||
|
// Reply type Result
|
||||||
|
// integer reply, nil
|
||||||
|
// bulk string parsed reply, nil
|
||||||
|
// nil 0, ErrNil
|
||||||
|
// other 0, error
|
||||||
|
func Int64(reply interface{}, err error) (int64, error) {
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case int64:
|
||||||
|
return reply, nil
|
||||||
|
case []byte:
|
||||||
|
n, err := strconv.ParseInt(string(reply), 10, 64)
|
||||||
|
return n, err
|
||||||
|
case nil:
|
||||||
|
return 0, ErrNil
|
||||||
|
case Error:
|
||||||
|
return 0, reply
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("redigo: unexpected type for Int64, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
var errNegativeInt = errors.New("redigo: unexpected value for Uint64")
|
||||||
|
|
||||||
|
// Uint64 is a helper that converts a command reply to 64 bit integer. If err is
|
||||||
|
// not equal to nil, then Int returns 0, err. Otherwise, Int64 converts the
|
||||||
|
// reply to an int64 as follows:
|
||||||
|
//
|
||||||
|
// Reply type Result
|
||||||
|
// integer reply, nil
|
||||||
|
// bulk string parsed reply, nil
|
||||||
|
// nil 0, ErrNil
|
||||||
|
// other 0, error
|
||||||
|
func Uint64(reply interface{}, err error) (uint64, error) {
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case int64:
|
||||||
|
if reply < 0 {
|
||||||
|
return 0, errNegativeInt
|
||||||
|
}
|
||||||
|
return uint64(reply), nil
|
||||||
|
case []byte:
|
||||||
|
n, err := strconv.ParseUint(string(reply), 10, 64)
|
||||||
|
return n, err
|
||||||
|
case nil:
|
||||||
|
return 0, ErrNil
|
||||||
|
case Error:
|
||||||
|
return 0, reply
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("redigo: unexpected type for Uint64, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float64 is a helper that converts a command reply to 64 bit float. If err is
|
||||||
|
// not equal to nil, then Float64 returns 0, err. Otherwise, Float64 converts
|
||||||
|
// the reply to an int as follows:
|
||||||
|
//
|
||||||
|
// Reply type Result
|
||||||
|
// bulk string parsed reply, nil
|
||||||
|
// nil 0, ErrNil
|
||||||
|
// other 0, error
|
||||||
|
func Float64(reply interface{}, err error) (float64, error) {
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case []byte:
|
||||||
|
n, err := strconv.ParseFloat(string(reply), 64)
|
||||||
|
return n, err
|
||||||
|
case nil:
|
||||||
|
return 0, ErrNil
|
||||||
|
case Error:
|
||||||
|
return 0, reply
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("redigo: unexpected type for Float64, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String is a helper that converts a command reply to a string. If err is not
|
||||||
|
// equal to nil, then String returns "", err. Otherwise String converts the
|
||||||
|
// reply to a string as follows:
|
||||||
|
//
|
||||||
|
// Reply type Result
|
||||||
|
// bulk string string(reply), nil
|
||||||
|
// simple string reply, nil
|
||||||
|
// nil "", ErrNil
|
||||||
|
// other "", error
|
||||||
|
func String(reply interface{}, err error) (string, error) {
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case []byte:
|
||||||
|
return string(reply), nil
|
||||||
|
case string:
|
||||||
|
return reply, nil
|
||||||
|
case nil:
|
||||||
|
return "", ErrNil
|
||||||
|
case Error:
|
||||||
|
return "", reply
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("redigo: unexpected type for String, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes is a helper that converts a command reply to a slice of bytes. If err
|
||||||
|
// is not equal to nil, then Bytes returns nil, err. Otherwise Bytes converts
|
||||||
|
// the reply to a slice of bytes as follows:
|
||||||
|
//
|
||||||
|
// Reply type Result
|
||||||
|
// bulk string reply, nil
|
||||||
|
// simple string []byte(reply), nil
|
||||||
|
// nil nil, ErrNil
|
||||||
|
// other nil, error
|
||||||
|
func Bytes(reply interface{}, err error) ([]byte, error) {
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case []byte:
|
||||||
|
return reply, nil
|
||||||
|
case string:
|
||||||
|
return []byte(reply), nil
|
||||||
|
case nil:
|
||||||
|
return nil, ErrNil
|
||||||
|
case Error:
|
||||||
|
return nil, reply
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("redigo: unexpected type for Bytes, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bool is a helper that converts a command reply to a boolean. If err is not
|
||||||
|
// equal to nil, then Bool returns false, err. Otherwise Bool converts the
|
||||||
|
// reply to boolean as follows:
|
||||||
|
//
|
||||||
|
// Reply type Result
|
||||||
|
// integer value != 0, nil
|
||||||
|
// bulk string strconv.ParseBool(reply)
|
||||||
|
// nil false, ErrNil
|
||||||
|
// other false, error
|
||||||
|
func Bool(reply interface{}, err error) (bool, error) {
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case int64:
|
||||||
|
return reply != 0, nil
|
||||||
|
case []byte:
|
||||||
|
return strconv.ParseBool(string(reply))
|
||||||
|
case nil:
|
||||||
|
return false, ErrNil
|
||||||
|
case Error:
|
||||||
|
return false, reply
|
||||||
|
}
|
||||||
|
return false, fmt.Errorf("redigo: unexpected type for Bool, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiBulk is a helper that converts an array command reply to a []interface{}.
|
||||||
|
//
|
||||||
|
// Deprecated: Use Values instead.
|
||||||
|
func MultiBulk(reply interface{}, err error) ([]interface{}, error) { return Values(reply, err) }
|
||||||
|
|
||||||
|
// Values is a helper that converts an array command reply to a []interface{}.
|
||||||
|
// If err is not equal to nil, then Values returns nil, err. Otherwise, Values
|
||||||
|
// converts the reply as follows:
|
||||||
|
//
|
||||||
|
// Reply type Result
|
||||||
|
// array reply, nil
|
||||||
|
// nil nil, ErrNil
|
||||||
|
// other nil, error
|
||||||
|
func Values(reply interface{}, err error) ([]interface{}, error) {
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
return reply, nil
|
||||||
|
case nil:
|
||||||
|
return nil, ErrNil
|
||||||
|
case Error:
|
||||||
|
return nil, reply
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("redigo: unexpected type for Values, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strings is a helper that converts an array command reply to a []string. If
|
||||||
|
// err is not equal to nil, then Strings returns nil, err. Nil array items are
|
||||||
|
// converted to "" in the output slice. Strings returns an error if an array
|
||||||
|
// item is not a bulk string or nil.
|
||||||
|
func Strings(reply interface{}, err error) ([]string, error) {
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
result := make([]string, len(reply))
|
||||||
|
for i := range reply {
|
||||||
|
if reply[i] == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p, ok := reply[i].([]byte)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("redigo: unexpected element type for Strings, got type %T", reply[i])
|
||||||
|
}
|
||||||
|
result[i] = string(p)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
case nil:
|
||||||
|
return nil, ErrNil
|
||||||
|
case Error:
|
||||||
|
return nil, reply
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("redigo: unexpected type for Strings, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByteSlices is a helper that converts an array command reply to a [][]byte.
|
||||||
|
// If err is not equal to nil, then ByteSlices returns nil, err. Nil array
|
||||||
|
// items are stay nil. ByteSlices returns an error if an array item is not a
|
||||||
|
// bulk string or nil.
|
||||||
|
func ByteSlices(reply interface{}, err error) ([][]byte, error) {
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
result := make([][]byte, len(reply))
|
||||||
|
for i := range reply {
|
||||||
|
if reply[i] == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p, ok := reply[i].([]byte)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("redigo: unexpected element type for ByteSlices, got type %T", reply[i])
|
||||||
|
}
|
||||||
|
result[i] = p
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
case nil:
|
||||||
|
return nil, ErrNil
|
||||||
|
case Error:
|
||||||
|
return nil, reply
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("redigo: unexpected type for ByteSlices, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ints is a helper that converts an array command reply to a []int. If
|
||||||
|
// err is not equal to nil, then Ints returns nil, err.
|
||||||
|
func Ints(reply interface{}, err error) ([]int, error) {
|
||||||
|
var ints []int
|
||||||
|
values, err := Values(reply, err)
|
||||||
|
if err != nil {
|
||||||
|
return ints, err
|
||||||
|
}
|
||||||
|
if err := ScanSlice(values, &ints); err != nil {
|
||||||
|
return ints, err
|
||||||
|
}
|
||||||
|
return ints, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringMap is a helper that converts an array of strings (alternating key, value)
|
||||||
|
// into a map[string]string. The HGETALL and CONFIG GET commands return replies in this format.
|
||||||
|
// Requires an even number of values in result.
|
||||||
|
func StringMap(result interface{}, err error) (map[string]string, error) {
|
||||||
|
values, err := Values(result, err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(values)%2 != 0 {
|
||||||
|
return nil, errors.New("redigo: StringMap expects even number of values result")
|
||||||
|
}
|
||||||
|
m := make(map[string]string, len(values)/2)
|
||||||
|
for i := 0; i < len(values); i += 2 {
|
||||||
|
key, okKey := values[i].([]byte)
|
||||||
|
value, okValue := values[i+1].([]byte)
|
||||||
|
if !okKey || !okValue {
|
||||||
|
return nil, errors.New("redigo: ScanMap key not a bulk string value")
|
||||||
|
}
|
||||||
|
m[string(key)] = string(value)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntMap is a helper that converts an array of strings (alternating key, value)
|
||||||
|
// into a map[string]int. The HGETALL commands return replies in this format.
|
||||||
|
// Requires an even number of values in result.
|
||||||
|
func IntMap(result interface{}, err error) (map[string]int, error) {
|
||||||
|
values, err := Values(result, err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(values)%2 != 0 {
|
||||||
|
return nil, errors.New("redigo: IntMap expects even number of values result")
|
||||||
|
}
|
||||||
|
m := make(map[string]int, len(values)/2)
|
||||||
|
for i := 0; i < len(values); i += 2 {
|
||||||
|
key, ok := values[i].([]byte)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("redigo: ScanMap key not a bulk string value")
|
||||||
|
}
|
||||||
|
value, err := Int(values[i+1], nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m[string(key)] = value
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64Map is a helper that converts an array of strings (alternating key, value)
|
||||||
|
// into a map[string]int64. The HGETALL commands return replies in this format.
|
||||||
|
// Requires an even number of values in result.
|
||||||
|
func Int64Map(result interface{}, err error) (map[string]int64, error) {
|
||||||
|
values, err := Values(result, err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(values)%2 != 0 {
|
||||||
|
return nil, errors.New("redigo: Int64Map expects even number of values result")
|
||||||
|
}
|
||||||
|
m := make(map[string]int64, len(values)/2)
|
||||||
|
for i := 0; i < len(values); i += 2 {
|
||||||
|
key, ok := values[i].([]byte)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("redigo: ScanMap key not a bulk string value")
|
||||||
|
}
|
||||||
|
value, err := Int64(values[i+1], nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m[string(key)] = value
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Positions is a helper that converts an array of positions (lat, long)
|
||||||
|
// into a [][2]float64. The GEOPOS command returns replies in this format.
|
||||||
|
func Positions(result interface{}, err error) ([]*[2]float64, error) {
|
||||||
|
values, err := Values(result, err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
positions := make([]*[2]float64, len(values))
|
||||||
|
for i := range values {
|
||||||
|
if values[i] == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p, ok := values[i].([]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("redigo: unexpected element type for interface slice, got type %T", values[i])
|
||||||
|
}
|
||||||
|
if len(p) != 2 {
|
||||||
|
return nil, fmt.Errorf("redigo: unexpected number of values for a member position, got %d", len(p))
|
||||||
|
}
|
||||||
|
lat, err := Float64(p[0], nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
long, err := Float64(p[1], nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
positions[i] = &[2]float64{lat, long}
|
||||||
|
}
|
||||||
|
return positions, nil
|
||||||
|
}
|
|
@ -0,0 +1,555 @@
|
||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ensureLen(d reflect.Value, n int) {
|
||||||
|
if n > d.Cap() {
|
||||||
|
d.Set(reflect.MakeSlice(d.Type(), n, n))
|
||||||
|
} else {
|
||||||
|
d.SetLen(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cannotConvert(d reflect.Value, s interface{}) error {
|
||||||
|
var sname string
|
||||||
|
switch s.(type) {
|
||||||
|
case string:
|
||||||
|
sname = "Redis simple string"
|
||||||
|
case Error:
|
||||||
|
sname = "Redis error"
|
||||||
|
case int64:
|
||||||
|
sname = "Redis integer"
|
||||||
|
case []byte:
|
||||||
|
sname = "Redis bulk string"
|
||||||
|
case []interface{}:
|
||||||
|
sname = "Redis array"
|
||||||
|
default:
|
||||||
|
sname = reflect.TypeOf(s).String()
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot convert from %s to %s", sname, d.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertAssignBulkString(d reflect.Value, s []byte) (err error) {
|
||||||
|
switch d.Type().Kind() {
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
var x float64
|
||||||
|
x, err = strconv.ParseFloat(string(s), d.Type().Bits())
|
||||||
|
d.SetFloat(x)
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
var x int64
|
||||||
|
x, err = strconv.ParseInt(string(s), 10, d.Type().Bits())
|
||||||
|
d.SetInt(x)
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
var x uint64
|
||||||
|
x, err = strconv.ParseUint(string(s), 10, d.Type().Bits())
|
||||||
|
d.SetUint(x)
|
||||||
|
case reflect.Bool:
|
||||||
|
var x bool
|
||||||
|
x, err = strconv.ParseBool(string(s))
|
||||||
|
d.SetBool(x)
|
||||||
|
case reflect.String:
|
||||||
|
d.SetString(string(s))
|
||||||
|
case reflect.Slice:
|
||||||
|
if d.Type().Elem().Kind() != reflect.Uint8 {
|
||||||
|
err = cannotConvert(d, s)
|
||||||
|
} else {
|
||||||
|
d.SetBytes(s)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err = cannotConvert(d, s)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertAssignInt(d reflect.Value, s int64) (err error) {
|
||||||
|
switch d.Type().Kind() {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
d.SetInt(s)
|
||||||
|
if d.Int() != s {
|
||||||
|
err = strconv.ErrRange
|
||||||
|
d.SetInt(0)
|
||||||
|
}
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
if s < 0 {
|
||||||
|
err = strconv.ErrRange
|
||||||
|
} else {
|
||||||
|
x := uint64(s)
|
||||||
|
d.SetUint(x)
|
||||||
|
if d.Uint() != x {
|
||||||
|
err = strconv.ErrRange
|
||||||
|
d.SetUint(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case reflect.Bool:
|
||||||
|
d.SetBool(s != 0)
|
||||||
|
default:
|
||||||
|
err = cannotConvert(d, s)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertAssignValue(d reflect.Value, s interface{}) (err error) {
|
||||||
|
switch s := s.(type) {
|
||||||
|
case []byte:
|
||||||
|
err = convertAssignBulkString(d, s)
|
||||||
|
case int64:
|
||||||
|
err = convertAssignInt(d, s)
|
||||||
|
default:
|
||||||
|
err = cannotConvert(d, s)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertAssignArray(d reflect.Value, s []interface{}) error {
|
||||||
|
if d.Type().Kind() != reflect.Slice {
|
||||||
|
return cannotConvert(d, s)
|
||||||
|
}
|
||||||
|
ensureLen(d, len(s))
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
if err := convertAssignValue(d.Index(i), s[i]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertAssign(d interface{}, s interface{}) (err error) {
|
||||||
|
// Handle the most common destination types using type switches and
|
||||||
|
// fall back to reflection for all other types.
|
||||||
|
switch s := s.(type) {
|
||||||
|
case nil:
|
||||||
|
// ingore
|
||||||
|
case []byte:
|
||||||
|
switch d := d.(type) {
|
||||||
|
case *string:
|
||||||
|
*d = string(s)
|
||||||
|
case *int:
|
||||||
|
*d, err = strconv.Atoi(string(s))
|
||||||
|
case *bool:
|
||||||
|
*d, err = strconv.ParseBool(string(s))
|
||||||
|
case *[]byte:
|
||||||
|
*d = s
|
||||||
|
case *interface{}:
|
||||||
|
*d = s
|
||||||
|
case nil:
|
||||||
|
// skip value
|
||||||
|
default:
|
||||||
|
if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr {
|
||||||
|
err = cannotConvert(d, s)
|
||||||
|
} else {
|
||||||
|
err = convertAssignBulkString(d.Elem(), s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case int64:
|
||||||
|
switch d := d.(type) {
|
||||||
|
case *int:
|
||||||
|
x := int(s)
|
||||||
|
if int64(x) != s {
|
||||||
|
err = strconv.ErrRange
|
||||||
|
x = 0
|
||||||
|
}
|
||||||
|
*d = x
|
||||||
|
case *bool:
|
||||||
|
*d = s != 0
|
||||||
|
case *interface{}:
|
||||||
|
*d = s
|
||||||
|
case nil:
|
||||||
|
// skip value
|
||||||
|
default:
|
||||||
|
if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr {
|
||||||
|
err = cannotConvert(d, s)
|
||||||
|
} else {
|
||||||
|
err = convertAssignInt(d.Elem(), s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case string:
|
||||||
|
switch d := d.(type) {
|
||||||
|
case *string:
|
||||||
|
*d = string(s)
|
||||||
|
default:
|
||||||
|
err = cannotConvert(reflect.ValueOf(d), s)
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
switch d := d.(type) {
|
||||||
|
case *[]interface{}:
|
||||||
|
*d = s
|
||||||
|
case *interface{}:
|
||||||
|
*d = s
|
||||||
|
case nil:
|
||||||
|
// skip value
|
||||||
|
default:
|
||||||
|
if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr {
|
||||||
|
err = cannotConvert(d, s)
|
||||||
|
} else {
|
||||||
|
err = convertAssignArray(d.Elem(), s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case Error:
|
||||||
|
err = s
|
||||||
|
default:
|
||||||
|
err = cannotConvert(reflect.ValueOf(d), s)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan copies from src to the values pointed at by dest.
|
||||||
|
//
|
||||||
|
// The values pointed at by dest must be an integer, float, boolean, string,
|
||||||
|
// []byte, interface{} or slices of these types. Scan uses the standard strconv
|
||||||
|
// package to convert bulk strings to numeric and boolean types.
|
||||||
|
//
|
||||||
|
// If a dest value is nil, then the corresponding src value is skipped.
|
||||||
|
//
|
||||||
|
// If a src element is nil, then the corresponding dest value is not modified.
|
||||||
|
//
|
||||||
|
// To enable easy use of Scan in a loop, Scan returns the slice of src
|
||||||
|
// following the copied values.
|
||||||
|
func Scan(src []interface{}, dest ...interface{}) ([]interface{}, error) {
|
||||||
|
if len(src) < len(dest) {
|
||||||
|
return nil, errors.New("redigo.Scan: array short")
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
for i, d := range dest {
|
||||||
|
err = convertAssign(d, src[i])
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("redigo.Scan: cannot assign to dest %d: %v", i, err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return src[len(dest):], err
|
||||||
|
}
|
||||||
|
|
||||||
|
type fieldSpec struct {
|
||||||
|
name string
|
||||||
|
index []int
|
||||||
|
omitEmpty bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type structSpec struct {
|
||||||
|
m map[string]*fieldSpec
|
||||||
|
l []*fieldSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *structSpec) fieldSpec(name []byte) *fieldSpec {
|
||||||
|
return ss.m[string(name)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func compileStructSpec(t reflect.Type, depth map[string]int, index []int, ss *structSpec) {
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
f := t.Field(i)
|
||||||
|
switch {
|
||||||
|
case f.PkgPath != "" && !f.Anonymous:
|
||||||
|
// Ignore unexported fields.
|
||||||
|
case f.Anonymous:
|
||||||
|
// TODO: Handle pointers. Requires change to decoder and
|
||||||
|
// protection against infinite recursion.
|
||||||
|
if f.Type.Kind() == reflect.Struct {
|
||||||
|
compileStructSpec(f.Type, depth, append(index, i), ss)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
fs := &fieldSpec{name: f.Name}
|
||||||
|
tag := f.Tag.Get("redis")
|
||||||
|
p := strings.Split(tag, ",")
|
||||||
|
if len(p) > 0 {
|
||||||
|
if p[0] == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(p[0]) > 0 {
|
||||||
|
fs.name = p[0]
|
||||||
|
}
|
||||||
|
for _, s := range p[1:] {
|
||||||
|
switch s {
|
||||||
|
case "omitempty":
|
||||||
|
fs.omitEmpty = true
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("redigo: unknown field tag %s for type %s", s, t.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d, found := depth[fs.name]
|
||||||
|
if !found {
|
||||||
|
d = 1 << 30
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case len(index) == d:
|
||||||
|
// At same depth, remove from result.
|
||||||
|
delete(ss.m, fs.name)
|
||||||
|
j := 0
|
||||||
|
for i := 0; i < len(ss.l); i++ {
|
||||||
|
if fs.name != ss.l[i].name {
|
||||||
|
ss.l[j] = ss.l[i]
|
||||||
|
j += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ss.l = ss.l[:j]
|
||||||
|
case len(index) < d:
|
||||||
|
fs.index = make([]int, len(index)+1)
|
||||||
|
copy(fs.index, index)
|
||||||
|
fs.index[len(index)] = i
|
||||||
|
depth[fs.name] = len(index)
|
||||||
|
ss.m[fs.name] = fs
|
||||||
|
ss.l = append(ss.l, fs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
structSpecMutex sync.RWMutex
|
||||||
|
structSpecCache = make(map[reflect.Type]*structSpec)
|
||||||
|
defaultFieldSpec = &fieldSpec{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func structSpecForType(t reflect.Type) *structSpec {
|
||||||
|
|
||||||
|
structSpecMutex.RLock()
|
||||||
|
ss, found := structSpecCache[t]
|
||||||
|
structSpecMutex.RUnlock()
|
||||||
|
if found {
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
structSpecMutex.Lock()
|
||||||
|
defer structSpecMutex.Unlock()
|
||||||
|
ss, found = structSpecCache[t]
|
||||||
|
if found {
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
ss = &structSpec{m: make(map[string]*fieldSpec)}
|
||||||
|
compileStructSpec(t, make(map[string]int), nil, ss)
|
||||||
|
structSpecCache[t] = ss
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
var errScanStructValue = errors.New("redigo.ScanStruct: value must be non-nil pointer to a struct")
|
||||||
|
|
||||||
|
// ScanStruct scans alternating names and values from src to a struct. The
|
||||||
|
// HGETALL and CONFIG GET commands return replies in this format.
|
||||||
|
//
|
||||||
|
// ScanStruct uses exported field names to match values in the response. Use
|
||||||
|
// 'redis' field tag to override the name:
|
||||||
|
//
|
||||||
|
// Field int `redis:"myName"`
|
||||||
|
//
|
||||||
|
// Fields with the tag redis:"-" are ignored.
|
||||||
|
//
|
||||||
|
// Integer, float, boolean, string and []byte fields are supported. Scan uses the
|
||||||
|
// standard strconv package to convert bulk string values to numeric and
|
||||||
|
// boolean types.
|
||||||
|
//
|
||||||
|
// If a src element is nil, then the corresponding field is not modified.
|
||||||
|
func ScanStruct(src []interface{}, dest interface{}) error {
|
||||||
|
d := reflect.ValueOf(dest)
|
||||||
|
if d.Kind() != reflect.Ptr || d.IsNil() {
|
||||||
|
return errScanStructValue
|
||||||
|
}
|
||||||
|
d = d.Elem()
|
||||||
|
if d.Kind() != reflect.Struct {
|
||||||
|
return errScanStructValue
|
||||||
|
}
|
||||||
|
ss := structSpecForType(d.Type())
|
||||||
|
|
||||||
|
if len(src)%2 != 0 {
|
||||||
|
return errors.New("redigo.ScanStruct: number of values not a multiple of 2")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(src); i += 2 {
|
||||||
|
s := src[i+1]
|
||||||
|
if s == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name, ok := src[i].([]byte)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("redigo.ScanStruct: key %d not a bulk string value", i)
|
||||||
|
}
|
||||||
|
fs := ss.fieldSpec(name)
|
||||||
|
if fs == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil {
|
||||||
|
return fmt.Errorf("redigo.ScanStruct: cannot assign field %s: %v", fs.name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
errScanSliceValue = errors.New("redigo.ScanSlice: dest must be non-nil pointer to a struct")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ScanSlice scans src to the slice pointed to by dest. The elements the dest
|
||||||
|
// slice must be integer, float, boolean, string, struct or pointer to struct
|
||||||
|
// values.
|
||||||
|
//
|
||||||
|
// Struct fields must be integer, float, boolean or string values. All struct
|
||||||
|
// fields are used unless a subset is specified using fieldNames.
|
||||||
|
func ScanSlice(src []interface{}, dest interface{}, fieldNames ...string) error {
|
||||||
|
d := reflect.ValueOf(dest)
|
||||||
|
if d.Kind() != reflect.Ptr || d.IsNil() {
|
||||||
|
return errScanSliceValue
|
||||||
|
}
|
||||||
|
d = d.Elem()
|
||||||
|
if d.Kind() != reflect.Slice {
|
||||||
|
return errScanSliceValue
|
||||||
|
}
|
||||||
|
|
||||||
|
isPtr := false
|
||||||
|
t := d.Type().Elem()
|
||||||
|
if t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct {
|
||||||
|
isPtr = true
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Kind() != reflect.Struct {
|
||||||
|
ensureLen(d, len(src))
|
||||||
|
for i, s := range src {
|
||||||
|
if s == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := convertAssignValue(d.Index(i), s); err != nil {
|
||||||
|
return fmt.Errorf("redigo.ScanSlice: cannot assign element %d: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ss := structSpecForType(t)
|
||||||
|
fss := ss.l
|
||||||
|
if len(fieldNames) > 0 {
|
||||||
|
fss = make([]*fieldSpec, len(fieldNames))
|
||||||
|
for i, name := range fieldNames {
|
||||||
|
fss[i] = ss.m[name]
|
||||||
|
if fss[i] == nil {
|
||||||
|
return fmt.Errorf("redigo.ScanSlice: ScanSlice bad field name %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fss) == 0 {
|
||||||
|
return errors.New("redigo.ScanSlice: no struct fields")
|
||||||
|
}
|
||||||
|
|
||||||
|
n := len(src) / len(fss)
|
||||||
|
if n*len(fss) != len(src) {
|
||||||
|
return errors.New("redigo.ScanSlice: length not a multiple of struct field count")
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureLen(d, n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
d := d.Index(i)
|
||||||
|
if isPtr {
|
||||||
|
if d.IsNil() {
|
||||||
|
d.Set(reflect.New(t))
|
||||||
|
}
|
||||||
|
d = d.Elem()
|
||||||
|
}
|
||||||
|
for j, fs := range fss {
|
||||||
|
s := src[i*len(fss)+j]
|
||||||
|
if s == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil {
|
||||||
|
return fmt.Errorf("redigo.ScanSlice: cannot assign element %d to field %s: %v", i*len(fss)+j, fs.name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Args is a helper for constructing command arguments from structured values.
|
||||||
|
type Args []interface{}
|
||||||
|
|
||||||
|
// Add returns the result of appending value to args.
|
||||||
|
func (args Args) Add(value ...interface{}) Args {
|
||||||
|
return append(args, value...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFlat returns the result of appending the flattened value of v to args.
|
||||||
|
//
|
||||||
|
// Maps are flattened by appending the alternating keys and map values to args.
|
||||||
|
//
|
||||||
|
// Slices are flattened by appending the slice elements to args.
|
||||||
|
//
|
||||||
|
// Structs are flattened by appending the alternating names and values of
|
||||||
|
// exported fields to args. If v is a nil struct pointer, then nothing is
|
||||||
|
// appended. The 'redis' field tag overrides struct field names. See ScanStruct
|
||||||
|
// for more information on the use of the 'redis' field tag.
|
||||||
|
//
|
||||||
|
// Other types are appended to args as is.
|
||||||
|
func (args Args) AddFlat(v interface{}) Args {
|
||||||
|
rv := reflect.ValueOf(v)
|
||||||
|
switch rv.Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
args = flattenStruct(args, rv)
|
||||||
|
case reflect.Slice:
|
||||||
|
for i := 0; i < rv.Len(); i++ {
|
||||||
|
args = append(args, rv.Index(i).Interface())
|
||||||
|
}
|
||||||
|
case reflect.Map:
|
||||||
|
for _, k := range rv.MapKeys() {
|
||||||
|
args = append(args, k.Interface(), rv.MapIndex(k).Interface())
|
||||||
|
}
|
||||||
|
case reflect.Ptr:
|
||||||
|
if rv.Type().Elem().Kind() == reflect.Struct {
|
||||||
|
if !rv.IsNil() {
|
||||||
|
args = flattenStruct(args, rv.Elem())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
args = append(args, v)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
args = append(args, v)
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func flattenStruct(args Args, v reflect.Value) Args {
|
||||||
|
ss := structSpecForType(v.Type())
|
||||||
|
for _, fs := range ss.l {
|
||||||
|
fv := v.FieldByIndex(fs.index)
|
||||||
|
if fs.omitEmpty {
|
||||||
|
var empty = false
|
||||||
|
switch fv.Kind() {
|
||||||
|
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
|
||||||
|
empty = fv.Len() == 0
|
||||||
|
case reflect.Bool:
|
||||||
|
empty = !fv.Bool()
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
empty = fv.Int() == 0
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
|
empty = fv.Uint() == 0
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
empty = fv.Float() == 0
|
||||||
|
case reflect.Interface, reflect.Ptr:
|
||||||
|
empty = fv.IsNil()
|
||||||
|
}
|
||||||
|
if empty {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
args = append(args, fs.name, fv.Interface())
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Script encapsulates the source, hash and key count for a Lua script. See
|
||||||
|
// http://redis.io/commands/eval for information on scripts in Redis.
|
||||||
|
type Script struct {
|
||||||
|
keyCount int
|
||||||
|
src string
|
||||||
|
hash string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewScript returns a new script object. If keyCount is greater than or equal
|
||||||
|
// to zero, then the count is automatically inserted in the EVAL command
|
||||||
|
// argument list. If keyCount is less than zero, then the application supplies
|
||||||
|
// the count as the first value in the keysAndArgs argument to the Do, Send and
|
||||||
|
// SendHash methods.
|
||||||
|
func NewScript(keyCount int, src string) *Script {
|
||||||
|
h := sha1.New()
|
||||||
|
io.WriteString(h, src)
|
||||||
|
return &Script{keyCount, src, hex.EncodeToString(h.Sum(nil))}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Script) args(spec string, keysAndArgs []interface{}) []interface{} {
|
||||||
|
var args []interface{}
|
||||||
|
if s.keyCount < 0 {
|
||||||
|
args = make([]interface{}, 1+len(keysAndArgs))
|
||||||
|
args[0] = spec
|
||||||
|
copy(args[1:], keysAndArgs)
|
||||||
|
} else {
|
||||||
|
args = make([]interface{}, 2+len(keysAndArgs))
|
||||||
|
args[0] = spec
|
||||||
|
args[1] = s.keyCount
|
||||||
|
copy(args[2:], keysAndArgs)
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do evaluates the script. Under the covers, Do optimistically evaluates the
|
||||||
|
// script using the EVALSHA command. If the command fails because the script is
|
||||||
|
// not loaded, then Do evaluates the script using the EVAL command (thus
|
||||||
|
// causing the script to load).
|
||||||
|
func (s *Script) Do(c Conn, keysAndArgs ...interface{}) (interface{}, error) {
|
||||||
|
v, err := c.Do("EVALSHA", s.args(s.hash, keysAndArgs)...)
|
||||||
|
if e, ok := err.(Error); ok && strings.HasPrefix(string(e), "NOSCRIPT ") {
|
||||||
|
v, err = c.Do("EVAL", s.args(s.src, keysAndArgs)...)
|
||||||
|
}
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendHash evaluates the script without waiting for the reply. The script is
|
||||||
|
// evaluated with the EVALSHA command. The application must ensure that the
|
||||||
|
// script is loaded by a previous call to Send, Do or Load methods.
|
||||||
|
func (s *Script) SendHash(c Conn, keysAndArgs ...interface{}) error {
|
||||||
|
return c.Send("EVALSHA", s.args(s.hash, keysAndArgs)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send evaluates the script without waiting for the reply.
|
||||||
|
func (s *Script) Send(c Conn, keysAndArgs ...interface{}) error {
|
||||||
|
return c.Send("EVAL", s.args(s.src, keysAndArgs)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load loads the script without evaluating it.
|
||||||
|
func (s *Script) Load(c Conn) error {
|
||||||
|
_, err := c.Do("SCRIPT", "LOAD", s.src)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
Icon?
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
|
@ -0,0 +1,12 @@
|
||||||
|
sudo: false
|
||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- 1.2
|
||||||
|
- 1.3
|
||||||
|
- 1.4
|
||||||
|
- 1.5
|
||||||
|
- 1.6
|
||||||
|
- tip
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- mysql -e 'create database gotest;'
|
|
@ -0,0 +1,51 @@
|
||||||
|
# This is the official list of Go-MySQL-Driver authors for copyright purposes.
|
||||||
|
|
||||||
|
# If you are submitting a patch, please add your name or the name of the
|
||||||
|
# organization which holds the copyright to this list in alphabetical order.
|
||||||
|
|
||||||
|
# Names should be added to this file as
|
||||||
|
# Name <email address>
|
||||||
|
# The email address is not required for organizations.
|
||||||
|
# Please keep the list sorted.
|
||||||
|
|
||||||
|
|
||||||
|
# Individual Persons
|
||||||
|
|
||||||
|
Aaron Hopkins <go-sql-driver at die.net>
|
||||||
|
Arne Hormann <arnehormann at gmail.com>
|
||||||
|
Carlos Nieto <jose.carlos at menteslibres.net>
|
||||||
|
Chris Moos <chris at tech9computers.com>
|
||||||
|
Daniel Nichter <nil at codenode.com>
|
||||||
|
Daniël van Eeden <git at myname.nl>
|
||||||
|
DisposaBoy <disposaboy at dby.me>
|
||||||
|
Frederick Mayle <frederickmayle at gmail.com>
|
||||||
|
Gustavo Kristic <gkristic at gmail.com>
|
||||||
|
Hanno Braun <mail at hannobraun.com>
|
||||||
|
Henri Yandell <flamefew at gmail.com>
|
||||||
|
Hirotaka Yamamoto <ymmt2005 at gmail.com>
|
||||||
|
INADA Naoki <songofacandy at gmail.com>
|
||||||
|
James Harr <james.harr at gmail.com>
|
||||||
|
Jian Zhen <zhenjl at gmail.com>
|
||||||
|
Joshua Prunier <joshua.prunier at gmail.com>
|
||||||
|
Julien Lefevre <julien.lefevr at gmail.com>
|
||||||
|
Julien Schmidt <go-sql-driver at julienschmidt.com>
|
||||||
|
Kamil Dziedzic <kamil at klecza.pl>
|
||||||
|
Kevin Malachowski <kevin at chowski.com>
|
||||||
|
Leonardo YongUk Kim <dalinaum at gmail.com>
|
||||||
|
Luca Looz <luca.looz92 at gmail.com>
|
||||||
|
Lucas Liu <extrafliu at gmail.com>
|
||||||
|
Luke Scott <luke at webconnex.com>
|
||||||
|
Michael Woolnough <michael.woolnough at gmail.com>
|
||||||
|
Nicola Peduzzi <thenikso at gmail.com>
|
||||||
|
Runrioter Wung <runrioter at gmail.com>
|
||||||
|
Soroush Pour <me at soroushjp.com>
|
||||||
|
Stan Putrya <root.vagner at gmail.com>
|
||||||
|
Stanley Gunawan <gunawan.stanley at gmail.com>
|
||||||
|
Xiaobing Jiang <s7v7nislands at gmail.com>
|
||||||
|
Xiuming Chen <cc at cxm.cc>
|
||||||
|
|
||||||
|
# Organizations
|
||||||
|
|
||||||
|
Barracuda Networks, Inc.
|
||||||
|
Google Inc.
|
||||||
|
Stripe Inc.
|
|
@ -0,0 +1,103 @@
|
||||||
|
## HEAD
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
|
||||||
|
- Go 1.1 is no longer supported
|
||||||
|
- Use decimals field from MySQL to format time types (#249)
|
||||||
|
- Buffer optimizations (#269)
|
||||||
|
- TLS ServerName defaults to the host (#283)
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
|
||||||
|
- Enable microsecond resolution on TIME, DATETIME and TIMESTAMP (#249)
|
||||||
|
- Fixed handling of queries without columns and rows (#255)
|
||||||
|
- Fixed a panic when SetKeepAlive() failed (#298)
|
||||||
|
- Support receiving ERR packet while reading rows (#321)
|
||||||
|
- Fixed reading NULL length-encoded integers in MySQL 5.6+ (#349)
|
||||||
|
- Fixed absolute paths support in LOAD LOCAL DATA INFILE (#356)
|
||||||
|
- Actually zero out bytes in handshake response (#378)
|
||||||
|
- Fixed race condition in registering LOAD DATA INFILE handler (#383)
|
||||||
|
- Fixed tests with MySQL 5.7.9+ (#380)
|
||||||
|
- QueryUnescape TLS config names (#397)
|
||||||
|
- Fixed "broken pipe" error by writing to closed socket (#390)
|
||||||
|
|
||||||
|
New Features:
|
||||||
|
- Support for returning table alias on Columns() (#289, #359, #382)
|
||||||
|
- Placeholder interpolation, can be actived with the DSN parameter `interpolateParams=true` (#309, #318)
|
||||||
|
- Support for uint64 parameters with high bit set (#332, #345)
|
||||||
|
- Cleartext authentication plugin support (#327)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Version 1.2 (2014-06-03)
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
|
||||||
|
- We switched back to a "rolling release". `go get` installs the current master branch again
|
||||||
|
- Version v1 of the driver will not be maintained anymore. Go 1.0 is no longer supported by this driver
|
||||||
|
- Exported errors to allow easy checking from application code
|
||||||
|
- Enabled TCP Keepalives on TCP connections
|
||||||
|
- Optimized INFILE handling (better buffer size calculation, lazy init, ...)
|
||||||
|
- The DSN parser also checks for a missing separating slash
|
||||||
|
- Faster binary date / datetime to string formatting
|
||||||
|
- Also exported the MySQLWarning type
|
||||||
|
- mysqlConn.Close returns the first error encountered instead of ignoring all errors
|
||||||
|
- writePacket() automatically writes the packet size to the header
|
||||||
|
- readPacket() uses an iterative approach instead of the recursive approach to merge splitted packets
|
||||||
|
|
||||||
|
New Features:
|
||||||
|
|
||||||
|
- `RegisterDial` allows the usage of a custom dial function to establish the network connection
|
||||||
|
- Setting the connection collation is possible with the `collation` DSN parameter. This parameter should be preferred over the `charset` parameter
|
||||||
|
- Logging of critical errors is configurable with `SetLogger`
|
||||||
|
- Google CloudSQL support
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
|
||||||
|
- Allow more than 32 parameters in prepared statements
|
||||||
|
- Various old_password fixes
|
||||||
|
- Fixed TestConcurrent test to pass Go's race detection
|
||||||
|
- Fixed appendLengthEncodedInteger for large numbers
|
||||||
|
- Renamed readLengthEnodedString to readLengthEncodedString and skipLengthEnodedString to skipLengthEncodedString (fixed typo)
|
||||||
|
|
||||||
|
|
||||||
|
## Version 1.1 (2013-11-02)
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
|
||||||
|
- Go-MySQL-Driver now requires Go 1.1
|
||||||
|
- Connections now use the collation `utf8_general_ci` by default. Adding `&charset=UTF8` to the DSN should not be necessary anymore
|
||||||
|
- Made closing rows and connections error tolerant. This allows for example deferring rows.Close() without checking for errors
|
||||||
|
- `[]byte(nil)` is now treated as a NULL value. Before, it was treated like an empty string / `[]byte("")`
|
||||||
|
- DSN parameter values must now be url.QueryEscape'ed. This allows text values to contain special characters, such as '&'.
|
||||||
|
- Use the IO buffer also for writing. This results in zero allocations (by the driver) for most queries
|
||||||
|
- Optimized the buffer for reading
|
||||||
|
- stmt.Query now caches column metadata
|
||||||
|
- New Logo
|
||||||
|
- Changed the copyright header to include all contributors
|
||||||
|
- Improved the LOAD INFILE documentation
|
||||||
|
- The driver struct is now exported to make the driver directly accessible
|
||||||
|
- Refactored the driver tests
|
||||||
|
- Added more benchmarks and moved all to a separate file
|
||||||
|
- Other small refactoring
|
||||||
|
|
||||||
|
New Features:
|
||||||
|
|
||||||
|
- Added *old_passwords* support: Required in some cases, but must be enabled by adding `allowOldPasswords=true` to the DSN since it is insecure
|
||||||
|
- Added a `clientFoundRows` parameter: Return the number of matching rows instead of the number of rows changed on UPDATEs
|
||||||
|
- Added TLS/SSL support: Use a TLS/SSL encrypted connection to the server. Custom TLS configs can be registered and used
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
|
||||||
|
- Fixed MySQL 4.1 support: MySQL 4.1 sends packets with lengths which differ from the specification
|
||||||
|
- Convert to DB timezone when inserting `time.Time`
|
||||||
|
- Splitted packets (more than 16MB) are now merged correctly
|
||||||
|
- Fixed false positive `io.EOF` errors when the data was fully read
|
||||||
|
- Avoid panics on reuse of closed connections
|
||||||
|
- Fixed empty string producing false nil values
|
||||||
|
- Fixed sign byte for positive TIME fields
|
||||||
|
|
||||||
|
|
||||||
|
## Version 1.0 (2013-05-14)
|
||||||
|
|
||||||
|
Initial Release
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Contributing Guidelines
|
||||||
|
|
||||||
|
## Reporting Issues
|
||||||
|
|
||||||
|
Before creating a new Issue, please check first if a similar Issue [already exists](https://github.com/go-sql-driver/mysql/issues?state=open) or was [recently closed](https://github.com/go-sql-driver/mysql/issues?direction=desc&page=1&sort=updated&state=closed).
|
||||||
|
|
||||||
|
## Contributing Code
|
||||||
|
|
||||||
|
By contributing to this project, you share your code under the Mozilla Public License 2, as specified in the LICENSE file.
|
||||||
|
Don't forget to add yourself to the AUTHORS file.
|
||||||
|
|
||||||
|
### Code Review
|
||||||
|
|
||||||
|
Everyone is invited to review and comment on pull requests.
|
||||||
|
If it looks fine to you, comment with "LGTM" (Looks good to me).
|
||||||
|
|
||||||
|
If changes are required, notice the reviewers with "PTAL" (Please take another look) after committing the fixes.
|
||||||
|
|
||||||
|
Before merging the Pull Request, at least one [team member](https://github.com/go-sql-driver?tab=members) must have commented with "LGTM".
|
||||||
|
|
||||||
|
## Development Ideas
|
||||||
|
|
||||||
|
If you are looking for ideas for code contributions, please check our [Development Ideas](https://github.com/go-sql-driver/mysql/wiki/Development-Ideas) Wiki page.
|
|
@ -0,0 +1,21 @@
|
||||||
|
### Issue description
|
||||||
|
Tell us what should happen and what happens instead
|
||||||
|
|
||||||
|
### Example code
|
||||||
|
```go
|
||||||
|
If possible, please enter some example code here to reproduce the issue.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error log
|
||||||
|
```
|
||||||
|
If you have an error log, please paste it here.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
*Driver version (or git SHA):*
|
||||||
|
|
||||||
|
*Go version:* run `go version` in your console
|
||||||
|
|
||||||
|
*Server version:* E.g. MySQL 5.6, MariaDB 10.0.20
|
||||||
|
|
||||||
|
*Server OS:* E.g. Debian 8.1 (Jessie), Windows 10
|
|
@ -0,0 +1,373 @@
|
||||||
|
Mozilla Public License Version 2.0
|
||||||
|
==================================
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
1.1. "Contributor"
|
||||||
|
means each individual or legal entity that creates, contributes to
|
||||||
|
the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version"
|
||||||
|
means the combination of the Contributions of others (if any) used
|
||||||
|
by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution"
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software"
|
||||||
|
means Source Code Form to which the initial Contributor has attached
|
||||||
|
the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case
|
||||||
|
including portions thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses"
|
||||||
|
means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
1.6. "Executable Form"
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work"
|
||||||
|
means a work that combines Covered Software with other material, in
|
||||||
|
a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License"
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable"
|
||||||
|
means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and
|
||||||
|
all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications"
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor
|
||||||
|
means any patent claim(s), including without limitation, method,
|
||||||
|
process, and apparatus claims, in any patent Licensable by such
|
||||||
|
Contributor that would be infringed, but for the grant of the
|
||||||
|
License, by the making, using, selling, offering for sale, having
|
||||||
|
made, import, or transfer of either its Contributions or its
|
||||||
|
Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License"
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU
|
||||||
|
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form"
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your")
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, "You" includes any entity that
|
||||||
|
controls, is controlled by, or is under common control with You. For
|
||||||
|
purposes of this definition, "control" means (a) the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity,
|
||||||
|
whether by contract or otherwise, or (b) ownership of more than
|
||||||
|
fifty percent (50%) of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||||
|
for sale, have made, import, and otherwise transfer either its
|
||||||
|
Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution
|
||||||
|
become effective for each Contribution on the date the Contributor first
|
||||||
|
distributes such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under
|
||||||
|
this License. No additional rights or licenses will be implied from the
|
||||||
|
distribution or licensing of Covered Software under this License.
|
||||||
|
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||||
|
Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software;
|
||||||
|
or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's
|
||||||
|
modifications of Covered Software, or (ii) the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||||
|
its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks,
|
||||||
|
or logos of any Contributor (except as may be necessary to comply with
|
||||||
|
the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this
|
||||||
|
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||||
|
permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its
|
||||||
|
Contributions are its original creation(s) or it has sufficient rights
|
||||||
|
to grant the rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under
|
||||||
|
applicable copyright doctrines of fair use, fair dealing, or other
|
||||||
|
equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||||
|
in Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under
|
||||||
|
the terms of this License. You must inform recipients that the Source
|
||||||
|
Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not
|
||||||
|
attempt to alter or restrict the recipients' rights in the Source Code
|
||||||
|
Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code
|
||||||
|
Form, as described in Section 3.1, and You must inform recipients of
|
||||||
|
the Executable Form how they can obtain a copy of such Source Code
|
||||||
|
Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this
|
||||||
|
License, or sublicense it under different terms, provided that the
|
||||||
|
license for the Executable Form does not attempt to limit or alter
|
||||||
|
the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for
|
||||||
|
the Covered Software. If the Larger Work is a combination of Covered
|
||||||
|
Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this
|
||||||
|
License permits You to additionally distribute such Covered Software
|
||||||
|
under the terms of such Secondary License(s), so that the recipient of
|
||||||
|
the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices
|
||||||
|
(including copyright notices, patent notices, disclaimers of warranty,
|
||||||
|
or limitations of liability) contained within the Source Code Form of
|
||||||
|
the Covered Software, except that You may alter any license notices to
|
||||||
|
the extent required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on
|
||||||
|
behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by
|
||||||
|
You alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this
|
||||||
|
License with respect to some or all of the Covered Software due to
|
||||||
|
statute, judicial order, or regulation then You must: (a) comply with
|
||||||
|
the terms of this License to the maximum extent possible; and (b)
|
||||||
|
describe the limitations and the code they affect. Such description must
|
||||||
|
be placed in a text file included with all distributions of the Covered
|
||||||
|
Software under this License. Except to the extent prohibited by statute
|
||||||
|
or regulation, such description must be sufficiently detailed for a
|
||||||
|
recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
--------------
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically
|
||||||
|
if You fail to comply with any of its terms. However, if You become
|
||||||
|
compliant, then the rights granted under this License from a particular
|
||||||
|
Contributor are reinstated (a) provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||||
|
ongoing basis, if such Contributor fails to notify You of the
|
||||||
|
non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular
|
||||||
|
Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the
|
||||||
|
first time You have received notice of non-compliance with this License
|
||||||
|
from such Contributor, and You become compliant prior to 30 days after
|
||||||
|
Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions,
|
||||||
|
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||||
|
directly or indirectly infringes any patent, then the rights granted to
|
||||||
|
You by any and all Contributors for the Covered Software under Section
|
||||||
|
2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||||
|
end user license agreements (excluding distributors and resellers) which
|
||||||
|
have been validly granted by You or Your distributors under this License
|
||||||
|
prior to termination shall survive termination.
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 6. Disclaimer of Warranty *
|
||||||
|
* ------------------------- *
|
||||||
|
* *
|
||||||
|
* Covered Software is provided under this License on an "as is" *
|
||||||
|
* basis, without warranty of any kind, either expressed, implied, or *
|
||||||
|
* statutory, including, without limitation, warranties that the *
|
||||||
|
* Covered Software is free of defects, merchantable, fit for a *
|
||||||
|
* particular purpose or non-infringing. The entire risk as to the *
|
||||||
|
* quality and performance of the Covered Software is with You. *
|
||||||
|
* Should any Covered Software prove defective in any respect, You *
|
||||||
|
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||||
|
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||||
|
* essential part of this License. No use of any Covered Software is *
|
||||||
|
* authorized under this License except under this disclaimer. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 7. Limitation of Liability *
|
||||||
|
* -------------------------- *
|
||||||
|
* *
|
||||||
|
* Under no circumstances and under no legal theory, whether tort *
|
||||||
|
* (including negligence), contract, or otherwise, shall any *
|
||||||
|
* Contributor, or anyone who distributes Covered Software as *
|
||||||
|
* permitted above, be liable to You for any direct, indirect, *
|
||||||
|
* special, incidental, or consequential damages of any character *
|
||||||
|
* including, without limitation, damages for lost profits, loss of *
|
||||||
|
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||||
|
* and all other commercial damages or losses, even if such party *
|
||||||
|
* shall have been informed of the possibility of such damages. This *
|
||||||
|
* limitation of liability shall not apply to liability for death or *
|
||||||
|
* personal injury resulting from such party's negligence to the *
|
||||||
|
* extent applicable law prohibits such limitation. Some *
|
||||||
|
* jurisdictions do not allow the exclusion or limitation of *
|
||||||
|
* incidental or consequential damages, so this exclusion and *
|
||||||
|
* limitation may not apply to You. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the
|
||||||
|
courts of a jurisdiction where the defendant maintains its principal
|
||||||
|
place of business and such litigation shall be governed by laws of that
|
||||||
|
jurisdiction, without reference to its conflict-of-law provisions.
|
||||||
|
Nothing in this Section shall prevent a party's ability to bring
|
||||||
|
cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject
|
||||||
|
matter hereof. If any provision of this License is held to be
|
||||||
|
unenforceable, such provision shall be reformed only to the extent
|
||||||
|
necessary to make it enforceable. Any law or regulation which provides
|
||||||
|
that the language of a contract shall be construed against the drafter
|
||||||
|
shall not be used to construe this License against a Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version
|
||||||
|
of the License under which You originally received the Covered Software,
|
||||||
|
or under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a
|
||||||
|
modified version of this License if you rename the license and remove
|
||||||
|
any references to the name of the license steward (except to note that
|
||||||
|
such modified license differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||||
|
Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
Exhibit A - Source Code Form License Notice
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular
|
||||||
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
file in a relevant directory) where a recipient would be likely to look
|
||||||
|
for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
defined by the Mozilla Public License, v. 2.0.
|
|
@ -0,0 +1,9 @@
|
||||||
|
### Description
|
||||||
|
Please explain the changes you made here.
|
||||||
|
|
||||||
|
### Checklist
|
||||||
|
- [ ] Code compiles correctly
|
||||||
|
- [ ] Created tests which fail without the change (if possible)
|
||||||
|
- [ ] All tests passing
|
||||||
|
- [ ] Extended the README / documentation, if necessary
|
||||||
|
- [ ] Added myself / the copyright holder to the AUTHORS file
|
|
@ -0,0 +1,420 @@
|
||||||
|
# Go-MySQL-Driver
|
||||||
|
|
||||||
|
A MySQL-Driver for Go's [database/sql](http://golang.org/pkg/database/sql) package
|
||||||
|
|
||||||
|
![Go-MySQL-Driver logo](https://raw.github.com/wiki/go-sql-driver/mysql/gomysql_m.png "Golang Gopher holding the MySQL Dolphin")
|
||||||
|
|
||||||
|
**Latest stable Release:** [Version 1.2 (June 03, 2014)](https://github.com/go-sql-driver/mysql/releases)
|
||||||
|
|
||||||
|
[![Build Status](https://travis-ci.org/go-sql-driver/mysql.png?branch=master)](https://travis-ci.org/go-sql-driver/mysql)
|
||||||
|
|
||||||
|
---------------------------------------
|
||||||
|
* [Features](#features)
|
||||||
|
* [Requirements](#requirements)
|
||||||
|
* [Installation](#installation)
|
||||||
|
* [Usage](#usage)
|
||||||
|
* [DSN (Data Source Name)](#dsn-data-source-name)
|
||||||
|
* [Password](#password)
|
||||||
|
* [Protocol](#protocol)
|
||||||
|
* [Address](#address)
|
||||||
|
* [Parameters](#parameters)
|
||||||
|
* [Examples](#examples)
|
||||||
|
* [LOAD DATA LOCAL INFILE support](#load-data-local-infile-support)
|
||||||
|
* [time.Time support](#timetime-support)
|
||||||
|
* [Unicode support](#unicode-support)
|
||||||
|
* [Testing / Development](#testing--development)
|
||||||
|
* [License](#license)
|
||||||
|
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
## Features
|
||||||
|
* Lightweight and [fast](https://github.com/go-sql-driver/sql-benchmark "golang MySQL-Driver performance")
|
||||||
|
* Native Go implementation. No C-bindings, just pure Go
|
||||||
|
* Connections over TCP/IPv4, TCP/IPv6, Unix domain sockets or [custom protocols](http://godoc.org/github.com/go-sql-driver/mysql#DialFunc)
|
||||||
|
* Automatic handling of broken connections
|
||||||
|
* Automatic Connection Pooling *(by database/sql package)*
|
||||||
|
* Supports queries larger than 16MB
|
||||||
|
* Full [`sql.RawBytes`](http://golang.org/pkg/database/sql/#RawBytes) support.
|
||||||
|
* Intelligent `LONG DATA` handling in prepared statements
|
||||||
|
* Secure `LOAD DATA LOCAL INFILE` support with file Whitelisting and `io.Reader` support
|
||||||
|
* Optional `time.Time` parsing
|
||||||
|
* Optional placeholder interpolation
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
* Go 1.2 or higher
|
||||||
|
* MySQL (4.1+), MariaDB, Percona Server, Google CloudSQL or Sphinx (2.2.3+)
|
||||||
|
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
Simple install the package to your [$GOPATH](http://code.google.com/p/go-wiki/wiki/GOPATH "GOPATH") with the [go tool](http://golang.org/cmd/go/ "go command") from shell:
|
||||||
|
```bash
|
||||||
|
$ go get github.com/go-sql-driver/mysql
|
||||||
|
```
|
||||||
|
Make sure [Git is installed](http://git-scm.com/downloads) on your machine and in your system's `PATH`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
_Go MySQL Driver_ is an implementation of Go's `database/sql/driver` interface. You only need to import the driver and can use the full [`database/sql`](http://golang.org/pkg/database/sql) API then.
|
||||||
|
|
||||||
|
Use `mysql` as `driverName` and a valid [DSN](#dsn-data-source-name) as `dataSourceName`:
|
||||||
|
```go
|
||||||
|
import "database/sql"
|
||||||
|
import _ "github.com/go-sql-driver/mysql"
|
||||||
|
|
||||||
|
db, err := sql.Open("mysql", "user:password@/dbname")
|
||||||
|
```
|
||||||
|
|
||||||
|
[Examples are available in our Wiki](https://github.com/go-sql-driver/mysql/wiki/Examples "Go-MySQL-Driver Examples").
|
||||||
|
|
||||||
|
|
||||||
|
### DSN (Data Source Name)
|
||||||
|
|
||||||
|
The Data Source Name has a common format, like e.g. [PEAR DB](http://pear.php.net/manual/en/package.database.db.intro-dsn.php) uses it, but without type-prefix (optional parts marked by squared brackets):
|
||||||
|
```
|
||||||
|
[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]
|
||||||
|
```
|
||||||
|
|
||||||
|
A DSN in its fullest form:
|
||||||
|
```
|
||||||
|
username:password@protocol(address)/dbname?param=value
|
||||||
|
```
|
||||||
|
|
||||||
|
Except for the databasename, all values are optional. So the minimal DSN is:
|
||||||
|
```
|
||||||
|
/dbname
|
||||||
|
```
|
||||||
|
|
||||||
|
If you do not want to preselect a database, leave `dbname` empty:
|
||||||
|
```
|
||||||
|
/
|
||||||
|
```
|
||||||
|
This has the same effect as an empty DSN string:
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, [Config.FormatDSN](https://godoc.org/github.com/go-sql-driver/mysql#Config.FormatDSN) can be used to create a DSN string by filling a struct.
|
||||||
|
|
||||||
|
#### Password
|
||||||
|
Passwords can consist of any character. Escaping is **not** necessary.
|
||||||
|
|
||||||
|
#### Protocol
|
||||||
|
See [net.Dial](http://golang.org/pkg/net/#Dial) for more information which networks are available.
|
||||||
|
In general you should use an Unix domain socket if available and TCP otherwise for best performance.
|
||||||
|
|
||||||
|
#### Address
|
||||||
|
For TCP and UDP networks, addresses have the form `host:port`.
|
||||||
|
If `host` is a literal IPv6 address, it must be enclosed in square brackets.
|
||||||
|
The functions [net.JoinHostPort](http://golang.org/pkg/net/#JoinHostPort) and [net.SplitHostPort](http://golang.org/pkg/net/#SplitHostPort) manipulate addresses in this form.
|
||||||
|
|
||||||
|
For Unix domain sockets the address is the absolute path to the MySQL-Server-socket, e.g. `/var/run/mysqld/mysqld.sock` or `/tmp/mysql.sock`.
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
*Parameters are case-sensitive!*
|
||||||
|
|
||||||
|
Notice that any of `true`, `TRUE`, `True` or `1` is accepted to stand for a true boolean value. Not surprisingly, false can be specified as any of: `false`, `FALSE`, `False` or `0`.
|
||||||
|
|
||||||
|
##### `allowAllFiles`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: bool
|
||||||
|
Valid Values: true, false
|
||||||
|
Default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
`allowAllFiles=true` disables the file Whitelist for `LOAD DATA LOCAL INFILE` and allows *all* files.
|
||||||
|
[*Might be insecure!*](http://dev.mysql.com/doc/refman/5.7/en/load-data-local.html)
|
||||||
|
|
||||||
|
##### `allowCleartextPasswords`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: bool
|
||||||
|
Valid Values: true, false
|
||||||
|
Default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
`allowCleartextPasswords=true` allows using the [cleartext client side plugin](http://dev.mysql.com/doc/en/cleartext-authentication-plugin.html) if required by an account, such as one defined with the [PAM authentication plugin](http://dev.mysql.com/doc/en/pam-authentication-plugin.html). Sending passwords in clear text may be a security problem in some configurations. To avoid problems if there is any possibility that the password would be intercepted, clients should connect to MySQL Server using a method that protects the password. Possibilities include [TLS / SSL](#tls), IPsec, or a private network.
|
||||||
|
|
||||||
|
##### `allowOldPasswords`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: bool
|
||||||
|
Valid Values: true, false
|
||||||
|
Default: false
|
||||||
|
```
|
||||||
|
`allowOldPasswords=true` allows the usage of the insecure old password method. This should be avoided, but is necessary in some cases. See also [the old_passwords wiki page](https://github.com/go-sql-driver/mysql/wiki/old_passwords).
|
||||||
|
|
||||||
|
##### `charset`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: string
|
||||||
|
Valid Values: <name>
|
||||||
|
Default: none
|
||||||
|
```
|
||||||
|
|
||||||
|
Sets the charset used for client-server interaction (`"SET NAMES <value>"`). If multiple charsets are set (separated by a comma), the following charset is used if setting the charset failes. This enables for example support for `utf8mb4` ([introduced in MySQL 5.5.3](http://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html)) with fallback to `utf8` for older servers (`charset=utf8mb4,utf8`).
|
||||||
|
|
||||||
|
Usage of the `charset` parameter is discouraged because it issues additional queries to the server.
|
||||||
|
Unless you need the fallback behavior, please use `collation` instead.
|
||||||
|
|
||||||
|
##### `collation`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: string
|
||||||
|
Valid Values: <name>
|
||||||
|
Default: utf8_general_ci
|
||||||
|
```
|
||||||
|
|
||||||
|
Sets the collation used for client-server interaction on connection. In contrast to `charset`, `collation` does not issue additional queries. If the specified collation is unavailable on the target server, the connection will fail.
|
||||||
|
|
||||||
|
A list of valid charsets for a server is retrievable with `SHOW COLLATION`.
|
||||||
|
|
||||||
|
##### `clientFoundRows`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: bool
|
||||||
|
Valid Values: true, false
|
||||||
|
Default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
`clientFoundRows=true` causes an UPDATE to return the number of matching rows instead of the number of rows changed.
|
||||||
|
|
||||||
|
##### `columnsWithAlias`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: bool
|
||||||
|
Valid Values: true, false
|
||||||
|
Default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
When `columnsWithAlias` is true, calls to `sql.Rows.Columns()` will return the table alias and the column name separated by a dot. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
SELECT u.id FROM users as u
|
||||||
|
```
|
||||||
|
|
||||||
|
will return `u.id` instead of just `id` if `columnsWithAlias=true`.
|
||||||
|
|
||||||
|
##### `interpolateParams`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: bool
|
||||||
|
Valid Values: true, false
|
||||||
|
Default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
If `interpolateParams` is true, placeholders (`?`) in calls to `db.Query()` and `db.Exec()` are interpolated into a single query string with given parameters. This reduces the number of roundtrips, since the driver has to prepare a statement, execute it with given parameters and close the statement again with `interpolateParams=false`.
|
||||||
|
|
||||||
|
*This can not be used together with the multibyte encodings BIG5, CP932, GB2312, GBK or SJIS. These are blacklisted as they may [introduce a SQL injection vulnerability](http://stackoverflow.com/a/12118602/3430118)!*
|
||||||
|
|
||||||
|
##### `loc`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: string
|
||||||
|
Valid Values: <escaped name>
|
||||||
|
Default: UTC
|
||||||
|
```
|
||||||
|
|
||||||
|
Sets the location for time.Time values (when using `parseTime=true`). *"Local"* sets the system's location. See [time.LoadLocation](http://golang.org/pkg/time/#LoadLocation) for details.
|
||||||
|
|
||||||
|
Note that this sets the location for time.Time values but does not change MySQL's [time_zone setting](https://dev.mysql.com/doc/refman/5.5/en/time-zone-support.html). For that see the [time_zone system variable](#system-variables), which can also be set as a DSN parameter.
|
||||||
|
|
||||||
|
Please keep in mind, that param values must be [url.QueryEscape](http://golang.org/pkg/net/url/#QueryEscape)'ed. Alternatively you can manually replace the `/` with `%2F`. For example `US/Pacific` would be `loc=US%2FPacific`.
|
||||||
|
|
||||||
|
##### `multiStatements`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: bool
|
||||||
|
Valid Values: true, false
|
||||||
|
Default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
Allow multiple statements in one query. While this allows batch queries, it also greatly increases the risk of SQL injections. Only the result of the first query is returned, all other results are silently discarded.
|
||||||
|
|
||||||
|
When `multiStatements` is used, `?` parameters must only be used in the first statement.
|
||||||
|
|
||||||
|
|
||||||
|
##### `parseTime`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: bool
|
||||||
|
Valid Values: true, false
|
||||||
|
Default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
`parseTime=true` changes the output type of `DATE` and `DATETIME` values to `time.Time` instead of `[]byte` / `string`
|
||||||
|
|
||||||
|
|
||||||
|
##### `readTimeout`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: decimal number
|
||||||
|
Default: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
I/O read timeout. The value must be a decimal number with an unit suffix ( *"ms"*, *"s"*, *"m"*, *"h"* ), such as *"30s"*, *"0.5m"* or *"1m30s"*.
|
||||||
|
|
||||||
|
|
||||||
|
##### `strict`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: bool
|
||||||
|
Valid Values: true, false
|
||||||
|
Default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
`strict=true` enables the strict mode in which MySQL warnings are treated as errors.
|
||||||
|
|
||||||
|
By default MySQL also treats notes as warnings. Use [`sql_notes=false`](http://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_sql_notes) to ignore notes. See the [examples](#examples) for an DSN example.
|
||||||
|
|
||||||
|
|
||||||
|
##### `timeout`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: decimal number
|
||||||
|
Default: OS default
|
||||||
|
```
|
||||||
|
|
||||||
|
*Driver* side connection timeout. The value must be a decimal number with an unit suffix ( *"ms"*, *"s"*, *"m"*, *"h"* ), such as *"30s"*, *"0.5m"* or *"1m30s"*. To set a server side timeout, use the parameter [`wait_timeout`](http://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_wait_timeout).
|
||||||
|
|
||||||
|
|
||||||
|
##### `tls`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: bool / string
|
||||||
|
Valid Values: true, false, skip-verify, <name>
|
||||||
|
Default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
`tls=true` enables TLS / SSL encrypted connection to the server. Use `skip-verify` if you want to use a self-signed or invalid certificate (server side). Use a custom value registered with [`mysql.RegisterTLSConfig`](http://godoc.org/github.com/go-sql-driver/mysql#RegisterTLSConfig).
|
||||||
|
|
||||||
|
|
||||||
|
##### `writeTimeout`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: decimal number
|
||||||
|
Default: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
I/O write timeout. The value must be a decimal number with an unit suffix ( *"ms"*, *"s"*, *"m"*, *"h"* ), such as *"30s"*, *"0.5m"* or *"1m30s"*.
|
||||||
|
|
||||||
|
|
||||||
|
##### System Variables
|
||||||
|
|
||||||
|
All other parameters are interpreted as system variables:
|
||||||
|
* `autocommit`: `"SET autocommit=<value>"`
|
||||||
|
* [`time_zone`](https://dev.mysql.com/doc/refman/5.5/en/time-zone-support.html): `"SET time_zone=<value>"`
|
||||||
|
* [`tx_isolation`](https://dev.mysql.com/doc/refman/5.5/en/server-system-variables.html#sysvar_tx_isolation): `"SET tx_isolation=<value>"`
|
||||||
|
* `param`: `"SET <param>=<value>"`
|
||||||
|
|
||||||
|
*The values must be [url.QueryEscape](http://golang.org/pkg/net/url/#QueryEscape)'ed!*
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
```
|
||||||
|
user@unix(/path/to/socket)/dbname
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
root:pw@unix(/tmp/mysql.sock)/myDatabase?loc=Local
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
user:password@tcp(localhost:5555)/dbname?tls=skip-verify&autocommit=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the [strict mode](#strict) but ignore notes:
|
||||||
|
```
|
||||||
|
user:password@/dbname?strict=true&sql_notes=false
|
||||||
|
```
|
||||||
|
|
||||||
|
TCP via IPv6:
|
||||||
|
```
|
||||||
|
user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname?timeout=90s&collation=utf8mb4_unicode_ci
|
||||||
|
```
|
||||||
|
|
||||||
|
TCP on a remote host, e.g. Amazon RDS:
|
||||||
|
```
|
||||||
|
id:password@tcp(your-amazonaws-uri.com:3306)/dbname
|
||||||
|
```
|
||||||
|
|
||||||
|
Google Cloud SQL on App Engine:
|
||||||
|
```
|
||||||
|
user@cloudsql(project-id:instance-name)/dbname
|
||||||
|
```
|
||||||
|
|
||||||
|
TCP using default port (3306) on localhost:
|
||||||
|
```
|
||||||
|
user:password@tcp/dbname?charset=utf8mb4,utf8&sys_var=esc%40ped
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the default protocol (tcp) and host (localhost:3306):
|
||||||
|
```
|
||||||
|
user:password@/dbname
|
||||||
|
```
|
||||||
|
|
||||||
|
No Database preselected:
|
||||||
|
```
|
||||||
|
user:password@/
|
||||||
|
```
|
||||||
|
|
||||||
|
### `LOAD DATA LOCAL INFILE` support
|
||||||
|
For this feature you need direct access to the package. Therefore you must change the import path (no `_`):
|
||||||
|
```go
|
||||||
|
import "github.com/go-sql-driver/mysql"
|
||||||
|
```
|
||||||
|
|
||||||
|
Files must be whitelisted by registering them with `mysql.RegisterLocalFile(filepath)` (recommended) or the Whitelist check must be deactivated by using the DSN parameter `allowAllFiles=true` ([*Might be insecure!*](http://dev.mysql.com/doc/refman/5.7/en/load-data-local.html)).
|
||||||
|
|
||||||
|
To use a `io.Reader` a handler function must be registered with `mysql.RegisterReaderHandler(name, handler)` which returns a `io.Reader` or `io.ReadCloser`. The Reader is available with the filepath `Reader::<name>` then. Choose different names for different handlers and `DeregisterReaderHandler` when you don't need it anymore.
|
||||||
|
|
||||||
|
See the [godoc of Go-MySQL-Driver](http://godoc.org/github.com/go-sql-driver/mysql "golang mysql driver documentation") for details.
|
||||||
|
|
||||||
|
|
||||||
|
### `time.Time` support
|
||||||
|
The default internal output type of MySQL `DATE` and `DATETIME` values is `[]byte` which allows you to scan the value into a `[]byte`, `string` or `sql.RawBytes` variable in your programm.
|
||||||
|
|
||||||
|
However, many want to scan MySQL `DATE` and `DATETIME` values into `time.Time` variables, which is the logical opposite in Go to `DATE` and `DATETIME` in MySQL. You can do that by changing the internal output type from `[]byte` to `time.Time` with the DSN parameter `parseTime=true`. You can set the default [`time.Time` location](http://golang.org/pkg/time/#Location) with the `loc` DSN parameter.
|
||||||
|
|
||||||
|
**Caution:** As of Go 1.1, this makes `time.Time` the only variable type you can scan `DATE` and `DATETIME` values into. This breaks for example [`sql.RawBytes` support](https://github.com/go-sql-driver/mysql/wiki/Examples#rawbytes).
|
||||||
|
|
||||||
|
Alternatively you can use the [`NullTime`](http://godoc.org/github.com/go-sql-driver/mysql#NullTime) type as the scan destination, which works with both `time.Time` and `string` / `[]byte`.
|
||||||
|
|
||||||
|
|
||||||
|
### Unicode support
|
||||||
|
Since version 1.1 Go-MySQL-Driver automatically uses the collation `utf8_general_ci` by default.
|
||||||
|
|
||||||
|
Other collations / charsets can be set using the [`collation`](#collation) DSN parameter.
|
||||||
|
|
||||||
|
Version 1.0 of the driver recommended adding `&charset=utf8` (alias for `SET NAMES utf8`) to the DSN to enable proper UTF-8 support. This is not necessary anymore. The [`collation`](#collation) parameter should be preferred to set another collation / charset than the default.
|
||||||
|
|
||||||
|
See http://dev.mysql.com/doc/refman/5.7/en/charset-unicode.html for more details on MySQL's Unicode support.
|
||||||
|
|
||||||
|
|
||||||
|
## Testing / Development
|
||||||
|
To run the driver tests you may need to adjust the configuration. See the [Testing Wiki-Page](https://github.com/go-sql-driver/mysql/wiki/Testing "Testing") for details.
|
||||||
|
|
||||||
|
Go-MySQL-Driver is not feature-complete yet. Your help is very appreciated.
|
||||||
|
If you want to contribute, you can work on an [open issue](https://github.com/go-sql-driver/mysql/issues?state=open) or review a [pull request](https://github.com/go-sql-driver/mysql/pulls).
|
||||||
|
|
||||||
|
See the [Contribution Guidelines](https://github.com/go-sql-driver/mysql/blob/master/CONTRIBUTING.md) for details.
|
||||||
|
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
## License
|
||||||
|
Go-MySQL-Driver is licensed under the [Mozilla Public License Version 2.0](https://raw.github.com/go-sql-driver/mysql/master/LICENSE)
|
||||||
|
|
||||||
|
Mozilla summarizes the license scope as follows:
|
||||||
|
> MPL: The copyleft applies to any files containing MPLed code.
|
||||||
|
|
||||||
|
|
||||||
|
That means:
|
||||||
|
* You can **use** the **unchanged** source code both in private and commercially
|
||||||
|
* When distributing, you **must publish** the source code of any **changed files** licensed under the MPL 2.0 under a) the MPL 2.0 itself or b) a compatible license (e.g. GPL 3.0 or Apache License 2.0)
|
||||||
|
* You **needn't publish** the source code of your library as long as the files licensed under the MPL 2.0 are **unchanged**
|
||||||
|
|
||||||
|
Please read the [MPL 2.0 FAQ](http://www.mozilla.org/MPL/2.0/FAQ.html) if you have further questions regarding the license.
|
||||||
|
|
||||||
|
You can read the full terms here: [LICENSE](https://raw.github.com/go-sql-driver/mysql/master/LICENSE)
|
||||||
|
|
||||||
|
![Go Gopher and MySQL Dolphin](https://raw.github.com/wiki/go-sql-driver/mysql/go-mysql-driver_m.jpg "Golang Gopher transporting the MySQL Dolphin in a wheelbarrow")
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||||
|
//
|
||||||
|
// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
// +build appengine
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"appengine/cloudsql"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterDial("cloudsql", cloudsql.Dial)
|
||||||
|
}
|
|
@ -0,0 +1,147 @@
|
||||||
|
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||||
|
//
|
||||||
|
// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultBufSize = 4096
|
||||||
|
|
||||||
|
// A buffer which is used for both reading and writing.
|
||||||
|
// This is possible since communication on each connection is synchronous.
|
||||||
|
// In other words, we can't write and read simultaneously on the same connection.
|
||||||
|
// The buffer is similar to bufio.Reader / Writer but zero-copy-ish
|
||||||
|
// Also highly optimized for this particular use case.
|
||||||
|
type buffer struct {
|
||||||
|
buf []byte
|
||||||
|
nc net.Conn
|
||||||
|
idx int
|
||||||
|
length int
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBuffer(nc net.Conn) buffer {
|
||||||
|
var b [defaultBufSize]byte
|
||||||
|
return buffer{
|
||||||
|
buf: b[:],
|
||||||
|
nc: nc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill reads into the buffer until at least _need_ bytes are in it
|
||||||
|
func (b *buffer) fill(need int) error {
|
||||||
|
n := b.length
|
||||||
|
|
||||||
|
// move existing data to the beginning
|
||||||
|
if n > 0 && b.idx > 0 {
|
||||||
|
copy(b.buf[0:n], b.buf[b.idx:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// grow buffer if necessary
|
||||||
|
// TODO: let the buffer shrink again at some point
|
||||||
|
// Maybe keep the org buf slice and swap back?
|
||||||
|
if need > len(b.buf) {
|
||||||
|
// Round up to the next multiple of the default size
|
||||||
|
newBuf := make([]byte, ((need/defaultBufSize)+1)*defaultBufSize)
|
||||||
|
copy(newBuf, b.buf)
|
||||||
|
b.buf = newBuf
|
||||||
|
}
|
||||||
|
|
||||||
|
b.idx = 0
|
||||||
|
|
||||||
|
for {
|
||||||
|
if b.timeout > 0 {
|
||||||
|
if err := b.nc.SetReadDeadline(time.Now().Add(b.timeout)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nn, err := b.nc.Read(b.buf[n:])
|
||||||
|
n += nn
|
||||||
|
|
||||||
|
switch err {
|
||||||
|
case nil:
|
||||||
|
if n < need {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.length = n
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case io.EOF:
|
||||||
|
if n >= need {
|
||||||
|
b.length = n
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns next N bytes from buffer.
|
||||||
|
// The returned slice is only guaranteed to be valid until the next read
|
||||||
|
func (b *buffer) readNext(need int) ([]byte, error) {
|
||||||
|
if b.length < need {
|
||||||
|
// refill
|
||||||
|
if err := b.fill(need); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := b.idx
|
||||||
|
b.idx += need
|
||||||
|
b.length -= need
|
||||||
|
return b.buf[offset:b.idx], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns a buffer with the requested size.
|
||||||
|
// If possible, a slice from the existing buffer is returned.
|
||||||
|
// Otherwise a bigger buffer is made.
|
||||||
|
// Only one buffer (total) can be used at a time.
|
||||||
|
func (b *buffer) takeBuffer(length int) []byte {
|
||||||
|
if b.length > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// test (cheap) general case first
|
||||||
|
if length <= defaultBufSize || length <= cap(b.buf) {
|
||||||
|
return b.buf[:length]
|
||||||
|
}
|
||||||
|
|
||||||
|
if length < maxPacketSize {
|
||||||
|
b.buf = make([]byte, length)
|
||||||
|
return b.buf
|
||||||
|
}
|
||||||
|
return make([]byte, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// shortcut which can be used if the requested buffer is guaranteed to be
|
||||||
|
// smaller than defaultBufSize
|
||||||
|
// Only one buffer (total) can be used at a time.
|
||||||
|
func (b *buffer) takeSmallBuffer(length int) []byte {
|
||||||
|
if b.length == 0 {
|
||||||
|
return b.buf[:length]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// takeCompleteBuffer returns the complete existing buffer.
|
||||||
|
// This can be used if the necessary buffer size is unknown.
|
||||||
|
// Only one buffer (total) can be used at a time.
|
||||||
|
func (b *buffer) takeCompleteBuffer() []byte {
|
||||||
|
if b.length == 0 {
|
||||||
|
return b.buf
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,250 @@
|
||||||
|
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||||
|
//
|
||||||
|
// Copyright 2014 The Go-MySQL-Driver Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
const defaultCollation = "utf8_general_ci"
|
||||||
|
|
||||||
|
// A list of available collations mapped to the internal ID.
|
||||||
|
// To update this map use the following MySQL query:
|
||||||
|
// SELECT COLLATION_NAME, ID FROM information_schema.COLLATIONS
|
||||||
|
var collations = map[string]byte{
|
||||||
|
"big5_chinese_ci": 1,
|
||||||
|
"latin2_czech_cs": 2,
|
||||||
|
"dec8_swedish_ci": 3,
|
||||||
|
"cp850_general_ci": 4,
|
||||||
|
"latin1_german1_ci": 5,
|
||||||
|
"hp8_english_ci": 6,
|
||||||
|
"koi8r_general_ci": 7,
|
||||||
|
"latin1_swedish_ci": 8,
|
||||||
|
"latin2_general_ci": 9,
|
||||||
|
"swe7_swedish_ci": 10,
|
||||||
|
"ascii_general_ci": 11,
|
||||||
|
"ujis_japanese_ci": 12,
|
||||||
|
"sjis_japanese_ci": 13,
|
||||||
|
"cp1251_bulgarian_ci": 14,
|
||||||
|
"latin1_danish_ci": 15,
|
||||||
|
"hebrew_general_ci": 16,
|
||||||
|
"tis620_thai_ci": 18,
|
||||||
|
"euckr_korean_ci": 19,
|
||||||
|
"latin7_estonian_cs": 20,
|
||||||
|
"latin2_hungarian_ci": 21,
|
||||||
|
"koi8u_general_ci": 22,
|
||||||
|
"cp1251_ukrainian_ci": 23,
|
||||||
|
"gb2312_chinese_ci": 24,
|
||||||
|
"greek_general_ci": 25,
|
||||||
|
"cp1250_general_ci": 26,
|
||||||
|
"latin2_croatian_ci": 27,
|
||||||
|
"gbk_chinese_ci": 28,
|
||||||
|
"cp1257_lithuanian_ci": 29,
|
||||||
|
"latin5_turkish_ci": 30,
|
||||||
|
"latin1_german2_ci": 31,
|
||||||
|
"armscii8_general_ci": 32,
|
||||||
|
"utf8_general_ci": 33,
|
||||||
|
"cp1250_czech_cs": 34,
|
||||||
|
"ucs2_general_ci": 35,
|
||||||
|
"cp866_general_ci": 36,
|
||||||
|
"keybcs2_general_ci": 37,
|
||||||
|
"macce_general_ci": 38,
|
||||||
|
"macroman_general_ci": 39,
|
||||||
|
"cp852_general_ci": 40,
|
||||||
|
"latin7_general_ci": 41,
|
||||||
|
"latin7_general_cs": 42,
|
||||||
|
"macce_bin": 43,
|
||||||
|
"cp1250_croatian_ci": 44,
|
||||||
|
"utf8mb4_general_ci": 45,
|
||||||
|
"utf8mb4_bin": 46,
|
||||||
|
"latin1_bin": 47,
|
||||||
|
"latin1_general_ci": 48,
|
||||||
|
"latin1_general_cs": 49,
|
||||||
|
"cp1251_bin": 50,
|
||||||
|
"cp1251_general_ci": 51,
|
||||||
|
"cp1251_general_cs": 52,
|
||||||
|
"macroman_bin": 53,
|
||||||
|
"utf16_general_ci": 54,
|
||||||
|
"utf16_bin": 55,
|
||||||
|
"utf16le_general_ci": 56,
|
||||||
|
"cp1256_general_ci": 57,
|
||||||
|
"cp1257_bin": 58,
|
||||||
|
"cp1257_general_ci": 59,
|
||||||
|
"utf32_general_ci": 60,
|
||||||
|
"utf32_bin": 61,
|
||||||
|
"utf16le_bin": 62,
|
||||||
|
"binary": 63,
|
||||||
|
"armscii8_bin": 64,
|
||||||
|
"ascii_bin": 65,
|
||||||
|
"cp1250_bin": 66,
|
||||||
|
"cp1256_bin": 67,
|
||||||
|
"cp866_bin": 68,
|
||||||
|
"dec8_bin": 69,
|
||||||
|
"greek_bin": 70,
|
||||||
|
"hebrew_bin": 71,
|
||||||
|
"hp8_bin": 72,
|
||||||
|
"keybcs2_bin": 73,
|
||||||
|
"koi8r_bin": 74,
|
||||||
|
"koi8u_bin": 75,
|
||||||
|
"latin2_bin": 77,
|
||||||
|
"latin5_bin": 78,
|
||||||
|
"latin7_bin": 79,
|
||||||
|
"cp850_bin": 80,
|
||||||
|
"cp852_bin": 81,
|
||||||
|
"swe7_bin": 82,
|
||||||
|
"utf8_bin": 83,
|
||||||
|
"big5_bin": 84,
|
||||||
|
"euckr_bin": 85,
|
||||||
|
"gb2312_bin": 86,
|
||||||
|
"gbk_bin": 87,
|
||||||
|
"sjis_bin": 88,
|
||||||
|
"tis620_bin": 89,
|
||||||
|
"ucs2_bin": 90,
|
||||||
|
"ujis_bin": 91,
|
||||||
|
"geostd8_general_ci": 92,
|
||||||
|
"geostd8_bin": 93,
|
||||||
|
"latin1_spanish_ci": 94,
|
||||||
|
"cp932_japanese_ci": 95,
|
||||||
|
"cp932_bin": 96,
|
||||||
|
"eucjpms_japanese_ci": 97,
|
||||||
|
"eucjpms_bin": 98,
|
||||||
|
"cp1250_polish_ci": 99,
|
||||||
|
"utf16_unicode_ci": 101,
|
||||||
|
"utf16_icelandic_ci": 102,
|
||||||
|
"utf16_latvian_ci": 103,
|
||||||
|
"utf16_romanian_ci": 104,
|
||||||
|
"utf16_slovenian_ci": 105,
|
||||||
|
"utf16_polish_ci": 106,
|
||||||
|
"utf16_estonian_ci": 107,
|
||||||
|
"utf16_spanish_ci": 108,
|
||||||
|
"utf16_swedish_ci": 109,
|
||||||
|
"utf16_turkish_ci": 110,
|
||||||
|
"utf16_czech_ci": 111,
|
||||||
|
"utf16_danish_ci": 112,
|
||||||
|
"utf16_lithuanian_ci": 113,
|
||||||
|
"utf16_slovak_ci": 114,
|
||||||
|
"utf16_spanish2_ci": 115,
|
||||||
|
"utf16_roman_ci": 116,
|
||||||
|
"utf16_persian_ci": 117,
|
||||||
|
"utf16_esperanto_ci": 118,
|
||||||
|
"utf16_hungarian_ci": 119,
|
||||||
|
"utf16_sinhala_ci": 120,
|
||||||
|
"utf16_german2_ci": 121,
|
||||||
|
"utf16_croatian_ci": 122,
|
||||||
|
"utf16_unicode_520_ci": 123,
|
||||||
|
"utf16_vietnamese_ci": 124,
|
||||||
|
"ucs2_unicode_ci": 128,
|
||||||
|
"ucs2_icelandic_ci": 129,
|
||||||
|
"ucs2_latvian_ci": 130,
|
||||||
|
"ucs2_romanian_ci": 131,
|
||||||
|
"ucs2_slovenian_ci": 132,
|
||||||
|
"ucs2_polish_ci": 133,
|
||||||
|
"ucs2_estonian_ci": 134,
|
||||||
|
"ucs2_spanish_ci": 135,
|
||||||
|
"ucs2_swedish_ci": 136,
|
||||||
|
"ucs2_turkish_ci": 137,
|
||||||
|
"ucs2_czech_ci": 138,
|
||||||
|
"ucs2_danish_ci": 139,
|
||||||
|
"ucs2_lithuanian_ci": 140,
|
||||||
|
"ucs2_slovak_ci": 141,
|
||||||
|
"ucs2_spanish2_ci": 142,
|
||||||
|
"ucs2_roman_ci": 143,
|
||||||
|
"ucs2_persian_ci": 144,
|
||||||
|
"ucs2_esperanto_ci": 145,
|
||||||
|
"ucs2_hungarian_ci": 146,
|
||||||
|
"ucs2_sinhala_ci": 147,
|
||||||
|
"ucs2_german2_ci": 148,
|
||||||
|
"ucs2_croatian_ci": 149,
|
||||||
|
"ucs2_unicode_520_ci": 150,
|
||||||
|
"ucs2_vietnamese_ci": 151,
|
||||||
|
"ucs2_general_mysql500_ci": 159,
|
||||||
|
"utf32_unicode_ci": 160,
|
||||||
|
"utf32_icelandic_ci": 161,
|
||||||
|
"utf32_latvian_ci": 162,
|
||||||
|
"utf32_romanian_ci": 163,
|
||||||
|
"utf32_slovenian_ci": 164,
|
||||||
|
"utf32_polish_ci": 165,
|
||||||
|
"utf32_estonian_ci": 166,
|
||||||
|
"utf32_spanish_ci": 167,
|
||||||
|
"utf32_swedish_ci": 168,
|
||||||
|
"utf32_turkish_ci": 169,
|
||||||
|
"utf32_czech_ci": 170,
|
||||||
|
"utf32_danish_ci": 171,
|
||||||
|
"utf32_lithuanian_ci": 172,
|
||||||
|
"utf32_slovak_ci": 173,
|
||||||
|
"utf32_spanish2_ci": 174,
|
||||||
|
"utf32_roman_ci": 175,
|
||||||
|
"utf32_persian_ci": 176,
|
||||||
|
"utf32_esperanto_ci": 177,
|
||||||
|
"utf32_hungarian_ci": 178,
|
||||||
|
"utf32_sinhala_ci": 179,
|
||||||
|
"utf32_german2_ci": 180,
|
||||||
|
"utf32_croatian_ci": 181,
|
||||||
|
"utf32_unicode_520_ci": 182,
|
||||||
|
"utf32_vietnamese_ci": 183,
|
||||||
|
"utf8_unicode_ci": 192,
|
||||||
|
"utf8_icelandic_ci": 193,
|
||||||
|
"utf8_latvian_ci": 194,
|
||||||
|
"utf8_romanian_ci": 195,
|
||||||
|
"utf8_slovenian_ci": 196,
|
||||||
|
"utf8_polish_ci": 197,
|
||||||
|
"utf8_estonian_ci": 198,
|
||||||
|
"utf8_spanish_ci": 199,
|
||||||
|
"utf8_swedish_ci": 200,
|
||||||
|
"utf8_turkish_ci": 201,
|
||||||
|
"utf8_czech_ci": 202,
|
||||||
|
"utf8_danish_ci": 203,
|
||||||
|
"utf8_lithuanian_ci": 204,
|
||||||
|
"utf8_slovak_ci": 205,
|
||||||
|
"utf8_spanish2_ci": 206,
|
||||||
|
"utf8_roman_ci": 207,
|
||||||
|
"utf8_persian_ci": 208,
|
||||||
|
"utf8_esperanto_ci": 209,
|
||||||
|
"utf8_hungarian_ci": 210,
|
||||||
|
"utf8_sinhala_ci": 211,
|
||||||
|
"utf8_german2_ci": 212,
|
||||||
|
"utf8_croatian_ci": 213,
|
||||||
|
"utf8_unicode_520_ci": 214,
|
||||||
|
"utf8_vietnamese_ci": 215,
|
||||||
|
"utf8_general_mysql500_ci": 223,
|
||||||
|
"utf8mb4_unicode_ci": 224,
|
||||||
|
"utf8mb4_icelandic_ci": 225,
|
||||||
|
"utf8mb4_latvian_ci": 226,
|
||||||
|
"utf8mb4_romanian_ci": 227,
|
||||||
|
"utf8mb4_slovenian_ci": 228,
|
||||||
|
"utf8mb4_polish_ci": 229,
|
||||||
|
"utf8mb4_estonian_ci": 230,
|
||||||
|
"utf8mb4_spanish_ci": 231,
|
||||||
|
"utf8mb4_swedish_ci": 232,
|
||||||
|
"utf8mb4_turkish_ci": 233,
|
||||||
|
"utf8mb4_czech_ci": 234,
|
||||||
|
"utf8mb4_danish_ci": 235,
|
||||||
|
"utf8mb4_lithuanian_ci": 236,
|
||||||
|
"utf8mb4_slovak_ci": 237,
|
||||||
|
"utf8mb4_spanish2_ci": 238,
|
||||||
|
"utf8mb4_roman_ci": 239,
|
||||||
|
"utf8mb4_persian_ci": 240,
|
||||||
|
"utf8mb4_esperanto_ci": 241,
|
||||||
|
"utf8mb4_hungarian_ci": 242,
|
||||||
|
"utf8mb4_sinhala_ci": 243,
|
||||||
|
"utf8mb4_german2_ci": 244,
|
||||||
|
"utf8mb4_croatian_ci": 245,
|
||||||
|
"utf8mb4_unicode_520_ci": 246,
|
||||||
|
"utf8mb4_vietnamese_ci": 247,
|
||||||
|
}
|
||||||
|
|
||||||
|
// A blacklist of collations which is unsafe to interpolate parameters.
|
||||||
|
// These multibyte encodings may contains 0x5c (`\`) in their trailing bytes.
|
||||||
|
var unsafeCollations = map[string]bool{
|
||||||
|
"big5_chinese_ci": true,
|
||||||
|
"sjis_japanese_ci": true,
|
||||||
|
"gbk_chinese_ci": true,
|
||||||
|
"big5_bin": true,
|
||||||
|
"gb2312_bin": true,
|
||||||
|
"gbk_bin": true,
|
||||||
|
"sjis_bin": true,
|
||||||
|
"cp932_japanese_ci": true,
|
||||||
|
"cp932_bin": true,
|
||||||
|
}
|
|
@ -0,0 +1,372 @@
|
||||||
|
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||||
|
//
|
||||||
|
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mysqlConn struct {
|
||||||
|
buf buffer
|
||||||
|
netConn net.Conn
|
||||||
|
affectedRows uint64
|
||||||
|
insertId uint64
|
||||||
|
cfg *Config
|
||||||
|
maxPacketAllowed int
|
||||||
|
maxWriteSize int
|
||||||
|
writeTimeout time.Duration
|
||||||
|
flags clientFlag
|
||||||
|
status statusFlag
|
||||||
|
sequence uint8
|
||||||
|
parseTime bool
|
||||||
|
strict bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles parameters set in DSN after the connection is established
|
||||||
|
func (mc *mysqlConn) handleParams() (err error) {
|
||||||
|
for param, val := range mc.cfg.Params {
|
||||||
|
switch param {
|
||||||
|
// Charset
|
||||||
|
case "charset":
|
||||||
|
charsets := strings.Split(val, ",")
|
||||||
|
for i := range charsets {
|
||||||
|
// ignore errors here - a charset may not exist
|
||||||
|
err = mc.exec("SET NAMES " + charsets[i])
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// System Vars
|
||||||
|
default:
|
||||||
|
err = mc.exec("SET " + param + "=" + val + "")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) Begin() (driver.Tx, error) {
|
||||||
|
if mc.netConn == nil {
|
||||||
|
errLog.Print(ErrInvalidConn)
|
||||||
|
return nil, driver.ErrBadConn
|
||||||
|
}
|
||||||
|
err := mc.exec("START TRANSACTION")
|
||||||
|
if err == nil {
|
||||||
|
return &mysqlTx{mc}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) Close() (err error) {
|
||||||
|
// Makes Close idempotent
|
||||||
|
if mc.netConn != nil {
|
||||||
|
err = mc.writeCommandPacket(comQuit)
|
||||||
|
}
|
||||||
|
|
||||||
|
mc.cleanup()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closes the network connection and unsets internal variables. Do not call this
|
||||||
|
// function after successfully authentication, call Close instead. This function
|
||||||
|
// is called before auth or on auth failure because MySQL will have already
|
||||||
|
// closed the network connection.
|
||||||
|
func (mc *mysqlConn) cleanup() {
|
||||||
|
// Makes cleanup idempotent
|
||||||
|
if mc.netConn != nil {
|
||||||
|
if err := mc.netConn.Close(); err != nil {
|
||||||
|
errLog.Print(err)
|
||||||
|
}
|
||||||
|
mc.netConn = nil
|
||||||
|
}
|
||||||
|
mc.cfg = nil
|
||||||
|
mc.buf.nc = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) Prepare(query string) (driver.Stmt, error) {
|
||||||
|
if mc.netConn == nil {
|
||||||
|
errLog.Print(ErrInvalidConn)
|
||||||
|
return nil, driver.ErrBadConn
|
||||||
|
}
|
||||||
|
// Send command
|
||||||
|
err := mc.writeCommandPacketStr(comStmtPrepare, query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt := &mysqlStmt{
|
||||||
|
mc: mc,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read Result
|
||||||
|
columnCount, err := stmt.readPrepareResultPacket()
|
||||||
|
if err == nil {
|
||||||
|
if stmt.paramCount > 0 {
|
||||||
|
if err = mc.readUntilEOF(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if columnCount > 0 {
|
||||||
|
err = mc.readUntilEOF()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stmt, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (string, error) {
|
||||||
|
buf := mc.buf.takeCompleteBuffer()
|
||||||
|
if buf == nil {
|
||||||
|
// can not take the buffer. Something must be wrong with the connection
|
||||||
|
errLog.Print(ErrBusyBuffer)
|
||||||
|
return "", driver.ErrBadConn
|
||||||
|
}
|
||||||
|
buf = buf[:0]
|
||||||
|
argPos := 0
|
||||||
|
|
||||||
|
for i := 0; i < len(query); i++ {
|
||||||
|
q := strings.IndexByte(query[i:], '?')
|
||||||
|
if q == -1 {
|
||||||
|
buf = append(buf, query[i:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
buf = append(buf, query[i:i+q]...)
|
||||||
|
i += q
|
||||||
|
|
||||||
|
arg := args[argPos]
|
||||||
|
argPos++
|
||||||
|
|
||||||
|
if arg == nil {
|
||||||
|
buf = append(buf, "NULL"...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := arg.(type) {
|
||||||
|
case int64:
|
||||||
|
buf = strconv.AppendInt(buf, v, 10)
|
||||||
|
case float64:
|
||||||
|
buf = strconv.AppendFloat(buf, v, 'g', -1, 64)
|
||||||
|
case bool:
|
||||||
|
if v {
|
||||||
|
buf = append(buf, '1')
|
||||||
|
} else {
|
||||||
|
buf = append(buf, '0')
|
||||||
|
}
|
||||||
|
case time.Time:
|
||||||
|
if v.IsZero() {
|
||||||
|
buf = append(buf, "'0000-00-00'"...)
|
||||||
|
} else {
|
||||||
|
v := v.In(mc.cfg.Loc)
|
||||||
|
v = v.Add(time.Nanosecond * 500) // To round under microsecond
|
||||||
|
year := v.Year()
|
||||||
|
year100 := year / 100
|
||||||
|
year1 := year % 100
|
||||||
|
month := v.Month()
|
||||||
|
day := v.Day()
|
||||||
|
hour := v.Hour()
|
||||||
|
minute := v.Minute()
|
||||||
|
second := v.Second()
|
||||||
|
micro := v.Nanosecond() / 1000
|
||||||
|
|
||||||
|
buf = append(buf, []byte{
|
||||||
|
'\'',
|
||||||
|
digits10[year100], digits01[year100],
|
||||||
|
digits10[year1], digits01[year1],
|
||||||
|
'-',
|
||||||
|
digits10[month], digits01[month],
|
||||||
|
'-',
|
||||||
|
digits10[day], digits01[day],
|
||||||
|
' ',
|
||||||
|
digits10[hour], digits01[hour],
|
||||||
|
':',
|
||||||
|
digits10[minute], digits01[minute],
|
||||||
|
':',
|
||||||
|
digits10[second], digits01[second],
|
||||||
|
}...)
|
||||||
|
|
||||||
|
if micro != 0 {
|
||||||
|
micro10000 := micro / 10000
|
||||||
|
micro100 := micro / 100 % 100
|
||||||
|
micro1 := micro % 100
|
||||||
|
buf = append(buf, []byte{
|
||||||
|
'.',
|
||||||
|
digits10[micro10000], digits01[micro10000],
|
||||||
|
digits10[micro100], digits01[micro100],
|
||||||
|
digits10[micro1], digits01[micro1],
|
||||||
|
}...)
|
||||||
|
}
|
||||||
|
buf = append(buf, '\'')
|
||||||
|
}
|
||||||
|
case []byte:
|
||||||
|
if v == nil {
|
||||||
|
buf = append(buf, "NULL"...)
|
||||||
|
} else {
|
||||||
|
buf = append(buf, "_binary'"...)
|
||||||
|
if mc.status&statusNoBackslashEscapes == 0 {
|
||||||
|
buf = escapeBytesBackslash(buf, v)
|
||||||
|
} else {
|
||||||
|
buf = escapeBytesQuotes(buf, v)
|
||||||
|
}
|
||||||
|
buf = append(buf, '\'')
|
||||||
|
}
|
||||||
|
case string:
|
||||||
|
buf = append(buf, '\'')
|
||||||
|
if mc.status&statusNoBackslashEscapes == 0 {
|
||||||
|
buf = escapeStringBackslash(buf, v)
|
||||||
|
} else {
|
||||||
|
buf = escapeStringQuotes(buf, v)
|
||||||
|
}
|
||||||
|
buf = append(buf, '\'')
|
||||||
|
default:
|
||||||
|
return "", driver.ErrSkip
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buf)+4 > mc.maxPacketAllowed {
|
||||||
|
return "", driver.ErrSkip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if argPos != len(args) {
|
||||||
|
return "", driver.ErrSkip
|
||||||
|
}
|
||||||
|
return string(buf), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) Exec(query string, args []driver.Value) (driver.Result, error) {
|
||||||
|
if mc.netConn == nil {
|
||||||
|
errLog.Print(ErrInvalidConn)
|
||||||
|
return nil, driver.ErrBadConn
|
||||||
|
}
|
||||||
|
if len(args) != 0 {
|
||||||
|
if !mc.cfg.InterpolateParams {
|
||||||
|
return nil, driver.ErrSkip
|
||||||
|
}
|
||||||
|
// try to interpolate the parameters to save extra roundtrips for preparing and closing a statement
|
||||||
|
prepared, err := mc.interpolateParams(query, args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
query = prepared
|
||||||
|
args = nil
|
||||||
|
}
|
||||||
|
mc.affectedRows = 0
|
||||||
|
mc.insertId = 0
|
||||||
|
|
||||||
|
err := mc.exec(query)
|
||||||
|
if err == nil {
|
||||||
|
return &mysqlResult{
|
||||||
|
affectedRows: int64(mc.affectedRows),
|
||||||
|
insertId: int64(mc.insertId),
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal function to execute commands
|
||||||
|
func (mc *mysqlConn) exec(query string) error {
|
||||||
|
// Send command
|
||||||
|
err := mc.writeCommandPacketStr(comQuery, query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read Result
|
||||||
|
resLen, err := mc.readResultSetHeaderPacket()
|
||||||
|
if err == nil && resLen > 0 {
|
||||||
|
if err = mc.readUntilEOF(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mc.readUntilEOF()
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) Query(query string, args []driver.Value) (driver.Rows, error) {
|
||||||
|
if mc.netConn == nil {
|
||||||
|
errLog.Print(ErrInvalidConn)
|
||||||
|
return nil, driver.ErrBadConn
|
||||||
|
}
|
||||||
|
if len(args) != 0 {
|
||||||
|
if !mc.cfg.InterpolateParams {
|
||||||
|
return nil, driver.ErrSkip
|
||||||
|
}
|
||||||
|
// try client-side prepare to reduce roundtrip
|
||||||
|
prepared, err := mc.interpolateParams(query, args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
query = prepared
|
||||||
|
args = nil
|
||||||
|
}
|
||||||
|
// Send command
|
||||||
|
err := mc.writeCommandPacketStr(comQuery, query)
|
||||||
|
if err == nil {
|
||||||
|
// Read Result
|
||||||
|
var resLen int
|
||||||
|
resLen, err = mc.readResultSetHeaderPacket()
|
||||||
|
if err == nil {
|
||||||
|
rows := new(textRows)
|
||||||
|
rows.mc = mc
|
||||||
|
|
||||||
|
if resLen == 0 {
|
||||||
|
// no columns, no more data
|
||||||
|
return emptyRows{}, nil
|
||||||
|
}
|
||||||
|
// Columns
|
||||||
|
rows.columns, err = mc.readColumns(resLen)
|
||||||
|
return rows, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets the value of the given MySQL System Variable
|
||||||
|
// The returned byte slice is only valid until the next read
|
||||||
|
func (mc *mysqlConn) getSystemVar(name string) ([]byte, error) {
|
||||||
|
// Send command
|
||||||
|
if err := mc.writeCommandPacketStr(comQuery, "SELECT @@"+name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read Result
|
||||||
|
resLen, err := mc.readResultSetHeaderPacket()
|
||||||
|
if err == nil {
|
||||||
|
rows := new(textRows)
|
||||||
|
rows.mc = mc
|
||||||
|
rows.columns = []mysqlField{{fieldType: fieldTypeVarChar}}
|
||||||
|
|
||||||
|
if resLen > 0 {
|
||||||
|
// Columns
|
||||||
|
if err := mc.readUntilEOF(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dest := make([]driver.Value, resLen)
|
||||||
|
if err = rows.readRow(dest); err == nil {
|
||||||
|
return dest[0].([]byte), mc.readUntilEOF()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
|
@ -0,0 +1,163 @@
|
||||||
|
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||||
|
//
|
||||||
|
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
const (
|
||||||
|
minProtocolVersion byte = 10
|
||||||
|
maxPacketSize = 1<<24 - 1
|
||||||
|
timeFormat = "2006-01-02 15:04:05.999999"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MySQL constants documentation:
|
||||||
|
// http://dev.mysql.com/doc/internals/en/client-server-protocol.html
|
||||||
|
|
||||||
|
const (
|
||||||
|
iOK byte = 0x00
|
||||||
|
iLocalInFile byte = 0xfb
|
||||||
|
iEOF byte = 0xfe
|
||||||
|
iERR byte = 0xff
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://dev.mysql.com/doc/internals/en/capability-flags.html#packet-Protocol::CapabilityFlags
|
||||||
|
type clientFlag uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
clientLongPassword clientFlag = 1 << iota
|
||||||
|
clientFoundRows
|
||||||
|
clientLongFlag
|
||||||
|
clientConnectWithDB
|
||||||
|
clientNoSchema
|
||||||
|
clientCompress
|
||||||
|
clientODBC
|
||||||
|
clientLocalFiles
|
||||||
|
clientIgnoreSpace
|
||||||
|
clientProtocol41
|
||||||
|
clientInteractive
|
||||||
|
clientSSL
|
||||||
|
clientIgnoreSIGPIPE
|
||||||
|
clientTransactions
|
||||||
|
clientReserved
|
||||||
|
clientSecureConn
|
||||||
|
clientMultiStatements
|
||||||
|
clientMultiResults
|
||||||
|
clientPSMultiResults
|
||||||
|
clientPluginAuth
|
||||||
|
clientConnectAttrs
|
||||||
|
clientPluginAuthLenEncClientData
|
||||||
|
clientCanHandleExpiredPasswords
|
||||||
|
clientSessionTrack
|
||||||
|
clientDeprecateEOF
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
comQuit byte = iota + 1
|
||||||
|
comInitDB
|
||||||
|
comQuery
|
||||||
|
comFieldList
|
||||||
|
comCreateDB
|
||||||
|
comDropDB
|
||||||
|
comRefresh
|
||||||
|
comShutdown
|
||||||
|
comStatistics
|
||||||
|
comProcessInfo
|
||||||
|
comConnect
|
||||||
|
comProcessKill
|
||||||
|
comDebug
|
||||||
|
comPing
|
||||||
|
comTime
|
||||||
|
comDelayedInsert
|
||||||
|
comChangeUser
|
||||||
|
comBinlogDump
|
||||||
|
comTableDump
|
||||||
|
comConnectOut
|
||||||
|
comRegisterSlave
|
||||||
|
comStmtPrepare
|
||||||
|
comStmtExecute
|
||||||
|
comStmtSendLongData
|
||||||
|
comStmtClose
|
||||||
|
comStmtReset
|
||||||
|
comSetOption
|
||||||
|
comStmtFetch
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://dev.mysql.com/doc/internals/en/com-query-response.html#packet-Protocol::ColumnType
|
||||||
|
const (
|
||||||
|
fieldTypeDecimal byte = iota
|
||||||
|
fieldTypeTiny
|
||||||
|
fieldTypeShort
|
||||||
|
fieldTypeLong
|
||||||
|
fieldTypeFloat
|
||||||
|
fieldTypeDouble
|
||||||
|
fieldTypeNULL
|
||||||
|
fieldTypeTimestamp
|
||||||
|
fieldTypeLongLong
|
||||||
|
fieldTypeInt24
|
||||||
|
fieldTypeDate
|
||||||
|
fieldTypeTime
|
||||||
|
fieldTypeDateTime
|
||||||
|
fieldTypeYear
|
||||||
|
fieldTypeNewDate
|
||||||
|
fieldTypeVarChar
|
||||||
|
fieldTypeBit
|
||||||
|
)
|
||||||
|
const (
|
||||||
|
fieldTypeJSON byte = iota + 0xf5
|
||||||
|
fieldTypeNewDecimal
|
||||||
|
fieldTypeEnum
|
||||||
|
fieldTypeSet
|
||||||
|
fieldTypeTinyBLOB
|
||||||
|
fieldTypeMediumBLOB
|
||||||
|
fieldTypeLongBLOB
|
||||||
|
fieldTypeBLOB
|
||||||
|
fieldTypeVarString
|
||||||
|
fieldTypeString
|
||||||
|
fieldTypeGeometry
|
||||||
|
)
|
||||||
|
|
||||||
|
type fieldFlag uint16
|
||||||
|
|
||||||
|
const (
|
||||||
|
flagNotNULL fieldFlag = 1 << iota
|
||||||
|
flagPriKey
|
||||||
|
flagUniqueKey
|
||||||
|
flagMultipleKey
|
||||||
|
flagBLOB
|
||||||
|
flagUnsigned
|
||||||
|
flagZeroFill
|
||||||
|
flagBinary
|
||||||
|
flagEnum
|
||||||
|
flagAutoIncrement
|
||||||
|
flagTimestamp
|
||||||
|
flagSet
|
||||||
|
flagUnknown1
|
||||||
|
flagUnknown2
|
||||||
|
flagUnknown3
|
||||||
|
flagUnknown4
|
||||||
|
)
|
||||||
|
|
||||||
|
// http://dev.mysql.com/doc/internals/en/status-flags.html
|
||||||
|
type statusFlag uint16
|
||||||
|
|
||||||
|
const (
|
||||||
|
statusInTrans statusFlag = 1 << iota
|
||||||
|
statusInAutocommit
|
||||||
|
statusReserved // Not in documentation
|
||||||
|
statusMoreResultsExists
|
||||||
|
statusNoGoodIndexUsed
|
||||||
|
statusNoIndexUsed
|
||||||
|
statusCursorExists
|
||||||
|
statusLastRowSent
|
||||||
|
statusDbDropped
|
||||||
|
statusNoBackslashEscapes
|
||||||
|
statusMetadataChanged
|
||||||
|
statusQueryWasSlow
|
||||||
|
statusPsOutParams
|
||||||
|
statusInTransReadonly
|
||||||
|
statusSessionStateChanged
|
||||||
|
)
|
|
@ -0,0 +1,167 @@
|
||||||
|
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
// Package mysql provides a MySQL driver for Go's database/sql package
|
||||||
|
//
|
||||||
|
// The driver should be used via the database/sql package:
|
||||||
|
//
|
||||||
|
// import "database/sql"
|
||||||
|
// import _ "github.com/go-sql-driver/mysql"
|
||||||
|
//
|
||||||
|
// db, err := sql.Open("mysql", "user:password@/dbname")
|
||||||
|
//
|
||||||
|
// See https://github.com/go-sql-driver/mysql#usage for details
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"database/sql/driver"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MySQLDriver is exported to make the driver directly accessible.
|
||||||
|
// In general the driver is used via the database/sql package.
|
||||||
|
type MySQLDriver struct{}
|
||||||
|
|
||||||
|
// DialFunc is a function which can be used to establish the network connection.
|
||||||
|
// Custom dial functions must be registered with RegisterDial
|
||||||
|
type DialFunc func(addr string) (net.Conn, error)
|
||||||
|
|
||||||
|
var dials map[string]DialFunc
|
||||||
|
|
||||||
|
// RegisterDial registers a custom dial function. It can then be used by the
|
||||||
|
// network address mynet(addr), where mynet is the registered new network.
|
||||||
|
// addr is passed as a parameter to the dial function.
|
||||||
|
func RegisterDial(net string, dial DialFunc) {
|
||||||
|
if dials == nil {
|
||||||
|
dials = make(map[string]DialFunc)
|
||||||
|
}
|
||||||
|
dials[net] = dial
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open new Connection.
|
||||||
|
// See https://github.com/go-sql-driver/mysql#dsn-data-source-name for how
|
||||||
|
// the DSN string is formated
|
||||||
|
func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// New mysqlConn
|
||||||
|
mc := &mysqlConn{
|
||||||
|
maxPacketAllowed: maxPacketSize,
|
||||||
|
maxWriteSize: maxPacketSize - 1,
|
||||||
|
}
|
||||||
|
mc.cfg, err = ParseDSN(dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mc.parseTime = mc.cfg.ParseTime
|
||||||
|
mc.strict = mc.cfg.Strict
|
||||||
|
|
||||||
|
// Connect to Server
|
||||||
|
if dial, ok := dials[mc.cfg.Net]; ok {
|
||||||
|
mc.netConn, err = dial(mc.cfg.Addr)
|
||||||
|
} else {
|
||||||
|
nd := net.Dialer{Timeout: mc.cfg.Timeout}
|
||||||
|
mc.netConn, err = nd.Dial(mc.cfg.Net, mc.cfg.Addr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable TCP Keepalives on TCP connections
|
||||||
|
if tc, ok := mc.netConn.(*net.TCPConn); ok {
|
||||||
|
if err := tc.SetKeepAlive(true); err != nil {
|
||||||
|
// Don't send COM_QUIT before handshake.
|
||||||
|
mc.netConn.Close()
|
||||||
|
mc.netConn = nil
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mc.buf = newBuffer(mc.netConn)
|
||||||
|
|
||||||
|
// Set I/O timeouts
|
||||||
|
mc.buf.timeout = mc.cfg.ReadTimeout
|
||||||
|
mc.writeTimeout = mc.cfg.WriteTimeout
|
||||||
|
|
||||||
|
// Reading Handshake Initialization Packet
|
||||||
|
cipher, err := mc.readInitPacket()
|
||||||
|
if err != nil {
|
||||||
|
mc.cleanup()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send Client Authentication Packet
|
||||||
|
if err = mc.writeAuthPacket(cipher); err != nil {
|
||||||
|
mc.cleanup()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle response to auth packet, switch methods if possible
|
||||||
|
if err = handleAuthResult(mc, cipher); err != nil {
|
||||||
|
// Authentication failed and MySQL has already closed the connection
|
||||||
|
// (https://dev.mysql.com/doc/internals/en/authentication-fails.html).
|
||||||
|
// Do not send COM_QUIT, just cleanup and return the error.
|
||||||
|
mc.cleanup()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get max allowed packet size
|
||||||
|
maxap, err := mc.getSystemVar("max_allowed_packet")
|
||||||
|
if err != nil {
|
||||||
|
mc.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mc.maxPacketAllowed = stringToInt(maxap) - 1
|
||||||
|
if mc.maxPacketAllowed < maxPacketSize {
|
||||||
|
mc.maxWriteSize = mc.maxPacketAllowed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle DSN Params
|
||||||
|
err = mc.handleParams()
|
||||||
|
if err != nil {
|
||||||
|
mc.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAuthResult(mc *mysqlConn, cipher []byte) error {
|
||||||
|
// Read Result Packet
|
||||||
|
err := mc.readResultOK()
|
||||||
|
if err == nil {
|
||||||
|
return nil // auth successful
|
||||||
|
}
|
||||||
|
|
||||||
|
if mc.cfg == nil {
|
||||||
|
return err // auth failed and retry not possible
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry auth if configured to do so.
|
||||||
|
if mc.cfg.AllowOldPasswords && err == ErrOldPassword {
|
||||||
|
// Retry with old authentication method. Note: there are edge cases
|
||||||
|
// where this should work but doesn't; this is currently "wontfix":
|
||||||
|
// https://github.com/go-sql-driver/mysql/issues/184
|
||||||
|
if err = mc.writeOldAuthPacket(cipher); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = mc.readResultOK()
|
||||||
|
} else if mc.cfg.AllowCleartextPasswords && err == ErrCleartextPassword {
|
||||||
|
// Retry with clear text password for
|
||||||
|
// http://dev.mysql.com/doc/refman/5.7/en/cleartext-authentication-plugin.html
|
||||||
|
// http://dev.mysql.com/doc/refman/5.7/en/pam-authentication-plugin.html
|
||||||
|
if err = mc.writeClearAuthPacket(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = mc.readResultOK()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
sql.Register("mysql", &MySQLDriver{})
|
||||||
|
}
|
|
@ -0,0 +1,513 @@
|
||||||
|
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||||
|
//
|
||||||
|
// Copyright 2016 The Go-MySQL-Driver Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errInvalidDSNUnescaped = errors.New("invalid DSN: did you forget to escape a param value?")
|
||||||
|
errInvalidDSNAddr = errors.New("invalid DSN: network address not terminated (missing closing brace)")
|
||||||
|
errInvalidDSNNoSlash = errors.New("invalid DSN: missing the slash separating the database name")
|
||||||
|
errInvalidDSNUnsafeCollation = errors.New("invalid DSN: interpolateParams can not be used with unsafe collations")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config is a configuration parsed from a DSN string
|
||||||
|
type Config struct {
|
||||||
|
User string // Username
|
||||||
|
Passwd string // Password (requires User)
|
||||||
|
Net string // Network type
|
||||||
|
Addr string // Network address (requires Net)
|
||||||
|
DBName string // Database name
|
||||||
|
Params map[string]string // Connection parameters
|
||||||
|
Collation string // Connection collation
|
||||||
|
Loc *time.Location // Location for time.Time values
|
||||||
|
TLSConfig string // TLS configuration name
|
||||||
|
tls *tls.Config // TLS configuration
|
||||||
|
Timeout time.Duration // Dial timeout
|
||||||
|
ReadTimeout time.Duration // I/O read timeout
|
||||||
|
WriteTimeout time.Duration // I/O write timeout
|
||||||
|
|
||||||
|
AllowAllFiles bool // Allow all files to be used with LOAD DATA LOCAL INFILE
|
||||||
|
AllowCleartextPasswords bool // Allows the cleartext client side plugin
|
||||||
|
AllowOldPasswords bool // Allows the old insecure password method
|
||||||
|
ClientFoundRows bool // Return number of matching rows instead of rows changed
|
||||||
|
ColumnsWithAlias bool // Prepend table alias to column names
|
||||||
|
InterpolateParams bool // Interpolate placeholders into query string
|
||||||
|
MultiStatements bool // Allow multiple statements in one query
|
||||||
|
ParseTime bool // Parse time values to time.Time
|
||||||
|
Strict bool // Return warnings as errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatDSN formats the given Config into a DSN string which can be passed to
|
||||||
|
// the driver.
|
||||||
|
func (cfg *Config) FormatDSN() string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
// [username[:password]@]
|
||||||
|
if len(cfg.User) > 0 {
|
||||||
|
buf.WriteString(cfg.User)
|
||||||
|
if len(cfg.Passwd) > 0 {
|
||||||
|
buf.WriteByte(':')
|
||||||
|
buf.WriteString(cfg.Passwd)
|
||||||
|
}
|
||||||
|
buf.WriteByte('@')
|
||||||
|
}
|
||||||
|
|
||||||
|
// [protocol[(address)]]
|
||||||
|
if len(cfg.Net) > 0 {
|
||||||
|
buf.WriteString(cfg.Net)
|
||||||
|
if len(cfg.Addr) > 0 {
|
||||||
|
buf.WriteByte('(')
|
||||||
|
buf.WriteString(cfg.Addr)
|
||||||
|
buf.WriteByte(')')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// /dbname
|
||||||
|
buf.WriteByte('/')
|
||||||
|
buf.WriteString(cfg.DBName)
|
||||||
|
|
||||||
|
// [?param1=value1&...¶mN=valueN]
|
||||||
|
hasParam := false
|
||||||
|
|
||||||
|
if cfg.AllowAllFiles {
|
||||||
|
hasParam = true
|
||||||
|
buf.WriteString("?allowAllFiles=true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.AllowCleartextPasswords {
|
||||||
|
if hasParam {
|
||||||
|
buf.WriteString("&allowCleartextPasswords=true")
|
||||||
|
} else {
|
||||||
|
hasParam = true
|
||||||
|
buf.WriteString("?allowCleartextPasswords=true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.AllowOldPasswords {
|
||||||
|
if hasParam {
|
||||||
|
buf.WriteString("&allowOldPasswords=true")
|
||||||
|
} else {
|
||||||
|
hasParam = true
|
||||||
|
buf.WriteString("?allowOldPasswords=true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.ClientFoundRows {
|
||||||
|
if hasParam {
|
||||||
|
buf.WriteString("&clientFoundRows=true")
|
||||||
|
} else {
|
||||||
|
hasParam = true
|
||||||
|
buf.WriteString("?clientFoundRows=true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if col := cfg.Collation; col != defaultCollation && len(col) > 0 {
|
||||||
|
if hasParam {
|
||||||
|
buf.WriteString("&collation=")
|
||||||
|
} else {
|
||||||
|
hasParam = true
|
||||||
|
buf.WriteString("?collation=")
|
||||||
|
}
|
||||||
|
buf.WriteString(col)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.ColumnsWithAlias {
|
||||||
|
if hasParam {
|
||||||
|
buf.WriteString("&columnsWithAlias=true")
|
||||||
|
} else {
|
||||||
|
hasParam = true
|
||||||
|
buf.WriteString("?columnsWithAlias=true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.InterpolateParams {
|
||||||
|
if hasParam {
|
||||||
|
buf.WriteString("&interpolateParams=true")
|
||||||
|
} else {
|
||||||
|
hasParam = true
|
||||||
|
buf.WriteString("?interpolateParams=true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Loc != time.UTC && cfg.Loc != nil {
|
||||||
|
if hasParam {
|
||||||
|
buf.WriteString("&loc=")
|
||||||
|
} else {
|
||||||
|
hasParam = true
|
||||||
|
buf.WriteString("?loc=")
|
||||||
|
}
|
||||||
|
buf.WriteString(url.QueryEscape(cfg.Loc.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.MultiStatements {
|
||||||
|
if hasParam {
|
||||||
|
buf.WriteString("&multiStatements=true")
|
||||||
|
} else {
|
||||||
|
hasParam = true
|
||||||
|
buf.WriteString("?multiStatements=true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.ParseTime {
|
||||||
|
if hasParam {
|
||||||
|
buf.WriteString("&parseTime=true")
|
||||||
|
} else {
|
||||||
|
hasParam = true
|
||||||
|
buf.WriteString("?parseTime=true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.ReadTimeout > 0 {
|
||||||
|
if hasParam {
|
||||||
|
buf.WriteString("&readTimeout=")
|
||||||
|
} else {
|
||||||
|
hasParam = true
|
||||||
|
buf.WriteString("?readTimeout=")
|
||||||
|
}
|
||||||
|
buf.WriteString(cfg.ReadTimeout.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Strict {
|
||||||
|
if hasParam {
|
||||||
|
buf.WriteString("&strict=true")
|
||||||
|
} else {
|
||||||
|
hasParam = true
|
||||||
|
buf.WriteString("?strict=true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Timeout > 0 {
|
||||||
|
if hasParam {
|
||||||
|
buf.WriteString("&timeout=")
|
||||||
|
} else {
|
||||||
|
hasParam = true
|
||||||
|
buf.WriteString("?timeout=")
|
||||||
|
}
|
||||||
|
buf.WriteString(cfg.Timeout.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.TLSConfig) > 0 {
|
||||||
|
if hasParam {
|
||||||
|
buf.WriteString("&tls=")
|
||||||
|
} else {
|
||||||
|
hasParam = true
|
||||||
|
buf.WriteString("?tls=")
|
||||||
|
}
|
||||||
|
buf.WriteString(url.QueryEscape(cfg.TLSConfig))
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.WriteTimeout > 0 {
|
||||||
|
if hasParam {
|
||||||
|
buf.WriteString("&writeTimeout=")
|
||||||
|
} else {
|
||||||
|
hasParam = true
|
||||||
|
buf.WriteString("?writeTimeout=")
|
||||||
|
}
|
||||||
|
buf.WriteString(cfg.WriteTimeout.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// other params
|
||||||
|
if cfg.Params != nil {
|
||||||
|
for param, value := range cfg.Params {
|
||||||
|
if hasParam {
|
||||||
|
buf.WriteByte('&')
|
||||||
|
} else {
|
||||||
|
hasParam = true
|
||||||
|
buf.WriteByte('?')
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteString(param)
|
||||||
|
buf.WriteByte('=')
|
||||||
|
buf.WriteString(url.QueryEscape(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseDSN parses the DSN string to a Config
|
||||||
|
func ParseDSN(dsn string) (cfg *Config, err error) {
|
||||||
|
// New config with some default values
|
||||||
|
cfg = &Config{
|
||||||
|
Loc: time.UTC,
|
||||||
|
Collation: defaultCollation,
|
||||||
|
}
|
||||||
|
|
||||||
|
// [user[:password]@][net[(addr)]]/dbname[?param1=value1¶mN=valueN]
|
||||||
|
// Find the last '/' (since the password or the net addr might contain a '/')
|
||||||
|
foundSlash := false
|
||||||
|
for i := len(dsn) - 1; i >= 0; i-- {
|
||||||
|
if dsn[i] == '/' {
|
||||||
|
foundSlash = true
|
||||||
|
var j, k int
|
||||||
|
|
||||||
|
// left part is empty if i <= 0
|
||||||
|
if i > 0 {
|
||||||
|
// [username[:password]@][protocol[(address)]]
|
||||||
|
// Find the last '@' in dsn[:i]
|
||||||
|
for j = i; j >= 0; j-- {
|
||||||
|
if dsn[j] == '@' {
|
||||||
|
// username[:password]
|
||||||
|
// Find the first ':' in dsn[:j]
|
||||||
|
for k = 0; k < j; k++ {
|
||||||
|
if dsn[k] == ':' {
|
||||||
|
cfg.Passwd = dsn[k+1 : j]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg.User = dsn[:k]
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [protocol[(address)]]
|
||||||
|
// Find the first '(' in dsn[j+1:i]
|
||||||
|
for k = j + 1; k < i; k++ {
|
||||||
|
if dsn[k] == '(' {
|
||||||
|
// dsn[i-1] must be == ')' if an address is specified
|
||||||
|
if dsn[i-1] != ')' {
|
||||||
|
if strings.ContainsRune(dsn[k+1:i], ')') {
|
||||||
|
return nil, errInvalidDSNUnescaped
|
||||||
|
}
|
||||||
|
return nil, errInvalidDSNAddr
|
||||||
|
}
|
||||||
|
cfg.Addr = dsn[k+1 : i-1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg.Net = dsn[j+1 : k]
|
||||||
|
}
|
||||||
|
|
||||||
|
// dbname[?param1=value1&...¶mN=valueN]
|
||||||
|
// Find the first '?' in dsn[i+1:]
|
||||||
|
for j = i + 1; j < len(dsn); j++ {
|
||||||
|
if dsn[j] == '?' {
|
||||||
|
if err = parseDSNParams(cfg, dsn[j+1:]); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg.DBName = dsn[i+1 : j]
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundSlash && len(dsn) > 0 {
|
||||||
|
return nil, errInvalidDSNNoSlash
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.InterpolateParams && unsafeCollations[cfg.Collation] {
|
||||||
|
return nil, errInvalidDSNUnsafeCollation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default network if empty
|
||||||
|
if cfg.Net == "" {
|
||||||
|
cfg.Net = "tcp"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default address if empty
|
||||||
|
if cfg.Addr == "" {
|
||||||
|
switch cfg.Net {
|
||||||
|
case "tcp":
|
||||||
|
cfg.Addr = "127.0.0.1:3306"
|
||||||
|
case "unix":
|
||||||
|
cfg.Addr = "/tmp/mysql.sock"
|
||||||
|
default:
|
||||||
|
return nil, errors.New("default addr for network '" + cfg.Net + "' unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseDSNParams parses the DSN "query string"
|
||||||
|
// Values must be url.QueryEscape'ed
|
||||||
|
func parseDSNParams(cfg *Config, params string) (err error) {
|
||||||
|
for _, v := range strings.Split(params, "&") {
|
||||||
|
param := strings.SplitN(v, "=", 2)
|
||||||
|
if len(param) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// cfg params
|
||||||
|
switch value := param[1]; param[0] {
|
||||||
|
|
||||||
|
// Disable INFILE whitelist / enable all files
|
||||||
|
case "allowAllFiles":
|
||||||
|
var isBool bool
|
||||||
|
cfg.AllowAllFiles, isBool = readBool(value)
|
||||||
|
if !isBool {
|
||||||
|
return errors.New("invalid bool value: " + value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use cleartext authentication mode (MySQL 5.5.10+)
|
||||||
|
case "allowCleartextPasswords":
|
||||||
|
var isBool bool
|
||||||
|
cfg.AllowCleartextPasswords, isBool = readBool(value)
|
||||||
|
if !isBool {
|
||||||
|
return errors.New("invalid bool value: " + value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use old authentication mode (pre MySQL 4.1)
|
||||||
|
case "allowOldPasswords":
|
||||||
|
var isBool bool
|
||||||
|
cfg.AllowOldPasswords, isBool = readBool(value)
|
||||||
|
if !isBool {
|
||||||
|
return errors.New("invalid bool value: " + value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch "rowsAffected" mode
|
||||||
|
case "clientFoundRows":
|
||||||
|
var isBool bool
|
||||||
|
cfg.ClientFoundRows, isBool = readBool(value)
|
||||||
|
if !isBool {
|
||||||
|
return errors.New("invalid bool value: " + value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collation
|
||||||
|
case "collation":
|
||||||
|
cfg.Collation = value
|
||||||
|
break
|
||||||
|
|
||||||
|
case "columnsWithAlias":
|
||||||
|
var isBool bool
|
||||||
|
cfg.ColumnsWithAlias, isBool = readBool(value)
|
||||||
|
if !isBool {
|
||||||
|
return errors.New("invalid bool value: " + value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compression
|
||||||
|
case "compress":
|
||||||
|
return errors.New("compression not implemented yet")
|
||||||
|
|
||||||
|
// Enable client side placeholder substitution
|
||||||
|
case "interpolateParams":
|
||||||
|
var isBool bool
|
||||||
|
cfg.InterpolateParams, isBool = readBool(value)
|
||||||
|
if !isBool {
|
||||||
|
return errors.New("invalid bool value: " + value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time Location
|
||||||
|
case "loc":
|
||||||
|
if value, err = url.QueryUnescape(value); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cfg.Loc, err = time.LoadLocation(value)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// multiple statements in one query
|
||||||
|
case "multiStatements":
|
||||||
|
var isBool bool
|
||||||
|
cfg.MultiStatements, isBool = readBool(value)
|
||||||
|
if !isBool {
|
||||||
|
return errors.New("invalid bool value: " + value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// time.Time parsing
|
||||||
|
case "parseTime":
|
||||||
|
var isBool bool
|
||||||
|
cfg.ParseTime, isBool = readBool(value)
|
||||||
|
if !isBool {
|
||||||
|
return errors.New("invalid bool value: " + value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// I/O read Timeout
|
||||||
|
case "readTimeout":
|
||||||
|
cfg.ReadTimeout, err = time.ParseDuration(value)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strict mode
|
||||||
|
case "strict":
|
||||||
|
var isBool bool
|
||||||
|
cfg.Strict, isBool = readBool(value)
|
||||||
|
if !isBool {
|
||||||
|
return errors.New("invalid bool value: " + value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial Timeout
|
||||||
|
case "timeout":
|
||||||
|
cfg.Timeout, err = time.ParseDuration(value)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLS-Encryption
|
||||||
|
case "tls":
|
||||||
|
boolValue, isBool := readBool(value)
|
||||||
|
if isBool {
|
||||||
|
if boolValue {
|
||||||
|
cfg.TLSConfig = "true"
|
||||||
|
cfg.tls = &tls.Config{}
|
||||||
|
} else {
|
||||||
|
cfg.TLSConfig = "false"
|
||||||
|
}
|
||||||
|
} else if vl := strings.ToLower(value); vl == "skip-verify" {
|
||||||
|
cfg.TLSConfig = vl
|
||||||
|
cfg.tls = &tls.Config{InsecureSkipVerify: true}
|
||||||
|
} else {
|
||||||
|
name, err := url.QueryUnescape(value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid value for TLS config name: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tlsConfig, ok := tlsConfigRegister[name]; ok {
|
||||||
|
if len(tlsConfig.ServerName) == 0 && !tlsConfig.InsecureSkipVerify {
|
||||||
|
host, _, err := net.SplitHostPort(cfg.Addr)
|
||||||
|
if err == nil {
|
||||||
|
tlsConfig.ServerName = host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.TLSConfig = name
|
||||||
|
cfg.tls = tlsConfig
|
||||||
|
} else {
|
||||||
|
return errors.New("invalid value / unknown config name: " + name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// I/O write Timeout
|
||||||
|
case "writeTimeout":
|
||||||
|
cfg.WriteTimeout, err = time.ParseDuration(value)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// lazy init
|
||||||
|
if cfg.Params == nil {
|
||||||
|
cfg.Params = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Params[param[0]], err = url.QueryUnescape(value); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||||
|
//
|
||||||
|
// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Various errors the driver might return. Can change between driver versions.
|
||||||
|
var (
|
||||||
|
ErrInvalidConn = errors.New("invalid connection")
|
||||||
|
ErrMalformPkt = errors.New("malformed packet")
|
||||||
|
ErrNoTLS = errors.New("TLS requested but server does not support TLS")
|
||||||
|
ErrOldPassword = errors.New("this user requires old password authentication. If you still want to use it, please add 'allowOldPasswords=1' to your DSN. See also https://github.com/go-sql-driver/mysql/wiki/old_passwords")
|
||||||
|
ErrCleartextPassword = errors.New("this user requires clear text authentication. If you still want to use it, please add 'allowCleartextPasswords=1' to your DSN")
|
||||||
|
ErrUnknownPlugin = errors.New("this authentication plugin is not supported")
|
||||||
|
ErrOldProtocol = errors.New("MySQL server does not support required protocol 41+")
|
||||||
|
ErrPktSync = errors.New("commands out of sync. You can't run this command now")
|
||||||
|
ErrPktSyncMul = errors.New("commands out of sync. Did you run multiple statements at once?")
|
||||||
|
ErrPktTooLarge = errors.New("packet for query is too large. Try adjusting the 'max_allowed_packet' variable on the server")
|
||||||
|
ErrBusyBuffer = errors.New("busy buffer")
|
||||||
|
)
|
||||||
|
|
||||||
|
var errLog = Logger(log.New(os.Stderr, "[mysql] ", log.Ldate|log.Ltime|log.Lshortfile))
|
||||||
|
|
||||||
|
// Logger is used to log critical error messages.
|
||||||
|
type Logger interface {
|
||||||
|
Print(v ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLogger is used to set the logger for critical errors.
|
||||||
|
// The initial logger is os.Stderr.
|
||||||
|
func SetLogger(logger Logger) error {
|
||||||
|
if logger == nil {
|
||||||
|
return errors.New("logger is nil")
|
||||||
|
}
|
||||||
|
errLog = logger
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MySQLError is an error type which represents a single MySQL error
|
||||||
|
type MySQLError struct {
|
||||||
|
Number uint16
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *MySQLError) Error() string {
|
||||||
|
return fmt.Sprintf("Error %d: %s", me.Number, me.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MySQLWarnings is an error type which represents a group of one or more MySQL
|
||||||
|
// warnings
|
||||||
|
type MySQLWarnings []MySQLWarning
|
||||||
|
|
||||||
|
func (mws MySQLWarnings) Error() string {
|
||||||
|
var msg string
|
||||||
|
for i, warning := range mws {
|
||||||
|
if i > 0 {
|
||||||
|
msg += "\r\n"
|
||||||
|
}
|
||||||
|
msg += fmt.Sprintf(
|
||||||
|
"%s %s: %s",
|
||||||
|
warning.Level,
|
||||||
|
warning.Code,
|
||||||
|
warning.Message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// MySQLWarning is an error type which represents a single MySQL warning.
|
||||||
|
// Warnings are returned in groups only. See MySQLWarnings
|
||||||
|
type MySQLWarning struct {
|
||||||
|
Level string
|
||||||
|
Code string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) getWarnings() (err error) {
|
||||||
|
rows, err := mc.Query("SHOW WARNINGS", nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var warnings = MySQLWarnings{}
|
||||||
|
var values = make([]driver.Value, 3)
|
||||||
|
|
||||||
|
for {
|
||||||
|
err = rows.Next(values)
|
||||||
|
switch err {
|
||||||
|
case nil:
|
||||||
|
warning := MySQLWarning{}
|
||||||
|
|
||||||
|
if raw, ok := values[0].([]byte); ok {
|
||||||
|
warning.Level = string(raw)
|
||||||
|
} else {
|
||||||
|
warning.Level = fmt.Sprintf("%s", values[0])
|
||||||
|
}
|
||||||
|
if raw, ok := values[1].([]byte); ok {
|
||||||
|
warning.Code = string(raw)
|
||||||
|
} else {
|
||||||
|
warning.Code = fmt.Sprintf("%s", values[1])
|
||||||
|
}
|
||||||
|
if raw, ok := values[2].([]byte); ok {
|
||||||
|
warning.Message = string(raw)
|
||||||
|
} else {
|
||||||
|
warning.Message = fmt.Sprintf("%s", values[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
warnings = append(warnings, warning)
|
||||||
|
|
||||||
|
case io.EOF:
|
||||||
|
return warnings
|
||||||
|
|
||||||
|
default:
|
||||||
|
rows.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,181 @@
|
||||||
|
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||||
|
//
|
||||||
|
// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
fileRegister map[string]bool
|
||||||
|
fileRegisterLock sync.RWMutex
|
||||||
|
readerRegister map[string]func() io.Reader
|
||||||
|
readerRegisterLock sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterLocalFile adds the given file to the file whitelist,
|
||||||
|
// so that it can be used by "LOAD DATA LOCAL INFILE <filepath>".
|
||||||
|
// Alternatively you can allow the use of all local files with
|
||||||
|
// the DSN parameter 'allowAllFiles=true'
|
||||||
|
//
|
||||||
|
// filePath := "/home/gopher/data.csv"
|
||||||
|
// mysql.RegisterLocalFile(filePath)
|
||||||
|
// err := db.Exec("LOAD DATA LOCAL INFILE '" + filePath + "' INTO TABLE foo")
|
||||||
|
// if err != nil {
|
||||||
|
// ...
|
||||||
|
//
|
||||||
|
func RegisterLocalFile(filePath string) {
|
||||||
|
fileRegisterLock.Lock()
|
||||||
|
// lazy map init
|
||||||
|
if fileRegister == nil {
|
||||||
|
fileRegister = make(map[string]bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileRegister[strings.Trim(filePath, `"`)] = true
|
||||||
|
fileRegisterLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeregisterLocalFile removes the given filepath from the whitelist.
|
||||||
|
func DeregisterLocalFile(filePath string) {
|
||||||
|
fileRegisterLock.Lock()
|
||||||
|
delete(fileRegister, strings.Trim(filePath, `"`))
|
||||||
|
fileRegisterLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterReaderHandler registers a handler function which is used
|
||||||
|
// to receive a io.Reader.
|
||||||
|
// The Reader can be used by "LOAD DATA LOCAL INFILE Reader::<name>".
|
||||||
|
// If the handler returns a io.ReadCloser Close() is called when the
|
||||||
|
// request is finished.
|
||||||
|
//
|
||||||
|
// mysql.RegisterReaderHandler("data", func() io.Reader {
|
||||||
|
// var csvReader io.Reader // Some Reader that returns CSV data
|
||||||
|
// ... // Open Reader here
|
||||||
|
// return csvReader
|
||||||
|
// })
|
||||||
|
// err := db.Exec("LOAD DATA LOCAL INFILE 'Reader::data' INTO TABLE foo")
|
||||||
|
// if err != nil {
|
||||||
|
// ...
|
||||||
|
//
|
||||||
|
func RegisterReaderHandler(name string, handler func() io.Reader) {
|
||||||
|
readerRegisterLock.Lock()
|
||||||
|
// lazy map init
|
||||||
|
if readerRegister == nil {
|
||||||
|
readerRegister = make(map[string]func() io.Reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
readerRegister[name] = handler
|
||||||
|
readerRegisterLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeregisterReaderHandler removes the ReaderHandler function with
|
||||||
|
// the given name from the registry.
|
||||||
|
func DeregisterReaderHandler(name string) {
|
||||||
|
readerRegisterLock.Lock()
|
||||||
|
delete(readerRegister, name)
|
||||||
|
readerRegisterLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func deferredClose(err *error, closer io.Closer) {
|
||||||
|
closeErr := closer.Close()
|
||||||
|
if *err == nil {
|
||||||
|
*err = closeErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) handleInFileRequest(name string) (err error) {
|
||||||
|
var rdr io.Reader
|
||||||
|
var data []byte
|
||||||
|
packetSize := 16 * 1024 // 16KB is small enough for disk readahead and large enough for TCP
|
||||||
|
if mc.maxWriteSize < packetSize {
|
||||||
|
packetSize = mc.maxWriteSize
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx := strings.Index(name, "Reader::"); idx == 0 || (idx > 0 && name[idx-1] == '/') { // io.Reader
|
||||||
|
// The server might return an an absolute path. See issue #355.
|
||||||
|
name = name[idx+8:]
|
||||||
|
|
||||||
|
readerRegisterLock.RLock()
|
||||||
|
handler, inMap := readerRegister[name]
|
||||||
|
readerRegisterLock.RUnlock()
|
||||||
|
|
||||||
|
if inMap {
|
||||||
|
rdr = handler()
|
||||||
|
if rdr != nil {
|
||||||
|
if cl, ok := rdr.(io.Closer); ok {
|
||||||
|
defer deferredClose(&err, cl)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("Reader '%s' is <nil>", name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("Reader '%s' is not registered", name)
|
||||||
|
}
|
||||||
|
} else { // File
|
||||||
|
name = strings.Trim(name, `"`)
|
||||||
|
fileRegisterLock.RLock()
|
||||||
|
fr := fileRegister[name]
|
||||||
|
fileRegisterLock.RUnlock()
|
||||||
|
if mc.cfg.AllowAllFiles || fr {
|
||||||
|
var file *os.File
|
||||||
|
var fi os.FileInfo
|
||||||
|
|
||||||
|
if file, err = os.Open(name); err == nil {
|
||||||
|
defer deferredClose(&err, file)
|
||||||
|
|
||||||
|
// get file size
|
||||||
|
if fi, err = file.Stat(); err == nil {
|
||||||
|
rdr = file
|
||||||
|
if fileSize := int(fi.Size()); fileSize < packetSize {
|
||||||
|
packetSize = fileSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("local file '%s' is not registered", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// send content packets
|
||||||
|
if err == nil {
|
||||||
|
data := make([]byte, 4+packetSize)
|
||||||
|
var n int
|
||||||
|
for err == nil {
|
||||||
|
n, err = rdr.Read(data[4:])
|
||||||
|
if n > 0 {
|
||||||
|
if ioErr := mc.writePacket(data[:4+n]); ioErr != nil {
|
||||||
|
return ioErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// send empty packet (termination)
|
||||||
|
if data == nil {
|
||||||
|
data = make([]byte, 4)
|
||||||
|
}
|
||||||
|
if ioErr := mc.writePacket(data[:4]); ioErr != nil {
|
||||||
|
return ioErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// read OK packet
|
||||||
|
if err == nil {
|
||||||
|
return mc.readResultOK()
|
||||||
|
}
|
||||||
|
|
||||||
|
mc.readPacket()
|
||||||
|
return err
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,22 @@
|
||||||
|
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||||
|
//
|
||||||
|
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
type mysqlResult struct {
|
||||||
|
affectedRows int64
|
||||||
|
insertId int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (res *mysqlResult) LastInsertId() (int64, error) {
|
||||||
|
return res.insertId, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (res *mysqlResult) RowsAffected() (int64, error) {
|
||||||
|
return res.affectedRows, nil
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||||
|
//
|
||||||
|
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mysqlField struct {
|
||||||
|
tableName string
|
||||||
|
name string
|
||||||
|
flags fieldFlag
|
||||||
|
fieldType byte
|
||||||
|
decimals byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type mysqlRows struct {
|
||||||
|
mc *mysqlConn
|
||||||
|
columns []mysqlField
|
||||||
|
}
|
||||||
|
|
||||||
|
type binaryRows struct {
|
||||||
|
mysqlRows
|
||||||
|
}
|
||||||
|
|
||||||
|
type textRows struct {
|
||||||
|
mysqlRows
|
||||||
|
}
|
||||||
|
|
||||||
|
type emptyRows struct{}
|
||||||
|
|
||||||
|
func (rows *mysqlRows) Columns() []string {
|
||||||
|
columns := make([]string, len(rows.columns))
|
||||||
|
if rows.mc != nil && rows.mc.cfg.ColumnsWithAlias {
|
||||||
|
for i := range columns {
|
||||||
|
if tableName := rows.columns[i].tableName; len(tableName) > 0 {
|
||||||
|
columns[i] = tableName + "." + rows.columns[i].name
|
||||||
|
} else {
|
||||||
|
columns[i] = rows.columns[i].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for i := range columns {
|
||||||
|
columns[i] = rows.columns[i].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return columns
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rows *mysqlRows) Close() error {
|
||||||
|
mc := rows.mc
|
||||||
|
if mc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if mc.netConn == nil {
|
||||||
|
return ErrInvalidConn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove unread packets from stream
|
||||||
|
err := mc.readUntilEOF()
|
||||||
|
if err == nil {
|
||||||
|
if err = mc.discardResults(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.mc = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rows *binaryRows) Next(dest []driver.Value) error {
|
||||||
|
if mc := rows.mc; mc != nil {
|
||||||
|
if mc.netConn == nil {
|
||||||
|
return ErrInvalidConn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch next row from stream
|
||||||
|
return rows.readRow(dest)
|
||||||
|
}
|
||||||
|
return io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rows *textRows) Next(dest []driver.Value) error {
|
||||||
|
if mc := rows.mc; mc != nil {
|
||||||
|
if mc.netConn == nil {
|
||||||
|
return ErrInvalidConn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch next row from stream
|
||||||
|
return rows.readRow(dest)
|
||||||
|
}
|
||||||
|
return io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rows emptyRows) Columns() []string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rows emptyRows) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rows emptyRows) Next(dest []driver.Value) error {
|
||||||
|
return io.EOF
|
||||||
|
}
|
|
@ -0,0 +1,150 @@
|
||||||
|
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||||
|
//
|
||||||
|
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mysqlStmt struct {
|
||||||
|
mc *mysqlConn
|
||||||
|
id uint32
|
||||||
|
paramCount int
|
||||||
|
columns []mysqlField // cached from the first query
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stmt *mysqlStmt) Close() error {
|
||||||
|
if stmt.mc == nil || stmt.mc.netConn == nil {
|
||||||
|
errLog.Print(ErrInvalidConn)
|
||||||
|
return driver.ErrBadConn
|
||||||
|
}
|
||||||
|
|
||||||
|
err := stmt.mc.writeCommandPacketUint32(comStmtClose, stmt.id)
|
||||||
|
stmt.mc = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stmt *mysqlStmt) NumInput() int {
|
||||||
|
return stmt.paramCount
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stmt *mysqlStmt) ColumnConverter(idx int) driver.ValueConverter {
|
||||||
|
return converter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stmt *mysqlStmt) Exec(args []driver.Value) (driver.Result, error) {
|
||||||
|
if stmt.mc.netConn == nil {
|
||||||
|
errLog.Print(ErrInvalidConn)
|
||||||
|
return nil, driver.ErrBadConn
|
||||||
|
}
|
||||||
|
// Send command
|
||||||
|
err := stmt.writeExecutePacket(args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mc := stmt.mc
|
||||||
|
|
||||||
|
mc.affectedRows = 0
|
||||||
|
mc.insertId = 0
|
||||||
|
|
||||||
|
// Read Result
|
||||||
|
resLen, err := mc.readResultSetHeaderPacket()
|
||||||
|
if err == nil {
|
||||||
|
if resLen > 0 {
|
||||||
|
// Columns
|
||||||
|
err = mc.readUntilEOF()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rows
|
||||||
|
err = mc.readUntilEOF()
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
return &mysqlResult{
|
||||||
|
affectedRows: int64(mc.affectedRows),
|
||||||
|
insertId: int64(mc.insertId),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stmt *mysqlStmt) Query(args []driver.Value) (driver.Rows, error) {
|
||||||
|
if stmt.mc.netConn == nil {
|
||||||
|
errLog.Print(ErrInvalidConn)
|
||||||
|
return nil, driver.ErrBadConn
|
||||||
|
}
|
||||||
|
// Send command
|
||||||
|
err := stmt.writeExecutePacket(args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mc := stmt.mc
|
||||||
|
|
||||||
|
// Read Result
|
||||||
|
resLen, err := mc.readResultSetHeaderPacket()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := new(binaryRows)
|
||||||
|
|
||||||
|
if resLen > 0 {
|
||||||
|
rows.mc = mc
|
||||||
|
// Columns
|
||||||
|
// If not cached, read them and cache them
|
||||||
|
if stmt.columns == nil {
|
||||||
|
rows.columns, err = mc.readColumns(resLen)
|
||||||
|
stmt.columns = rows.columns
|
||||||
|
} else {
|
||||||
|
rows.columns = stmt.columns
|
||||||
|
err = mc.readUntilEOF()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type converter struct{}
|
||||||
|
|
||||||
|
func (c converter) ConvertValue(v interface{}) (driver.Value, error) {
|
||||||
|
if driver.IsValue(v) {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rv := reflect.ValueOf(v)
|
||||||
|
switch rv.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
// indirect pointers
|
||||||
|
if rv.IsNil() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return c.ConvertValue(rv.Elem().Interface())
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return rv.Int(), nil
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32:
|
||||||
|
return int64(rv.Uint()), nil
|
||||||
|
case reflect.Uint64:
|
||||||
|
u64 := rv.Uint()
|
||||||
|
if u64 >= 1<<63 {
|
||||||
|
return strconv.FormatUint(u64, 10), nil
|
||||||
|
}
|
||||||
|
return int64(u64), nil
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return rv.Float(), nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unsupported type %T, a %s", v, rv.Kind())
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||||
|
//
|
||||||
|
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
type mysqlTx struct {
|
||||||
|
mc *mysqlConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tx *mysqlTx) Commit() (err error) {
|
||||||
|
if tx.mc == nil || tx.mc.netConn == nil {
|
||||||
|
return ErrInvalidConn
|
||||||
|
}
|
||||||
|
err = tx.mc.exec("COMMIT")
|
||||||
|
tx.mc = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tx *mysqlTx) Rollback() (err error) {
|
||||||
|
if tx.mc == nil || tx.mc.netConn == nil {
|
||||||
|
return ErrInvalidConn
|
||||||
|
}
|
||||||
|
err = tx.mc.exec("ROLLBACK")
|
||||||
|
tx.mc = nil
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,740 @@
|
||||||
|
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||||
|
//
|
||||||
|
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/tls"
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
tlsConfigRegister map[string]*tls.Config // Register for custom tls.Configs
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterTLSConfig registers a custom tls.Config to be used with sql.Open.
|
||||||
|
// Use the key as a value in the DSN where tls=value.
|
||||||
|
//
|
||||||
|
// rootCertPool := x509.NewCertPool()
|
||||||
|
// pem, err := ioutil.ReadFile("/path/ca-cert.pem")
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
|
// if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
|
||||||
|
// log.Fatal("Failed to append PEM.")
|
||||||
|
// }
|
||||||
|
// clientCert := make([]tls.Certificate, 0, 1)
|
||||||
|
// certs, err := tls.LoadX509KeyPair("/path/client-cert.pem", "/path/client-key.pem")
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
|
// clientCert = append(clientCert, certs)
|
||||||
|
// mysql.RegisterTLSConfig("custom", &tls.Config{
|
||||||
|
// RootCAs: rootCertPool,
|
||||||
|
// Certificates: clientCert,
|
||||||
|
// })
|
||||||
|
// db, err := sql.Open("mysql", "user@tcp(localhost:3306)/test?tls=custom")
|
||||||
|
//
|
||||||
|
func RegisterTLSConfig(key string, config *tls.Config) error {
|
||||||
|
if _, isBool := readBool(key); isBool || strings.ToLower(key) == "skip-verify" {
|
||||||
|
return fmt.Errorf("key '%s' is reserved", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tlsConfigRegister == nil {
|
||||||
|
tlsConfigRegister = make(map[string]*tls.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConfigRegister[key] = config
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeregisterTLSConfig removes the tls.Config associated with key.
|
||||||
|
func DeregisterTLSConfig(key string) {
|
||||||
|
if tlsConfigRegister != nil {
|
||||||
|
delete(tlsConfigRegister, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the bool value of the input.
|
||||||
|
// The 2nd return value indicates if the input was a valid bool value
|
||||||
|
func readBool(input string) (value bool, valid bool) {
|
||||||
|
switch input {
|
||||||
|
case "1", "true", "TRUE", "True":
|
||||||
|
return true, true
|
||||||
|
case "0", "false", "FALSE", "False":
|
||||||
|
return false, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not a valid bool value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************************************************
|
||||||
|
* Authentication *
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
// Encrypt password using 4.1+ method
|
||||||
|
func scramblePassword(scramble, password []byte) []byte {
|
||||||
|
if len(password) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// stage1Hash = SHA1(password)
|
||||||
|
crypt := sha1.New()
|
||||||
|
crypt.Write(password)
|
||||||
|
stage1 := crypt.Sum(nil)
|
||||||
|
|
||||||
|
// scrambleHash = SHA1(scramble + SHA1(stage1Hash))
|
||||||
|
// inner Hash
|
||||||
|
crypt.Reset()
|
||||||
|
crypt.Write(stage1)
|
||||||
|
hash := crypt.Sum(nil)
|
||||||
|
|
||||||
|
// outer Hash
|
||||||
|
crypt.Reset()
|
||||||
|
crypt.Write(scramble)
|
||||||
|
crypt.Write(hash)
|
||||||
|
scramble = crypt.Sum(nil)
|
||||||
|
|
||||||
|
// token = scrambleHash XOR stage1Hash
|
||||||
|
for i := range scramble {
|
||||||
|
scramble[i] ^= stage1[i]
|
||||||
|
}
|
||||||
|
return scramble
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt password using pre 4.1 (old password) method
|
||||||
|
// https://github.com/atcurtis/mariadb/blob/master/mysys/my_rnd.c
|
||||||
|
type myRnd struct {
|
||||||
|
seed1, seed2 uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
const myRndMaxVal = 0x3FFFFFFF
|
||||||
|
|
||||||
|
// Pseudo random number generator
|
||||||
|
func newMyRnd(seed1, seed2 uint32) *myRnd {
|
||||||
|
return &myRnd{
|
||||||
|
seed1: seed1 % myRndMaxVal,
|
||||||
|
seed2: seed2 % myRndMaxVal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tested to be equivalent to MariaDB's floating point variant
|
||||||
|
// http://play.golang.org/p/QHvhd4qved
|
||||||
|
// http://play.golang.org/p/RG0q4ElWDx
|
||||||
|
func (r *myRnd) NextByte() byte {
|
||||||
|
r.seed1 = (r.seed1*3 + r.seed2) % myRndMaxVal
|
||||||
|
r.seed2 = (r.seed1 + r.seed2 + 33) % myRndMaxVal
|
||||||
|
|
||||||
|
return byte(uint64(r.seed1) * 31 / myRndMaxVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate binary hash from byte string using insecure pre 4.1 method
|
||||||
|
func pwHash(password []byte) (result [2]uint32) {
|
||||||
|
var add uint32 = 7
|
||||||
|
var tmp uint32
|
||||||
|
|
||||||
|
result[0] = 1345345333
|
||||||
|
result[1] = 0x12345671
|
||||||
|
|
||||||
|
for _, c := range password {
|
||||||
|
// skip spaces and tabs in password
|
||||||
|
if c == ' ' || c == '\t' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp = uint32(c)
|
||||||
|
result[0] ^= (((result[0] & 63) + add) * tmp) + (result[0] << 8)
|
||||||
|
result[1] += (result[1] << 8) ^ result[0]
|
||||||
|
add += tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove sign bit (1<<31)-1)
|
||||||
|
result[0] &= 0x7FFFFFFF
|
||||||
|
result[1] &= 0x7FFFFFFF
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt password using insecure pre 4.1 method
|
||||||
|
func scrambleOldPassword(scramble, password []byte) []byte {
|
||||||
|
if len(password) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
scramble = scramble[:8]
|
||||||
|
|
||||||
|
hashPw := pwHash(password)
|
||||||
|
hashSc := pwHash(scramble)
|
||||||
|
|
||||||
|
r := newMyRnd(hashPw[0]^hashSc[0], hashPw[1]^hashSc[1])
|
||||||
|
|
||||||
|
var out [8]byte
|
||||||
|
for i := range out {
|
||||||
|
out[i] = r.NextByte() + 64
|
||||||
|
}
|
||||||
|
|
||||||
|
mask := r.NextByte()
|
||||||
|
for i := range out {
|
||||||
|
out[i] ^= mask
|
||||||
|
}
|
||||||
|
|
||||||
|
return out[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************************************************
|
||||||
|
* Time related utils *
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
// NullTime represents a time.Time that may be NULL.
|
||||||
|
// NullTime implements the Scanner interface so
|
||||||
|
// it can be used as a scan destination:
|
||||||
|
//
|
||||||
|
// var nt NullTime
|
||||||
|
// err := db.QueryRow("SELECT time FROM foo WHERE id=?", id).Scan(&nt)
|
||||||
|
// ...
|
||||||
|
// if nt.Valid {
|
||||||
|
// // use nt.Time
|
||||||
|
// } else {
|
||||||
|
// // NULL value
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// This NullTime implementation is not driver-specific
|
||||||
|
type NullTime struct {
|
||||||
|
Time time.Time
|
||||||
|
Valid bool // Valid is true if Time is not NULL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan implements the Scanner interface.
|
||||||
|
// The value type must be time.Time or string / []byte (formatted time-string),
|
||||||
|
// otherwise Scan fails.
|
||||||
|
func (nt *NullTime) Scan(value interface{}) (err error) {
|
||||||
|
if value == nil {
|
||||||
|
nt.Time, nt.Valid = time.Time{}, false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := value.(type) {
|
||||||
|
case time.Time:
|
||||||
|
nt.Time, nt.Valid = v, true
|
||||||
|
return
|
||||||
|
case []byte:
|
||||||
|
nt.Time, err = parseDateTime(string(v), time.UTC)
|
||||||
|
nt.Valid = (err == nil)
|
||||||
|
return
|
||||||
|
case string:
|
||||||
|
nt.Time, err = parseDateTime(v, time.UTC)
|
||||||
|
nt.Valid = (err == nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nt.Valid = false
|
||||||
|
return fmt.Errorf("Can't convert %T to time.Time", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements the driver Valuer interface.
|
||||||
|
func (nt NullTime) Value() (driver.Value, error) {
|
||||||
|
if !nt.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nt.Time, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDateTime(str string, loc *time.Location) (t time.Time, err error) {
|
||||||
|
base := "0000-00-00 00:00:00.0000000"
|
||||||
|
switch len(str) {
|
||||||
|
case 10, 19, 21, 22, 23, 24, 25, 26: // up to "YYYY-MM-DD HH:MM:SS.MMMMMM"
|
||||||
|
if str == base[:len(str)] {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t, err = time.Parse(timeFormat[:len(str)], str)
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("invalid time string: %s", str)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust location
|
||||||
|
if err == nil && loc != time.UTC {
|
||||||
|
y, mo, d := t.Date()
|
||||||
|
h, mi, s := t.Clock()
|
||||||
|
t, err = time.Date(y, mo, d, h, mi, s, t.Nanosecond(), loc), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseBinaryDateTime(num uint64, data []byte, loc *time.Location) (driver.Value, error) {
|
||||||
|
switch num {
|
||||||
|
case 0:
|
||||||
|
return time.Time{}, nil
|
||||||
|
case 4:
|
||||||
|
return time.Date(
|
||||||
|
int(binary.LittleEndian.Uint16(data[:2])), // year
|
||||||
|
time.Month(data[2]), // month
|
||||||
|
int(data[3]), // day
|
||||||
|
0, 0, 0, 0,
|
||||||
|
loc,
|
||||||
|
), nil
|
||||||
|
case 7:
|
||||||
|
return time.Date(
|
||||||
|
int(binary.LittleEndian.Uint16(data[:2])), // year
|
||||||
|
time.Month(data[2]), // month
|
||||||
|
int(data[3]), // day
|
||||||
|
int(data[4]), // hour
|
||||||
|
int(data[5]), // minutes
|
||||||
|
int(data[6]), // seconds
|
||||||
|
0,
|
||||||
|
loc,
|
||||||
|
), nil
|
||||||
|
case 11:
|
||||||
|
return time.Date(
|
||||||
|
int(binary.LittleEndian.Uint16(data[:2])), // year
|
||||||
|
time.Month(data[2]), // month
|
||||||
|
int(data[3]), // day
|
||||||
|
int(data[4]), // hour
|
||||||
|
int(data[5]), // minutes
|
||||||
|
int(data[6]), // seconds
|
||||||
|
int(binary.LittleEndian.Uint32(data[7:11]))*1000, // nanoseconds
|
||||||
|
loc,
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid DATETIME packet length %d", num)
|
||||||
|
}
|
||||||
|
|
||||||
|
// zeroDateTime is used in formatBinaryDateTime to avoid an allocation
|
||||||
|
// if the DATE or DATETIME has the zero value.
|
||||||
|
// It must never be changed.
|
||||||
|
// The current behavior depends on database/sql copying the result.
|
||||||
|
var zeroDateTime = []byte("0000-00-00 00:00:00.000000")
|
||||||
|
|
||||||
|
const digits01 = "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
|
||||||
|
const digits10 = "0000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999"
|
||||||
|
|
||||||
|
func formatBinaryDateTime(src []byte, length uint8, justTime bool) (driver.Value, error) {
|
||||||
|
// length expects the deterministic length of the zero value,
|
||||||
|
// negative time and 100+ hours are automatically added if needed
|
||||||
|
if len(src) == 0 {
|
||||||
|
if justTime {
|
||||||
|
return zeroDateTime[11 : 11+length], nil
|
||||||
|
}
|
||||||
|
return zeroDateTime[:length], nil
|
||||||
|
}
|
||||||
|
var dst []byte // return value
|
||||||
|
var pt, p1, p2, p3 byte // current digit pair
|
||||||
|
var zOffs byte // offset of value in zeroDateTime
|
||||||
|
if justTime {
|
||||||
|
switch length {
|
||||||
|
case
|
||||||
|
8, // time (can be up to 10 when negative and 100+ hours)
|
||||||
|
10, 11, 12, 13, 14, 15: // time with fractional seconds
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("illegal TIME length %d", length)
|
||||||
|
}
|
||||||
|
switch len(src) {
|
||||||
|
case 8, 12:
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid TIME packet length %d", len(src))
|
||||||
|
}
|
||||||
|
// +2 to enable negative time and 100+ hours
|
||||||
|
dst = make([]byte, 0, length+2)
|
||||||
|
if src[0] == 1 {
|
||||||
|
dst = append(dst, '-')
|
||||||
|
}
|
||||||
|
if src[1] != 0 {
|
||||||
|
hour := uint16(src[1])*24 + uint16(src[5])
|
||||||
|
pt = byte(hour / 100)
|
||||||
|
p1 = byte(hour - 100*uint16(pt))
|
||||||
|
dst = append(dst, digits01[pt])
|
||||||
|
} else {
|
||||||
|
p1 = src[5]
|
||||||
|
}
|
||||||
|
zOffs = 11
|
||||||
|
src = src[6:]
|
||||||
|
} else {
|
||||||
|
switch length {
|
||||||
|
case 10, 19, 21, 22, 23, 24, 25, 26:
|
||||||
|
default:
|
||||||
|
t := "DATE"
|
||||||
|
if length > 10 {
|
||||||
|
t += "TIME"
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("illegal %s length %d", t, length)
|
||||||
|
}
|
||||||
|
switch len(src) {
|
||||||
|
case 4, 7, 11:
|
||||||
|
default:
|
||||||
|
t := "DATE"
|
||||||
|
if length > 10 {
|
||||||
|
t += "TIME"
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("illegal %s packet length %d", t, len(src))
|
||||||
|
}
|
||||||
|
dst = make([]byte, 0, length)
|
||||||
|
// start with the date
|
||||||
|
year := binary.LittleEndian.Uint16(src[:2])
|
||||||
|
pt = byte(year / 100)
|
||||||
|
p1 = byte(year - 100*uint16(pt))
|
||||||
|
p2, p3 = src[2], src[3]
|
||||||
|
dst = append(dst,
|
||||||
|
digits10[pt], digits01[pt],
|
||||||
|
digits10[p1], digits01[p1], '-',
|
||||||
|
digits10[p2], digits01[p2], '-',
|
||||||
|
digits10[p3], digits01[p3],
|
||||||
|
)
|
||||||
|
if length == 10 {
|
||||||
|
return dst, nil
|
||||||
|
}
|
||||||
|
if len(src) == 4 {
|
||||||
|
return append(dst, zeroDateTime[10:length]...), nil
|
||||||
|
}
|
||||||
|
dst = append(dst, ' ')
|
||||||
|
p1 = src[4] // hour
|
||||||
|
src = src[5:]
|
||||||
|
}
|
||||||
|
// p1 is 2-digit hour, src is after hour
|
||||||
|
p2, p3 = src[0], src[1]
|
||||||
|
dst = append(dst,
|
||||||
|
digits10[p1], digits01[p1], ':',
|
||||||
|
digits10[p2], digits01[p2], ':',
|
||||||
|
digits10[p3], digits01[p3],
|
||||||
|
)
|
||||||
|
if length <= byte(len(dst)) {
|
||||||
|
return dst, nil
|
||||||
|
}
|
||||||
|
src = src[2:]
|
||||||
|
if len(src) == 0 {
|
||||||
|
return append(dst, zeroDateTime[19:zOffs+length]...), nil
|
||||||
|
}
|
||||||
|
microsecs := binary.LittleEndian.Uint32(src[:4])
|
||||||
|
p1 = byte(microsecs / 10000)
|
||||||
|
microsecs -= 10000 * uint32(p1)
|
||||||
|
p2 = byte(microsecs / 100)
|
||||||
|
microsecs -= 100 * uint32(p2)
|
||||||
|
p3 = byte(microsecs)
|
||||||
|
switch decimals := zOffs + length - 20; decimals {
|
||||||
|
default:
|
||||||
|
return append(dst, '.',
|
||||||
|
digits10[p1], digits01[p1],
|
||||||
|
digits10[p2], digits01[p2],
|
||||||
|
digits10[p3], digits01[p3],
|
||||||
|
), nil
|
||||||
|
case 1:
|
||||||
|
return append(dst, '.',
|
||||||
|
digits10[p1],
|
||||||
|
), nil
|
||||||
|
case 2:
|
||||||
|
return append(dst, '.',
|
||||||
|
digits10[p1], digits01[p1],
|
||||||
|
), nil
|
||||||
|
case 3:
|
||||||
|
return append(dst, '.',
|
||||||
|
digits10[p1], digits01[p1],
|
||||||
|
digits10[p2],
|
||||||
|
), nil
|
||||||
|
case 4:
|
||||||
|
return append(dst, '.',
|
||||||
|
digits10[p1], digits01[p1],
|
||||||
|
digits10[p2], digits01[p2],
|
||||||
|
), nil
|
||||||
|
case 5:
|
||||||
|
return append(dst, '.',
|
||||||
|
digits10[p1], digits01[p1],
|
||||||
|
digits10[p2], digits01[p2],
|
||||||
|
digits10[p3],
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************************************************
|
||||||
|
* Convert from and to bytes *
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
func uint64ToBytes(n uint64) []byte {
|
||||||
|
return []byte{
|
||||||
|
byte(n),
|
||||||
|
byte(n >> 8),
|
||||||
|
byte(n >> 16),
|
||||||
|
byte(n >> 24),
|
||||||
|
byte(n >> 32),
|
||||||
|
byte(n >> 40),
|
||||||
|
byte(n >> 48),
|
||||||
|
byte(n >> 56),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func uint64ToString(n uint64) []byte {
|
||||||
|
var a [20]byte
|
||||||
|
i := 20
|
||||||
|
|
||||||
|
// U+0030 = 0
|
||||||
|
// ...
|
||||||
|
// U+0039 = 9
|
||||||
|
|
||||||
|
var q uint64
|
||||||
|
for n >= 10 {
|
||||||
|
i--
|
||||||
|
q = n / 10
|
||||||
|
a[i] = uint8(n-q*10) + 0x30
|
||||||
|
n = q
|
||||||
|
}
|
||||||
|
|
||||||
|
i--
|
||||||
|
a[i] = uint8(n) + 0x30
|
||||||
|
|
||||||
|
return a[i:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// treats string value as unsigned integer representation
|
||||||
|
func stringToInt(b []byte) int {
|
||||||
|
val := 0
|
||||||
|
for i := range b {
|
||||||
|
val *= 10
|
||||||
|
val += int(b[i] - 0x30)
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the string read as a bytes slice, wheter the value is NULL,
|
||||||
|
// the number of bytes read and an error, in case the string is longer than
|
||||||
|
// the input slice
|
||||||
|
func readLengthEncodedString(b []byte) ([]byte, bool, int, error) {
|
||||||
|
// Get length
|
||||||
|
num, isNull, n := readLengthEncodedInteger(b)
|
||||||
|
if num < 1 {
|
||||||
|
return b[n:n], isNull, n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
n += int(num)
|
||||||
|
|
||||||
|
// Check data length
|
||||||
|
if len(b) >= n {
|
||||||
|
return b[n-int(num) : n], false, n, nil
|
||||||
|
}
|
||||||
|
return nil, false, n, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the number of bytes skipped and an error, in case the string is
|
||||||
|
// longer than the input slice
|
||||||
|
func skipLengthEncodedString(b []byte) (int, error) {
|
||||||
|
// Get length
|
||||||
|
num, _, n := readLengthEncodedInteger(b)
|
||||||
|
if num < 1 {
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
n += int(num)
|
||||||
|
|
||||||
|
// Check data length
|
||||||
|
if len(b) >= n {
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
return n, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the number read, whether the value is NULL and the number of bytes read
|
||||||
|
func readLengthEncodedInteger(b []byte) (uint64, bool, int) {
|
||||||
|
// See issue #349
|
||||||
|
if len(b) == 0 {
|
||||||
|
return 0, true, 1
|
||||||
|
}
|
||||||
|
switch b[0] {
|
||||||
|
|
||||||
|
// 251: NULL
|
||||||
|
case 0xfb:
|
||||||
|
return 0, true, 1
|
||||||
|
|
||||||
|
// 252: value of following 2
|
||||||
|
case 0xfc:
|
||||||
|
return uint64(b[1]) | uint64(b[2])<<8, false, 3
|
||||||
|
|
||||||
|
// 253: value of following 3
|
||||||
|
case 0xfd:
|
||||||
|
return uint64(b[1]) | uint64(b[2])<<8 | uint64(b[3])<<16, false, 4
|
||||||
|
|
||||||
|
// 254: value of following 8
|
||||||
|
case 0xfe:
|
||||||
|
return uint64(b[1]) | uint64(b[2])<<8 | uint64(b[3])<<16 |
|
||||||
|
uint64(b[4])<<24 | uint64(b[5])<<32 | uint64(b[6])<<40 |
|
||||||
|
uint64(b[7])<<48 | uint64(b[8])<<56,
|
||||||
|
false, 9
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0-250: value of first byte
|
||||||
|
return uint64(b[0]), false, 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodes a uint64 value and appends it to the given bytes slice
|
||||||
|
func appendLengthEncodedInteger(b []byte, n uint64) []byte {
|
||||||
|
switch {
|
||||||
|
case n <= 250:
|
||||||
|
return append(b, byte(n))
|
||||||
|
|
||||||
|
case n <= 0xffff:
|
||||||
|
return append(b, 0xfc, byte(n), byte(n>>8))
|
||||||
|
|
||||||
|
case n <= 0xffffff:
|
||||||
|
return append(b, 0xfd, byte(n), byte(n>>8), byte(n>>16))
|
||||||
|
}
|
||||||
|
return append(b, 0xfe, byte(n), byte(n>>8), byte(n>>16), byte(n>>24),
|
||||||
|
byte(n>>32), byte(n>>40), byte(n>>48), byte(n>>56))
|
||||||
|
}
|
||||||
|
|
||||||
|
// reserveBuffer checks cap(buf) and expand buffer to len(buf) + appendSize.
|
||||||
|
// If cap(buf) is not enough, reallocate new buffer.
|
||||||
|
func reserveBuffer(buf []byte, appendSize int) []byte {
|
||||||
|
newSize := len(buf) + appendSize
|
||||||
|
if cap(buf) < newSize {
|
||||||
|
// Grow buffer exponentially
|
||||||
|
newBuf := make([]byte, len(buf)*2+appendSize)
|
||||||
|
copy(newBuf, buf)
|
||||||
|
buf = newBuf
|
||||||
|
}
|
||||||
|
return buf[:newSize]
|
||||||
|
}
|
||||||
|
|
||||||
|
// escapeBytesBackslash escapes []byte with backslashes (\)
|
||||||
|
// This escapes the contents of a string (provided as []byte) by adding backslashes before special
|
||||||
|
// characters, and turning others into specific escape sequences, such as
|
||||||
|
// turning newlines into \n and null bytes into \0.
|
||||||
|
// https://github.com/mysql/mysql-server/blob/mysql-5.7.5/mysys/charset.c#L823-L932
|
||||||
|
func escapeBytesBackslash(buf, v []byte) []byte {
|
||||||
|
pos := len(buf)
|
||||||
|
buf = reserveBuffer(buf, len(v)*2)
|
||||||
|
|
||||||
|
for _, c := range v {
|
||||||
|
switch c {
|
||||||
|
case '\x00':
|
||||||
|
buf[pos] = '\\'
|
||||||
|
buf[pos+1] = '0'
|
||||||
|
pos += 2
|
||||||
|
case '\n':
|
||||||
|
buf[pos] = '\\'
|
||||||
|
buf[pos+1] = 'n'
|
||||||
|
pos += 2
|
||||||
|
case '\r':
|
||||||
|
buf[pos] = '\\'
|
||||||
|
buf[pos+1] = 'r'
|
||||||
|
pos += 2
|
||||||
|
case '\x1a':
|
||||||
|
buf[pos] = '\\'
|
||||||
|
buf[pos+1] = 'Z'
|
||||||
|
pos += 2
|
||||||
|
case '\'':
|
||||||
|
buf[pos] = '\\'
|
||||||
|
buf[pos+1] = '\''
|
||||||
|
pos += 2
|
||||||
|
case '"':
|
||||||
|
buf[pos] = '\\'
|
||||||
|
buf[pos+1] = '"'
|
||||||
|
pos += 2
|
||||||
|
case '\\':
|
||||||
|
buf[pos] = '\\'
|
||||||
|
buf[pos+1] = '\\'
|
||||||
|
pos += 2
|
||||||
|
default:
|
||||||
|
buf[pos] = c
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf[:pos]
|
||||||
|
}
|
||||||
|
|
||||||
|
// escapeStringBackslash is similar to escapeBytesBackslash but for string.
|
||||||
|
func escapeStringBackslash(buf []byte, v string) []byte {
|
||||||
|
pos := len(buf)
|
||||||
|
buf = reserveBuffer(buf, len(v)*2)
|
||||||
|
|
||||||
|
for i := 0; i < len(v); i++ {
|
||||||
|
c := v[i]
|
||||||
|
switch c {
|
||||||
|
case '\x00':
|
||||||
|
buf[pos] = '\\'
|
||||||
|
buf[pos+1] = '0'
|
||||||
|
pos += 2
|
||||||
|
case '\n':
|
||||||
|
buf[pos] = '\\'
|
||||||
|
buf[pos+1] = 'n'
|
||||||
|
pos += 2
|
||||||
|
case '\r':
|
||||||
|
buf[pos] = '\\'
|
||||||
|
buf[pos+1] = 'r'
|
||||||
|
pos += 2
|
||||||
|
case '\x1a':
|
||||||
|
buf[pos] = '\\'
|
||||||
|
buf[pos+1] = 'Z'
|
||||||
|
pos += 2
|
||||||
|
case '\'':
|
||||||
|
buf[pos] = '\\'
|
||||||
|
buf[pos+1] = '\''
|
||||||
|
pos += 2
|
||||||
|
case '"':
|
||||||
|
buf[pos] = '\\'
|
||||||
|
buf[pos+1] = '"'
|
||||||
|
pos += 2
|
||||||
|
case '\\':
|
||||||
|
buf[pos] = '\\'
|
||||||
|
buf[pos+1] = '\\'
|
||||||
|
pos += 2
|
||||||
|
default:
|
||||||
|
buf[pos] = c
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf[:pos]
|
||||||
|
}
|
||||||
|
|
||||||
|
// escapeBytesQuotes escapes apostrophes in []byte by doubling them up.
|
||||||
|
// This escapes the contents of a string by doubling up any apostrophes that
|
||||||
|
// it contains. This is used when the NO_BACKSLASH_ESCAPES SQL_MODE is in
|
||||||
|
// effect on the server.
|
||||||
|
// https://github.com/mysql/mysql-server/blob/mysql-5.7.5/mysys/charset.c#L963-L1038
|
||||||
|
func escapeBytesQuotes(buf, v []byte) []byte {
|
||||||
|
pos := len(buf)
|
||||||
|
buf = reserveBuffer(buf, len(v)*2)
|
||||||
|
|
||||||
|
for _, c := range v {
|
||||||
|
if c == '\'' {
|
||||||
|
buf[pos] = '\''
|
||||||
|
buf[pos+1] = '\''
|
||||||
|
pos += 2
|
||||||
|
} else {
|
||||||
|
buf[pos] = c
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf[:pos]
|
||||||
|
}
|
||||||
|
|
||||||
|
// escapeStringQuotes is similar to escapeBytesQuotes but for string.
|
||||||
|
func escapeStringQuotes(buf []byte, v string) []byte {
|
||||||
|
pos := len(buf)
|
||||||
|
buf = reserveBuffer(buf, len(v)*2)
|
||||||
|
|
||||||
|
for i := 0; i < len(v); i++ {
|
||||||
|
c := v[i]
|
||||||
|
if c == '\'' {
|
||||||
|
buf[pos] = '\''
|
||||||
|
buf[pos+1] = '\''
|
||||||
|
pos += 2
|
||||||
|
} else {
|
||||||
|
buf[pos] = c
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf[:pos]
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
# This is the official list of Freetype-Go authors for copyright purposes.
|
||||||
|
# This file is distinct from the CONTRIBUTORS files.
|
||||||
|
# See the latter for an explanation.
|
||||||
|
#
|
||||||
|
# Freetype-Go is derived from Freetype, which is written in C. The latter
|
||||||
|
# is copyright 1996-2010 David Turner, Robert Wilhelm, and Werner Lemberg.
|
||||||
|
|
||||||
|
# Names should be added to this file as
|
||||||
|
# Name or Organization <email address>
|
||||||
|
# The email address is not required for organizations.
|
||||||
|
|
||||||
|
# Please keep the list sorted.
|
||||||
|
|
||||||
|
Google Inc.
|
||||||
|
Jeff R. Allen <jra@nella.org>
|
||||||
|
Maksim Kochkin <maxxarts@gmail.com>
|
||||||
|
Michael Fogleman <fogleman@gmail.com>
|
||||||
|
Rémy Oudompheng <oudomphe@phare.normalesup.org>
|
||||||
|
Roger Peppe <rogpeppe@gmail.com>
|
||||||
|
Steven Edwards <steven@stephenwithav.com>
|
|
@ -0,0 +1,38 @@
|
||||||
|
# This is the official list of people who can contribute
|
||||||
|
# (and typically have contributed) code to the Freetype-Go repository.
|
||||||
|
# The AUTHORS file lists the copyright holders; this file
|
||||||
|
# lists people. For example, Google employees are listed here
|
||||||
|
# but not in AUTHORS, because Google holds the copyright.
|
||||||
|
#
|
||||||
|
# The submission process automatically checks to make sure
|
||||||
|
# that people submitting code are listed in this file (by email address).
|
||||||
|
#
|
||||||
|
# Names should be added to this file only after verifying that
|
||||||
|
# the individual or the individual's organization has agreed to
|
||||||
|
# the appropriate Contributor License Agreement, found here:
|
||||||
|
#
|
||||||
|
# http://code.google.com/legal/individual-cla-v1.0.html
|
||||||
|
# http://code.google.com/legal/corporate-cla-v1.0.html
|
||||||
|
#
|
||||||
|
# The agreement for individuals can be filled out on the web.
|
||||||
|
#
|
||||||
|
# When adding J Random Contributor's name to this file,
|
||||||
|
# either J's name or J's organization's name should be
|
||||||
|
# added to the AUTHORS file, depending on whether the
|
||||||
|
# individual or corporate CLA was used.
|
||||||
|
|
||||||
|
# Names should be added to this file like so:
|
||||||
|
# Name <email address>
|
||||||
|
|
||||||
|
# Please keep the list sorted.
|
||||||
|
|
||||||
|
Andrew Gerrand <adg@golang.org>
|
||||||
|
Jeff R. Allen <jra@nella.org> <jeff.allen@gmail.com>
|
||||||
|
Maksim Kochkin <maxxarts@gmail.com>
|
||||||
|
Michael Fogleman <fogleman@gmail.com>
|
||||||
|
Nigel Tao <nigeltao@golang.org>
|
||||||
|
Rémy Oudompheng <oudomphe@phare.normalesup.org> <remyoudompheng@gmail.com>
|
||||||
|
Rob Pike <r@golang.org>
|
||||||
|
Roger Peppe <rogpeppe@gmail.com>
|
||||||
|
Russ Cox <rsc@golang.org>
|
||||||
|
Steven Edwards <steven@stephenwithav.com>
|
|
@ -0,0 +1,12 @@
|
||||||
|
Use of the Freetype-Go software is subject to your choice of exactly one of
|
||||||
|
the following two licenses:
|
||||||
|
* The FreeType License, which is similar to the original BSD license with
|
||||||
|
an advertising clause, or
|
||||||
|
* The GNU General Public License (GPL), version 2 or later.
|
||||||
|
|
||||||
|
The text of these licenses are available in the licenses/ftl.txt and the
|
||||||
|
licenses/gpl.txt files respectively. They are also available at
|
||||||
|
http://freetype.sourceforge.net/license.html
|
||||||
|
|
||||||
|
The Luxi fonts in the testdata directory are licensed separately. See the
|
||||||
|
testdata/COPYING file for details.
|
|
@ -0,0 +1,21 @@
|
||||||
|
The Freetype font rasterizer in the Go programming language.
|
||||||
|
|
||||||
|
To download and install from source:
|
||||||
|
$ go get github.com/golang/freetype
|
||||||
|
|
||||||
|
It is an incomplete port:
|
||||||
|
* It only supports TrueType fonts, and not Type 1 fonts nor bitmap fonts.
|
||||||
|
* It only supports the Unicode encoding.
|
||||||
|
|
||||||
|
There are also some implementation differences:
|
||||||
|
* It uses a 26.6 fixed point co-ordinate system everywhere internally,
|
||||||
|
as opposed to the original Freetype's mix of 26.6 (or 10.6 for 16-bit
|
||||||
|
systems) in some places, and 24.8 in the "smooth" rasterizer.
|
||||||
|
|
||||||
|
Freetype-Go is derived from Freetype, which is written in C. Freetype is
|
||||||
|
copyright 1996-2010 David Turner, Robert Wilhelm, and Werner Lemberg.
|
||||||
|
Freetype-Go is copyright The Freetype-Go Authors, who are listed in the
|
||||||
|
AUTHORS file.
|
||||||
|
|
||||||
|
Unless otherwise noted, the Freetype-Go source files are distributed
|
||||||
|
under the BSD-style license found in the LICENSE file.
|
|
@ -0,0 +1,245 @@
|
||||||
|
// Copyright 2010 The Freetype-Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by your choice of either the
|
||||||
|
// FreeType License or the GNU General Public License version 2 (or
|
||||||
|
// any later version), both of which can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package raster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// maxAbs returns the maximum of abs(a) and abs(b).
|
||||||
|
func maxAbs(a, b fixed.Int26_6) fixed.Int26_6 {
|
||||||
|
if a < 0 {
|
||||||
|
a = -a
|
||||||
|
}
|
||||||
|
if b < 0 {
|
||||||
|
b = -b
|
||||||
|
}
|
||||||
|
if a < b {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
// pNeg returns the vector -p, or equivalently p rotated by 180 degrees.
|
||||||
|
func pNeg(p fixed.Point26_6) fixed.Point26_6 {
|
||||||
|
return fixed.Point26_6{-p.X, -p.Y}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pDot returns the dot product p·q.
|
||||||
|
func pDot(p fixed.Point26_6, q fixed.Point26_6) fixed.Int52_12 {
|
||||||
|
px, py := int64(p.X), int64(p.Y)
|
||||||
|
qx, qy := int64(q.X), int64(q.Y)
|
||||||
|
return fixed.Int52_12(px*qx + py*qy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pLen returns the length of the vector p.
|
||||||
|
func pLen(p fixed.Point26_6) fixed.Int26_6 {
|
||||||
|
// TODO(nigeltao): use fixed point math.
|
||||||
|
x := float64(p.X)
|
||||||
|
y := float64(p.Y)
|
||||||
|
return fixed.Int26_6(math.Sqrt(x*x + y*y))
|
||||||
|
}
|
||||||
|
|
||||||
|
// pNorm returns the vector p normalized to the given length, or zero if p is
|
||||||
|
// degenerate.
|
||||||
|
func pNorm(p fixed.Point26_6, length fixed.Int26_6) fixed.Point26_6 {
|
||||||
|
d := pLen(p)
|
||||||
|
if d == 0 {
|
||||||
|
return fixed.Point26_6{}
|
||||||
|
}
|
||||||
|
s, t := int64(length), int64(d)
|
||||||
|
x := int64(p.X) * s / t
|
||||||
|
y := int64(p.Y) * s / t
|
||||||
|
return fixed.Point26_6{fixed.Int26_6(x), fixed.Int26_6(y)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pRot45CW returns the vector p rotated clockwise by 45 degrees.
|
||||||
|
//
|
||||||
|
// Note that the Y-axis grows downwards, so {1, 0}.Rot45CW is {1/√2, 1/√2}.
|
||||||
|
func pRot45CW(p fixed.Point26_6) fixed.Point26_6 {
|
||||||
|
// 181/256 is approximately 1/√2, or sin(π/4).
|
||||||
|
px, py := int64(p.X), int64(p.Y)
|
||||||
|
qx := (+px - py) * 181 / 256
|
||||||
|
qy := (+px + py) * 181 / 256
|
||||||
|
return fixed.Point26_6{fixed.Int26_6(qx), fixed.Int26_6(qy)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pRot90CW returns the vector p rotated clockwise by 90 degrees.
|
||||||
|
//
|
||||||
|
// Note that the Y-axis grows downwards, so {1, 0}.Rot90CW is {0, 1}.
|
||||||
|
func pRot90CW(p fixed.Point26_6) fixed.Point26_6 {
|
||||||
|
return fixed.Point26_6{-p.Y, p.X}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pRot135CW returns the vector p rotated clockwise by 135 degrees.
|
||||||
|
//
|
||||||
|
// Note that the Y-axis grows downwards, so {1, 0}.Rot135CW is {-1/√2, 1/√2}.
|
||||||
|
func pRot135CW(p fixed.Point26_6) fixed.Point26_6 {
|
||||||
|
// 181/256 is approximately 1/√2, or sin(π/4).
|
||||||
|
px, py := int64(p.X), int64(p.Y)
|
||||||
|
qx := (-px - py) * 181 / 256
|
||||||
|
qy := (+px - py) * 181 / 256
|
||||||
|
return fixed.Point26_6{fixed.Int26_6(qx), fixed.Int26_6(qy)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pRot45CCW returns the vector p rotated counter-clockwise by 45 degrees.
|
||||||
|
//
|
||||||
|
// Note that the Y-axis grows downwards, so {1, 0}.Rot45CCW is {1/√2, -1/√2}.
|
||||||
|
func pRot45CCW(p fixed.Point26_6) fixed.Point26_6 {
|
||||||
|
// 181/256 is approximately 1/√2, or sin(π/4).
|
||||||
|
px, py := int64(p.X), int64(p.Y)
|
||||||
|
qx := (+px + py) * 181 / 256
|
||||||
|
qy := (-px + py) * 181 / 256
|
||||||
|
return fixed.Point26_6{fixed.Int26_6(qx), fixed.Int26_6(qy)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pRot90CCW returns the vector p rotated counter-clockwise by 90 degrees.
|
||||||
|
//
|
||||||
|
// Note that the Y-axis grows downwards, so {1, 0}.Rot90CCW is {0, -1}.
|
||||||
|
func pRot90CCW(p fixed.Point26_6) fixed.Point26_6 {
|
||||||
|
return fixed.Point26_6{p.Y, -p.X}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pRot135CCW returns the vector p rotated counter-clockwise by 135 degrees.
|
||||||
|
//
|
||||||
|
// Note that the Y-axis grows downwards, so {1, 0}.Rot135CCW is {-1/√2, -1/√2}.
|
||||||
|
func pRot135CCW(p fixed.Point26_6) fixed.Point26_6 {
|
||||||
|
// 181/256 is approximately 1/√2, or sin(π/4).
|
||||||
|
px, py := int64(p.X), int64(p.Y)
|
||||||
|
qx := (-px + py) * 181 / 256
|
||||||
|
qy := (-px - py) * 181 / 256
|
||||||
|
return fixed.Point26_6{fixed.Int26_6(qx), fixed.Int26_6(qy)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Adder accumulates points on a curve.
|
||||||
|
type Adder interface {
|
||||||
|
// Start starts a new curve at the given point.
|
||||||
|
Start(a fixed.Point26_6)
|
||||||
|
// Add1 adds a linear segment to the current curve.
|
||||||
|
Add1(b fixed.Point26_6)
|
||||||
|
// Add2 adds a quadratic segment to the current curve.
|
||||||
|
Add2(b, c fixed.Point26_6)
|
||||||
|
// Add3 adds a cubic segment to the current curve.
|
||||||
|
Add3(b, c, d fixed.Point26_6)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Path is a sequence of curves, and a curve is a start point followed by a
|
||||||
|
// sequence of linear, quadratic or cubic segments.
|
||||||
|
type Path []fixed.Int26_6
|
||||||
|
|
||||||
|
// String returns a human-readable representation of a Path.
|
||||||
|
func (p Path) String() string {
|
||||||
|
s := ""
|
||||||
|
for i := 0; i < len(p); {
|
||||||
|
if i != 0 {
|
||||||
|
s += " "
|
||||||
|
}
|
||||||
|
switch p[i] {
|
||||||
|
case 0:
|
||||||
|
s += "S0" + fmt.Sprint([]fixed.Int26_6(p[i+1:i+3]))
|
||||||
|
i += 4
|
||||||
|
case 1:
|
||||||
|
s += "A1" + fmt.Sprint([]fixed.Int26_6(p[i+1:i+3]))
|
||||||
|
i += 4
|
||||||
|
case 2:
|
||||||
|
s += "A2" + fmt.Sprint([]fixed.Int26_6(p[i+1:i+5]))
|
||||||
|
i += 6
|
||||||
|
case 3:
|
||||||
|
s += "A3" + fmt.Sprint([]fixed.Int26_6(p[i+1:i+7]))
|
||||||
|
i += 8
|
||||||
|
default:
|
||||||
|
panic("freetype/raster: bad path")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear cancels any previous calls to p.Start or p.AddXxx.
|
||||||
|
func (p *Path) Clear() {
|
||||||
|
*p = (*p)[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts a new curve at the given point.
|
||||||
|
func (p *Path) Start(a fixed.Point26_6) {
|
||||||
|
*p = append(*p, 0, a.X, a.Y, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add1 adds a linear segment to the current curve.
|
||||||
|
func (p *Path) Add1(b fixed.Point26_6) {
|
||||||
|
*p = append(*p, 1, b.X, b.Y, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add2 adds a quadratic segment to the current curve.
|
||||||
|
func (p *Path) Add2(b, c fixed.Point26_6) {
|
||||||
|
*p = append(*p, 2, b.X, b.Y, c.X, c.Y, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add3 adds a cubic segment to the current curve.
|
||||||
|
func (p *Path) Add3(b, c, d fixed.Point26_6) {
|
||||||
|
*p = append(*p, 3, b.X, b.Y, c.X, c.Y, d.X, d.Y, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddPath adds the Path q to p.
|
||||||
|
func (p *Path) AddPath(q Path) {
|
||||||
|
*p = append(*p, q...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddStroke adds a stroked Path.
|
||||||
|
func (p *Path) AddStroke(q Path, width fixed.Int26_6, cr Capper, jr Joiner) {
|
||||||
|
Stroke(p, q, width, cr, jr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// firstPoint returns the first point in a non-empty Path.
|
||||||
|
func (p Path) firstPoint() fixed.Point26_6 {
|
||||||
|
return fixed.Point26_6{p[1], p[2]}
|
||||||
|
}
|
||||||
|
|
||||||
|
// lastPoint returns the last point in a non-empty Path.
|
||||||
|
func (p Path) lastPoint() fixed.Point26_6 {
|
||||||
|
return fixed.Point26_6{p[len(p)-3], p[len(p)-2]}
|
||||||
|
}
|
||||||
|
|
||||||
|
// addPathReversed adds q reversed to p.
|
||||||
|
// For example, if q consists of a linear segment from A to B followed by a
|
||||||
|
// quadratic segment from B to C to D, then the values of q looks like:
|
||||||
|
// index: 01234567890123
|
||||||
|
// value: 0AA01BB12CCDD2
|
||||||
|
// So, when adding q backwards to p, we want to Add2(C, B) followed by Add1(A).
|
||||||
|
func addPathReversed(p Adder, q Path) {
|
||||||
|
if len(q) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
i := len(q) - 1
|
||||||
|
for {
|
||||||
|
switch q[i] {
|
||||||
|
case 0:
|
||||||
|
return
|
||||||
|
case 1:
|
||||||
|
i -= 4
|
||||||
|
p.Add1(
|
||||||
|
fixed.Point26_6{q[i-2], q[i-1]},
|
||||||
|
)
|
||||||
|
case 2:
|
||||||
|
i -= 6
|
||||||
|
p.Add2(
|
||||||
|
fixed.Point26_6{q[i+2], q[i+3]},
|
||||||
|
fixed.Point26_6{q[i-2], q[i-1]},
|
||||||
|
)
|
||||||
|
case 3:
|
||||||
|
i -= 8
|
||||||
|
p.Add3(
|
||||||
|
fixed.Point26_6{q[i+4], q[i+5]},
|
||||||
|
fixed.Point26_6{q[i+2], q[i+3]},
|
||||||
|
fixed.Point26_6{q[i-2], q[i-1]},
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
panic("freetype/raster: bad path")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,287 @@
|
||||||
|
// Copyright 2010 The Freetype-Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by your choice of either the
|
||||||
|
// FreeType License or the GNU General Public License version 2 (or
|
||||||
|
// any later version), both of which can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package raster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/draw"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Span is a horizontal segment of pixels with constant alpha. X0 is an
|
||||||
|
// inclusive bound and X1 is exclusive, the same as for slices. A fully opaque
|
||||||
|
// Span has Alpha == 0xffff.
|
||||||
|
type Span struct {
|
||||||
|
Y, X0, X1 int
|
||||||
|
Alpha uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Painter knows how to paint a batch of Spans. Rasterization may involve
|
||||||
|
// Painting multiple batches, and done will be true for the final batch. The
|
||||||
|
// Spans' Y values are monotonically increasing during a rasterization. Paint
|
||||||
|
// may use all of ss as scratch space during the call.
|
||||||
|
type Painter interface {
|
||||||
|
Paint(ss []Span, done bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The PainterFunc type adapts an ordinary function to the Painter interface.
|
||||||
|
type PainterFunc func(ss []Span, done bool)
|
||||||
|
|
||||||
|
// Paint just delegates the call to f.
|
||||||
|
func (f PainterFunc) Paint(ss []Span, done bool) { f(ss, done) }
|
||||||
|
|
||||||
|
// An AlphaOverPainter is a Painter that paints Spans onto a *image.Alpha using
|
||||||
|
// the Over Porter-Duff composition operator.
|
||||||
|
type AlphaOverPainter struct {
|
||||||
|
Image *image.Alpha
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paint satisfies the Painter interface.
|
||||||
|
func (r AlphaOverPainter) Paint(ss []Span, done bool) {
|
||||||
|
b := r.Image.Bounds()
|
||||||
|
for _, s := range ss {
|
||||||
|
if s.Y < b.Min.Y {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s.Y >= b.Max.Y {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.X0 < b.Min.X {
|
||||||
|
s.X0 = b.Min.X
|
||||||
|
}
|
||||||
|
if s.X1 > b.Max.X {
|
||||||
|
s.X1 = b.Max.X
|
||||||
|
}
|
||||||
|
if s.X0 >= s.X1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
base := (s.Y-r.Image.Rect.Min.Y)*r.Image.Stride - r.Image.Rect.Min.X
|
||||||
|
p := r.Image.Pix[base+s.X0 : base+s.X1]
|
||||||
|
a := int(s.Alpha >> 8)
|
||||||
|
for i, c := range p {
|
||||||
|
v := int(c)
|
||||||
|
p[i] = uint8((v*255 + (255-v)*a) / 255)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAlphaOverPainter creates a new AlphaOverPainter for the given image.
|
||||||
|
func NewAlphaOverPainter(m *image.Alpha) AlphaOverPainter {
|
||||||
|
return AlphaOverPainter{m}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An AlphaSrcPainter is a Painter that paints Spans onto a *image.Alpha using
|
||||||
|
// the Src Porter-Duff composition operator.
|
||||||
|
type AlphaSrcPainter struct {
|
||||||
|
Image *image.Alpha
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paint satisfies the Painter interface.
|
||||||
|
func (r AlphaSrcPainter) Paint(ss []Span, done bool) {
|
||||||
|
b := r.Image.Bounds()
|
||||||
|
for _, s := range ss {
|
||||||
|
if s.Y < b.Min.Y {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s.Y >= b.Max.Y {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.X0 < b.Min.X {
|
||||||
|
s.X0 = b.Min.X
|
||||||
|
}
|
||||||
|
if s.X1 > b.Max.X {
|
||||||
|
s.X1 = b.Max.X
|
||||||
|
}
|
||||||
|
if s.X0 >= s.X1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
base := (s.Y-r.Image.Rect.Min.Y)*r.Image.Stride - r.Image.Rect.Min.X
|
||||||
|
p := r.Image.Pix[base+s.X0 : base+s.X1]
|
||||||
|
color := uint8(s.Alpha >> 8)
|
||||||
|
for i := range p {
|
||||||
|
p[i] = color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAlphaSrcPainter creates a new AlphaSrcPainter for the given image.
|
||||||
|
func NewAlphaSrcPainter(m *image.Alpha) AlphaSrcPainter {
|
||||||
|
return AlphaSrcPainter{m}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An RGBAPainter is a Painter that paints Spans onto a *image.RGBA.
|
||||||
|
type RGBAPainter struct {
|
||||||
|
// Image is the image to compose onto.
|
||||||
|
Image *image.RGBA
|
||||||
|
// Op is the Porter-Duff composition operator.
|
||||||
|
Op draw.Op
|
||||||
|
// cr, cg, cb and ca are the 16-bit color to paint the spans.
|
||||||
|
cr, cg, cb, ca uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paint satisfies the Painter interface.
|
||||||
|
func (r *RGBAPainter) Paint(ss []Span, done bool) {
|
||||||
|
b := r.Image.Bounds()
|
||||||
|
for _, s := range ss {
|
||||||
|
if s.Y < b.Min.Y {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s.Y >= b.Max.Y {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.X0 < b.Min.X {
|
||||||
|
s.X0 = b.Min.X
|
||||||
|
}
|
||||||
|
if s.X1 > b.Max.X {
|
||||||
|
s.X1 = b.Max.X
|
||||||
|
}
|
||||||
|
if s.X0 >= s.X1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// This code mimics drawGlyphOver in $GOROOT/src/image/draw/draw.go.
|
||||||
|
ma := s.Alpha
|
||||||
|
const m = 1<<16 - 1
|
||||||
|
i0 := (s.Y-r.Image.Rect.Min.Y)*r.Image.Stride + (s.X0-r.Image.Rect.Min.X)*4
|
||||||
|
i1 := i0 + (s.X1-s.X0)*4
|
||||||
|
if r.Op == draw.Over {
|
||||||
|
for i := i0; i < i1; i += 4 {
|
||||||
|
dr := uint32(r.Image.Pix[i+0])
|
||||||
|
dg := uint32(r.Image.Pix[i+1])
|
||||||
|
db := uint32(r.Image.Pix[i+2])
|
||||||
|
da := uint32(r.Image.Pix[i+3])
|
||||||
|
a := (m - (r.ca * ma / m)) * 0x101
|
||||||
|
r.Image.Pix[i+0] = uint8((dr*a + r.cr*ma) / m >> 8)
|
||||||
|
r.Image.Pix[i+1] = uint8((dg*a + r.cg*ma) / m >> 8)
|
||||||
|
r.Image.Pix[i+2] = uint8((db*a + r.cb*ma) / m >> 8)
|
||||||
|
r.Image.Pix[i+3] = uint8((da*a + r.ca*ma) / m >> 8)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for i := i0; i < i1; i += 4 {
|
||||||
|
r.Image.Pix[i+0] = uint8(r.cr * ma / m >> 8)
|
||||||
|
r.Image.Pix[i+1] = uint8(r.cg * ma / m >> 8)
|
||||||
|
r.Image.Pix[i+2] = uint8(r.cb * ma / m >> 8)
|
||||||
|
r.Image.Pix[i+3] = uint8(r.ca * ma / m >> 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetColor sets the color to paint the spans.
|
||||||
|
func (r *RGBAPainter) SetColor(c color.Color) {
|
||||||
|
r.cr, r.cg, r.cb, r.ca = c.RGBA()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRGBAPainter creates a new RGBAPainter for the given image.
|
||||||
|
func NewRGBAPainter(m *image.RGBA) *RGBAPainter {
|
||||||
|
return &RGBAPainter{Image: m}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A MonochromePainter wraps another Painter, quantizing each Span's alpha to
|
||||||
|
// be either fully opaque or fully transparent.
|
||||||
|
type MonochromePainter struct {
|
||||||
|
Painter Painter
|
||||||
|
y, x0, x1 int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paint delegates to the wrapped Painter after quantizing each Span's alpha
|
||||||
|
// value and merging adjacent fully opaque Spans.
|
||||||
|
func (m *MonochromePainter) Paint(ss []Span, done bool) {
|
||||||
|
// We compact the ss slice, discarding any Spans whose alpha quantizes to zero.
|
||||||
|
j := 0
|
||||||
|
for _, s := range ss {
|
||||||
|
if s.Alpha >= 0x8000 {
|
||||||
|
if m.y == s.Y && m.x1 == s.X0 {
|
||||||
|
m.x1 = s.X1
|
||||||
|
} else {
|
||||||
|
ss[j] = Span{m.y, m.x0, m.x1, 1<<16 - 1}
|
||||||
|
j++
|
||||||
|
m.y, m.x0, m.x1 = s.Y, s.X0, s.X1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if done {
|
||||||
|
// Flush the accumulated Span.
|
||||||
|
finalSpan := Span{m.y, m.x0, m.x1, 1<<16 - 1}
|
||||||
|
if j < len(ss) {
|
||||||
|
ss[j] = finalSpan
|
||||||
|
j++
|
||||||
|
m.Painter.Paint(ss[:j], true)
|
||||||
|
} else if j == len(ss) {
|
||||||
|
m.Painter.Paint(ss, false)
|
||||||
|
if cap(ss) > 0 {
|
||||||
|
ss = ss[:1]
|
||||||
|
} else {
|
||||||
|
ss = make([]Span, 1)
|
||||||
|
}
|
||||||
|
ss[0] = finalSpan
|
||||||
|
m.Painter.Paint(ss, true)
|
||||||
|
} else {
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
// Reset the accumulator, so that this Painter can be re-used.
|
||||||
|
m.y, m.x0, m.x1 = 0, 0, 0
|
||||||
|
} else {
|
||||||
|
m.Painter.Paint(ss[:j], false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMonochromePainter creates a new MonochromePainter that wraps the given
|
||||||
|
// Painter.
|
||||||
|
func NewMonochromePainter(p Painter) *MonochromePainter {
|
||||||
|
return &MonochromePainter{Painter: p}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A GammaCorrectionPainter wraps another Painter, performing gamma-correction
|
||||||
|
// on each Span's alpha value.
|
||||||
|
type GammaCorrectionPainter struct {
|
||||||
|
// Painter is the wrapped Painter.
|
||||||
|
Painter Painter
|
||||||
|
// a is the precomputed alpha values for linear interpolation, with fully
|
||||||
|
// opaque == 0xffff.
|
||||||
|
a [256]uint16
|
||||||
|
// gammaIsOne is whether gamma correction is a no-op.
|
||||||
|
gammaIsOne bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paint delegates to the wrapped Painter after performing gamma-correction on
|
||||||
|
// each Span.
|
||||||
|
func (g *GammaCorrectionPainter) Paint(ss []Span, done bool) {
|
||||||
|
if !g.gammaIsOne {
|
||||||
|
const n = 0x101
|
||||||
|
for i, s := range ss {
|
||||||
|
if s.Alpha == 0 || s.Alpha == 0xffff {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p, q := s.Alpha/n, s.Alpha%n
|
||||||
|
// The resultant alpha is a linear interpolation of g.a[p] and g.a[p+1].
|
||||||
|
a := uint32(g.a[p])*(n-q) + uint32(g.a[p+1])*q
|
||||||
|
ss[i].Alpha = (a + n/2) / n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.Painter.Paint(ss, done)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGamma sets the gamma value.
|
||||||
|
func (g *GammaCorrectionPainter) SetGamma(gamma float64) {
|
||||||
|
g.gammaIsOne = gamma == 1
|
||||||
|
if g.gammaIsOne {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := 0; i < 256; i++ {
|
||||||
|
a := float64(i) / 0xff
|
||||||
|
a = math.Pow(a, gamma)
|
||||||
|
g.a[i] = uint16(0xffff * a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGammaCorrectionPainter creates a new GammaCorrectionPainter that wraps
|
||||||
|
// the given Painter.
|
||||||
|
func NewGammaCorrectionPainter(p Painter, gamma float64) *GammaCorrectionPainter {
|
||||||
|
g := &GammaCorrectionPainter{Painter: p}
|
||||||
|
g.SetGamma(gamma)
|
||||||
|
return g
|
||||||
|
}
|
|
@ -0,0 +1,483 @@
|
||||||
|
// Copyright 2010 The Freetype-Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by your choice of either the
|
||||||
|
// FreeType License or the GNU General Public License version 2 (or
|
||||||
|
// any later version), both of which can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package raster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Two points are considered practically equal if the square of the distance
|
||||||
|
// between them is less than one quarter (i.e. 1024 / 4096).
|
||||||
|
const epsilon = fixed.Int52_12(1024)
|
||||||
|
|
||||||
|
// A Capper signifies how to begin or end a stroked path.
|
||||||
|
type Capper interface {
|
||||||
|
// Cap adds a cap to p given a pivot point and the normal vector of a
|
||||||
|
// terminal segment. The normal's length is half of the stroke width.
|
||||||
|
Cap(p Adder, halfWidth fixed.Int26_6, pivot, n1 fixed.Point26_6)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The CapperFunc type adapts an ordinary function to be a Capper.
|
||||||
|
type CapperFunc func(Adder, fixed.Int26_6, fixed.Point26_6, fixed.Point26_6)
|
||||||
|
|
||||||
|
func (f CapperFunc) Cap(p Adder, halfWidth fixed.Int26_6, pivot, n1 fixed.Point26_6) {
|
||||||
|
f(p, halfWidth, pivot, n1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Joiner signifies how to join interior nodes of a stroked path.
|
||||||
|
type Joiner interface {
|
||||||
|
// Join adds a join to the two sides of a stroked path given a pivot
|
||||||
|
// point and the normal vectors of the trailing and leading segments.
|
||||||
|
// Both normals have length equal to half of the stroke width.
|
||||||
|
Join(lhs, rhs Adder, halfWidth fixed.Int26_6, pivot, n0, n1 fixed.Point26_6)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The JoinerFunc type adapts an ordinary function to be a Joiner.
|
||||||
|
type JoinerFunc func(lhs, rhs Adder, halfWidth fixed.Int26_6, pivot, n0, n1 fixed.Point26_6)
|
||||||
|
|
||||||
|
func (f JoinerFunc) Join(lhs, rhs Adder, halfWidth fixed.Int26_6, pivot, n0, n1 fixed.Point26_6) {
|
||||||
|
f(lhs, rhs, halfWidth, pivot, n0, n1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoundCapper adds round caps to a stroked path.
|
||||||
|
var RoundCapper Capper = CapperFunc(roundCapper)
|
||||||
|
|
||||||
|
func roundCapper(p Adder, halfWidth fixed.Int26_6, pivot, n1 fixed.Point26_6) {
|
||||||
|
// The cubic Bézier approximation to a circle involves the magic number
|
||||||
|
// (√2 - 1) * 4/3, which is approximately 35/64.
|
||||||
|
const k = 35
|
||||||
|
e := pRot90CCW(n1)
|
||||||
|
side := pivot.Add(e)
|
||||||
|
start, end := pivot.Sub(n1), pivot.Add(n1)
|
||||||
|
d, e := n1.Mul(k), e.Mul(k)
|
||||||
|
p.Add3(start.Add(e), side.Sub(d), side)
|
||||||
|
p.Add3(side.Add(d), end.Add(e), end)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ButtCapper adds butt caps to a stroked path.
|
||||||
|
var ButtCapper Capper = CapperFunc(buttCapper)
|
||||||
|
|
||||||
|
func buttCapper(p Adder, halfWidth fixed.Int26_6, pivot, n1 fixed.Point26_6) {
|
||||||
|
p.Add1(pivot.Add(n1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SquareCapper adds square caps to a stroked path.
|
||||||
|
var SquareCapper Capper = CapperFunc(squareCapper)
|
||||||
|
|
||||||
|
func squareCapper(p Adder, halfWidth fixed.Int26_6, pivot, n1 fixed.Point26_6) {
|
||||||
|
e := pRot90CCW(n1)
|
||||||
|
side := pivot.Add(e)
|
||||||
|
p.Add1(side.Sub(n1))
|
||||||
|
p.Add1(side.Add(n1))
|
||||||
|
p.Add1(pivot.Add(n1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoundJoiner adds round joins to a stroked path.
|
||||||
|
var RoundJoiner Joiner = JoinerFunc(roundJoiner)
|
||||||
|
|
||||||
|
func roundJoiner(lhs, rhs Adder, haflWidth fixed.Int26_6, pivot, n0, n1 fixed.Point26_6) {
|
||||||
|
dot := pDot(pRot90CW(n0), n1)
|
||||||
|
if dot >= 0 {
|
||||||
|
addArc(lhs, pivot, n0, n1)
|
||||||
|
rhs.Add1(pivot.Sub(n1))
|
||||||
|
} else {
|
||||||
|
lhs.Add1(pivot.Add(n1))
|
||||||
|
addArc(rhs, pivot, pNeg(n0), pNeg(n1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BevelJoiner adds bevel joins to a stroked path.
|
||||||
|
var BevelJoiner Joiner = JoinerFunc(bevelJoiner)
|
||||||
|
|
||||||
|
func bevelJoiner(lhs, rhs Adder, haflWidth fixed.Int26_6, pivot, n0, n1 fixed.Point26_6) {
|
||||||
|
lhs.Add1(pivot.Add(n1))
|
||||||
|
rhs.Add1(pivot.Sub(n1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// addArc adds a circular arc from pivot+n0 to pivot+n1 to p. The shorter of
|
||||||
|
// the two possible arcs is taken, i.e. the one spanning <= 180 degrees. The
|
||||||
|
// two vectors n0 and n1 must be of equal length.
|
||||||
|
func addArc(p Adder, pivot, n0, n1 fixed.Point26_6) {
|
||||||
|
// r2 is the square of the length of n0.
|
||||||
|
r2 := pDot(n0, n0)
|
||||||
|
if r2 < epsilon {
|
||||||
|
// The arc radius is so small that we collapse to a straight line.
|
||||||
|
p.Add1(pivot.Add(n1))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// We approximate the arc by 0, 1, 2 or 3 45-degree quadratic segments plus
|
||||||
|
// a final quadratic segment from s to n1. Each 45-degree segment has
|
||||||
|
// control points {1, 0}, {1, tan(π/8)} and {1/√2, 1/√2} suitably scaled,
|
||||||
|
// rotated and translated. tan(π/8) is approximately 27/64.
|
||||||
|
const tpo8 = 27
|
||||||
|
var s fixed.Point26_6
|
||||||
|
// We determine which octant the angle between n0 and n1 is in via three
|
||||||
|
// dot products. m0, m1 and m2 are n0 rotated clockwise by 45, 90 and 135
|
||||||
|
// degrees.
|
||||||
|
m0 := pRot45CW(n0)
|
||||||
|
m1 := pRot90CW(n0)
|
||||||
|
m2 := pRot90CW(m0)
|
||||||
|
if pDot(m1, n1) >= 0 {
|
||||||
|
if pDot(n0, n1) >= 0 {
|
||||||
|
if pDot(m2, n1) <= 0 {
|
||||||
|
// n1 is between 0 and 45 degrees clockwise of n0.
|
||||||
|
s = n0
|
||||||
|
} else {
|
||||||
|
// n1 is between 45 and 90 degrees clockwise of n0.
|
||||||
|
p.Add2(pivot.Add(n0).Add(m1.Mul(tpo8)), pivot.Add(m0))
|
||||||
|
s = m0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pm1, n0t := pivot.Add(m1), n0.Mul(tpo8)
|
||||||
|
p.Add2(pivot.Add(n0).Add(m1.Mul(tpo8)), pivot.Add(m0))
|
||||||
|
p.Add2(pm1.Add(n0t), pm1)
|
||||||
|
if pDot(m0, n1) >= 0 {
|
||||||
|
// n1 is between 90 and 135 degrees clockwise of n0.
|
||||||
|
s = m1
|
||||||
|
} else {
|
||||||
|
// n1 is between 135 and 180 degrees clockwise of n0.
|
||||||
|
p.Add2(pm1.Sub(n0t), pivot.Add(m2))
|
||||||
|
s = m2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if pDot(n0, n1) >= 0 {
|
||||||
|
if pDot(m0, n1) >= 0 {
|
||||||
|
// n1 is between 0 and 45 degrees counter-clockwise of n0.
|
||||||
|
s = n0
|
||||||
|
} else {
|
||||||
|
// n1 is between 45 and 90 degrees counter-clockwise of n0.
|
||||||
|
p.Add2(pivot.Add(n0).Sub(m1.Mul(tpo8)), pivot.Sub(m2))
|
||||||
|
s = pNeg(m2)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pm1, n0t := pivot.Sub(m1), n0.Mul(tpo8)
|
||||||
|
p.Add2(pivot.Add(n0).Sub(m1.Mul(tpo8)), pivot.Sub(m2))
|
||||||
|
p.Add2(pm1.Add(n0t), pm1)
|
||||||
|
if pDot(m2, n1) <= 0 {
|
||||||
|
// n1 is between 90 and 135 degrees counter-clockwise of n0.
|
||||||
|
s = pNeg(m1)
|
||||||
|
} else {
|
||||||
|
// n1 is between 135 and 180 degrees counter-clockwise of n0.
|
||||||
|
p.Add2(pm1.Sub(n0t), pivot.Sub(m0))
|
||||||
|
s = pNeg(m0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The final quadratic segment has two endpoints s and n1 and the middle
|
||||||
|
// control point is a multiple of s.Add(n1), i.e. it is on the angle
|
||||||
|
// bisector of those two points. The multiple ranges between 128/256 and
|
||||||
|
// 150/256 as the angle between s and n1 ranges between 0 and 45 degrees.
|
||||||
|
//
|
||||||
|
// When the angle is 0 degrees (i.e. s and n1 are coincident) then
|
||||||
|
// s.Add(n1) is twice s and so the middle control point of the degenerate
|
||||||
|
// quadratic segment should be half s.Add(n1), and half = 128/256.
|
||||||
|
//
|
||||||
|
// When the angle is 45 degrees then 150/256 is the ratio of the lengths of
|
||||||
|
// the two vectors {1, tan(π/8)} and {1 + 1/√2, 1/√2}.
|
||||||
|
//
|
||||||
|
// d is the normalized dot product between s and n1. Since the angle ranges
|
||||||
|
// between 0 and 45 degrees then d ranges between 256/256 and 181/256.
|
||||||
|
d := 256 * pDot(s, n1) / r2
|
||||||
|
multiple := fixed.Int26_6(150-(150-128)*(d-181)/(256-181)) >> 2
|
||||||
|
p.Add2(pivot.Add(s.Add(n1).Mul(multiple)), pivot.Add(n1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// midpoint returns the midpoint of two Points.
|
||||||
|
func midpoint(a, b fixed.Point26_6) fixed.Point26_6 {
|
||||||
|
return fixed.Point26_6{(a.X + b.X) / 2, (a.Y + b.Y) / 2}
|
||||||
|
}
|
||||||
|
|
||||||
|
// angleGreaterThan45 returns whether the angle between two vectors is more
|
||||||
|
// than 45 degrees.
|
||||||
|
func angleGreaterThan45(v0, v1 fixed.Point26_6) bool {
|
||||||
|
v := pRot45CCW(v0)
|
||||||
|
return pDot(v, v1) < 0 || pDot(pRot90CW(v), v1) < 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// interpolate returns the point (1-t)*a + t*b.
|
||||||
|
func interpolate(a, b fixed.Point26_6, t fixed.Int52_12) fixed.Point26_6 {
|
||||||
|
s := 1<<12 - t
|
||||||
|
x := s*fixed.Int52_12(a.X) + t*fixed.Int52_12(b.X)
|
||||||
|
y := s*fixed.Int52_12(a.Y) + t*fixed.Int52_12(b.Y)
|
||||||
|
return fixed.Point26_6{fixed.Int26_6(x >> 12), fixed.Int26_6(y >> 12)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// curviest2 returns the value of t for which the quadratic parametric curve
|
||||||
|
// (1-t)²*a + 2*t*(1-t).b + t²*c has maximum curvature.
|
||||||
|
//
|
||||||
|
// The curvature of the parametric curve f(t) = (x(t), y(t)) is
|
||||||
|
// |x′y″-y′x″| / (x′²+y′²)^(3/2).
|
||||||
|
//
|
||||||
|
// Let d = b-a and e = c-2*b+a, so that f′(t) = 2*d+2*e*t and f″(t) = 2*e.
|
||||||
|
// The curvature's numerator is (2*dx+2*ex*t)*(2*ey)-(2*dy+2*ey*t)*(2*ex),
|
||||||
|
// which simplifies to 4*dx*ey-4*dy*ex, which is constant with respect to t.
|
||||||
|
//
|
||||||
|
// Thus, curvature is extreme where the denominator is extreme, i.e. where
|
||||||
|
// (x′²+y′²) is extreme. The first order condition is that
|
||||||
|
// 2*x′*x″+2*y′*y″ = 0, or (dx+ex*t)*ex + (dy+ey*t)*ey = 0.
|
||||||
|
// Solving for t gives t = -(dx*ex+dy*ey) / (ex*ex+ey*ey).
|
||||||
|
func curviest2(a, b, c fixed.Point26_6) fixed.Int52_12 {
|
||||||
|
dx := int64(b.X - a.X)
|
||||||
|
dy := int64(b.Y - a.Y)
|
||||||
|
ex := int64(c.X - 2*b.X + a.X)
|
||||||
|
ey := int64(c.Y - 2*b.Y + a.Y)
|
||||||
|
if ex == 0 && ey == 0 {
|
||||||
|
return 2048
|
||||||
|
}
|
||||||
|
return fixed.Int52_12(-4096 * (dx*ex + dy*ey) / (ex*ex + ey*ey))
|
||||||
|
}
|
||||||
|
|
||||||
|
// A stroker holds state for stroking a path.
|
||||||
|
type stroker struct {
|
||||||
|
// p is the destination that records the stroked path.
|
||||||
|
p Adder
|
||||||
|
// u is the half-width of the stroke.
|
||||||
|
u fixed.Int26_6
|
||||||
|
// cr and jr specify how to end and connect path segments.
|
||||||
|
cr Capper
|
||||||
|
jr Joiner
|
||||||
|
// r is the reverse path. Stroking a path involves constructing two
|
||||||
|
// parallel paths 2*u apart. The first path is added immediately to p,
|
||||||
|
// the second path is accumulated in r and eventually added in reverse.
|
||||||
|
r Path
|
||||||
|
// a is the most recent segment point. anorm is the segment normal of
|
||||||
|
// length u at that point.
|
||||||
|
a, anorm fixed.Point26_6
|
||||||
|
}
|
||||||
|
|
||||||
|
// addNonCurvy2 adds a quadratic segment to the stroker, where the segment
|
||||||
|
// defined by (k.a, b, c) achieves maximum curvature at either k.a or c.
|
||||||
|
func (k *stroker) addNonCurvy2(b, c fixed.Point26_6) {
|
||||||
|
// We repeatedly divide the segment at its middle until it is straight
|
||||||
|
// enough to approximate the stroke by just translating the control points.
|
||||||
|
// ds and ps are stacks of depths and points. t is the top of the stack.
|
||||||
|
const maxDepth = 5
|
||||||
|
var (
|
||||||
|
ds [maxDepth + 1]int
|
||||||
|
ps [2*maxDepth + 3]fixed.Point26_6
|
||||||
|
t int
|
||||||
|
)
|
||||||
|
// Initially the ps stack has one quadratic segment of depth zero.
|
||||||
|
ds[0] = 0
|
||||||
|
ps[2] = k.a
|
||||||
|
ps[1] = b
|
||||||
|
ps[0] = c
|
||||||
|
anorm := k.anorm
|
||||||
|
var cnorm fixed.Point26_6
|
||||||
|
|
||||||
|
for {
|
||||||
|
depth := ds[t]
|
||||||
|
a := ps[2*t+2]
|
||||||
|
b := ps[2*t+1]
|
||||||
|
c := ps[2*t+0]
|
||||||
|
ab := b.Sub(a)
|
||||||
|
bc := c.Sub(b)
|
||||||
|
abIsSmall := pDot(ab, ab) < fixed.Int52_12(1<<12)
|
||||||
|
bcIsSmall := pDot(bc, bc) < fixed.Int52_12(1<<12)
|
||||||
|
if abIsSmall && bcIsSmall {
|
||||||
|
// Approximate the segment by a circular arc.
|
||||||
|
cnorm = pRot90CCW(pNorm(bc, k.u))
|
||||||
|
mac := midpoint(a, c)
|
||||||
|
addArc(k.p, mac, anorm, cnorm)
|
||||||
|
addArc(&k.r, mac, pNeg(anorm), pNeg(cnorm))
|
||||||
|
} else if depth < maxDepth && angleGreaterThan45(ab, bc) {
|
||||||
|
// Divide the segment in two and push both halves on the stack.
|
||||||
|
mab := midpoint(a, b)
|
||||||
|
mbc := midpoint(b, c)
|
||||||
|
t++
|
||||||
|
ds[t+0] = depth + 1
|
||||||
|
ds[t-1] = depth + 1
|
||||||
|
ps[2*t+2] = a
|
||||||
|
ps[2*t+1] = mab
|
||||||
|
ps[2*t+0] = midpoint(mab, mbc)
|
||||||
|
ps[2*t-1] = mbc
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
// Translate the control points.
|
||||||
|
bnorm := pRot90CCW(pNorm(c.Sub(a), k.u))
|
||||||
|
cnorm = pRot90CCW(pNorm(bc, k.u))
|
||||||
|
k.p.Add2(b.Add(bnorm), c.Add(cnorm))
|
||||||
|
k.r.Add2(b.Sub(bnorm), c.Sub(cnorm))
|
||||||
|
}
|
||||||
|
if t == 0 {
|
||||||
|
k.a, k.anorm = c, cnorm
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t--
|
||||||
|
anorm = cnorm
|
||||||
|
}
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add1 adds a linear segment to the stroker.
|
||||||
|
func (k *stroker) Add1(b fixed.Point26_6) {
|
||||||
|
bnorm := pRot90CCW(pNorm(b.Sub(k.a), k.u))
|
||||||
|
if len(k.r) == 0 {
|
||||||
|
k.p.Start(k.a.Add(bnorm))
|
||||||
|
k.r.Start(k.a.Sub(bnorm))
|
||||||
|
} else {
|
||||||
|
k.jr.Join(k.p, &k.r, k.u, k.a, k.anorm, bnorm)
|
||||||
|
}
|
||||||
|
k.p.Add1(b.Add(bnorm))
|
||||||
|
k.r.Add1(b.Sub(bnorm))
|
||||||
|
k.a, k.anorm = b, bnorm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add2 adds a quadratic segment to the stroker.
|
||||||
|
func (k *stroker) Add2(b, c fixed.Point26_6) {
|
||||||
|
ab := b.Sub(k.a)
|
||||||
|
bc := c.Sub(b)
|
||||||
|
abnorm := pRot90CCW(pNorm(ab, k.u))
|
||||||
|
if len(k.r) == 0 {
|
||||||
|
k.p.Start(k.a.Add(abnorm))
|
||||||
|
k.r.Start(k.a.Sub(abnorm))
|
||||||
|
} else {
|
||||||
|
k.jr.Join(k.p, &k.r, k.u, k.a, k.anorm, abnorm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Approximate nearly-degenerate quadratics by linear segments.
|
||||||
|
abIsSmall := pDot(ab, ab) < epsilon
|
||||||
|
bcIsSmall := pDot(bc, bc) < epsilon
|
||||||
|
if abIsSmall || bcIsSmall {
|
||||||
|
acnorm := pRot90CCW(pNorm(c.Sub(k.a), k.u))
|
||||||
|
k.p.Add1(c.Add(acnorm))
|
||||||
|
k.r.Add1(c.Sub(acnorm))
|
||||||
|
k.a, k.anorm = c, acnorm
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The quadratic segment (k.a, b, c) has a point of maximum curvature.
|
||||||
|
// If this occurs at an end point, we process the segment as a whole.
|
||||||
|
t := curviest2(k.a, b, c)
|
||||||
|
if t <= 0 || 4096 <= t {
|
||||||
|
k.addNonCurvy2(b, c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we perform a de Casteljau decomposition at the point of
|
||||||
|
// maximum curvature and process the two straighter parts.
|
||||||
|
mab := interpolate(k.a, b, t)
|
||||||
|
mbc := interpolate(b, c, t)
|
||||||
|
mabc := interpolate(mab, mbc, t)
|
||||||
|
|
||||||
|
// If the vectors ab and bc are close to being in opposite directions,
|
||||||
|
// then the decomposition can become unstable, so we approximate the
|
||||||
|
// quadratic segment by two linear segments joined by an arc.
|
||||||
|
bcnorm := pRot90CCW(pNorm(bc, k.u))
|
||||||
|
if pDot(abnorm, bcnorm) < -fixed.Int52_12(k.u)*fixed.Int52_12(k.u)*2047/2048 {
|
||||||
|
pArc := pDot(abnorm, bc) < 0
|
||||||
|
|
||||||
|
k.p.Add1(mabc.Add(abnorm))
|
||||||
|
if pArc {
|
||||||
|
z := pRot90CW(abnorm)
|
||||||
|
addArc(k.p, mabc, abnorm, z)
|
||||||
|
addArc(k.p, mabc, z, bcnorm)
|
||||||
|
}
|
||||||
|
k.p.Add1(mabc.Add(bcnorm))
|
||||||
|
k.p.Add1(c.Add(bcnorm))
|
||||||
|
|
||||||
|
k.r.Add1(mabc.Sub(abnorm))
|
||||||
|
if !pArc {
|
||||||
|
z := pRot90CW(abnorm)
|
||||||
|
addArc(&k.r, mabc, pNeg(abnorm), z)
|
||||||
|
addArc(&k.r, mabc, z, pNeg(bcnorm))
|
||||||
|
}
|
||||||
|
k.r.Add1(mabc.Sub(bcnorm))
|
||||||
|
k.r.Add1(c.Sub(bcnorm))
|
||||||
|
|
||||||
|
k.a, k.anorm = c, bcnorm
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the decomposed parts.
|
||||||
|
k.addNonCurvy2(mab, mabc)
|
||||||
|
k.addNonCurvy2(mbc, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add3 adds a cubic segment to the stroker.
|
||||||
|
func (k *stroker) Add3(b, c, d fixed.Point26_6) {
|
||||||
|
panic("freetype/raster: stroke unimplemented for cubic segments")
|
||||||
|
}
|
||||||
|
|
||||||
|
// stroke adds the stroked Path q to p, where q consists of exactly one curve.
|
||||||
|
func (k *stroker) stroke(q Path) {
|
||||||
|
// Stroking is implemented by deriving two paths each k.u apart from q.
|
||||||
|
// The left-hand-side path is added immediately to k.p; the right-hand-side
|
||||||
|
// path is accumulated in k.r. Once we've finished adding the LHS to k.p,
|
||||||
|
// we add the RHS in reverse order.
|
||||||
|
k.r = make(Path, 0, len(q))
|
||||||
|
k.a = fixed.Point26_6{q[1], q[2]}
|
||||||
|
for i := 4; i < len(q); {
|
||||||
|
switch q[i] {
|
||||||
|
case 1:
|
||||||
|
k.Add1(
|
||||||
|
fixed.Point26_6{q[i+1], q[i+2]},
|
||||||
|
)
|
||||||
|
i += 4
|
||||||
|
case 2:
|
||||||
|
k.Add2(
|
||||||
|
fixed.Point26_6{q[i+1], q[i+2]},
|
||||||
|
fixed.Point26_6{q[i+3], q[i+4]},
|
||||||
|
)
|
||||||
|
i += 6
|
||||||
|
case 3:
|
||||||
|
k.Add3(
|
||||||
|
fixed.Point26_6{q[i+1], q[i+2]},
|
||||||
|
fixed.Point26_6{q[i+3], q[i+4]},
|
||||||
|
fixed.Point26_6{q[i+5], q[i+6]},
|
||||||
|
)
|
||||||
|
i += 8
|
||||||
|
default:
|
||||||
|
panic("freetype/raster: bad path")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(k.r) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO(nigeltao): if q is a closed curve then we should join the first and
|
||||||
|
// last segments instead of capping them.
|
||||||
|
k.cr.Cap(k.p, k.u, q.lastPoint(), pNeg(k.anorm))
|
||||||
|
addPathReversed(k.p, k.r)
|
||||||
|
pivot := q.firstPoint()
|
||||||
|
k.cr.Cap(k.p, k.u, pivot, pivot.Sub(fixed.Point26_6{k.r[1], k.r[2]}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stroke adds q stroked with the given width to p. The result is typically
|
||||||
|
// self-intersecting and should be rasterized with UseNonZeroWinding.
|
||||||
|
// cr and jr may be nil, which defaults to a RoundCapper or RoundJoiner.
|
||||||
|
func Stroke(p Adder, q Path, width fixed.Int26_6, cr Capper, jr Joiner) {
|
||||||
|
if len(q) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cr == nil {
|
||||||
|
cr = RoundCapper
|
||||||
|
}
|
||||||
|
if jr == nil {
|
||||||
|
jr = RoundJoiner
|
||||||
|
}
|
||||||
|
if q[0] != 0 {
|
||||||
|
panic("freetype/raster: bad path")
|
||||||
|
}
|
||||||
|
s := stroker{p: p, u: width / 2, cr: cr, jr: jr}
|
||||||
|
i := 0
|
||||||
|
for j := 4; j < len(q); {
|
||||||
|
switch q[j] {
|
||||||
|
case 0:
|
||||||
|
s.stroke(q[i:j])
|
||||||
|
i, j = j, j+4
|
||||||
|
case 1:
|
||||||
|
j += 4
|
||||||
|
case 2:
|
||||||
|
j += 6
|
||||||
|
case 3:
|
||||||
|
j += 8
|
||||||
|
default:
|
||||||
|
panic("freetype/raster: bad path")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.stroke(q[i:])
|
||||||
|
}
|
|
@ -0,0 +1,507 @@
|
||||||
|
// Copyright 2015 The Freetype-Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by your choice of either the
|
||||||
|
// FreeType License or the GNU General Public License version 2 (or
|
||||||
|
// any later version), both of which can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package truetype
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/raster"
|
||||||
|
"golang.org/x/image/font"
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
)
|
||||||
|
|
||||||
|
func powerOf2(i int) bool {
|
||||||
|
return i != 0 && (i&(i-1)) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options are optional arguments to NewFace.
|
||||||
|
type Options struct {
|
||||||
|
// Size is the font size in points, as in "a 10 point font size".
|
||||||
|
//
|
||||||
|
// A zero value means to use a 12 point font size.
|
||||||
|
Size float64
|
||||||
|
|
||||||
|
// DPI is the dots-per-inch resolution.
|
||||||
|
//
|
||||||
|
// A zero value means to use 72 DPI.
|
||||||
|
DPI float64
|
||||||
|
|
||||||
|
// Hinting is how to quantize the glyph nodes.
|
||||||
|
//
|
||||||
|
// A zero value means to use no hinting.
|
||||||
|
Hinting font.Hinting
|
||||||
|
|
||||||
|
// GlyphCacheEntries is the number of entries in the glyph mask image
|
||||||
|
// cache.
|
||||||
|
//
|
||||||
|
// If non-zero, it must be a power of 2.
|
||||||
|
//
|
||||||
|
// A zero value means to use 512 entries.
|
||||||
|
GlyphCacheEntries int
|
||||||
|
|
||||||
|
// SubPixelsX is the number of sub-pixel locations a glyph's dot is
|
||||||
|
// quantized to, in the horizontal direction. For example, a value of 8
|
||||||
|
// means that the dot is quantized to 1/8th of a pixel. This quantization
|
||||||
|
// only affects the glyph mask image, not its bounding box or advance
|
||||||
|
// width. A higher value gives a more faithful glyph image, but reduces the
|
||||||
|
// effectiveness of the glyph cache.
|
||||||
|
//
|
||||||
|
// If non-zero, it must be a power of 2, and be between 1 and 64 inclusive.
|
||||||
|
//
|
||||||
|
// A zero value means to use 4 sub-pixel locations.
|
||||||
|
SubPixelsX int
|
||||||
|
|
||||||
|
// SubPixelsY is the number of sub-pixel locations a glyph's dot is
|
||||||
|
// quantized to, in the vertical direction. For example, a value of 8
|
||||||
|
// means that the dot is quantized to 1/8th of a pixel. This quantization
|
||||||
|
// only affects the glyph mask image, not its bounding box or advance
|
||||||
|
// width. A higher value gives a more faithful glyph image, but reduces the
|
||||||
|
// effectiveness of the glyph cache.
|
||||||
|
//
|
||||||
|
// If non-zero, it must be a power of 2, and be between 1 and 64 inclusive.
|
||||||
|
//
|
||||||
|
// A zero value means to use 1 sub-pixel location.
|
||||||
|
SubPixelsY int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Options) size() float64 {
|
||||||
|
if o != nil && o.Size > 0 {
|
||||||
|
return o.Size
|
||||||
|
}
|
||||||
|
return 12
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Options) dpi() float64 {
|
||||||
|
if o != nil && o.DPI > 0 {
|
||||||
|
return o.DPI
|
||||||
|
}
|
||||||
|
return 72
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Options) hinting() font.Hinting {
|
||||||
|
if o != nil {
|
||||||
|
switch o.Hinting {
|
||||||
|
case font.HintingVertical, font.HintingFull:
|
||||||
|
// TODO: support vertical hinting.
|
||||||
|
return font.HintingFull
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return font.HintingNone
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Options) glyphCacheEntries() int {
|
||||||
|
if o != nil && powerOf2(o.GlyphCacheEntries) {
|
||||||
|
return o.GlyphCacheEntries
|
||||||
|
}
|
||||||
|
// 512 is 128 * 4 * 1, which lets us cache 128 glyphs at 4 * 1 subpixel
|
||||||
|
// locations in the X and Y direction.
|
||||||
|
return 512
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Options) subPixelsX() (value uint32, halfQuantum, mask fixed.Int26_6) {
|
||||||
|
if o != nil {
|
||||||
|
switch o.SubPixelsX {
|
||||||
|
case 1, 2, 4, 8, 16, 32, 64:
|
||||||
|
return subPixels(o.SubPixelsX)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This default value of 4 isn't based on anything scientific, merely as
|
||||||
|
// small a number as possible that looks almost as good as no quantization,
|
||||||
|
// or returning subPixels(64).
|
||||||
|
return subPixels(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Options) subPixelsY() (value uint32, halfQuantum, mask fixed.Int26_6) {
|
||||||
|
if o != nil {
|
||||||
|
switch o.SubPixelsX {
|
||||||
|
case 1, 2, 4, 8, 16, 32, 64:
|
||||||
|
return subPixels(o.SubPixelsX)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This default value of 1 isn't based on anything scientific, merely that
|
||||||
|
// vertical sub-pixel glyph rendering is pretty rare. Baseline locations
|
||||||
|
// can usually afford to snap to the pixel grid, so the vertical direction
|
||||||
|
// doesn't have the deal with the horizontal's fractional advance widths.
|
||||||
|
return subPixels(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// subPixels returns q and the bias and mask that leads to q quantized
|
||||||
|
// sub-pixel locations per full pixel.
|
||||||
|
//
|
||||||
|
// For example, q == 4 leads to a bias of 8 and a mask of 0xfffffff0, or -16,
|
||||||
|
// because we want to round fractions of fixed.Int26_6 as:
|
||||||
|
// - 0 to 7 rounds to 0.
|
||||||
|
// - 8 to 23 rounds to 16.
|
||||||
|
// - 24 to 39 rounds to 32.
|
||||||
|
// - 40 to 55 rounds to 48.
|
||||||
|
// - 56 to 63 rounds to 64.
|
||||||
|
// which means to add 8 and then bitwise-and with -16, in two's complement
|
||||||
|
// representation.
|
||||||
|
//
|
||||||
|
// When q == 1, we want bias == 32 and mask == -64.
|
||||||
|
// When q == 2, we want bias == 16 and mask == -32.
|
||||||
|
// When q == 4, we want bias == 8 and mask == -16.
|
||||||
|
// ...
|
||||||
|
// When q == 64, we want bias == 0 and mask == -1. (The no-op case).
|
||||||
|
// The pattern is clear.
|
||||||
|
func subPixels(q int) (value uint32, bias, mask fixed.Int26_6) {
|
||||||
|
return uint32(q), 32 / fixed.Int26_6(q), -64 / fixed.Int26_6(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
// glyphCacheEntry caches the arguments and return values of rasterize.
|
||||||
|
type glyphCacheEntry struct {
|
||||||
|
key glyphCacheKey
|
||||||
|
val glyphCacheVal
|
||||||
|
}
|
||||||
|
|
||||||
|
type glyphCacheKey struct {
|
||||||
|
index Index
|
||||||
|
fx, fy uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
type glyphCacheVal struct {
|
||||||
|
advanceWidth fixed.Int26_6
|
||||||
|
offset image.Point
|
||||||
|
gw int
|
||||||
|
gh int
|
||||||
|
}
|
||||||
|
|
||||||
|
type indexCacheEntry struct {
|
||||||
|
rune rune
|
||||||
|
index Index
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFace returns a new font.Face for the given Font.
|
||||||
|
func NewFace(f *Font, opts *Options) font.Face {
|
||||||
|
a := &face{
|
||||||
|
f: f,
|
||||||
|
hinting: opts.hinting(),
|
||||||
|
scale: fixed.Int26_6(0.5 + (opts.size() * opts.dpi() * 64 / 72)),
|
||||||
|
glyphCache: make([]glyphCacheEntry, opts.glyphCacheEntries()),
|
||||||
|
}
|
||||||
|
a.subPixelX, a.subPixelBiasX, a.subPixelMaskX = opts.subPixelsX()
|
||||||
|
a.subPixelY, a.subPixelBiasY, a.subPixelMaskY = opts.subPixelsY()
|
||||||
|
|
||||||
|
// Fill the cache with invalid entries. Valid glyph cache entries have fx
|
||||||
|
// and fy in the range [0, 64). Valid index cache entries have rune >= 0.
|
||||||
|
for i := range a.glyphCache {
|
||||||
|
a.glyphCache[i].key.fy = 0xff
|
||||||
|
}
|
||||||
|
for i := range a.indexCache {
|
||||||
|
a.indexCache[i].rune = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the rasterizer's bounds to be big enough to handle the largest glyph.
|
||||||
|
b := f.Bounds(a.scale)
|
||||||
|
xmin := +int(b.Min.X) >> 6
|
||||||
|
ymin := -int(b.Max.Y) >> 6
|
||||||
|
xmax := +int(b.Max.X+63) >> 6
|
||||||
|
ymax := -int(b.Min.Y-63) >> 6
|
||||||
|
a.maxw = xmax - xmin
|
||||||
|
a.maxh = ymax - ymin
|
||||||
|
a.masks = image.NewAlpha(image.Rect(0, 0, a.maxw, a.maxh*len(a.glyphCache)))
|
||||||
|
a.r.SetBounds(a.maxw, a.maxh)
|
||||||
|
a.p = facePainter{a}
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
type face struct {
|
||||||
|
f *Font
|
||||||
|
hinting font.Hinting
|
||||||
|
scale fixed.Int26_6
|
||||||
|
subPixelX uint32
|
||||||
|
subPixelBiasX fixed.Int26_6
|
||||||
|
subPixelMaskX fixed.Int26_6
|
||||||
|
subPixelY uint32
|
||||||
|
subPixelBiasY fixed.Int26_6
|
||||||
|
subPixelMaskY fixed.Int26_6
|
||||||
|
masks *image.Alpha
|
||||||
|
glyphCache []glyphCacheEntry
|
||||||
|
r raster.Rasterizer
|
||||||
|
p raster.Painter
|
||||||
|
paintOffset int
|
||||||
|
maxw int
|
||||||
|
maxh int
|
||||||
|
glyphBuf GlyphBuf
|
||||||
|
indexCache [indexCacheLen]indexCacheEntry
|
||||||
|
|
||||||
|
// TODO: clip rectangle?
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexCacheLen = 256
|
||||||
|
|
||||||
|
func (a *face) index(r rune) Index {
|
||||||
|
const mask = indexCacheLen - 1
|
||||||
|
c := &a.indexCache[r&mask]
|
||||||
|
if c.rune == r {
|
||||||
|
return c.index
|
||||||
|
}
|
||||||
|
i := a.f.Index(r)
|
||||||
|
c.rune = r
|
||||||
|
c.index = i
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close satisfies the font.Face interface.
|
||||||
|
func (a *face) Close() error { return nil }
|
||||||
|
|
||||||
|
// Metrics satisfies the font.Face interface.
|
||||||
|
func (a *face) Metrics() font.Metrics {
|
||||||
|
scale := float64(a.scale)
|
||||||
|
fupe := float64(a.f.FUnitsPerEm())
|
||||||
|
return font.Metrics{
|
||||||
|
Height: a.scale,
|
||||||
|
Ascent: fixed.Int26_6(math.Ceil(scale * float64(+a.f.ascent) / fupe)),
|
||||||
|
Descent: fixed.Int26_6(math.Ceil(scale * float64(-a.f.descent) / fupe)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kern satisfies the font.Face interface.
|
||||||
|
func (a *face) Kern(r0, r1 rune) fixed.Int26_6 {
|
||||||
|
i0 := a.index(r0)
|
||||||
|
i1 := a.index(r1)
|
||||||
|
kern := a.f.Kern(a.scale, i0, i1)
|
||||||
|
if a.hinting != font.HintingNone {
|
||||||
|
kern = (kern + 32) &^ 63
|
||||||
|
}
|
||||||
|
return kern
|
||||||
|
}
|
||||||
|
|
||||||
|
// Glyph satisfies the font.Face interface.
|
||||||
|
func (a *face) Glyph(dot fixed.Point26_6, r rune) (
|
||||||
|
dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) {
|
||||||
|
|
||||||
|
// Quantize to the sub-pixel granularity.
|
||||||
|
dotX := (dot.X + a.subPixelBiasX) & a.subPixelMaskX
|
||||||
|
dotY := (dot.Y + a.subPixelBiasY) & a.subPixelMaskY
|
||||||
|
|
||||||
|
// Split the coordinates into their integer and fractional parts.
|
||||||
|
ix, fx := int(dotX>>6), dotX&0x3f
|
||||||
|
iy, fy := int(dotY>>6), dotY&0x3f
|
||||||
|
|
||||||
|
index := a.index(r)
|
||||||
|
cIndex := uint32(index)
|
||||||
|
cIndex = cIndex*a.subPixelX - uint32(fx/a.subPixelMaskX)
|
||||||
|
cIndex = cIndex*a.subPixelY - uint32(fy/a.subPixelMaskY)
|
||||||
|
cIndex &= uint32(len(a.glyphCache) - 1)
|
||||||
|
a.paintOffset = a.maxh * int(cIndex)
|
||||||
|
k := glyphCacheKey{
|
||||||
|
index: index,
|
||||||
|
fx: uint8(fx),
|
||||||
|
fy: uint8(fy),
|
||||||
|
}
|
||||||
|
var v glyphCacheVal
|
||||||
|
if a.glyphCache[cIndex].key != k {
|
||||||
|
var ok bool
|
||||||
|
v, ok = a.rasterize(index, fx, fy)
|
||||||
|
if !ok {
|
||||||
|
return image.Rectangle{}, nil, image.Point{}, 0, false
|
||||||
|
}
|
||||||
|
a.glyphCache[cIndex] = glyphCacheEntry{k, v}
|
||||||
|
} else {
|
||||||
|
v = a.glyphCache[cIndex].val
|
||||||
|
}
|
||||||
|
|
||||||
|
dr.Min = image.Point{
|
||||||
|
X: ix + v.offset.X,
|
||||||
|
Y: iy + v.offset.Y,
|
||||||
|
}
|
||||||
|
dr.Max = image.Point{
|
||||||
|
X: dr.Min.X + v.gw,
|
||||||
|
Y: dr.Min.Y + v.gh,
|
||||||
|
}
|
||||||
|
return dr, a.masks, image.Point{Y: a.paintOffset}, v.advanceWidth, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *face) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) {
|
||||||
|
if err := a.glyphBuf.Load(a.f, a.scale, a.index(r), a.hinting); err != nil {
|
||||||
|
return fixed.Rectangle26_6{}, 0, false
|
||||||
|
}
|
||||||
|
xmin := +a.glyphBuf.Bounds.Min.X
|
||||||
|
ymin := -a.glyphBuf.Bounds.Max.Y
|
||||||
|
xmax := +a.glyphBuf.Bounds.Max.X
|
||||||
|
ymax := -a.glyphBuf.Bounds.Min.Y
|
||||||
|
if xmin > xmax || ymin > ymax {
|
||||||
|
return fixed.Rectangle26_6{}, 0, false
|
||||||
|
}
|
||||||
|
return fixed.Rectangle26_6{
|
||||||
|
Min: fixed.Point26_6{
|
||||||
|
X: xmin,
|
||||||
|
Y: ymin,
|
||||||
|
},
|
||||||
|
Max: fixed.Point26_6{
|
||||||
|
X: xmax,
|
||||||
|
Y: ymax,
|
||||||
|
},
|
||||||
|
}, a.glyphBuf.AdvanceWidth, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *face) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) {
|
||||||
|
if err := a.glyphBuf.Load(a.f, a.scale, a.index(r), a.hinting); err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return a.glyphBuf.AdvanceWidth, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// rasterize returns the advance width, integer-pixel offset to render at, and
|
||||||
|
// the width and height of the given glyph at the given sub-pixel offsets.
|
||||||
|
//
|
||||||
|
// The 26.6 fixed point arguments fx and fy must be in the range [0, 1).
|
||||||
|
func (a *face) rasterize(index Index, fx, fy fixed.Int26_6) (v glyphCacheVal, ok bool) {
|
||||||
|
if err := a.glyphBuf.Load(a.f, a.scale, index, a.hinting); err != nil {
|
||||||
|
return glyphCacheVal{}, false
|
||||||
|
}
|
||||||
|
// Calculate the integer-pixel bounds for the glyph.
|
||||||
|
xmin := int(fx+a.glyphBuf.Bounds.Min.X) >> 6
|
||||||
|
ymin := int(fy-a.glyphBuf.Bounds.Max.Y) >> 6
|
||||||
|
xmax := int(fx+a.glyphBuf.Bounds.Max.X+0x3f) >> 6
|
||||||
|
ymax := int(fy-a.glyphBuf.Bounds.Min.Y+0x3f) >> 6
|
||||||
|
if xmin > xmax || ymin > ymax {
|
||||||
|
return glyphCacheVal{}, false
|
||||||
|
}
|
||||||
|
// A TrueType's glyph's nodes can have negative co-ordinates, but the
|
||||||
|
// rasterizer clips anything left of x=0 or above y=0. xmin and ymin are
|
||||||
|
// the pixel offsets, based on the font's FUnit metrics, that let a
|
||||||
|
// negative co-ordinate in TrueType space be non-negative in rasterizer
|
||||||
|
// space. xmin and ymin are typically <= 0.
|
||||||
|
fx -= fixed.Int26_6(xmin << 6)
|
||||||
|
fy -= fixed.Int26_6(ymin << 6)
|
||||||
|
// Rasterize the glyph's vectors.
|
||||||
|
a.r.Clear()
|
||||||
|
pixOffset := a.paintOffset * a.maxw
|
||||||
|
clear(a.masks.Pix[pixOffset : pixOffset+a.maxw*a.maxh])
|
||||||
|
e0 := 0
|
||||||
|
for _, e1 := range a.glyphBuf.Ends {
|
||||||
|
a.drawContour(a.glyphBuf.Points[e0:e1], fx, fy)
|
||||||
|
e0 = e1
|
||||||
|
}
|
||||||
|
a.r.Rasterize(a.p)
|
||||||
|
return glyphCacheVal{
|
||||||
|
a.glyphBuf.AdvanceWidth,
|
||||||
|
image.Point{xmin, ymin},
|
||||||
|
xmax - xmin,
|
||||||
|
ymax - ymin,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func clear(pix []byte) {
|
||||||
|
for i := range pix {
|
||||||
|
pix[i] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawContour draws the given closed contour with the given offset.
|
||||||
|
func (a *face) drawContour(ps []Point, dx, dy fixed.Int26_6) {
|
||||||
|
if len(ps) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The low bit of each point's Flags value is whether the point is on the
|
||||||
|
// curve. Truetype fonts only have quadratic Bézier curves, not cubics.
|
||||||
|
// Thus, two consecutive off-curve points imply an on-curve point in the
|
||||||
|
// middle of those two.
|
||||||
|
//
|
||||||
|
// See http://chanae.walon.org/pub/ttf/ttf_glyphs.htm for more details.
|
||||||
|
|
||||||
|
// ps[0] is a truetype.Point measured in FUnits and positive Y going
|
||||||
|
// upwards. start is the same thing measured in fixed point units and
|
||||||
|
// positive Y going downwards, and offset by (dx, dy).
|
||||||
|
start := fixed.Point26_6{
|
||||||
|
X: dx + ps[0].X,
|
||||||
|
Y: dy - ps[0].Y,
|
||||||
|
}
|
||||||
|
var others []Point
|
||||||
|
if ps[0].Flags&0x01 != 0 {
|
||||||
|
others = ps[1:]
|
||||||
|
} else {
|
||||||
|
last := fixed.Point26_6{
|
||||||
|
X: dx + ps[len(ps)-1].X,
|
||||||
|
Y: dy - ps[len(ps)-1].Y,
|
||||||
|
}
|
||||||
|
if ps[len(ps)-1].Flags&0x01 != 0 {
|
||||||
|
start = last
|
||||||
|
others = ps[:len(ps)-1]
|
||||||
|
} else {
|
||||||
|
start = fixed.Point26_6{
|
||||||
|
X: (start.X + last.X) / 2,
|
||||||
|
Y: (start.Y + last.Y) / 2,
|
||||||
|
}
|
||||||
|
others = ps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.r.Start(start)
|
||||||
|
q0, on0 := start, true
|
||||||
|
for _, p := range others {
|
||||||
|
q := fixed.Point26_6{
|
||||||
|
X: dx + p.X,
|
||||||
|
Y: dy - p.Y,
|
||||||
|
}
|
||||||
|
on := p.Flags&0x01 != 0
|
||||||
|
if on {
|
||||||
|
if on0 {
|
||||||
|
a.r.Add1(q)
|
||||||
|
} else {
|
||||||
|
a.r.Add2(q0, q)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if on0 {
|
||||||
|
// No-op.
|
||||||
|
} else {
|
||||||
|
mid := fixed.Point26_6{
|
||||||
|
X: (q0.X + q.X) / 2,
|
||||||
|
Y: (q0.Y + q.Y) / 2,
|
||||||
|
}
|
||||||
|
a.r.Add2(q0, mid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
q0, on0 = q, on
|
||||||
|
}
|
||||||
|
// Close the curve.
|
||||||
|
if on0 {
|
||||||
|
a.r.Add1(start)
|
||||||
|
} else {
|
||||||
|
a.r.Add2(q0, start)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// facePainter is like a raster.AlphaSrcPainter, with an additional Y offset
|
||||||
|
// (face.paintOffset) to the painted spans.
|
||||||
|
type facePainter struct {
|
||||||
|
a *face
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p facePainter) Paint(ss []raster.Span, done bool) {
|
||||||
|
m := p.a.masks
|
||||||
|
b := m.Bounds()
|
||||||
|
b.Min.Y = p.a.paintOffset
|
||||||
|
b.Max.Y = p.a.paintOffset + p.a.maxh
|
||||||
|
for _, s := range ss {
|
||||||
|
s.Y += p.a.paintOffset
|
||||||
|
if s.Y < b.Min.Y {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s.Y >= b.Max.Y {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.X0 < b.Min.X {
|
||||||
|
s.X0 = b.Min.X
|
||||||
|
}
|
||||||
|
if s.X1 > b.Max.X {
|
||||||
|
s.X1 = b.Max.X
|
||||||
|
}
|
||||||
|
if s.X0 >= s.X1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
base := (s.Y-m.Rect.Min.Y)*m.Stride - m.Rect.Min.X
|
||||||
|
p := m.Pix[base+s.X0 : base+s.X1]
|
||||||
|
color := uint8(s.Alpha >> 8)
|
||||||
|
for i := range p {
|
||||||
|
p[i] = color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,522 @@
|
||||||
|
// Copyright 2010 The Freetype-Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by your choice of either the
|
||||||
|
// FreeType License or the GNU General Public License version 2 (or
|
||||||
|
// any later version), both of which can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package truetype
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/image/font"
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: implement VerticalHinting.
|
||||||
|
|
||||||
|
// A Point is a co-ordinate pair plus whether it is 'on' a contour or an 'off'
|
||||||
|
// control point.
|
||||||
|
type Point struct {
|
||||||
|
X, Y fixed.Int26_6
|
||||||
|
// The Flags' LSB means whether or not this Point is 'on' the contour.
|
||||||
|
// Other bits are reserved for internal use.
|
||||||
|
Flags uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// A GlyphBuf holds a glyph's contours. A GlyphBuf can be re-used to load a
|
||||||
|
// series of glyphs from a Font.
|
||||||
|
type GlyphBuf struct {
|
||||||
|
// AdvanceWidth is the glyph's advance width.
|
||||||
|
AdvanceWidth fixed.Int26_6
|
||||||
|
// Bounds is the glyph's bounding box.
|
||||||
|
Bounds fixed.Rectangle26_6
|
||||||
|
// Points contains all Points from all contours of the glyph. If hinting
|
||||||
|
// was used to load a glyph then Unhinted contains those Points before they
|
||||||
|
// were hinted, and InFontUnits contains those Points before they were
|
||||||
|
// hinted and scaled.
|
||||||
|
Points, Unhinted, InFontUnits []Point
|
||||||
|
// Ends is the point indexes of the end point of each contour. The length
|
||||||
|
// of Ends is the number of contours in the glyph. The i'th contour
|
||||||
|
// consists of points Points[Ends[i-1]:Ends[i]], where Ends[-1] is
|
||||||
|
// interpreted to mean zero.
|
||||||
|
Ends []int
|
||||||
|
|
||||||
|
font *Font
|
||||||
|
scale fixed.Int26_6
|
||||||
|
hinting font.Hinting
|
||||||
|
hinter hinter
|
||||||
|
// phantomPoints are the co-ordinates of the synthetic phantom points
|
||||||
|
// used for hinting and bounding box calculations.
|
||||||
|
phantomPoints [4]Point
|
||||||
|
// pp1x is the X co-ordinate of the first phantom point. The '1' is
|
||||||
|
// using 1-based indexing; pp1x is almost always phantomPoints[0].X.
|
||||||
|
// TODO: eliminate this and consistently use phantomPoints[0].X.
|
||||||
|
pp1x fixed.Int26_6
|
||||||
|
// metricsSet is whether the glyph's metrics have been set yet. For a
|
||||||
|
// compound glyph, a sub-glyph may override the outer glyph's metrics.
|
||||||
|
metricsSet bool
|
||||||
|
// tmp is a scratch buffer.
|
||||||
|
tmp []Point
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flags for decoding a glyph's contours. These flags are documented at
|
||||||
|
// http://developer.apple.com/fonts/TTRefMan/RM06/Chap6glyf.html.
|
||||||
|
const (
|
||||||
|
flagOnCurve = 1 << iota
|
||||||
|
flagXShortVector
|
||||||
|
flagYShortVector
|
||||||
|
flagRepeat
|
||||||
|
flagPositiveXShortVector
|
||||||
|
flagPositiveYShortVector
|
||||||
|
|
||||||
|
// The remaining flags are for internal use.
|
||||||
|
flagTouchedX
|
||||||
|
flagTouchedY
|
||||||
|
)
|
||||||
|
|
||||||
|
// The same flag bits (0x10 and 0x20) are overloaded to have two meanings,
|
||||||
|
// dependent on the value of the flag{X,Y}ShortVector bits.
|
||||||
|
const (
|
||||||
|
flagThisXIsSame = flagPositiveXShortVector
|
||||||
|
flagThisYIsSame = flagPositiveYShortVector
|
||||||
|
)
|
||||||
|
|
||||||
|
// Load loads a glyph's contours from a Font, overwriting any previously loaded
|
||||||
|
// contours for this GlyphBuf. scale is the number of 26.6 fixed point units in
|
||||||
|
// 1 em, i is the glyph index, and h is the hinting policy.
|
||||||
|
func (g *GlyphBuf) Load(f *Font, scale fixed.Int26_6, i Index, h font.Hinting) error {
|
||||||
|
g.Points = g.Points[:0]
|
||||||
|
g.Unhinted = g.Unhinted[:0]
|
||||||
|
g.InFontUnits = g.InFontUnits[:0]
|
||||||
|
g.Ends = g.Ends[:0]
|
||||||
|
g.font = f
|
||||||
|
g.hinting = h
|
||||||
|
g.scale = scale
|
||||||
|
g.pp1x = 0
|
||||||
|
g.phantomPoints = [4]Point{}
|
||||||
|
g.metricsSet = false
|
||||||
|
|
||||||
|
if h != font.HintingNone {
|
||||||
|
if err := g.hinter.init(f, scale); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := g.load(0, i, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// TODO: this selection of either g.pp1x or g.phantomPoints[0].X isn't ideal,
|
||||||
|
// and should be cleaned up once we have all the testScaling tests passing,
|
||||||
|
// plus additional tests for Freetype-Go's bounding boxes matching C Freetype's.
|
||||||
|
pp1x := g.pp1x
|
||||||
|
if h != font.HintingNone {
|
||||||
|
pp1x = g.phantomPoints[0].X
|
||||||
|
}
|
||||||
|
if pp1x != 0 {
|
||||||
|
for i := range g.Points {
|
||||||
|
g.Points[i].X -= pp1x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
advanceWidth := g.phantomPoints[1].X - g.phantomPoints[0].X
|
||||||
|
if h != font.HintingNone {
|
||||||
|
if len(f.hdmx) >= 8 {
|
||||||
|
if n := u32(f.hdmx, 4); n > 3+uint32(i) {
|
||||||
|
for hdmx := f.hdmx[8:]; uint32(len(hdmx)) >= n; hdmx = hdmx[n:] {
|
||||||
|
if fixed.Int26_6(hdmx[0]) == scale>>6 {
|
||||||
|
advanceWidth = fixed.Int26_6(hdmx[2+i]) << 6
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
advanceWidth = (advanceWidth + 32) &^ 63
|
||||||
|
}
|
||||||
|
g.AdvanceWidth = advanceWidth
|
||||||
|
|
||||||
|
// Set g.Bounds to the 'control box', which is the bounding box of the
|
||||||
|
// Bézier curves' control points. This is easier to calculate, no smaller
|
||||||
|
// than and often equal to the tightest possible bounding box of the curves
|
||||||
|
// themselves. This approach is what C Freetype does. We can't just scale
|
||||||
|
// the nominal bounding box in the glyf data as the hinting process and
|
||||||
|
// phantom point adjustment may move points outside of that box.
|
||||||
|
if len(g.Points) == 0 {
|
||||||
|
g.Bounds = fixed.Rectangle26_6{}
|
||||||
|
} else {
|
||||||
|
p := g.Points[0]
|
||||||
|
g.Bounds.Min.X = p.X
|
||||||
|
g.Bounds.Max.X = p.X
|
||||||
|
g.Bounds.Min.Y = p.Y
|
||||||
|
g.Bounds.Max.Y = p.Y
|
||||||
|
for _, p := range g.Points[1:] {
|
||||||
|
if g.Bounds.Min.X > p.X {
|
||||||
|
g.Bounds.Min.X = p.X
|
||||||
|
} else if g.Bounds.Max.X < p.X {
|
||||||
|
g.Bounds.Max.X = p.X
|
||||||
|
}
|
||||||
|
if g.Bounds.Min.Y > p.Y {
|
||||||
|
g.Bounds.Min.Y = p.Y
|
||||||
|
} else if g.Bounds.Max.Y < p.Y {
|
||||||
|
g.Bounds.Max.Y = p.Y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Snap the box to the grid, if hinting is on.
|
||||||
|
if h != font.HintingNone {
|
||||||
|
g.Bounds.Min.X &^= 63
|
||||||
|
g.Bounds.Min.Y &^= 63
|
||||||
|
g.Bounds.Max.X += 63
|
||||||
|
g.Bounds.Max.X &^= 63
|
||||||
|
g.Bounds.Max.Y += 63
|
||||||
|
g.Bounds.Max.Y &^= 63
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GlyphBuf) load(recursion uint32, i Index, useMyMetrics bool) (err error) {
|
||||||
|
// The recursion limit here is arbitrary, but defends against malformed glyphs.
|
||||||
|
if recursion >= 32 {
|
||||||
|
return UnsupportedError("excessive compound glyph recursion")
|
||||||
|
}
|
||||||
|
// Find the relevant slice of g.font.glyf.
|
||||||
|
var g0, g1 uint32
|
||||||
|
if g.font.locaOffsetFormat == locaOffsetFormatShort {
|
||||||
|
g0 = 2 * uint32(u16(g.font.loca, 2*int(i)))
|
||||||
|
g1 = 2 * uint32(u16(g.font.loca, 2*int(i)+2))
|
||||||
|
} else {
|
||||||
|
g0 = u32(g.font.loca, 4*int(i))
|
||||||
|
g1 = u32(g.font.loca, 4*int(i)+4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the contour count and nominal bounding box, from the first
|
||||||
|
// 10 bytes of the glyf data. boundsYMin and boundsXMax, at offsets 4
|
||||||
|
// and 6, are unused.
|
||||||
|
glyf, ne, boundsXMin, boundsYMax := []byte(nil), 0, fixed.Int26_6(0), fixed.Int26_6(0)
|
||||||
|
if g0+10 <= g1 {
|
||||||
|
glyf = g.font.glyf[g0:g1]
|
||||||
|
ne = int(int16(u16(glyf, 0)))
|
||||||
|
boundsXMin = fixed.Int26_6(int16(u16(glyf, 2)))
|
||||||
|
boundsYMax = fixed.Int26_6(int16(u16(glyf, 8)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the phantom points.
|
||||||
|
uhm, pp1x := g.font.unscaledHMetric(i), fixed.Int26_6(0)
|
||||||
|
uvm := g.font.unscaledVMetric(i, boundsYMax)
|
||||||
|
g.phantomPoints = [4]Point{
|
||||||
|
{X: boundsXMin - uhm.LeftSideBearing},
|
||||||
|
{X: boundsXMin - uhm.LeftSideBearing + uhm.AdvanceWidth},
|
||||||
|
{X: uhm.AdvanceWidth / 2, Y: boundsYMax + uvm.TopSideBearing},
|
||||||
|
{X: uhm.AdvanceWidth / 2, Y: boundsYMax + uvm.TopSideBearing - uvm.AdvanceHeight},
|
||||||
|
}
|
||||||
|
if len(glyf) == 0 {
|
||||||
|
g.addPhantomsAndScale(len(g.Points), len(g.Points), true, true)
|
||||||
|
copy(g.phantomPoints[:], g.Points[len(g.Points)-4:])
|
||||||
|
g.Points = g.Points[:len(g.Points)-4]
|
||||||
|
// TODO: also trim g.InFontUnits and g.Unhinted?
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load and hint the contours.
|
||||||
|
if ne < 0 {
|
||||||
|
if ne != -1 {
|
||||||
|
// http://developer.apple.com/fonts/TTRefMan/RM06/Chap6glyf.html says that
|
||||||
|
// "the values -2, -3, and so forth, are reserved for future use."
|
||||||
|
return UnsupportedError("negative number of contours")
|
||||||
|
}
|
||||||
|
pp1x = g.font.scale(g.scale * (boundsXMin - uhm.LeftSideBearing))
|
||||||
|
if err := g.loadCompound(recursion, uhm, i, glyf, useMyMetrics); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
np0, ne0 := len(g.Points), len(g.Ends)
|
||||||
|
program := g.loadSimple(glyf, ne)
|
||||||
|
g.addPhantomsAndScale(np0, np0, true, true)
|
||||||
|
pp1x = g.Points[len(g.Points)-4].X
|
||||||
|
if g.hinting != font.HintingNone {
|
||||||
|
if len(program) != 0 {
|
||||||
|
err := g.hinter.run(
|
||||||
|
program,
|
||||||
|
g.Points[np0:],
|
||||||
|
g.Unhinted[np0:],
|
||||||
|
g.InFontUnits[np0:],
|
||||||
|
g.Ends[ne0:],
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Drop the four phantom points.
|
||||||
|
g.InFontUnits = g.InFontUnits[:len(g.InFontUnits)-4]
|
||||||
|
g.Unhinted = g.Unhinted[:len(g.Unhinted)-4]
|
||||||
|
}
|
||||||
|
if useMyMetrics {
|
||||||
|
copy(g.phantomPoints[:], g.Points[len(g.Points)-4:])
|
||||||
|
}
|
||||||
|
g.Points = g.Points[:len(g.Points)-4]
|
||||||
|
if np0 != 0 {
|
||||||
|
// The hinting program expects the []Ends values to be indexed
|
||||||
|
// relative to the inner glyph, not the outer glyph, so we delay
|
||||||
|
// adding np0 until after the hinting program (if any) has run.
|
||||||
|
for i := ne0; i < len(g.Ends); i++ {
|
||||||
|
g.Ends[i] += np0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if useMyMetrics && !g.metricsSet {
|
||||||
|
g.metricsSet = true
|
||||||
|
g.pp1x = pp1x
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadOffset is the initial offset for loadSimple and loadCompound. The first
|
||||||
|
// 10 bytes are the number of contours and the bounding box.
|
||||||
|
const loadOffset = 10
|
||||||
|
|
||||||
|
func (g *GlyphBuf) loadSimple(glyf []byte, ne int) (program []byte) {
|
||||||
|
offset := loadOffset
|
||||||
|
for i := 0; i < ne; i++ {
|
||||||
|
g.Ends = append(g.Ends, 1+int(u16(glyf, offset)))
|
||||||
|
offset += 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note the TrueType hinting instructions.
|
||||||
|
instrLen := int(u16(glyf, offset))
|
||||||
|
offset += 2
|
||||||
|
program = glyf[offset : offset+instrLen]
|
||||||
|
offset += instrLen
|
||||||
|
|
||||||
|
if ne == 0 {
|
||||||
|
return program
|
||||||
|
}
|
||||||
|
|
||||||
|
np0 := len(g.Points)
|
||||||
|
np1 := np0 + int(g.Ends[len(g.Ends)-1])
|
||||||
|
|
||||||
|
// Decode the flags.
|
||||||
|
for i := np0; i < np1; {
|
||||||
|
c := uint32(glyf[offset])
|
||||||
|
offset++
|
||||||
|
g.Points = append(g.Points, Point{Flags: c})
|
||||||
|
i++
|
||||||
|
if c&flagRepeat != 0 {
|
||||||
|
count := glyf[offset]
|
||||||
|
offset++
|
||||||
|
for ; count > 0; count-- {
|
||||||
|
g.Points = append(g.Points, Point{Flags: c})
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the co-ordinates.
|
||||||
|
var x int16
|
||||||
|
for i := np0; i < np1; i++ {
|
||||||
|
f := g.Points[i].Flags
|
||||||
|
if f&flagXShortVector != 0 {
|
||||||
|
dx := int16(glyf[offset])
|
||||||
|
offset++
|
||||||
|
if f&flagPositiveXShortVector == 0 {
|
||||||
|
x -= dx
|
||||||
|
} else {
|
||||||
|
x += dx
|
||||||
|
}
|
||||||
|
} else if f&flagThisXIsSame == 0 {
|
||||||
|
x += int16(u16(glyf, offset))
|
||||||
|
offset += 2
|
||||||
|
}
|
||||||
|
g.Points[i].X = fixed.Int26_6(x)
|
||||||
|
}
|
||||||
|
var y int16
|
||||||
|
for i := np0; i < np1; i++ {
|
||||||
|
f := g.Points[i].Flags
|
||||||
|
if f&flagYShortVector != 0 {
|
||||||
|
dy := int16(glyf[offset])
|
||||||
|
offset++
|
||||||
|
if f&flagPositiveYShortVector == 0 {
|
||||||
|
y -= dy
|
||||||
|
} else {
|
||||||
|
y += dy
|
||||||
|
}
|
||||||
|
} else if f&flagThisYIsSame == 0 {
|
||||||
|
y += int16(u16(glyf, offset))
|
||||||
|
offset += 2
|
||||||
|
}
|
||||||
|
g.Points[i].Y = fixed.Int26_6(y)
|
||||||
|
}
|
||||||
|
|
||||||
|
return program
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GlyphBuf) loadCompound(recursion uint32, uhm HMetric, i Index,
|
||||||
|
glyf []byte, useMyMetrics bool) error {
|
||||||
|
|
||||||
|
// Flags for decoding a compound glyph. These flags are documented at
|
||||||
|
// http://developer.apple.com/fonts/TTRefMan/RM06/Chap6glyf.html.
|
||||||
|
const (
|
||||||
|
flagArg1And2AreWords = 1 << iota
|
||||||
|
flagArgsAreXYValues
|
||||||
|
flagRoundXYToGrid
|
||||||
|
flagWeHaveAScale
|
||||||
|
flagUnused
|
||||||
|
flagMoreComponents
|
||||||
|
flagWeHaveAnXAndYScale
|
||||||
|
flagWeHaveATwoByTwo
|
||||||
|
flagWeHaveInstructions
|
||||||
|
flagUseMyMetrics
|
||||||
|
flagOverlapCompound
|
||||||
|
)
|
||||||
|
np0, ne0 := len(g.Points), len(g.Ends)
|
||||||
|
offset := loadOffset
|
||||||
|
for {
|
||||||
|
flags := u16(glyf, offset)
|
||||||
|
component := Index(u16(glyf, offset+2))
|
||||||
|
dx, dy, transform, hasTransform := fixed.Int26_6(0), fixed.Int26_6(0), [4]int16{}, false
|
||||||
|
if flags&flagArg1And2AreWords != 0 {
|
||||||
|
dx = fixed.Int26_6(int16(u16(glyf, offset+4)))
|
||||||
|
dy = fixed.Int26_6(int16(u16(glyf, offset+6)))
|
||||||
|
offset += 8
|
||||||
|
} else {
|
||||||
|
dx = fixed.Int26_6(int16(int8(glyf[offset+4])))
|
||||||
|
dy = fixed.Int26_6(int16(int8(glyf[offset+5])))
|
||||||
|
offset += 6
|
||||||
|
}
|
||||||
|
if flags&flagArgsAreXYValues == 0 {
|
||||||
|
return UnsupportedError("compound glyph transform vector")
|
||||||
|
}
|
||||||
|
if flags&(flagWeHaveAScale|flagWeHaveAnXAndYScale|flagWeHaveATwoByTwo) != 0 {
|
||||||
|
hasTransform = true
|
||||||
|
switch {
|
||||||
|
case flags&flagWeHaveAScale != 0:
|
||||||
|
transform[0] = int16(u16(glyf, offset+0))
|
||||||
|
transform[3] = transform[0]
|
||||||
|
offset += 2
|
||||||
|
case flags&flagWeHaveAnXAndYScale != 0:
|
||||||
|
transform[0] = int16(u16(glyf, offset+0))
|
||||||
|
transform[3] = int16(u16(glyf, offset+2))
|
||||||
|
offset += 4
|
||||||
|
case flags&flagWeHaveATwoByTwo != 0:
|
||||||
|
transform[0] = int16(u16(glyf, offset+0))
|
||||||
|
transform[1] = int16(u16(glyf, offset+2))
|
||||||
|
transform[2] = int16(u16(glyf, offset+4))
|
||||||
|
transform[3] = int16(u16(glyf, offset+6))
|
||||||
|
offset += 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
savedPP := g.phantomPoints
|
||||||
|
np0 := len(g.Points)
|
||||||
|
componentUMM := useMyMetrics && (flags&flagUseMyMetrics != 0)
|
||||||
|
if err := g.load(recursion+1, component, componentUMM); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if flags&flagUseMyMetrics == 0 {
|
||||||
|
g.phantomPoints = savedPP
|
||||||
|
}
|
||||||
|
if hasTransform {
|
||||||
|
for j := np0; j < len(g.Points); j++ {
|
||||||
|
p := &g.Points[j]
|
||||||
|
newX := 0 +
|
||||||
|
fixed.Int26_6((int64(p.X)*int64(transform[0])+1<<13)>>14) +
|
||||||
|
fixed.Int26_6((int64(p.Y)*int64(transform[2])+1<<13)>>14)
|
||||||
|
newY := 0 +
|
||||||
|
fixed.Int26_6((int64(p.X)*int64(transform[1])+1<<13)>>14) +
|
||||||
|
fixed.Int26_6((int64(p.Y)*int64(transform[3])+1<<13)>>14)
|
||||||
|
p.X, p.Y = newX, newY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dx = g.font.scale(g.scale * dx)
|
||||||
|
dy = g.font.scale(g.scale * dy)
|
||||||
|
if flags&flagRoundXYToGrid != 0 {
|
||||||
|
dx = (dx + 32) &^ 63
|
||||||
|
dy = (dy + 32) &^ 63
|
||||||
|
}
|
||||||
|
for j := np0; j < len(g.Points); j++ {
|
||||||
|
p := &g.Points[j]
|
||||||
|
p.X += dx
|
||||||
|
p.Y += dy
|
||||||
|
}
|
||||||
|
// TODO: also adjust g.InFontUnits and g.Unhinted?
|
||||||
|
if flags&flagMoreComponents == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instrLen := 0
|
||||||
|
if g.hinting != font.HintingNone && offset+2 <= len(glyf) {
|
||||||
|
instrLen = int(u16(glyf, offset))
|
||||||
|
offset += 2
|
||||||
|
}
|
||||||
|
|
||||||
|
g.addPhantomsAndScale(np0, len(g.Points), false, instrLen > 0)
|
||||||
|
points, ends := g.Points[np0:], g.Ends[ne0:]
|
||||||
|
g.Points = g.Points[:len(g.Points)-4]
|
||||||
|
for j := range points {
|
||||||
|
points[j].Flags &^= flagTouchedX | flagTouchedY
|
||||||
|
}
|
||||||
|
|
||||||
|
if instrLen == 0 {
|
||||||
|
if !g.metricsSet {
|
||||||
|
copy(g.phantomPoints[:], points[len(points)-4:])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hint the compound glyph.
|
||||||
|
program := glyf[offset : offset+instrLen]
|
||||||
|
// Temporarily adjust the ends to be relative to this compound glyph.
|
||||||
|
if np0 != 0 {
|
||||||
|
for i := range ends {
|
||||||
|
ends[i] -= np0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Hinting instructions of a composite glyph completely refer to the
|
||||||
|
// (already) hinted subglyphs.
|
||||||
|
g.tmp = append(g.tmp[:0], points...)
|
||||||
|
if err := g.hinter.run(program, points, g.tmp, g.tmp, ends); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if np0 != 0 {
|
||||||
|
for i := range ends {
|
||||||
|
ends[i] += np0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !g.metricsSet {
|
||||||
|
copy(g.phantomPoints[:], points[len(points)-4:])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GlyphBuf) addPhantomsAndScale(np0, np1 int, simple, adjust bool) {
|
||||||
|
// Add the four phantom points.
|
||||||
|
g.Points = append(g.Points, g.phantomPoints[:]...)
|
||||||
|
// Scale the points.
|
||||||
|
if simple && g.hinting != font.HintingNone {
|
||||||
|
g.InFontUnits = append(g.InFontUnits, g.Points[np1:]...)
|
||||||
|
}
|
||||||
|
for i := np1; i < len(g.Points); i++ {
|
||||||
|
p := &g.Points[i]
|
||||||
|
p.X = g.font.scale(g.scale * p.X)
|
||||||
|
p.Y = g.font.scale(g.scale * p.Y)
|
||||||
|
}
|
||||||
|
if g.hinting == font.HintingNone {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Round the 1st phantom point to the grid, shifting all other points equally.
|
||||||
|
// Note that "all other points" starts from np0, not np1.
|
||||||
|
// TODO: delete this adjustment and the np0/np1 distinction, when
|
||||||
|
// we update the compatibility tests to C Freetype 2.5.3.
|
||||||
|
// See http://git.savannah.gnu.org/cgit/freetype/freetype2.git/commit/?id=05c786d990390a7ca18e62962641dac740bacb06
|
||||||
|
if adjust {
|
||||||
|
pp1x := g.Points[len(g.Points)-4].X
|
||||||
|
if dx := ((pp1x + 32) &^ 63) - pp1x; dx != 0 {
|
||||||
|
for i := np0; i < len(g.Points); i++ {
|
||||||
|
g.Points[i].X += dx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if simple {
|
||||||
|
g.Unhinted = append(g.Unhinted, g.Points[np1:]...)
|
||||||
|
}
|
||||||
|
// Round the 2nd and 4th phantom point to the grid.
|
||||||
|
p := &g.Points[len(g.Points)-3]
|
||||||
|
p.X = (p.X + 32) &^ 63
|
||||||
|
p = &g.Points[len(g.Points)-1]
|
||||||
|
p.Y = (p.Y + 32) &^ 63
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,289 @@
|
||||||
|
// Copyright 2012 The Freetype-Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by your choice of either the
|
||||||
|
// FreeType License or the GNU General Public License version 2 (or
|
||||||
|
// any later version), both of which can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package truetype
|
||||||
|
|
||||||
|
// The Truetype opcodes are summarized at
|
||||||
|
// https://developer.apple.com/fonts/TTRefMan/RM07/appendixA.html
|
||||||
|
|
||||||
|
const (
|
||||||
|
opSVTCA0 = 0x00 // Set freedom and projection Vectors To Coordinate Axis
|
||||||
|
opSVTCA1 = 0x01 // .
|
||||||
|
opSPVTCA0 = 0x02 // Set Projection Vector To Coordinate Axis
|
||||||
|
opSPVTCA1 = 0x03 // .
|
||||||
|
opSFVTCA0 = 0x04 // Set Freedom Vector to Coordinate Axis
|
||||||
|
opSFVTCA1 = 0x05 // .
|
||||||
|
opSPVTL0 = 0x06 // Set Projection Vector To Line
|
||||||
|
opSPVTL1 = 0x07 // .
|
||||||
|
opSFVTL0 = 0x08 // Set Freedom Vector To Line
|
||||||
|
opSFVTL1 = 0x09 // .
|
||||||
|
opSPVFS = 0x0a // Set Projection Vector From Stack
|
||||||
|
opSFVFS = 0x0b // Set Freedom Vector From Stack
|
||||||
|
opGPV = 0x0c // Get Projection Vector
|
||||||
|
opGFV = 0x0d // Get Freedom Vector
|
||||||
|
opSFVTPV = 0x0e // Set Freedom Vector To Projection Vector
|
||||||
|
opISECT = 0x0f // moves point p to the InterSECTion of two lines
|
||||||
|
opSRP0 = 0x10 // Set Reference Point 0
|
||||||
|
opSRP1 = 0x11 // Set Reference Point 1
|
||||||
|
opSRP2 = 0x12 // Set Reference Point 2
|
||||||
|
opSZP0 = 0x13 // Set Zone Pointer 0
|
||||||
|
opSZP1 = 0x14 // Set Zone Pointer 1
|
||||||
|
opSZP2 = 0x15 // Set Zone Pointer 2
|
||||||
|
opSZPS = 0x16 // Set Zone PointerS
|
||||||
|
opSLOOP = 0x17 // Set LOOP variable
|
||||||
|
opRTG = 0x18 // Round To Grid
|
||||||
|
opRTHG = 0x19 // Round To Half Grid
|
||||||
|
opSMD = 0x1a // Set Minimum Distance
|
||||||
|
opELSE = 0x1b // ELSE clause
|
||||||
|
opJMPR = 0x1c // JuMP Relative
|
||||||
|
opSCVTCI = 0x1d // Set Control Value Table Cut-In
|
||||||
|
opSSWCI = 0x1e // Set Single Width Cut-In
|
||||||
|
opSSW = 0x1f // Set Single Width
|
||||||
|
opDUP = 0x20 // DUPlicate top stack element
|
||||||
|
opPOP = 0x21 // POP top stack element
|
||||||
|
opCLEAR = 0x22 // CLEAR the stack
|
||||||
|
opSWAP = 0x23 // SWAP the top two elements on the stack
|
||||||
|
opDEPTH = 0x24 // DEPTH of the stack
|
||||||
|
opCINDEX = 0x25 // Copy the INDEXed element to the top of the stack
|
||||||
|
opMINDEX = 0x26 // Move the INDEXed element to the top of the stack
|
||||||
|
opALIGNPTS = 0x27 // ALIGN PoinTS
|
||||||
|
op_0x28 = 0x28 // deprecated
|
||||||
|
opUTP = 0x29 // UnTouch Point
|
||||||
|
opLOOPCALL = 0x2a // LOOP and CALL function
|
||||||
|
opCALL = 0x2b // CALL function
|
||||||
|
opFDEF = 0x2c // Function DEFinition
|
||||||
|
opENDF = 0x2d // END Function definition
|
||||||
|
opMDAP0 = 0x2e // Move Direct Absolute Point
|
||||||
|
opMDAP1 = 0x2f // .
|
||||||
|
opIUP0 = 0x30 // Interpolate Untouched Points through the outline
|
||||||
|
opIUP1 = 0x31 // .
|
||||||
|
opSHP0 = 0x32 // SHift Point using reference point
|
||||||
|
opSHP1 = 0x33 // .
|
||||||
|
opSHC0 = 0x34 // SHift Contour using reference point
|
||||||
|
opSHC1 = 0x35 // .
|
||||||
|
opSHZ0 = 0x36 // SHift Zone using reference point
|
||||||
|
opSHZ1 = 0x37 // .
|
||||||
|
opSHPIX = 0x38 // SHift point by a PIXel amount
|
||||||
|
opIP = 0x39 // Interpolate Point
|
||||||
|
opMSIRP0 = 0x3a // Move Stack Indirect Relative Point
|
||||||
|
opMSIRP1 = 0x3b // .
|
||||||
|
opALIGNRP = 0x3c // ALIGN to Reference Point
|
||||||
|
opRTDG = 0x3d // Round To Double Grid
|
||||||
|
opMIAP0 = 0x3e // Move Indirect Absolute Point
|
||||||
|
opMIAP1 = 0x3f // .
|
||||||
|
opNPUSHB = 0x40 // PUSH N Bytes
|
||||||
|
opNPUSHW = 0x41 // PUSH N Words
|
||||||
|
opWS = 0x42 // Write Store
|
||||||
|
opRS = 0x43 // Read Store
|
||||||
|
opWCVTP = 0x44 // Write Control Value Table in Pixel units
|
||||||
|
opRCVT = 0x45 // Read Control Value Table entry
|
||||||
|
opGC0 = 0x46 // Get Coordinate projected onto the projection vector
|
||||||
|
opGC1 = 0x47 // .
|
||||||
|
opSCFS = 0x48 // Sets Coordinate From the Stack using projection vector and freedom vector
|
||||||
|
opMD0 = 0x49 // Measure Distance
|
||||||
|
opMD1 = 0x4a // .
|
||||||
|
opMPPEM = 0x4b // Measure Pixels Per EM
|
||||||
|
opMPS = 0x4c // Measure Point Size
|
||||||
|
opFLIPON = 0x4d // set the auto FLIP Boolean to ON
|
||||||
|
opFLIPOFF = 0x4e // set the auto FLIP Boolean to OFF
|
||||||
|
opDEBUG = 0x4f // DEBUG call
|
||||||
|
opLT = 0x50 // Less Than
|
||||||
|
opLTEQ = 0x51 // Less Than or EQual
|
||||||
|
opGT = 0x52 // Greater Than
|
||||||
|
opGTEQ = 0x53 // Greater Than or EQual
|
||||||
|
opEQ = 0x54 // EQual
|
||||||
|
opNEQ = 0x55 // Not EQual
|
||||||
|
opODD = 0x56 // ODD
|
||||||
|
opEVEN = 0x57 // EVEN
|
||||||
|
opIF = 0x58 // IF test
|
||||||
|
opEIF = 0x59 // End IF
|
||||||
|
opAND = 0x5a // logical AND
|
||||||
|
opOR = 0x5b // logical OR
|
||||||
|
opNOT = 0x5c // logical NOT
|
||||||
|
opDELTAP1 = 0x5d // DELTA exception P1
|
||||||
|
opSDB = 0x5e // Set Delta Base in the graphics state
|
||||||
|
opSDS = 0x5f // Set Delta Shift in the graphics state
|
||||||
|
opADD = 0x60 // ADD
|
||||||
|
opSUB = 0x61 // SUBtract
|
||||||
|
opDIV = 0x62 // DIVide
|
||||||
|
opMUL = 0x63 // MULtiply
|
||||||
|
opABS = 0x64 // ABSolute value
|
||||||
|
opNEG = 0x65 // NEGate
|
||||||
|
opFLOOR = 0x66 // FLOOR
|
||||||
|
opCEILING = 0x67 // CEILING
|
||||||
|
opROUND00 = 0x68 // ROUND value
|
||||||
|
opROUND01 = 0x69 // .
|
||||||
|
opROUND10 = 0x6a // .
|
||||||
|
opROUND11 = 0x6b // .
|
||||||
|
opNROUND00 = 0x6c // No ROUNDing of value
|
||||||
|
opNROUND01 = 0x6d // .
|
||||||
|
opNROUND10 = 0x6e // .
|
||||||
|
opNROUND11 = 0x6f // .
|
||||||
|
opWCVTF = 0x70 // Write Control Value Table in Funits
|
||||||
|
opDELTAP2 = 0x71 // DELTA exception P2
|
||||||
|
opDELTAP3 = 0x72 // DELTA exception P3
|
||||||
|
opDELTAC1 = 0x73 // DELTA exception C1
|
||||||
|
opDELTAC2 = 0x74 // DELTA exception C2
|
||||||
|
opDELTAC3 = 0x75 // DELTA exception C3
|
||||||
|
opSROUND = 0x76 // Super ROUND
|
||||||
|
opS45ROUND = 0x77 // Super ROUND 45 degrees
|
||||||
|
opJROT = 0x78 // Jump Relative On True
|
||||||
|
opJROF = 0x79 // Jump Relative On False
|
||||||
|
opROFF = 0x7a // Round OFF
|
||||||
|
op_0x7b = 0x7b // deprecated
|
||||||
|
opRUTG = 0x7c // Round Up To Grid
|
||||||
|
opRDTG = 0x7d // Round Down To Grid
|
||||||
|
opSANGW = 0x7e // Set ANGle Weight
|
||||||
|
opAA = 0x7f // Adjust Angle
|
||||||
|
opFLIPPT = 0x80 // FLIP PoinT
|
||||||
|
opFLIPRGON = 0x81 // FLIP RanGe ON
|
||||||
|
opFLIPRGOFF = 0x82 // FLIP RanGe OFF
|
||||||
|
op_0x83 = 0x83 // deprecated
|
||||||
|
op_0x84 = 0x84 // deprecated
|
||||||
|
opSCANCTRL = 0x85 // SCAN conversion ConTRoL
|
||||||
|
opSDPVTL0 = 0x86 // Set Dual Projection Vector To Line
|
||||||
|
opSDPVTL1 = 0x87 // .
|
||||||
|
opGETINFO = 0x88 // GET INFOrmation
|
||||||
|
opIDEF = 0x89 // Instruction DEFinition
|
||||||
|
opROLL = 0x8a // ROLL the top three stack elements
|
||||||
|
opMAX = 0x8b // MAXimum of top two stack elements
|
||||||
|
opMIN = 0x8c // MINimum of top two stack elements
|
||||||
|
opSCANTYPE = 0x8d // SCANTYPE
|
||||||
|
opINSTCTRL = 0x8e // INSTRuction execution ConTRoL
|
||||||
|
op_0x8f = 0x8f
|
||||||
|
op_0x90 = 0x90
|
||||||
|
op_0x91 = 0x91
|
||||||
|
op_0x92 = 0x92
|
||||||
|
op_0x93 = 0x93
|
||||||
|
op_0x94 = 0x94
|
||||||
|
op_0x95 = 0x95
|
||||||
|
op_0x96 = 0x96
|
||||||
|
op_0x97 = 0x97
|
||||||
|
op_0x98 = 0x98
|
||||||
|
op_0x99 = 0x99
|
||||||
|
op_0x9a = 0x9a
|
||||||
|
op_0x9b = 0x9b
|
||||||
|
op_0x9c = 0x9c
|
||||||
|
op_0x9d = 0x9d
|
||||||
|
op_0x9e = 0x9e
|
||||||
|
op_0x9f = 0x9f
|
||||||
|
op_0xa0 = 0xa0
|
||||||
|
op_0xa1 = 0xa1
|
||||||
|
op_0xa2 = 0xa2
|
||||||
|
op_0xa3 = 0xa3
|
||||||
|
op_0xa4 = 0xa4
|
||||||
|
op_0xa5 = 0xa5
|
||||||
|
op_0xa6 = 0xa6
|
||||||
|
op_0xa7 = 0xa7
|
||||||
|
op_0xa8 = 0xa8
|
||||||
|
op_0xa9 = 0xa9
|
||||||
|
op_0xaa = 0xaa
|
||||||
|
op_0xab = 0xab
|
||||||
|
op_0xac = 0xac
|
||||||
|
op_0xad = 0xad
|
||||||
|
op_0xae = 0xae
|
||||||
|
op_0xaf = 0xaf
|
||||||
|
opPUSHB000 = 0xb0 // PUSH Bytes
|
||||||
|
opPUSHB001 = 0xb1 // .
|
||||||
|
opPUSHB010 = 0xb2 // .
|
||||||
|
opPUSHB011 = 0xb3 // .
|
||||||
|
opPUSHB100 = 0xb4 // .
|
||||||
|
opPUSHB101 = 0xb5 // .
|
||||||
|
opPUSHB110 = 0xb6 // .
|
||||||
|
opPUSHB111 = 0xb7 // .
|
||||||
|
opPUSHW000 = 0xb8 // PUSH Words
|
||||||
|
opPUSHW001 = 0xb9 // .
|
||||||
|
opPUSHW010 = 0xba // .
|
||||||
|
opPUSHW011 = 0xbb // .
|
||||||
|
opPUSHW100 = 0xbc // .
|
||||||
|
opPUSHW101 = 0xbd // .
|
||||||
|
opPUSHW110 = 0xbe // .
|
||||||
|
opPUSHW111 = 0xbf // .
|
||||||
|
opMDRP00000 = 0xc0 // Move Direct Relative Point
|
||||||
|
opMDRP00001 = 0xc1 // .
|
||||||
|
opMDRP00010 = 0xc2 // .
|
||||||
|
opMDRP00011 = 0xc3 // .
|
||||||
|
opMDRP00100 = 0xc4 // .
|
||||||
|
opMDRP00101 = 0xc5 // .
|
||||||
|
opMDRP00110 = 0xc6 // .
|
||||||
|
opMDRP00111 = 0xc7 // .
|
||||||
|
opMDRP01000 = 0xc8 // .
|
||||||
|
opMDRP01001 = 0xc9 // .
|
||||||
|
opMDRP01010 = 0xca // .
|
||||||
|
opMDRP01011 = 0xcb // .
|
||||||
|
opMDRP01100 = 0xcc // .
|
||||||
|
opMDRP01101 = 0xcd // .
|
||||||
|
opMDRP01110 = 0xce // .
|
||||||
|
opMDRP01111 = 0xcf // .
|
||||||
|
opMDRP10000 = 0xd0 // .
|
||||||
|
opMDRP10001 = 0xd1 // .
|
||||||
|
opMDRP10010 = 0xd2 // .
|
||||||
|
opMDRP10011 = 0xd3 // .
|
||||||
|
opMDRP10100 = 0xd4 // .
|
||||||
|
opMDRP10101 = 0xd5 // .
|
||||||
|
opMDRP10110 = 0xd6 // .
|
||||||
|
opMDRP10111 = 0xd7 // .
|
||||||
|
opMDRP11000 = 0xd8 // .
|
||||||
|
opMDRP11001 = 0xd9 // .
|
||||||
|
opMDRP11010 = 0xda // .
|
||||||
|
opMDRP11011 = 0xdb // .
|
||||||
|
opMDRP11100 = 0xdc // .
|
||||||
|
opMDRP11101 = 0xdd // .
|
||||||
|
opMDRP11110 = 0xde // .
|
||||||
|
opMDRP11111 = 0xdf // .
|
||||||
|
opMIRP00000 = 0xe0 // Move Indirect Relative Point
|
||||||
|
opMIRP00001 = 0xe1 // .
|
||||||
|
opMIRP00010 = 0xe2 // .
|
||||||
|
opMIRP00011 = 0xe3 // .
|
||||||
|
opMIRP00100 = 0xe4 // .
|
||||||
|
opMIRP00101 = 0xe5 // .
|
||||||
|
opMIRP00110 = 0xe6 // .
|
||||||
|
opMIRP00111 = 0xe7 // .
|
||||||
|
opMIRP01000 = 0xe8 // .
|
||||||
|
opMIRP01001 = 0xe9 // .
|
||||||
|
opMIRP01010 = 0xea // .
|
||||||
|
opMIRP01011 = 0xeb // .
|
||||||
|
opMIRP01100 = 0xec // .
|
||||||
|
opMIRP01101 = 0xed // .
|
||||||
|
opMIRP01110 = 0xee // .
|
||||||
|
opMIRP01111 = 0xef // .
|
||||||
|
opMIRP10000 = 0xf0 // .
|
||||||
|
opMIRP10001 = 0xf1 // .
|
||||||
|
opMIRP10010 = 0xf2 // .
|
||||||
|
opMIRP10011 = 0xf3 // .
|
||||||
|
opMIRP10100 = 0xf4 // .
|
||||||
|
opMIRP10101 = 0xf5 // .
|
||||||
|
opMIRP10110 = 0xf6 // .
|
||||||
|
opMIRP10111 = 0xf7 // .
|
||||||
|
opMIRP11000 = 0xf8 // .
|
||||||
|
opMIRP11001 = 0xf9 // .
|
||||||
|
opMIRP11010 = 0xfa // .
|
||||||
|
opMIRP11011 = 0xfb // .
|
||||||
|
opMIRP11100 = 0xfc // .
|
||||||
|
opMIRP11101 = 0xfd // .
|
||||||
|
opMIRP11110 = 0xfe // .
|
||||||
|
opMIRP11111 = 0xff // .
|
||||||
|
)
|
||||||
|
|
||||||
|
// popCount is the number of stack elements that each opcode pops.
|
||||||
|
var popCount = [256]uint8{
|
||||||
|
// 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f
|
||||||
|
0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 0, 0, 0, 5, // 0x00 - 0x0f
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, // 0x10 - 0x1f
|
||||||
|
1, 1, 0, 2, 0, 1, 1, 2, 0, 1, 2, 1, 1, 0, 1, 1, // 0x20 - 0x2f
|
||||||
|
0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 2, 2, 0, 0, 2, 2, // 0x30 - 0x3f
|
||||||
|
0, 0, 2, 1, 2, 1, 1, 1, 2, 2, 2, 0, 0, 0, 0, 0, // 0x40 - 0x4f
|
||||||
|
2, 2, 2, 2, 2, 2, 1, 1, 1, 0, 2, 2, 1, 1, 1, 1, // 0x50 - 0x5f
|
||||||
|
2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60 - 0x6f
|
||||||
|
2, 1, 1, 1, 1, 1, 1, 1, 2, 2, 0, 0, 0, 0, 1, 1, // 0x70 - 0x7f
|
||||||
|
0, 2, 2, 0, 0, 1, 2, 2, 1, 1, 3, 2, 2, 1, 2, 0, // 0x80 - 0x8f
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x90 - 0x9f
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xa0 - 0xaf
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xb0 - 0xbf
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0xc0 - 0xcf
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0xd0 - 0xdf
|
||||||
|
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xe0 - 0xef
|
||||||
|
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xf0 - 0xff
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
.idea
|
||||||
|
.git
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.prof
|
|
@ -0,0 +1,201 @@
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright {yyyy} {name of copyright owner}
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
|
@ -0,0 +1,65 @@
|
||||||
|
# gocaptcha
|
||||||
|
一个简单的Go语言实现的验证码
|
||||||
|
|
||||||
|
##图片实例
|
||||||
|
|
||||||
|
![image](https://raw.githubusercontent.com/lifei6671/gocaptcha/master/example/image_1.jpg)
|
||||||
|
![image](https://raw.githubusercontent.com/lifei6671/gocaptcha/master/example/image_2.jpg)
|
||||||
|
![image](https://raw.githubusercontent.com/lifei6671/gocaptcha/master/example/image_3.jpg)
|
||||||
|
![image](https://raw.githubusercontent.com/lifei6671/gocaptcha/master/example/image_4.jpg)
|
||||||
|
|
||||||
|
##简介
|
||||||
|
|
||||||
|
基于Golang实现的图片验证码生成库,可以实现随机字母个数,随机直线,随机噪点等。可以设置任意多字体,每个验证码随机选一种字体展示。
|
||||||
|
|
||||||
|
##实例
|
||||||
|
|
||||||
|
####使用:
|
||||||
|
|
||||||
|
```
|
||||||
|
go get github.com/lifei6671/gocaptcha/
|
||||||
|
```
|
||||||
|
|
||||||
|
####使用的类库
|
||||||
|
|
||||||
|
```
|
||||||
|
go get github.com/golang/freetype
|
||||||
|
go get github.com/golang/freetype/truetype
|
||||||
|
go get golang.org/x/image
|
||||||
|
```
|
||||||
|
天朝可以去 http://www.golangtc.com/download/package 或 https://gopm.io 下载
|
||||||
|
|
||||||
|
####代码
|
||||||
|
具体实例可以查看example目录,有生成的验证码图片。
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
func Get(w http.ResponseWriter, r *http.Request) {
|
||||||
|
//初始化一个验证码对象
|
||||||
|
captchaImage,err := gocaptcha.NewCaptchaImage(dx,dy,gocaptcha.RandLightColor());
|
||||||
|
|
||||||
|
//画上三条随机直线
|
||||||
|
captchaImage.Drawline(3);
|
||||||
|
|
||||||
|
//画边框
|
||||||
|
captchaImage.DrawBorder(gocaptcha.ColorToRGB(0x17A7A7A));
|
||||||
|
|
||||||
|
//画随机噪点
|
||||||
|
captchaImage.DrawNoise(gocaptcha.CaptchaComplexHigh);
|
||||||
|
|
||||||
|
//画随机文字噪点
|
||||||
|
captchaImage.DrawTextNoise(gocaptcha.CaptchaComplexLower);
|
||||||
|
//画验证码文字,可以预先保持到Session种或其他储存容器种
|
||||||
|
captchaImage.DrawText(gocaptcha.RandText(4));
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
//将验证码保持到输出流种,可以是文件或HTTP流等
|
||||||
|
captchaImage.SaveImage(w,gocaptcha.ImageFormatJpeg);
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,429 @@
|
||||||
|
package gocaptcha
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/draw"
|
||||||
|
"math"
|
||||||
|
"io"
|
||||||
|
"image/png"
|
||||||
|
"image/jpeg"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
"math/rand"
|
||||||
|
"github.com/golang/freetype"
|
||||||
|
"flag"
|
||||||
|
"golang.org/x/image/font"
|
||||||
|
"log"
|
||||||
|
"io/ioutil"
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
"image/gif"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dpi = flag.Float64("dpi", 72, "screen resolution in Dots Per Inch")
|
||||||
|
r = rand.New(rand.NewSource(time.Now().UnixNano()));
|
||||||
|
FontFamily []string = make([]string,0);
|
||||||
|
)
|
||||||
|
|
||||||
|
const txtChars = "AaCcDdEeFfGgHhJjKkLMmNnPpQqRrSsTtUuVvWwXxYtZ2346789";
|
||||||
|
|
||||||
|
|
||||||
|
const(
|
||||||
|
//图片格式
|
||||||
|
ImageFormatPng = iota
|
||||||
|
ImageFormatJpeg
|
||||||
|
ImageFormatGif
|
||||||
|
//验证码噪点强度
|
||||||
|
CaptchaComplexLower = iota
|
||||||
|
CaptchaComplexMedium
|
||||||
|
CaptchaComplexHigh
|
||||||
|
)
|
||||||
|
|
||||||
|
type CaptchaImage struct {
|
||||||
|
nrgba *image.NRGBA
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
Complex int
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取指定目录下的所有文件,不进入下一级目录搜索,可以匹配后缀过滤。
|
||||||
|
func ReadFonts(dirPth string, suffix string) (err error) {
|
||||||
|
files := make([]string, 0, 10)
|
||||||
|
dir, err := ioutil.ReadDir(dirPth)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
PthSep := string(os.PathSeparator)
|
||||||
|
suffix = strings.ToUpper(suffix) //忽略后缀匹配的大小写
|
||||||
|
for _, fi := range dir {
|
||||||
|
if fi.IsDir() { // 忽略目录
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(strings.ToUpper(fi.Name()), suffix) { //匹配文件
|
||||||
|
files = append(files, dirPth+PthSep+fi.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SetFontFamily(files...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//新建一个图片对象
|
||||||
|
func NewCaptchaImage(width int,height int,bgColor color.RGBA) (*CaptchaImage ,error){
|
||||||
|
|
||||||
|
m := image.NewNRGBA(image.Rect(0, 0, width, height));
|
||||||
|
|
||||||
|
draw.Draw(m, m.Bounds(), &image.Uniform{ bgColor }, image.ZP, draw.Src);
|
||||||
|
|
||||||
|
return &CaptchaImage{
|
||||||
|
nrgba: m,
|
||||||
|
height : height,
|
||||||
|
width : width,
|
||||||
|
},nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
//保存图片对象
|
||||||
|
func (this *CaptchaImage)SaveImage(w io.Writer ,imageFormat int) error{
|
||||||
|
|
||||||
|
if(imageFormat == ImageFormatPng){
|
||||||
|
return png.Encode(w, this.nrgba);
|
||||||
|
};
|
||||||
|
if(imageFormat == ImageFormatJpeg){
|
||||||
|
return jpeg.Encode(w,this.nrgba, &jpeg.Options{ 100 });
|
||||||
|
}
|
||||||
|
if(imageFormat == ImageFormatGif){
|
||||||
|
return gif.Encode(w,this.nrgba, &gif.Options{ NumColors : 256});
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("Not supported image format");
|
||||||
|
}
|
||||||
|
//添加一个较粗的空白直线
|
||||||
|
func (captcha *CaptchaImage) DrawHollowLine()(*CaptchaImage){
|
||||||
|
|
||||||
|
first := (captcha.width / 20);
|
||||||
|
end := first * 19;
|
||||||
|
|
||||||
|
lineColor := color.RGBA{ R : 245,G:250,B:251,A:255};
|
||||||
|
|
||||||
|
x1 := float64(r.Intn(first));
|
||||||
|
//y1 := float64(r.Intn(y)+y);
|
||||||
|
|
||||||
|
x2 := float64( r.Intn(first)+end);
|
||||||
|
|
||||||
|
multiple := float64(r.Intn(5)+3)/float64(5);
|
||||||
|
if(int(multiple*10) % 3 == 0){
|
||||||
|
multiple = multiple * -1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
w := captcha.height / 20;
|
||||||
|
|
||||||
|
for ;x1 < x2; x1 ++{
|
||||||
|
|
||||||
|
y := math.Sin(x1*math.Pi*multiple/float64(captcha.width)) * float64(captcha.height/3);
|
||||||
|
|
||||||
|
if(multiple < 0){
|
||||||
|
y = y + float64(captcha.height/2);
|
||||||
|
}
|
||||||
|
captcha.nrgba.Set(int(x1),int(y),lineColor);
|
||||||
|
|
||||||
|
for i:=0;i<=w;i++{
|
||||||
|
captcha.nrgba.Set(int(x1),int(y)+i,lineColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return captcha;
|
||||||
|
}
|
||||||
|
//画一条直线
|
||||||
|
func (captcha *CaptchaImage)Drawline(num int) (*CaptchaImage) {
|
||||||
|
|
||||||
|
first := (captcha.width / 10);
|
||||||
|
end := first * 9;
|
||||||
|
|
||||||
|
y := captcha.height / 3;
|
||||||
|
|
||||||
|
for i:=0;i<num;i++{
|
||||||
|
|
||||||
|
point1 := Point{ X : r.Intn(first),Y:r.Intn(y)};
|
||||||
|
point2 := Point{X : r.Intn(first)+end,Y:r.Intn(y)};
|
||||||
|
|
||||||
|
if(i % 2 == 0){
|
||||||
|
point1.Y = r.Intn(y)+y*2;
|
||||||
|
point2.Y = r.Intn(y);
|
||||||
|
}else{
|
||||||
|
point1.Y = r.Intn(y)+y*(i%2);
|
||||||
|
point2.Y = r.Intn(y) + y*2;
|
||||||
|
}
|
||||||
|
|
||||||
|
captcha.drawBeeline(point1,point2,RandDeepColor());
|
||||||
|
|
||||||
|
}
|
||||||
|
return captcha;
|
||||||
|
}
|
||||||
|
|
||||||
|
func (captcha *CaptchaImage)drawBeeline(point1 Point,point2 Point,lineColor color.RGBA){
|
||||||
|
dx := math.Abs(float64(point1.X - point2.X));
|
||||||
|
|
||||||
|
dy := math.Abs(float64(point2.Y - point1.Y))
|
||||||
|
sx, sy := 1, 1
|
||||||
|
if point1.X >= point2.X {
|
||||||
|
sx = -1
|
||||||
|
}
|
||||||
|
if point1.Y >= point2.Y {
|
||||||
|
sy = -1
|
||||||
|
}
|
||||||
|
err := dx - dy
|
||||||
|
for {
|
||||||
|
captcha.nrgba.Set(point1.X,point1.Y,lineColor);
|
||||||
|
captcha.nrgba.Set(point1.X+1,point1.Y,lineColor);
|
||||||
|
captcha.nrgba.Set(point1.X-1,point1.Y,lineColor);
|
||||||
|
captcha.nrgba.Set(point1.X+2,point1.Y,lineColor);
|
||||||
|
captcha.nrgba.Set(point1.X-2,point1.Y,lineColor);
|
||||||
|
if point1.X == point2.X && point1.Y == point2.Y {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e2 := err * 2
|
||||||
|
if e2 > -dy {
|
||||||
|
err -= dy
|
||||||
|
point1.X += sx
|
||||||
|
}
|
||||||
|
if e2 < dx {
|
||||||
|
err += dx
|
||||||
|
point1.Y += sy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//画边框
|
||||||
|
func (captcha *CaptchaImage) DrawBorder(borderColor color.RGBA) (*CaptchaImage){
|
||||||
|
for x :=0;x<captcha.width;x++{
|
||||||
|
captcha.nrgba.Set(x,0,borderColor);
|
||||||
|
captcha.nrgba.Set(x,captcha.height-1,borderColor);
|
||||||
|
}
|
||||||
|
for y:=0;y<captcha.height;y++{
|
||||||
|
captcha.nrgba.Set(0,y,borderColor);
|
||||||
|
captcha.nrgba.Set(captcha.width-1,y,borderColor);
|
||||||
|
}
|
||||||
|
return captcha;
|
||||||
|
}
|
||||||
|
|
||||||
|
//画噪点
|
||||||
|
func (captcha *CaptchaImage) DrawNoise(complex int) (*CaptchaImage){
|
||||||
|
density := 18;
|
||||||
|
if(complex == CaptchaComplexLower){
|
||||||
|
density = 28;
|
||||||
|
}else if(complex == CaptchaComplexMedium){
|
||||||
|
density = 18;
|
||||||
|
}else if(complex == CaptchaComplexHigh){
|
||||||
|
density = 8;
|
||||||
|
}
|
||||||
|
maxSize := (captcha.height * captcha.width)/density;
|
||||||
|
|
||||||
|
for i:=0;i<maxSize;i++{
|
||||||
|
|
||||||
|
rw := r.Intn(captcha.width);
|
||||||
|
rh := r.Intn(captcha.height);
|
||||||
|
|
||||||
|
captcha.nrgba.Set(rw,rh,RandColor());
|
||||||
|
size := r.Intn(maxSize);
|
||||||
|
if size%3 == 0{
|
||||||
|
captcha.nrgba.Set(rw+1,rh+1,RandColor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return captcha;
|
||||||
|
}
|
||||||
|
//画文字噪点
|
||||||
|
func (captcha *CaptchaImage) DrawTextNoise(complex int)(error){
|
||||||
|
density := 1500;
|
||||||
|
if(complex == CaptchaComplexLower){
|
||||||
|
density = 2000;
|
||||||
|
}else if(complex == CaptchaComplexMedium){
|
||||||
|
density = 1500;
|
||||||
|
}else if(complex == CaptchaComplexHigh){
|
||||||
|
density = 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
maxSize := (captcha.height * captcha.width)/density;
|
||||||
|
|
||||||
|
fmt.Println(maxSize);
|
||||||
|
|
||||||
|
r := rand.New(rand.NewSource(time.Now().UnixNano()));
|
||||||
|
|
||||||
|
c := freetype.NewContext();
|
||||||
|
c.SetDPI(*dpi)
|
||||||
|
|
||||||
|
c.SetClip(captcha.nrgba.Bounds());
|
||||||
|
c.SetDst(captcha.nrgba)
|
||||||
|
c.SetHinting(font.HintingFull)
|
||||||
|
rawFontSize := float64(captcha.height) / (1+ float64(r.Intn(7))/float64(10))
|
||||||
|
|
||||||
|
for i:=0;i<maxSize;i++{
|
||||||
|
|
||||||
|
rw := r.Intn(captcha.width);
|
||||||
|
rh := r.Intn(captcha.height);
|
||||||
|
|
||||||
|
text := RandText(1);
|
||||||
|
fontSize := rawFontSize/2 + float64(r.Intn(5));
|
||||||
|
|
||||||
|
c.SetSrc(image.NewUniform(RandLightColor()))
|
||||||
|
c.SetFontSize(fontSize);
|
||||||
|
f,err := RandFontFamily();
|
||||||
|
|
||||||
|
if(err != nil){
|
||||||
|
log.Println(err);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
c.SetFont(f)
|
||||||
|
pt := freetype.Pt(rw, rh);
|
||||||
|
|
||||||
|
_, err = c.DrawString(text, pt)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
//写字
|
||||||
|
func(captcha *CaptchaImage) DrawText(text string)(error){
|
||||||
|
c := freetype.NewContext();
|
||||||
|
c.SetDPI(*dpi)
|
||||||
|
|
||||||
|
c.SetClip(captcha.nrgba.Bounds());
|
||||||
|
c.SetDst(captcha.nrgba)
|
||||||
|
c.SetHinting(font.HintingFull)
|
||||||
|
|
||||||
|
fontWidth := captcha.width/ len(text);
|
||||||
|
|
||||||
|
|
||||||
|
for i,s := range text{
|
||||||
|
|
||||||
|
fontSize := float64(captcha.height) / (1+ float64(r.Intn(7))/float64(9))
|
||||||
|
|
||||||
|
c.SetSrc(image.NewUniform(RandDeepColor()))
|
||||||
|
c.SetFontSize(fontSize);
|
||||||
|
f,err := RandFontFamily();
|
||||||
|
|
||||||
|
if(err != nil){
|
||||||
|
log.Println(err);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
c.SetFont(f)
|
||||||
|
|
||||||
|
x := int(fontWidth)*i + int(fontWidth)/int(fontSize);
|
||||||
|
|
||||||
|
y := 5 + r.Intn(captcha.height/2) + int(fontSize/2);
|
||||||
|
|
||||||
|
pt := freetype.Pt(x, y);
|
||||||
|
|
||||||
|
_, err = c.DrawString(string(s), pt)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
//pt.Y += c.PointToFixed(*size * *spacing)
|
||||||
|
//pt.X += c.PointToFixed(*size);
|
||||||
|
}
|
||||||
|
return nil;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取所及字体
|
||||||
|
func RandFontFamily() (*truetype.Font ,error ){
|
||||||
|
fontfile := FontFamily[r.Intn(len(FontFamily))];
|
||||||
|
|
||||||
|
fontBytes, err := ioutil.ReadFile(fontfile)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return &truetype.Font{},err;
|
||||||
|
}
|
||||||
|
f, err := freetype.ParseFont(fontBytes)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return &truetype.Font{},err;
|
||||||
|
}
|
||||||
|
return f,nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
//随机生成深色系
|
||||||
|
func RandDeepColor() color.RGBA{
|
||||||
|
|
||||||
|
randColor := RandColor();
|
||||||
|
|
||||||
|
increase := float64(30 + r.Intn(255));
|
||||||
|
|
||||||
|
red := math.Abs(math.Min(float64(randColor.R) - increase,255));
|
||||||
|
|
||||||
|
green := math.Abs(math.Min(float64(randColor.G) - increase,255));
|
||||||
|
blue := math.Abs(math.Min(float64(randColor.B) - increase,255));
|
||||||
|
|
||||||
|
|
||||||
|
return color.RGBA{R:uint8(red), G : uint8(green),B:uint8(blue),A:uint8(255)};
|
||||||
|
}
|
||||||
|
//随机生成浅色
|
||||||
|
func RandLightColor() color.RGBA{
|
||||||
|
|
||||||
|
red := r.Intn(55) + 200;
|
||||||
|
green := r.Intn(55)+200;
|
||||||
|
blue := r.Intn(55) + 200;
|
||||||
|
|
||||||
|
|
||||||
|
return color.RGBA{R:uint8(red), G : uint8(green),B:uint8(blue),A:uint8(255)};
|
||||||
|
}
|
||||||
|
|
||||||
|
//生成随机颜色
|
||||||
|
func RandColor() color.RGBA{
|
||||||
|
|
||||||
|
red := r.Intn(255);
|
||||||
|
green := r.Intn(255);
|
||||||
|
blue := r.Intn(255);
|
||||||
|
if((red + green) > 400){
|
||||||
|
blue = 0;
|
||||||
|
}else{
|
||||||
|
blue = 400 - green -red;
|
||||||
|
}
|
||||||
|
if(blue > 255){
|
||||||
|
blue = 255;
|
||||||
|
}
|
||||||
|
return color.RGBA{R:uint8(red), G : uint8(green),B:uint8(blue),A:uint8(255)};
|
||||||
|
}
|
||||||
|
|
||||||
|
//生成随机字体
|
||||||
|
func RandText(num int) string {
|
||||||
|
textNum := len(txtChars);
|
||||||
|
text := "";
|
||||||
|
r := rand.New(rand.NewSource(time.Now().UnixNano()));
|
||||||
|
|
||||||
|
for i:=0;i<num ;i++ {
|
||||||
|
text = text + string(txtChars[r.Intn(textNum)]);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
//添加一个字体路径到字体库
|
||||||
|
func SetFontFamily(fontPath ... string){
|
||||||
|
|
||||||
|
FontFamily= append(FontFamily,fontPath...);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 颜色代码转换为RGB
|
||||||
|
* input int
|
||||||
|
* output int red, green, blue
|
||||||
|
**/
|
||||||
|
func ColorToRGB(colorVal int) (color.RGBA) {
|
||||||
|
|
||||||
|
red := colorVal >> 16
|
||||||
|
green := (colorVal & 0x00FF00) >> 8
|
||||||
|
blue := colorVal & 0x0000FF
|
||||||
|
|
||||||
|
|
||||||
|
return color.RGBA{
|
||||||
|
R:uint8(red),
|
||||||
|
G:uint8(green),
|
||||||
|
B:uint8(blue),
|
||||||
|
A:uint8(255),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package gocaptcha
|
||||||
|
|
||||||
|
type Point struct {
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPoint(x int,y int) *Point{
|
||||||
|
return &Point{ X :x,Y:y};
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
*.db
|
||||||
|
*.exe
|
||||||
|
*.dll
|
|
@ -0,0 +1,8 @@
|
||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- tip
|
||||||
|
before_install:
|
||||||
|
- go get github.com/mattn/goveralls
|
||||||
|
- go get golang.org/x/tools/cmd/cover
|
||||||
|
script:
|
||||||
|
- $HOME/gopath/bin/goveralls -repotoken 3qJVUE0iQwqnCbmNcDsjYu1nh4J4KIFXx
|
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Yasuhiro Matsumoto
|
||||||
|
|
||||||
|
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.
|
|
@ -0,0 +1,80 @@
|
||||||
|
go-sqlite3
|
||||||
|
==========
|
||||||
|
|
||||||
|
[![Build Status](https://travis-ci.org/mattn/go-sqlite3.svg?branch=master)](https://travis-ci.org/mattn/go-sqlite3)
|
||||||
|
[![Coverage Status](https://coveralls.io/repos/mattn/go-sqlite3/badge.svg?branch=master)](https://coveralls.io/r/mattn/go-sqlite3?branch=master)
|
||||||
|
[![GoDoc](https://godoc.org/github.com/mattn/go-sqlite3?status.svg)](http://godoc.org/github.com/mattn/go-sqlite3)
|
||||||
|
|
||||||
|
Description
|
||||||
|
-----------
|
||||||
|
|
||||||
|
sqlite3 driver conforming to the built-in database/sql interface
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
This package can be installed with the go get command:
|
||||||
|
|
||||||
|
go get github.com/mattn/go-sqlite3
|
||||||
|
|
||||||
|
_go-sqlite3_ is *cgo* package.
|
||||||
|
If you want to build your app using go-sqlite3, you need gcc.
|
||||||
|
However, if you install _go-sqlite3_ with `go install github.com/mattn/go-sqlite3`, you don't need gcc to build your app anymore.
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
API documentation can be found here: http://godoc.org/github.com/mattn/go-sqlite3
|
||||||
|
|
||||||
|
Examples can be found under the `./_example` directory
|
||||||
|
|
||||||
|
FAQ
|
||||||
|
---
|
||||||
|
|
||||||
|
* Want to build go-sqlite3 with libsqlite3 on my linux.
|
||||||
|
|
||||||
|
Use `go build --tags "libsqlite3 linux"`
|
||||||
|
|
||||||
|
* Want to build go-sqlite3 with libsqlite3 on OS X.
|
||||||
|
|
||||||
|
Install sqlite3 from homebrew: `brew install sqlite3`
|
||||||
|
Use `go build --tags "libsqlite3 darwin"`
|
||||||
|
|
||||||
|
* Want to build go-sqlite3 with icu extension.
|
||||||
|
|
||||||
|
Use `go build --tags "icu"`
|
||||||
|
|
||||||
|
* Can't build go-sqlite3 on windows 64bit.
|
||||||
|
|
||||||
|
> Probably, you are using go 1.0, go1.0 has a problem when it comes to compiling/linking on windows 64bit.
|
||||||
|
> See: https://github.com/mattn/go-sqlite3/issues/27
|
||||||
|
|
||||||
|
* Getting insert error while query is opened.
|
||||||
|
|
||||||
|
> You can pass some arguments into the connection string, for example, a URI.
|
||||||
|
> See: https://github.com/mattn/go-sqlite3/issues/39
|
||||||
|
|
||||||
|
* Do you want to cross compile? mingw on Linux or Mac?
|
||||||
|
|
||||||
|
> See: https://github.com/mattn/go-sqlite3/issues/106
|
||||||
|
> See also: http://www.limitlessfx.com/cross-compile-golang-app-for-windows-from-linux.html
|
||||||
|
|
||||||
|
* Want to get time.Time with current locale
|
||||||
|
|
||||||
|
Use `loc=auto` in SQLite3 filename schema like `file:foo.db?loc=auto`.
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
MIT: http://mattn.mit-license.org/2012
|
||||||
|
|
||||||
|
sqlite3-binding.c, sqlite3-binding.h, sqlite3ext.h
|
||||||
|
|
||||||
|
The -binding suffix was added to avoid build failures under gccgo.
|
||||||
|
|
||||||
|
In this repository, those files are an amalgamation of code that was copied from SQLite3. The license of that code is the same as the license of SQLite3.
|
||||||
|
|
||||||
|
Author
|
||||||
|
------
|
||||||
|
|
||||||
|
Yasuhiro Matsumoto (a.k.a mattn)
|
|
@ -0,0 +1,74 @@
|
||||||
|
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by an MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package sqlite3
|
||||||
|
|
||||||
|
/*
|
||||||
|
#ifndef USE_LIBSQLITE3
|
||||||
|
#include <sqlite3-binding.h>
|
||||||
|
#else
|
||||||
|
#include <sqlite3.h>
|
||||||
|
#endif
|
||||||
|
#include <stdlib.h>
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SQLiteBackup struct {
|
||||||
|
b *C.sqlite3_backup
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SQLiteConn) Backup(dest string, conn *SQLiteConn, src string) (*SQLiteBackup, error) {
|
||||||
|
destptr := C.CString(dest)
|
||||||
|
defer C.free(unsafe.Pointer(destptr))
|
||||||
|
srcptr := C.CString(src)
|
||||||
|
defer C.free(unsafe.Pointer(srcptr))
|
||||||
|
|
||||||
|
if b := C.sqlite3_backup_init(c.db, destptr, conn.db, srcptr); b != nil {
|
||||||
|
bb := &SQLiteBackup{b: b}
|
||||||
|
runtime.SetFinalizer(bb, (*SQLiteBackup).Finish)
|
||||||
|
return bb, nil
|
||||||
|
}
|
||||||
|
return nil, c.lastError()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backs up for one step. Calls the underlying `sqlite3_backup_step` function.
|
||||||
|
// This function returns a boolean indicating if the backup is done and
|
||||||
|
// an error signalling any other error. Done is returned if the underlying C
|
||||||
|
// function returns SQLITE_DONE (Code 101)
|
||||||
|
func (b *SQLiteBackup) Step(p int) (bool, error) {
|
||||||
|
ret := C.sqlite3_backup_step(b.b, C.int(p))
|
||||||
|
if ret == C.SQLITE_DONE {
|
||||||
|
return true, nil
|
||||||
|
} else if ret != 0 && ret != C.SQLITE_LOCKED && ret != C.SQLITE_BUSY {
|
||||||
|
return false, Error{Code: ErrNo(ret)}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *SQLiteBackup) Remaining() int {
|
||||||
|
return int(C.sqlite3_backup_remaining(b.b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *SQLiteBackup) PageCount() int {
|
||||||
|
return int(C.sqlite3_backup_pagecount(b.b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *SQLiteBackup) Finish() error {
|
||||||
|
return b.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *SQLiteBackup) Close() error {
|
||||||
|
ret := C.sqlite3_backup_finish(b.b)
|
||||||
|
if ret != 0 {
|
||||||
|
return Error{Code: ErrNo(ret)}
|
||||||
|
}
|
||||||
|
b.b = nil
|
||||||
|
runtime.SetFinalizer(b, nil)
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,336 @@
|
||||||
|
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by an MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package sqlite3
|
||||||
|
|
||||||
|
// You can't export a Go function to C and have definitions in the C
|
||||||
|
// preamble in the same file, so we have to have callbackTrampoline in
|
||||||
|
// its own file. Because we need a separate file anyway, the support
|
||||||
|
// code for SQLite custom functions is in here.
|
||||||
|
|
||||||
|
/*
|
||||||
|
#include <sqlite3-binding.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
void _sqlite3_result_text(sqlite3_context* ctx, const char* s);
|
||||||
|
void _sqlite3_result_blob(sqlite3_context* ctx, const void* b, int l);
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
//export callbackTrampoline
|
||||||
|
func callbackTrampoline(ctx *C.sqlite3_context, argc int, argv **C.sqlite3_value) {
|
||||||
|
args := (*[(math.MaxInt32 - 1) / unsafe.Sizeof((*C.sqlite3_value)(nil))]*C.sqlite3_value)(unsafe.Pointer(argv))[:argc:argc]
|
||||||
|
fi := lookupHandle(uintptr(C.sqlite3_user_data(ctx))).(*functionInfo)
|
||||||
|
fi.Call(ctx, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export stepTrampoline
|
||||||
|
func stepTrampoline(ctx *C.sqlite3_context, argc int, argv **C.sqlite3_value) {
|
||||||
|
args := (*[(math.MaxInt32 - 1) / unsafe.Sizeof((*C.sqlite3_value)(nil))]*C.sqlite3_value)(unsafe.Pointer(argv))[:argc:argc]
|
||||||
|
ai := lookupHandle(uintptr(C.sqlite3_user_data(ctx))).(*aggInfo)
|
||||||
|
ai.Step(ctx, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export doneTrampoline
|
||||||
|
func doneTrampoline(ctx *C.sqlite3_context) {
|
||||||
|
handle := uintptr(C.sqlite3_user_data(ctx))
|
||||||
|
ai := lookupHandle(handle).(*aggInfo)
|
||||||
|
ai.Done(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use handles to avoid passing Go pointers to C.
|
||||||
|
|
||||||
|
type handleVal struct {
|
||||||
|
db *SQLiteConn
|
||||||
|
val interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var handleLock sync.Mutex
|
||||||
|
var handleVals = make(map[uintptr]handleVal)
|
||||||
|
var handleIndex uintptr = 100
|
||||||
|
|
||||||
|
func newHandle(db *SQLiteConn, v interface{}) uintptr {
|
||||||
|
handleLock.Lock()
|
||||||
|
defer handleLock.Unlock()
|
||||||
|
i := handleIndex
|
||||||
|
handleIndex++
|
||||||
|
handleVals[i] = handleVal{db, v}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupHandle(handle uintptr) interface{} {
|
||||||
|
handleLock.Lock()
|
||||||
|
defer handleLock.Unlock()
|
||||||
|
r, ok := handleVals[handle]
|
||||||
|
if !ok {
|
||||||
|
if handle >= 100 && handle < handleIndex {
|
||||||
|
panic("deleted handle")
|
||||||
|
} else {
|
||||||
|
panic("invalid handle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r.val
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteHandles(db *SQLiteConn) {
|
||||||
|
handleLock.Lock()
|
||||||
|
defer handleLock.Unlock()
|
||||||
|
for handle, val := range handleVals {
|
||||||
|
if val.db == db {
|
||||||
|
delete(handleVals, handle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is only here so that tests can refer to it.
|
||||||
|
type callbackArgRaw C.sqlite3_value
|
||||||
|
|
||||||
|
type callbackArgConverter func(*C.sqlite3_value) (reflect.Value, error)
|
||||||
|
|
||||||
|
type callbackArgCast struct {
|
||||||
|
f callbackArgConverter
|
||||||
|
typ reflect.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c callbackArgCast) Run(v *C.sqlite3_value) (reflect.Value, error) {
|
||||||
|
val, err := c.f(v)
|
||||||
|
if err != nil {
|
||||||
|
return reflect.Value{}, err
|
||||||
|
}
|
||||||
|
if !val.Type().ConvertibleTo(c.typ) {
|
||||||
|
return reflect.Value{}, fmt.Errorf("cannot convert %s to %s", val.Type(), c.typ)
|
||||||
|
}
|
||||||
|
return val.Convert(c.typ), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackArgInt64(v *C.sqlite3_value) (reflect.Value, error) {
|
||||||
|
if C.sqlite3_value_type(v) != C.SQLITE_INTEGER {
|
||||||
|
return reflect.Value{}, fmt.Errorf("argument must be an INTEGER")
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(int64(C.sqlite3_value_int64(v))), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackArgBool(v *C.sqlite3_value) (reflect.Value, error) {
|
||||||
|
if C.sqlite3_value_type(v) != C.SQLITE_INTEGER {
|
||||||
|
return reflect.Value{}, fmt.Errorf("argument must be an INTEGER")
|
||||||
|
}
|
||||||
|
i := int64(C.sqlite3_value_int64(v))
|
||||||
|
val := false
|
||||||
|
if i != 0 {
|
||||||
|
val = true
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(val), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackArgFloat64(v *C.sqlite3_value) (reflect.Value, error) {
|
||||||
|
if C.sqlite3_value_type(v) != C.SQLITE_FLOAT {
|
||||||
|
return reflect.Value{}, fmt.Errorf("argument must be a FLOAT")
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(float64(C.sqlite3_value_double(v))), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackArgBytes(v *C.sqlite3_value) (reflect.Value, error) {
|
||||||
|
switch C.sqlite3_value_type(v) {
|
||||||
|
case C.SQLITE_BLOB:
|
||||||
|
l := C.sqlite3_value_bytes(v)
|
||||||
|
p := C.sqlite3_value_blob(v)
|
||||||
|
return reflect.ValueOf(C.GoBytes(p, l)), nil
|
||||||
|
case C.SQLITE_TEXT:
|
||||||
|
l := C.sqlite3_value_bytes(v)
|
||||||
|
c := unsafe.Pointer(C.sqlite3_value_text(v))
|
||||||
|
return reflect.ValueOf(C.GoBytes(c, l)), nil
|
||||||
|
default:
|
||||||
|
return reflect.Value{}, fmt.Errorf("argument must be BLOB or TEXT")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackArgString(v *C.sqlite3_value) (reflect.Value, error) {
|
||||||
|
switch C.sqlite3_value_type(v) {
|
||||||
|
case C.SQLITE_BLOB:
|
||||||
|
l := C.sqlite3_value_bytes(v)
|
||||||
|
p := (*C.char)(C.sqlite3_value_blob(v))
|
||||||
|
return reflect.ValueOf(C.GoStringN(p, l)), nil
|
||||||
|
case C.SQLITE_TEXT:
|
||||||
|
c := (*C.char)(unsafe.Pointer(C.sqlite3_value_text(v)))
|
||||||
|
return reflect.ValueOf(C.GoString(c)), nil
|
||||||
|
default:
|
||||||
|
return reflect.Value{}, fmt.Errorf("argument must be BLOB or TEXT")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackArgGeneric(v *C.sqlite3_value) (reflect.Value, error) {
|
||||||
|
switch C.sqlite3_value_type(v) {
|
||||||
|
case C.SQLITE_INTEGER:
|
||||||
|
return callbackArgInt64(v)
|
||||||
|
case C.SQLITE_FLOAT:
|
||||||
|
return callbackArgFloat64(v)
|
||||||
|
case C.SQLITE_TEXT:
|
||||||
|
return callbackArgString(v)
|
||||||
|
case C.SQLITE_BLOB:
|
||||||
|
return callbackArgBytes(v)
|
||||||
|
case C.SQLITE_NULL:
|
||||||
|
// Interpret NULL as a nil byte slice.
|
||||||
|
var ret []byte
|
||||||
|
return reflect.ValueOf(ret), nil
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackArg(typ reflect.Type) (callbackArgConverter, error) {
|
||||||
|
switch typ.Kind() {
|
||||||
|
case reflect.Interface:
|
||||||
|
if typ.NumMethod() != 0 {
|
||||||
|
return nil, errors.New("the only supported interface type is interface{}")
|
||||||
|
}
|
||||||
|
return callbackArgGeneric, nil
|
||||||
|
case reflect.Slice:
|
||||||
|
if typ.Elem().Kind() != reflect.Uint8 {
|
||||||
|
return nil, errors.New("the only supported slice type is []byte")
|
||||||
|
}
|
||||||
|
return callbackArgBytes, nil
|
||||||
|
case reflect.String:
|
||||||
|
return callbackArgString, nil
|
||||||
|
case reflect.Bool:
|
||||||
|
return callbackArgBool, nil
|
||||||
|
case reflect.Int64:
|
||||||
|
return callbackArgInt64, nil
|
||||||
|
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Int, reflect.Uint:
|
||||||
|
c := callbackArgCast{callbackArgInt64, typ}
|
||||||
|
return c.Run, nil
|
||||||
|
case reflect.Float64:
|
||||||
|
return callbackArgFloat64, nil
|
||||||
|
case reflect.Float32:
|
||||||
|
c := callbackArgCast{callbackArgFloat64, typ}
|
||||||
|
return c.Run, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("don't know how to convert to %s", typ)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackConvertArgs(argv []*C.sqlite3_value, converters []callbackArgConverter, variadic callbackArgConverter) ([]reflect.Value, error) {
|
||||||
|
var args []reflect.Value
|
||||||
|
|
||||||
|
if len(argv) < len(converters) {
|
||||||
|
return nil, fmt.Errorf("function requires at least %d arguments", len(converters))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, arg := range argv[:len(converters)] {
|
||||||
|
v, err := converters[i](arg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
args = append(args, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if variadic != nil {
|
||||||
|
for _, arg := range argv[len(converters):] {
|
||||||
|
v, err := variadic(arg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
args = append(args, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type callbackRetConverter func(*C.sqlite3_context, reflect.Value) error
|
||||||
|
|
||||||
|
func callbackRetInteger(ctx *C.sqlite3_context, v reflect.Value) error {
|
||||||
|
switch v.Type().Kind() {
|
||||||
|
case reflect.Int64:
|
||||||
|
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Int, reflect.Uint:
|
||||||
|
v = v.Convert(reflect.TypeOf(int64(0)))
|
||||||
|
case reflect.Bool:
|
||||||
|
b := v.Interface().(bool)
|
||||||
|
if b {
|
||||||
|
v = reflect.ValueOf(int64(1))
|
||||||
|
} else {
|
||||||
|
v = reflect.ValueOf(int64(0))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("cannot convert %s to INTEGER", v.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
C.sqlite3_result_int64(ctx, C.sqlite3_int64(v.Interface().(int64)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackRetFloat(ctx *C.sqlite3_context, v reflect.Value) error {
|
||||||
|
switch v.Type().Kind() {
|
||||||
|
case reflect.Float64:
|
||||||
|
case reflect.Float32:
|
||||||
|
v = v.Convert(reflect.TypeOf(float64(0)))
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("cannot convert %s to FLOAT", v.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
C.sqlite3_result_double(ctx, C.double(v.Interface().(float64)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackRetBlob(ctx *C.sqlite3_context, v reflect.Value) error {
|
||||||
|
if v.Type().Kind() != reflect.Slice || v.Type().Elem().Kind() != reflect.Uint8 {
|
||||||
|
return fmt.Errorf("cannot convert %s to BLOB", v.Type())
|
||||||
|
}
|
||||||
|
i := v.Interface()
|
||||||
|
if i == nil || len(i.([]byte)) == 0 {
|
||||||
|
C.sqlite3_result_null(ctx)
|
||||||
|
} else {
|
||||||
|
bs := i.([]byte)
|
||||||
|
C._sqlite3_result_blob(ctx, unsafe.Pointer(&bs[0]), C.int(len(bs)))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackRetText(ctx *C.sqlite3_context, v reflect.Value) error {
|
||||||
|
if v.Type().Kind() != reflect.String {
|
||||||
|
return fmt.Errorf("cannot convert %s to TEXT", v.Type())
|
||||||
|
}
|
||||||
|
C._sqlite3_result_text(ctx, C.CString(v.Interface().(string)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackRet(typ reflect.Type) (callbackRetConverter, error) {
|
||||||
|
switch typ.Kind() {
|
||||||
|
case reflect.Slice:
|
||||||
|
if typ.Elem().Kind() != reflect.Uint8 {
|
||||||
|
return nil, errors.New("the only supported slice type is []byte")
|
||||||
|
}
|
||||||
|
return callbackRetBlob, nil
|
||||||
|
case reflect.String:
|
||||||
|
return callbackRetText, nil
|
||||||
|
case reflect.Bool, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Int, reflect.Uint:
|
||||||
|
return callbackRetInteger, nil
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return callbackRetFloat, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("don't know how to convert to %s", typ)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackError(ctx *C.sqlite3_context, err error) {
|
||||||
|
cstr := C.CString(err.Error())
|
||||||
|
defer C.free(unsafe.Pointer(cstr))
|
||||||
|
C.sqlite3_result_error(ctx, cstr, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test support code. Tests are not allowed to import "C", so we can't
|
||||||
|
// declare any functions that use C.sqlite3_value.
|
||||||
|
func callbackSyntheticForTests(v reflect.Value, err error) callbackArgConverter {
|
||||||
|
return func(*C.sqlite3_value) (reflect.Value, error) {
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
/*
|
||||||
|
Package sqlite3 provides interface to SQLite3 databases.
|
||||||
|
|
||||||
|
This works as a driver for database/sql.
|
||||||
|
|
||||||
|
Installation
|
||||||
|
|
||||||
|
go get github.com/mattn/go-sqlite3
|
||||||
|
|
||||||
|
Supported Types
|
||||||
|
|
||||||
|
Currently, go-sqlite3 supports the following data types.
|
||||||
|
|
||||||
|
+------------------------------+
|
||||||
|
|go | sqlite3 |
|
||||||
|
|----------|-------------------|
|
||||||
|
|nil | null |
|
||||||
|
|int | integer |
|
||||||
|
|int64 | integer |
|
||||||
|
|float64 | float |
|
||||||
|
|bool | integer |
|
||||||
|
|[]byte | blob |
|
||||||
|
|string | text |
|
||||||
|
|time.Time | timestamp/datetime|
|
||||||
|
+------------------------------+
|
||||||
|
|
||||||
|
SQLite3 Extension
|
||||||
|
|
||||||
|
You can write your own extension module for sqlite3. For example, below is an
|
||||||
|
extension for a Regexp matcher operation.
|
||||||
|
|
||||||
|
#include <pcre.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <sqlite3ext.h>
|
||||||
|
|
||||||
|
SQLITE_EXTENSION_INIT1
|
||||||
|
static void regexp_func(sqlite3_context *context, int argc, sqlite3_value **argv) {
|
||||||
|
if (argc >= 2) {
|
||||||
|
const char *target = (const char *)sqlite3_value_text(argv[1]);
|
||||||
|
const char *pattern = (const char *)sqlite3_value_text(argv[0]);
|
||||||
|
const char* errstr = NULL;
|
||||||
|
int erroff = 0;
|
||||||
|
int vec[500];
|
||||||
|
int n, rc;
|
||||||
|
pcre* re = pcre_compile(pattern, 0, &errstr, &erroff, NULL);
|
||||||
|
rc = pcre_exec(re, NULL, target, strlen(target), 0, 0, vec, 500);
|
||||||
|
if (rc <= 0) {
|
||||||
|
sqlite3_result_error(context, errstr, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sqlite3_result_int(context, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
__declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
int sqlite3_extension_init(sqlite3 *db, char **errmsg,
|
||||||
|
const sqlite3_api_routines *api) {
|
||||||
|
SQLITE_EXTENSION_INIT2(api);
|
||||||
|
return sqlite3_create_function(db, "regexp", 2, SQLITE_UTF8,
|
||||||
|
(void*)db, regexp_func, NULL, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
It needs to be built as a so/dll shared library. And you need to register
|
||||||
|
the extension module like below.
|
||||||
|
|
||||||
|
sql.Register("sqlite3_with_extensions",
|
||||||
|
&sqlite3.SQLiteDriver{
|
||||||
|
Extensions: []string{
|
||||||
|
"sqlite3_mod_regexp",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
Then, you can use this extension.
|
||||||
|
|
||||||
|
rows, err := db.Query("select text from mytable where name regexp '^golang'")
|
||||||
|
|
||||||
|
Connection Hook
|
||||||
|
|
||||||
|
You can hook and inject your code when the connection is established. database/sql
|
||||||
|
doesn't provide a way to get native go-sqlite3 interfaces. So if you want,
|
||||||
|
you need to set ConnectHook and get the SQLiteConn.
|
||||||
|
|
||||||
|
sql.Register("sqlite3_with_hook_example",
|
||||||
|
&sqlite3.SQLiteDriver{
|
||||||
|
ConnectHook: func(conn *sqlite3.SQLiteConn) error {
|
||||||
|
sqlite3conn = append(sqlite3conn, conn)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
Go SQlite3 Extensions
|
||||||
|
|
||||||
|
If you want to register Go functions as SQLite extension functions,
|
||||||
|
call RegisterFunction from ConnectHook.
|
||||||
|
|
||||||
|
regex = func(re, s string) (bool, error) {
|
||||||
|
return regexp.MatchString(re, s)
|
||||||
|
}
|
||||||
|
sql.Register("sqlite3_with_go_func",
|
||||||
|
&sqlite3.SQLiteDriver{
|
||||||
|
ConnectHook: func(conn *sqlite3.SQLiteConn) error {
|
||||||
|
return conn.RegisterFunc("regexp", regex, true)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
See the documentation of RegisterFunc for more details.
|
||||||
|
|
||||||
|
*/
|
||||||
|
package sqlite3
|
|
@ -0,0 +1,128 @@
|
||||||
|
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by an MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package sqlite3
|
||||||
|
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
type ErrNo int
|
||||||
|
|
||||||
|
const ErrNoMask C.int = 0xff
|
||||||
|
|
||||||
|
type ErrNoExtended int
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
Code ErrNo /* The error code returned by SQLite */
|
||||||
|
ExtendedCode ErrNoExtended /* The extended error code returned by SQLite */
|
||||||
|
err string /* The error string returned by sqlite3_errmsg(),
|
||||||
|
this usually contains more specific details. */
|
||||||
|
}
|
||||||
|
|
||||||
|
// result codes from http://www.sqlite.org/c3ref/c_abort.html
|
||||||
|
var (
|
||||||
|
ErrError = ErrNo(1) /* SQL error or missing database */
|
||||||
|
ErrInternal = ErrNo(2) /* Internal logic error in SQLite */
|
||||||
|
ErrPerm = ErrNo(3) /* Access permission denied */
|
||||||
|
ErrAbort = ErrNo(4) /* Callback routine requested an abort */
|
||||||
|
ErrBusy = ErrNo(5) /* The database file is locked */
|
||||||
|
ErrLocked = ErrNo(6) /* A table in the database is locked */
|
||||||
|
ErrNomem = ErrNo(7) /* A malloc() failed */
|
||||||
|
ErrReadonly = ErrNo(8) /* Attempt to write a readonly database */
|
||||||
|
ErrInterrupt = ErrNo(9) /* Operation terminated by sqlite3_interrupt() */
|
||||||
|
ErrIoErr = ErrNo(10) /* Some kind of disk I/O error occurred */
|
||||||
|
ErrCorrupt = ErrNo(11) /* The database disk image is malformed */
|
||||||
|
ErrNotFound = ErrNo(12) /* Unknown opcode in sqlite3_file_control() */
|
||||||
|
ErrFull = ErrNo(13) /* Insertion failed because database is full */
|
||||||
|
ErrCantOpen = ErrNo(14) /* Unable to open the database file */
|
||||||
|
ErrProtocol = ErrNo(15) /* Database lock protocol error */
|
||||||
|
ErrEmpty = ErrNo(16) /* Database is empty */
|
||||||
|
ErrSchema = ErrNo(17) /* The database schema changed */
|
||||||
|
ErrTooBig = ErrNo(18) /* String or BLOB exceeds size limit */
|
||||||
|
ErrConstraint = ErrNo(19) /* Abort due to constraint violation */
|
||||||
|
ErrMismatch = ErrNo(20) /* Data type mismatch */
|
||||||
|
ErrMisuse = ErrNo(21) /* Library used incorrectly */
|
||||||
|
ErrNoLFS = ErrNo(22) /* Uses OS features not supported on host */
|
||||||
|
ErrAuth = ErrNo(23) /* Authorization denied */
|
||||||
|
ErrFormat = ErrNo(24) /* Auxiliary database format error */
|
||||||
|
ErrRange = ErrNo(25) /* 2nd parameter to sqlite3_bind out of range */
|
||||||
|
ErrNotADB = ErrNo(26) /* File opened that is not a database file */
|
||||||
|
ErrNotice = ErrNo(27) /* Notifications from sqlite3_log() */
|
||||||
|
ErrWarning = ErrNo(28) /* Warnings from sqlite3_log() */
|
||||||
|
)
|
||||||
|
|
||||||
|
func (err ErrNo) Error() string {
|
||||||
|
return Error{Code: err}.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrNo) Extend(by int) ErrNoExtended {
|
||||||
|
return ErrNoExtended(int(err) | (by << 8))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrNoExtended) Error() string {
|
||||||
|
return Error{Code: ErrNo(C.int(err) & ErrNoMask), ExtendedCode: err}.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err Error) Error() string {
|
||||||
|
if err.err != "" {
|
||||||
|
return err.err
|
||||||
|
}
|
||||||
|
return errorString(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// result codes from http://www.sqlite.org/c3ref/c_abort_rollback.html
|
||||||
|
var (
|
||||||
|
ErrIoErrRead = ErrIoErr.Extend(1)
|
||||||
|
ErrIoErrShortRead = ErrIoErr.Extend(2)
|
||||||
|
ErrIoErrWrite = ErrIoErr.Extend(3)
|
||||||
|
ErrIoErrFsync = ErrIoErr.Extend(4)
|
||||||
|
ErrIoErrDirFsync = ErrIoErr.Extend(5)
|
||||||
|
ErrIoErrTruncate = ErrIoErr.Extend(6)
|
||||||
|
ErrIoErrFstat = ErrIoErr.Extend(7)
|
||||||
|
ErrIoErrUnlock = ErrIoErr.Extend(8)
|
||||||
|
ErrIoErrRDlock = ErrIoErr.Extend(9)
|
||||||
|
ErrIoErrDelete = ErrIoErr.Extend(10)
|
||||||
|
ErrIoErrBlocked = ErrIoErr.Extend(11)
|
||||||
|
ErrIoErrNoMem = ErrIoErr.Extend(12)
|
||||||
|
ErrIoErrAccess = ErrIoErr.Extend(13)
|
||||||
|
ErrIoErrCheckReservedLock = ErrIoErr.Extend(14)
|
||||||
|
ErrIoErrLock = ErrIoErr.Extend(15)
|
||||||
|
ErrIoErrClose = ErrIoErr.Extend(16)
|
||||||
|
ErrIoErrDirClose = ErrIoErr.Extend(17)
|
||||||
|
ErrIoErrSHMOpen = ErrIoErr.Extend(18)
|
||||||
|
ErrIoErrSHMSize = ErrIoErr.Extend(19)
|
||||||
|
ErrIoErrSHMLock = ErrIoErr.Extend(20)
|
||||||
|
ErrIoErrSHMMap = ErrIoErr.Extend(21)
|
||||||
|
ErrIoErrSeek = ErrIoErr.Extend(22)
|
||||||
|
ErrIoErrDeleteNoent = ErrIoErr.Extend(23)
|
||||||
|
ErrIoErrMMap = ErrIoErr.Extend(24)
|
||||||
|
ErrIoErrGetTempPath = ErrIoErr.Extend(25)
|
||||||
|
ErrIoErrConvPath = ErrIoErr.Extend(26)
|
||||||
|
ErrLockedSharedCache = ErrLocked.Extend(1)
|
||||||
|
ErrBusyRecovery = ErrBusy.Extend(1)
|
||||||
|
ErrBusySnapshot = ErrBusy.Extend(2)
|
||||||
|
ErrCantOpenNoTempDir = ErrCantOpen.Extend(1)
|
||||||
|
ErrCantOpenIsDir = ErrCantOpen.Extend(2)
|
||||||
|
ErrCantOpenFullPath = ErrCantOpen.Extend(3)
|
||||||
|
ErrCantOpenConvPath = ErrCantOpen.Extend(4)
|
||||||
|
ErrCorruptVTab = ErrCorrupt.Extend(1)
|
||||||
|
ErrReadonlyRecovery = ErrReadonly.Extend(1)
|
||||||
|
ErrReadonlyCantLock = ErrReadonly.Extend(2)
|
||||||
|
ErrReadonlyRollback = ErrReadonly.Extend(3)
|
||||||
|
ErrReadonlyDbMoved = ErrReadonly.Extend(4)
|
||||||
|
ErrAbortRollback = ErrAbort.Extend(2)
|
||||||
|
ErrConstraintCheck = ErrConstraint.Extend(1)
|
||||||
|
ErrConstraintCommitHook = ErrConstraint.Extend(2)
|
||||||
|
ErrConstraintForeignKey = ErrConstraint.Extend(3)
|
||||||
|
ErrConstraintFunction = ErrConstraint.Extend(4)
|
||||||
|
ErrConstraintNotNull = ErrConstraint.Extend(5)
|
||||||
|
ErrConstraintPrimaryKey = ErrConstraint.Extend(6)
|
||||||
|
ErrConstraintTrigger = ErrConstraint.Extend(7)
|
||||||
|
ErrConstraintUnique = ErrConstraint.Extend(8)
|
||||||
|
ErrConstraintVTab = ErrConstraint.Extend(9)
|
||||||
|
ErrConstraintRowId = ErrConstraint.Extend(10)
|
||||||
|
ErrNoticeRecoverWAL = ErrNotice.Extend(1)
|
||||||
|
ErrNoticeRecoverRollback = ErrNotice.Extend(2)
|
||||||
|
ErrWarningAutoIndex = ErrWarning.Extend(1)
|
||||||
|
)
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,13 @@
|
||||||
|
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by an MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
// +build fts5
|
||||||
|
|
||||||
|
package sqlite3
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo CFLAGS: -DSQLITE_ENABLE_FTS5
|
||||||
|
#cgo LDFLAGS: -lm
|
||||||
|
*/
|
||||||
|
import "C"
|
|
@ -0,0 +1,13 @@
|
||||||
|
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by an MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
// +build icu
|
||||||
|
|
||||||
|
package sqlite3
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo LDFLAGS: -licuuc -licui18n
|
||||||
|
#cgo CFLAGS: -DSQLITE_ENABLE_ICU
|
||||||
|
*/
|
||||||
|
import "C"
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by an MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
// +build json1
|
||||||
|
|
||||||
|
package sqlite3
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo CFLAGS: -DSQLITE_ENABLE_JSON1
|
||||||
|
*/
|
||||||
|
import "C"
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by an MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
// +build libsqlite3
|
||||||
|
|
||||||
|
package sqlite3
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo CFLAGS: -DUSE_LIBSQLITE3
|
||||||
|
#cgo linux LDFLAGS: -lsqlite3
|
||||||
|
#cgo darwin LDFLAGS: -L/usr/local/opt/sqlite/lib -lsqlite3
|
||||||
|
*/
|
||||||
|
import "C"
|
|
@ -0,0 +1,63 @@
|
||||||
|
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by an MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
// +build !sqlite_omit_load_extension
|
||||||
|
|
||||||
|
package sqlite3
|
||||||
|
|
||||||
|
/*
|
||||||
|
#include <sqlite3-binding.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *SQLiteConn) loadExtensions(extensions []string) error {
|
||||||
|
rv := C.sqlite3_enable_load_extension(c.db, 1)
|
||||||
|
if rv != C.SQLITE_OK {
|
||||||
|
return errors.New(C.GoString(C.sqlite3_errmsg(c.db)))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, extension := range extensions {
|
||||||
|
cext := C.CString(extension)
|
||||||
|
defer C.free(unsafe.Pointer(cext))
|
||||||
|
rv = C.sqlite3_load_extension(c.db, cext, nil, nil)
|
||||||
|
if rv != C.SQLITE_OK {
|
||||||
|
return errors.New(C.GoString(C.sqlite3_errmsg(c.db)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rv = C.sqlite3_enable_load_extension(c.db, 0)
|
||||||
|
if rv != C.SQLITE_OK {
|
||||||
|
return errors.New(C.GoString(C.sqlite3_errmsg(c.db)))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SQLiteConn) LoadExtension(lib string, entry string) error {
|
||||||
|
rv := C.sqlite3_enable_load_extension(c.db, 1)
|
||||||
|
if rv != C.SQLITE_OK {
|
||||||
|
return errors.New(C.GoString(C.sqlite3_errmsg(c.db)))
|
||||||
|
}
|
||||||
|
|
||||||
|
clib := C.CString(lib)
|
||||||
|
defer C.free(unsafe.Pointer(clib))
|
||||||
|
centry := C.CString(entry)
|
||||||
|
defer C.free(unsafe.Pointer(centry))
|
||||||
|
|
||||||
|
rv = C.sqlite3_load_extension(c.db, clib, centry, nil)
|
||||||
|
if rv != C.SQLITE_OK {
|
||||||
|
return errors.New(C.GoString(C.sqlite3_errmsg(c.db)))
|
||||||
|
}
|
||||||
|
|
||||||
|
rv = C.sqlite3_enable_load_extension(c.db, 0)
|
||||||
|
if rv != C.SQLITE_OK {
|
||||||
|
return errors.New(C.GoString(C.sqlite3_errmsg(c.db)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by an MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
// +build sqlite_omit_load_extension
|
||||||
|
|
||||||
|
package sqlite3
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo CFLAGS: -DSQLITE_OMIT_LOAD_EXTENSION
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *SQLiteConn) loadExtensions(extensions []string) error {
|
||||||
|
return errors.New("Extensions have been disabled for static builds")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SQLiteConn) LoadExtension(lib string, entry string) error {
|
||||||
|
return errors.New("Extensions have been disabled for static builds")
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by an MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package sqlite3
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo CFLAGS: -I.
|
||||||
|
#cgo linux LDFLAGS: -ldl
|
||||||
|
*/
|
||||||
|
import "C"
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue