<commit_before>// The MIT License (MIT)

// Copyright (c) 2017 Fabian Wenzelmann

// 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.

package goauth

import (
	"database/sql"
	"errors"
	"fmt"
	"time"
)

// General SQL implementation with interfaces, MySQL etc. below.
type SQLSessionTemplate interface {
	InitQ() string
	GetQ() string
	CreateQ() string
	DeleteForUserQ() string
	DeleteInvalidQ() string
	DeleteKeyQ() string
	TimeFromScanType(val interface{}) (time.Time, error)
}

type SQLSessionHandler struct {
	DB                                                               *sql.DB
	InitQ, GetQ, CreateQ, DeleteForUserQ, DeleteInvalidQ, DeleteKeyQ string
	TableName                                                        string
	UserIDType                                                       string
	KeySize                                                          int
	TimeFromScanType                                                 func(val interface{}) (time.Time, error)
	ForceUIDuint                                                     bool
}

func NewSQLSessionHandler(db *sql.DB, t SQLSessionTemplate) *SQLSessionHandler {
	// I'm not so happy with this many lines of code, but I don't want to use
	// the reflect package or something either...
	c := SQLSessionHandler{DB: db, TableName: "user_sessions",
		UserIDType: "BIGINT UNSIGNED NOT NULL", KeySize: DefaultKeyLength,
		TimeFromScanType: t.TimeFromScanType, ForceUIDuint: false}
	c.InitQ = fmt.Sprintf(t.InitQ(), c.TableName, c.UserIDType, c.KeySize)
	c.GetQ = fmt.Sprintf(t.GetQ(), c.TableName)
	c.CreateQ = fmt.Sprintf(t.CreateQ(), c.TableName)
	c.DeleteForUserQ = fmt.Sprintf(t.DeleteForUserQ(), c.TableName)
	c.DeleteInvalidQ = fmt.Sprintf(t.DeleteInvalidQ(), c.TableName)
	c.DeleteKeyQ = fmt.Sprintf(t.DeleteKeyQ(), c.TableName)
	return &c
}

func NewMySQLController(db *sql.DB) *SessionController {
	handler := NewSQLSessionHandler(db, NewMySQLTemplate())
	return NewSessionController(handler)
}

func (c *SQLSessionHandler) Init() error {
	_, err := c.DB.Exec(c.InitQ)
	return err
}

func (c *SQLSessionHandler) GetData(key string) (*SessionKeyData, error) {
	var uid, createdVal, validUntilVal interface{}
	var err error
	row := c.DB.QueryRow(c.GetQ, key)
	if c.ForceUIDuint {
		var uidUint uint64
		err = row.Scan(&uidUint, &createdVal, &validUntilVal)
		uid = uidUint
	} else {
		err = row.Scan(&uid, &createdVal, &validUntilVal)
	}
	if err != nil {
		if err == sql.ErrNoRows {
			return nil, KeyNotFoundErr
		}
		return nil, err
	}
	created, err := c.TimeFromScanType(createdVal)
	if err != nil {
		return nil, err
	}
	validUntil, err := c.TimeFromScanType(validUntilVal)
	if err != nil {
		return nil, err
	}
	// everything ok
	val := SessionKeyData{User: uid, CreationTime: created, ValidUntil: validUntil}
	return &val, nil
}

func (c *SQLSessionHandler) CreateEntry(user UserKeyType, key string, validDuration time.Duration) (*SessionKeyData, error) {
	data := CurrentTimeKeyData(user, validDuration)
	_, err := c.DB.Exec(c.CreateQ, user, key, data.CreationTime, data.ValidUntil)
	if err != nil {
		return nil, err
	}
	return data, nil
}

func (c *SQLSessionHandler) DeleteEntriesForUser(user UserKeyType) (int64, error) {
	res, err := c.DB.Exec(c.DeleteForUserQ, user)
	if err != nil {
		return -1, err
	}
	num, err := res.RowsAffected()
	if err != nil {
		return -1, nil
	}
	return num, nil
}

func (c *SQLSessionHandler) DeleteInvalidKeys() (int64, error) {
	now := CurrentTime()
	res, err := c.DB.Exec(c.DeleteInvalidQ, now)
	if err != nil {
		return -1, err
	}
	num, err := res.RowsAffected()
	if err != nil {
		return -1, nil
	}
	return num, nil
}

func (c *SQLSessionHandler) DeleteKey(key string) error {
	_, err := c.DB.Exec(c.DeleteKeyQ, key)
	return err
}

type MySQLTemplate struct {
}

func NewMySQLTemplate() MySQLTemplate {
	return MySQLTemplate{}
}

func (t MySQLTemplate) InitQ() string {
	return `CREATE TABLE IF NOT EXISTS %s (
		user_id %s,
		session_key CHAR(%d),
    created DATETIME NOT NULL,
    valid_until DATETIME NOT NULL,
		PRIMARY KEY (session_key)
	);`
}

func (t MySQLTemplate) GetQ() string {
	return "SELECT user_id, created, valid_until FROM %s WHERE session_key = ?;"
}

func (t MySQLTemplate) CreateQ() string {
	return "INSERT INTO %s (user_id, session_key, created, valid_until) VALUES (?, ?, ?, ?);"
}

func (t MySQLTemplate) DeleteForUserQ() string {
	return "DELETE FROM %s WHERE user_id = ?;"
}

func (t MySQLTemplate) DeleteInvalidQ() string {
	return "DELETE FROM %s WHERE valid_until > ?;"
}

func (t MySQLTemplate) DeleteKeyQ() string {
	return "DELETE FROM %s WHERE session_key = ?"
}

func (t MySQLTemplate) TimeFromScanType(val interface{}) (time.Time, error) {
	// first check if we already got a time.Time because parseTime in
	// the MySQL driver is true
	if alreadyTime, ok := val.(time.Time); ok {
		return alreadyTime, nil
	}
	if bytes, ok := val.([]byte); ok {
		s := string(bytes)
		// let's hope this is correct... however who came up with THIS parse
		// function definition in Go?!
		return time.Parse("2006-01-02 15:04:05", s)
	} else {
		// we have to return some time... why not now.
		return time.Now().UTC(), errors.New("Invalid date in database, probably a bug if you end up here.")
	}
}

// USERS stuff

type SQLUserQueries struct {
	PwLength      int
	InitQuery     string
	InsertQuery   string
	ValidateQuery string
}

func MySQLUserQueries(pwLength int) *SQLUserQueries {
	initQ := `
	CREATE TABLE IF NOT EXISTS users (
		id SERIAL,
		username VARCHAR(150) NOT NULL,
		first_name VARCHAR(30) NOT NULL,
		last_name VARCHAR(30) NOT NULL,
		email VARCHAR(254),
		password CHAR(%d),
		is_active BOOL,
		last_login DATETIME,
		PRIMARY KEY(id),
		UNIQUE(username)
	);
	`
	initQ = fmt.Sprintf(initQ, pwLength)
	insertQ := `
	INSERT INTO users (username, first_name, last_name, email, password, is_active, last_login)
		VALUES(?, ?, ?, ?, ?, ?, ?);
	`
	validateQ := "SELECT id, password FROM users WHERE username = ?"
	return &SQLUserQueries{PwLength: pwLength, InitQuery: initQ,
		InsertQuery: insertQ, ValidateQuery: validateQ}
}

func SQLite3UserQueries(pwLength int) *SQLUserQueries {
	// nearly everything is the same as for mysql
	res := MySQLUserQueries(pwLength)
	initQ := `
	CREATE TABLE IF NOT EXISTS users (
		id INTEGER PRIMARY KEY,
		username VARCHAR(150) NOT NULL,
		first_name VARCHAR(30) NOT NULL,
		last_name VARCHAR(30) NOT NULL,
		email VARCHAR(254),
		password CHAR(%d),
		is_active BOOL,
		last_login DATETIME,
		UNIQUE(username)
	);
	`
	initQ = fmt.Sprintf(initQ, pwLength)
	res.InitQuery = initQ
	return res
}

type SQLUserHandler struct {
	*SQLUserQueries
	DB        *sql.DB
	PwHandler PasswordHandler
}

func NewSQLUserHandler(queries *SQLUserQueries, db *sql.DB, pwHandler PasswordHandler) *SQLUserHandler {
	if pwHandler == nil {
		pwHandler = NewBcryptHandler(-1)
	}
	return &SQLUserHandler{SQLUserQueries: queries, DB: db, PwHandler: pwHandler}
}

func NewMySQLUserHandler(db *sql.DB, pwHandler PasswordHandler) *SQLUserHandler {
	if pwHandler == nil {
		pwHandler = NewBcryptHandler(-1)
	}
	return NewSQLUserHandler(MySQLUserQueries(pwHandler.PasswordHashLength()),
		db, pwHandler)
}

func NewSQLite3UserHandler(db *sql.DB, pwHandler PasswordHandler) *SQLUserHandler {
	if pwHandler == nil {
		pwHandler = NewBcryptHandler(-1)
	}
	return NewSQLUserHandler(SQLite3UserQueries(pwHandler.PasswordHashLength()),
		db, pwHandler)
}

func (handler *SQLUserHandler) Init() error {
	_, err := handler.DB.Exec(handler.InitQuery)
	return err
}

func (handler *SQLUserHandler) Insert(userName, firstName, lastName, email string, plainPW []byte) (uint64, error) {
	now := CurrentTime()
	// try to encrypt the pw
	encrypted, encErr := handler.PwHandler.GenerateHash(plainPW)
	if encErr != nil {
		return NoUserID, encErr
	}

	res, err := handler.DB.Exec(handler.InsertQuery, userName, firstName, lastName, email, encrypted, true, now)
	if err != nil {
		return NoUserID, err
	}

	// insert worked, try to get the last insert id
	insertInt, getErr := res.LastInsertId()
	if getErr != nil {
		return NoUserID, nil
	}
	// Don't know if this is even possible, but ok
	if insertInt < 0 {
		return NoUserID, nil
	}
	// everything ok, we convert to uint64
	var insertId uint64 = uint64(insertInt)
	return insertId, nil
}

func (handler *SQLUserHandler) Validate(userName string, cleartextPwCheck []byte) (uint64, error) {
	// first try to get the id and the password
	row := handler.DB.QueryRow(handler.ValidateQuery, userName)
	var userId uint64
	var hashPw []byte
	if err := row.Scan(&userId, &hashPw); err != nil {
		if err == sql.ErrNoRows {
			return NoUserID, ErrUserNotFound
		}
		return NoUserID, err
	}
	// validate the password
	test, err := handler.PwHandler.CheckPassword(hashPw, cleartextPwCheck)
	if err != nil {
		return NoUserID, err
	}
	// no error, check if passwords did match
	if test {
		return userId, nil
	} else {
		return NoUserID, nil
	}
}
<commit_msg>more sqlite support, added parameter to block a database if the driver doesn't support multiple goroutines, such as sqlite<commit_after>// The MIT License (MIT)

// Copyright (c) 2017 Fabian Wenzelmann

// 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.

package goauth

import (
	"database/sql"
	"errors"
	"fmt"
	"sync"
	"time"
)

// General SQL implementation with interfaces, MySQL etc. below.
type SQLSessionTemplate interface {
	InitQ() string
	GetQ() string
	CreateQ() string
	DeleteForUserQ() string
	DeleteInvalidQ() string
	DeleteKeyQ() string
	TimeFromScanType(val interface{}) (time.Time, error)
}

type SQLSessionHandler struct {
	DB                                                               *sql.DB
	InitQ, GetQ, CreateQ, DeleteForUserQ, DeleteInvalidQ, DeleteKeyQ string
	TableName                                                        string
	UserIDType                                                       string
	KeySize                                                          int
	TimeFromScanType                                                 func(val interface{}) (time.Time, error)
	ForceUIDuint                                                     bool
	// this is required for example for sqlite, it does not support
	// multiple goroutines when writing!
	// I hope this does not slow us down too much...
	mutex   sync.RWMutex
	blockDB bool
}

func NewSQLSessionHandler(db *sql.DB, t SQLSessionTemplate, tableName, userIDType string, lockDB bool) *SQLSessionHandler {
	if tableName == "" {
		tableName = "user_sessions"
	}
	if userIDType == "" {
		userIDType = "BIGINT UNSIGNED NOT NULL"
	}
	// I'm not so happy with this many lines of code, but I don't want to use
	// the reflect package or something either...
	h := SQLSessionHandler{DB: db, TableName: tableName,
		UserIDType: userIDType, KeySize: DefaultKeyLength,
		TimeFromScanType: t.TimeFromScanType, ForceUIDuint: false, blockDB: lockDB}
	h.InitQ = fmt.Sprintf(t.InitQ(), h.TableName, h.UserIDType, h.KeySize)
	h.GetQ = fmt.Sprintf(t.GetQ(), h.TableName)
	h.CreateQ = fmt.Sprintf(t.CreateQ(), h.TableName)
	h.DeleteForUserQ = fmt.Sprintf(t.DeleteForUserQ(), h.TableName)
	h.DeleteInvalidQ = fmt.Sprintf(t.DeleteInvalidQ(), h.TableName)
	h.DeleteKeyQ = fmt.Sprintf(t.DeleteKeyQ(), h.TableName)
	return &h
}

func NewMySQLController(db *sql.DB, tableName, userIDType string) *SessionController {
	handler := NewSQLSessionHandler(db, NewMySQLSessionTemplate(), tableName, userIDType, false)
	return NewSessionController(handler)
}

func (c *SQLSessionHandler) Init() error {
	if c.blockDB {
		c.mutex.Lock()
		defer c.mutex.Unlock()
	}
	_, err := c.DB.Exec(c.InitQ)
	return err
}

func (c *SQLSessionHandler) GetData(key string) (*SessionKeyData, error) {
	if c.blockDB {
		c.mutex.RLock()
		defer c.mutex.RUnlock()
	}
	var uid, createdVal, validUntilVal interface{}
	var err error
	row := c.DB.QueryRow(c.GetQ, key)
	if c.ForceUIDuint {
		var uidUint uint64
		err = row.Scan(&uidUint, &createdVal, &validUntilVal)
		uid = uidUint
	} else {
		err = row.Scan(&uid, &createdVal, &validUntilVal)
	}
	if err != nil {
		if err == sql.ErrNoRows {
			return nil, KeyNotFoundErr
		}
		return nil, err
	}
	created, err := c.TimeFromScanType(createdVal)
	if err != nil {
		return nil, err
	}
	validUntil, err := c.TimeFromScanType(validUntilVal)
	if err != nil {
		return nil, err
	}
	// everything ok
	val := SessionKeyData{User: uid, CreationTime: created, ValidUntil: validUntil}
	return &val, nil
}

func (c *SQLSessionHandler) CreateEntry(user UserKeyType, key string, validDuration time.Duration) (*SessionKeyData, error) {
	if c.blockDB {
		c.mutex.Lock()
		defer c.mutex.Unlock()
	}
	data := CurrentTimeKeyData(user, validDuration)
	_, err := c.DB.Exec(c.CreateQ, user, key, data.CreationTime, data.ValidUntil)
	if err != nil {
		return nil, err
	}
	return data, nil
}

func (c *SQLSessionHandler) DeleteEntriesForUser(user UserKeyType) (int64, error) {
	if c.blockDB {
		c.mutex.Lock()
		defer c.mutex.Unlock()
	}
	res, err := c.DB.Exec(c.DeleteForUserQ, user)
	if err != nil {
		return -1, err
	}
	num, err := res.RowsAffected()
	if err != nil {
		return -1, nil
	}
	return num, nil
}

func (c *SQLSessionHandler) DeleteInvalidKeys() (int64, error) {
	now := CurrentTime()
	if c.blockDB {
		c.mutex.Lock()
		defer c.mutex.Unlock()
	}
	res, err := c.DB.Exec(c.DeleteInvalidQ, now)
	if err != nil {
		return -1, err
	}
	num, err := res.RowsAffected()
	if err != nil {
		return -1, nil
	}
	return num, nil
}

func (c *SQLSessionHandler) DeleteKey(key string) error {
	if c.blockDB {
		c.mutex.Lock()
		defer c.mutex.Unlock()
	}
	_, err := c.DB.Exec(c.DeleteKeyQ, key)
	return err
}

type MySQLSessionTemplate struct {
}

func NewMySQLSessionTemplate() MySQLSessionTemplate {
	return MySQLSessionTemplate{}
}

func (t MySQLSessionTemplate) InitQ() string {
	return `CREATE TABLE IF NOT EXISTS %s (
		user_id %s,
		session_key CHAR(%d) NOT NULL,
    created DATETIME NOT NULL,
    valid_until DATETIME NOT NULL,
		PRIMARY KEY (session_key)
	);`
}

func (t MySQLSessionTemplate) GetQ() string {
	return "SELECT user_id, created, valid_until FROM %s WHERE session_key = ?;"
}

func (t MySQLSessionTemplate) CreateQ() string {
	return "INSERT INTO %s (user_id, session_key, created, valid_until) VALUES (?, ?, ?, ?);"
}

func (t MySQLSessionTemplate) DeleteForUserQ() string {
	return "DELETE FROM %s WHERE user_id = ?;"
}

func (t MySQLSessionTemplate) DeleteInvalidQ() string {
	return "DELETE FROM %s WHERE valid_until > ?;"
}

func (t MySQLSessionTemplate) DeleteKeyQ() string {
	return "DELETE FROM %s WHERE session_key = ?"
}

func (t MySQLSessionTemplate) TimeFromScanType(val interface{}) (time.Time, error) {
	// first check if we already got a time.Time because parseTime in
	// the MySQL driver is true
	if alreadyTime, ok := val.(time.Time); ok {
		return alreadyTime, nil
	}
	if bytes, ok := val.([]byte); ok {
		s := string(bytes)
		// let's hope this is correct... however who came up with THIS parse
		// function definition in Go?!
		return time.Parse("2006-01-02 15:04:05", s)
	} else {
		// we have to return some time... why not now.
		return time.Now().UTC(), errors.New("Invalid date in database, probably a bug if you end up here.")
	}
}

type SQLite3SessionTemplate struct {
	// most of the stuff is the same as for SQL, so we can actually simply
	// delegate it to this template and just define the new queries
	MySQLSessionTemplate
}

func NewSQLite3SessionTemplate() *SQLite3SessionTemplate {
	return &SQLite3SessionTemplate{MySQLSessionTemplate: NewMySQLSessionTemplate()}
}

func (*SQLite3SessionTemplate) InitQ() string {
	return `CREATE TABLE IF NOT EXISTS %s (
		user_id %s,
		session_key CHAR(%d) NOT NULL PRIMARY KEY,
    created DATETIME NOT NULL,
    valid_until DATETIME NOT NULL
	);`
}

func NewSQLite3SessionController(db *sql.DB, tableName, userIDType string) *SessionController {
	handler := NewSQLSessionHandler(db, NewSQLite3SessionTemplate(), tableName, userIDType, true)
	return NewSessionController(handler)
}

// USERS stuff

type SQLUserQueries struct {
	PwLength      int
	InitQuery     string
	InsertQuery   string
	ValidateQuery string
}

func MySQLUserQueries(pwLength int) *SQLUserQueries {
	initQ := `
	CREATE TABLE IF NOT EXISTS users (
		id SERIAL,
		username VARCHAR(150) NOT NULL,
		first_name VARCHAR(30) NOT NULL,
		last_name VARCHAR(30) NOT NULL,
		email VARCHAR(254),
		password CHAR(%d),
		is_active BOOL,
		last_login DATETIME,
		PRIMARY KEY(id),
		UNIQUE(username)
	);
	`
	initQ = fmt.Sprintf(initQ, pwLength)
	insertQ := `
	INSERT INTO users (username, first_name, last_name, email, password, is_active, last_login)
		VALUES(?, ?, ?, ?, ?, ?, ?);
	`
	validateQ := "SELECT id, password FROM users WHERE username = ?"
	return &SQLUserQueries{PwLength: pwLength, InitQuery: initQ,
		InsertQuery: insertQ, ValidateQuery: validateQ}
}

func SQLite3UserQueries(pwLength int) *SQLUserQueries {
	// nearly everything is the same as for mysql
	res := MySQLUserQueries(pwLength)
	initQ := `
	CREATE TABLE IF NOT EXISTS users (
		id INTEGER PRIMARY KEY,
		username VARCHAR(150) NOT NULL,
		first_name VARCHAR(30) NOT NULL,
		last_name VARCHAR(30) NOT NULL,
		email VARCHAR(254),
		password CHAR(%d),
		is_active BOOL,
		last_login DATETIME,
		UNIQUE(username)
	);
	`
	initQ = fmt.Sprintf(initQ, pwLength)
	res.InitQuery = initQ
	return res
}

type SQLUserHandler struct {
	*SQLUserQueries
	DB        *sql.DB
	PwHandler PasswordHandler
	// required for example for sqlite
	blockDB bool
	mutex   sync.RWMutex
}

func NewSQLUserHandler(queries *SQLUserQueries, db *sql.DB, pwHandler PasswordHandler, blockDB bool) *SQLUserHandler {
	if pwHandler == nil {
		pwHandler = NewBcryptHandler(-1)
	}
	return &SQLUserHandler{SQLUserQueries: queries, DB: db, PwHandler: pwHandler, blockDB: blockDB}
}

func NewMySQLUserHandler(db *sql.DB, pwHandler PasswordHandler) *SQLUserHandler {
	if pwHandler == nil {
		pwHandler = NewBcryptHandler(-1)
	}
	return NewSQLUserHandler(MySQLUserQueries(pwHandler.PasswordHashLength()),
		db, pwHandler, false)
}

func NewSQLite3UserHandler(db *sql.DB, pwHandler PasswordHandler) *SQLUserHandler {
	if pwHandler == nil {
		pwHandler = NewBcryptHandler(-1)
	}
	return NewSQLUserHandler(SQLite3UserQueries(pwHandler.PasswordHashLength()),
		db, pwHandler, true)
}

func (handler *SQLUserHandler) Init() error {
	if handler.blockDB {
		handler.mutex.Lock()
		defer handler.mutex.Unlock()
	}
	_, err := handler.DB.Exec(handler.InitQuery)
	return err
}

func (handler *SQLUserHandler) Insert(userName, firstName, lastName, email string, plainPW []byte) (uint64, error) {
	now := CurrentTime()
	// try to encrypt the pw
	encrypted, encErr := handler.PwHandler.GenerateHash(plainPW)
	if encErr != nil {
		return NoUserID, encErr
	}

	if handler.blockDB {
		handler.mutex.Lock()
		defer handler.mutex.Unlock()
	}
	res, err := handler.DB.Exec(handler.InsertQuery, userName, firstName, lastName, email, encrypted, true, now)
	if err != nil {
		return NoUserID, err
	}

	// insert worked, try to get the last insert id
	insertInt, getErr := res.LastInsertId()
	if getErr != nil {
		return NoUserID, nil
	}
	// Don't know if this is even possible, but ok
	if insertInt < 0 {
		return NoUserID, nil
	}
	// everything ok, we convert to uint64
	var insertId uint64 = uint64(insertInt)
	return insertId, nil
}

func (handler *SQLUserHandler) Validate(userName string, cleartextPwCheck []byte) (uint64, error) {
	if handler.blockDB {
		handler.mutex.RLock()
		defer handler.mutex.RUnlock()
	}
	// first try to get the id and the password
	row := handler.DB.QueryRow(handler.ValidateQuery, userName)
	var userId uint64
	var hashPw []byte
	if err := row.Scan(&userId, &hashPw); err != nil {
		if err == sql.ErrNoRows {
			return NoUserID, ErrUserNotFound
		}
		return NoUserID, err
	}
	// validate the password
	test, err := handler.PwHandler.CheckPassword(hashPw, cleartextPwCheck)
	if err != nil {
		return NoUserID, err
	}
	// no error, check if passwords did match
	if test {
		return userId, nil
	} else {
		return NoUserID, nil
	}
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"io/ioutil"
	"math/rand"
	"os"
	"os/exec"
	"os/signal"
	"path/filepath"
	"syscall"
	"time"

	"github.com/jmoiron/sqlx"
	"github.com/lib/pq"
)

func sqlInterface(
	rootAccount *Account,
	transactions []*Transaction,
) {

	dbDir := filepath.Join(os.TempDir(), fmt.Sprintf("keep-%d", rand.Int63()))
	out, err := exec.Command("initdb", "-D", dbDir).CombinedOutput()
	ce(we(err, "%s", out))
	pt("db dir: %s\n", dbDir)
	defer exec.Command("rm", "-rf", dbDir).Run()

	port := 10000 + rand.Intn(50000)
	conf := `
		port = ` + fmt.Sprintf("%d", port) + `
	`
	err = ioutil.WriteFile(filepath.Join(dbDir, "postgresql.conf"), []byte(conf), 0644)
	ce(err)
	c := exec.Command("postgres", "-D", dbDir)
	c.SysProcAttr = &syscall.SysProcAttr{
		Setpgid: true,
	}
	ce(c.Start())
	defer syscall.Kill(-c.Process.Pid, syscall.SIGKILL)
	time.Sleep(time.Second)
	pt("db started\n")

	db, err := sqlx.Open("postgres", fmt.Sprintf("postgres://localhost:%d/postgres?sslmode=disable", port))
	ce(err)
	defer db.Close()
	tx := db.MustBegin()
	_, err = tx.Exec(`
		CREATE TABLE entries (
			id bigserial primary key,
			transaction bigint,
			transaction_description text,
			date timestamp with time zone,
			account text[],
			currency text,
			amount numeric,
			description text
		)
		`,
	)
	ce(err)

	for _, view := range views {
		_, err := tx.Exec(view)
		ce(err)
	}

	stmt, err := tx.Prepare(pq.CopyIn("entries",
		"transaction", "transaction_description",
		"date", "account",
		"currency", "amount",
		"description",
	))
	if err != nil {
		panic(err)
	}
	for tid, transaction := range transactions {
		for _, entry := range transaction.Entries {
			if _, err := stmt.Exec(
				tid,
				transaction.Description,
				entry.Time,
				func() (ret pq.StringArray) {
					acc := entry.Account
					for acc != rootAccount {
						ret = append(ret, acc.Name)
						acc = acc.Parent
					}
					for i := len(ret)/2 - 1; i >= 0; i-- {
						j := len(ret) - 1 - i
						ret[i], ret[j] = ret[j], ret[i]
					}
					return
				}(),
				entry.Currency,
				entry.Amount.FloatString(3),
				entry.Description,
			); err != nil {
				panic(err)
			}
		}
	}
	if _, err := stmt.Exec(); err != nil {
		panic(err)
	}
	if err := stmt.Close(); err != nil {
		panic(err)
	}
	ce(tx.Commit())
	pt("data loaded\n")

	sigs := make(chan os.Signal)
	go func() {
		for {
			<-sigs
		}
	}()
	signal.Notify(sigs, os.Interrupt)

	psql := exec.Command("psql", fmt.Sprintf("postgres://localhost:%d/postgres", port))
	psql.Stdout = os.Stdout
	psql.Stdin = os.Stdin
	psql.Stderr = os.Stderr
	ce(psql.Run())
}

var views = []string{
	// props
	`
	create view props as
	select distinct on (date, transaction)
	date
	,transaction_description as desc
	,account[2] as kind
	from entries
	where account[1] = '支出'
	and account[2] in (
		'数码',
		'物品',
		'衣物服饰',
		'消耗品',
		'保健品',
		'书籍',
		'药物',
		'性用品'
	)
	order by date desc, transaction
	`,

	// consumables
	`
	create view consumables as
	select distinct on (date, transaction)
	date, transaction_description
	from entries
	where account[1] = '支出'
	and account[2] in (
		'饮食',
		'消耗品',
		'药物',
		'保健品'
	)
	order by date desc, transaction
	`,

	// yearly
	"create view yearly as" + intervalStat("date_trunc('year', date)"),

	// seasonally
	"create view seasonally as" + intervalStat("date_trunc('year', date) + interval '3 month' * (extract(month from date)::int / 3)"),

	// monthly
	"create view monthly as" + intervalStat("date_trunc('month', date)"),

	// weekly
	"create view weekly as" + intervalStat("date_trunc('week', date)"),

	// daily
	"create view daily as" + intervalStat("date_trunc('day', date)"),

	// this year expenses
	`
	create view this_year_expenses as
	select
	currency, sum(amount), account[2],
	jsonb_pretty(jsonb_agg(
			to_char(date, 'YYYY-MM-DD')
			|| ' '
			|| currency
			|| amount::numeric(10,2)
			|| ' '
			|| transaction_description
			order by amount desc, date desc
	))
	from entries
	where extract(year from date) = extract(year from now())
	and account[1] = '支出'
	group by account[2],currency
	order by sum desc
	`,

	// balance sheet
	`
	create view balance_sheet as
	select

	(
		select string_agg(currency || amount, ' ') from (
			select sum(amount) as amount, currency
			from entries
			where account[1] = '资产'
			group by currency
		) t0
		where amount <> 0
	)::text
	|| E'\n-----\n'
	|| (
		select string_agg(
			kind || ' ' || currency || amount, E'\n'
			order by amount desc
		)
		from (
			select sum(amount) as amount, currency, account[2] as kind
			from entries
			where account[1] = '资产'
			group by currency, account[2]
		) t0
		where amount <> 0
	)
	AS 资产

	,(
		select string_agg(currency || amount, ' ') from (
			select sum(amount) as amount, currency
			from entries
			where account[1] = '负债'
			group by currency
		) t0
		where amount <> 0
	)::text
	|| E'\n-----\n'
	|| (
		select string_agg(
			kind || ' ' || currency || amount, E'\n'
			order by amount asc
		)
		from (
			select sum(amount) as amount, currency, account[2] as kind
			from entries
			where account[1] = '负债'
			group by currency, account[2]
		) t0
		where amount <> 0
	)
	AS 负债

	,(
		select string_agg(currency || amount, ' ') from (
			select sum(amount) + (
				select sum(amount)
				from entries e
				where account[1] = '负债'
				and currency = entries.currency
			) as amount, currency
			from entries
			where account[1] = '资产'
			group by currency
		) t0
		where amount <> 0
	)::text
	AS 净资产

	,(
		select string_agg(currency || amount, ' ') from (
			select sum(amount) as amount, currency
			from entries
			where account[1] = '负债'
			and date < now() + interval '1 year'
			group by currency
		) t0
		where amount <> 0
	)::text
	|| E'\n-----\n'
	|| (
		select string_agg(
			kind || ' ' || currency || amount, E'\n'
			order by amount asc
		)
		from (
			select sum(amount) as amount, currency, account[2] as kind
			from entries
			where account[1] = '负债'
			and date < now() + interval '1 year'
			group by currency, account[2]
		) t0
		where amount <> 0
	)
	AS 一年负债

	,(
		select string_agg(currency || amount, ' ') from (
			select sum(amount) + (
				select sum(amount)
				from entries e
				where account[1] = '负债'
				and currency = entries.currency
				and date < now() + interval '1 year'
			) as amount, currency
			from entries
			where account[1] = '资产'
			group by currency
		) t0
		where amount <> 0
	)::text
	AS 一年净资产

	`,

	// net_asset_changes
	`
	create view net_asset_changes as
	select
	to_char(d, 'YYYY-MM') AS 月份,
	c || net AS 净资产,
	d - lag(d, 1) over (partition by c order by d asc) as 日数,
	c || (net - lag(net, 1) over (partition by c order by d asc))::text as 变动
	from (
		select
		c,
		d,
		COALESCE((
			select sum(amount)
			from entries
			where account[1] = '资产'
			and currency = c
			and date < d
		), 0)
		+ COALESCE((
			select sum(amount)
			from entries
			where account[1] = '负债'
			and currency = c
			and date < d
		), 0) net
		from (
			select distinct date_trunc('month', date) as d , currency as c from entries
			where currency in ('￥', '$')
		) t0
		where d < now()
	) t1
	order by d desc
	`,

	//
}

func intervalStat(groupBy string) string {
	return `
	select
	to_char(` + groupBy + `, 'YYYY-MM-DD') as span,

	COALESCE(
		'支出' || currency || (sum(amount) filter (where account[1] = '支出'))::numeric(20,2)::text
		|| E'：\n'
		|| (
			select string_agg(
				currency || amount::numeric(20,2)::text || ' ' || account,
				E'\n'
				order by amount desc
			) from (
				select account[2] as account, currency, sum(amount) as amount
				from unnest(
					array_agg(id) filter (where account[1] = '支出')
				) as id
				join entries e2 using (id)
				group by account[2], currency
			) t0
		),
		'-'
	) || E'\n' as expenses,

	COALESCE(
		'收入' || currency || (sum(amount) filter (where account[1] = '收入'))::numeric(20,2)::text
		|| E'：\n'
		|| (
			select string_agg(
				currency || amount::numeric(20, 2)::text || ' ' || account,
				E'\n'
				order by amount asc
			) from (
				select account[2] as account, currency, sum(amount) as amount
				from unnest(
					array_agg(id) filter (where account[1] = '收入')
				) as id
				join entries e2 using (id)
				group by account[2], currency
			) t0
		),
		'-'
	) || E'\n' as income,

	COALESCE(
		'净资产' || currency || (
			-sum(amount) filter (where account[1] = '收入')
			-
			sum(amount) filter (where account[1] = '支出')
		)::numeric(20,2)::text,
		'-'
	) || E'\n' as net_income,

	COALESCE(
		E'资产：\n' || (
			select string_agg(
				currency
				|| amount::numeric(20, 2)::text || ' ' || account
				|| E'\n'
				|| '= 增' || pos_amount::numeric(20, 2)::text
				|| ' 减' || neg_amount::numeric(20, 2)::text,
				E'\n'
				order by amount desc
			) from (
				select account[2] as account, currency, sum(amount) as amount,
				COALESCE(sum(amount) filter (where amount >= 0), 0) as pos_amount,
				COALESCE(-sum(amount) filter (where amount < 0), 0) as neg_amount
				from unnest(
					array_agg(id) filter (where account[1] = '资产')
				) as id
				join entries e2 using (id)
				group by account[2], currency
			) t0
		),
		'-'
	) || E'\n' as equity,

	COALESCE(
		E'负债：\n' || (
			select string_agg(
				currency
				|| amount::numeric(20, 2)::text || ' ' || account
				|| E'\n'
				|| '= 还' || pos_amount::numeric(20, 2)::text
				|| ' 借' || neg_amount::numeric(20, 2)::text,
				E'\n'
				order by amount asc
			) from (
				select account[2] as account, currency, sum(amount) as amount,
				COALESCE(sum(amount) filter (where amount >= 0), 0) as pos_amount,
				COALESCE(-sum(amount) filter (where amount < 0), 0) as neg_amount
				from unnest(
					array_agg(id) filter (where account[1] = '负债')
				) as id
				join entries e2 using (id)
				group by account[2], currency
			) t0
		),
		'-'
	) || E'\n' as liability

	from
	entries

	group by ` + groupBy + `, currency
	order by span desc, currency asc
	`
}
<commit_msg>fix net_asset_changes<commit_after>package main

import (
	"fmt"
	"io/ioutil"
	"math/rand"
	"os"
	"os/exec"
	"os/signal"
	"path/filepath"
	"syscall"
	"time"

	"github.com/jmoiron/sqlx"
	"github.com/lib/pq"
)

func sqlInterface(
	rootAccount *Account,
	transactions []*Transaction,
) {

	dbDir := filepath.Join(os.TempDir(), fmt.Sprintf("keep-%d", rand.Int63()))
	out, err := exec.Command("initdb", "-D", dbDir).CombinedOutput()
	ce(we(err, "%s", out))
	pt("db dir: %s\n", dbDir)
	defer exec.Command("rm", "-rf", dbDir).Run()

	port := 10000 + rand.Intn(50000)
	conf := `
		port = ` + fmt.Sprintf("%d", port) + `
	`
	err = ioutil.WriteFile(filepath.Join(dbDir, "postgresql.conf"), []byte(conf), 0644)
	ce(err)
	c := exec.Command("postgres", "-D", dbDir)
	c.SysProcAttr = &syscall.SysProcAttr{
		Setpgid: true,
	}
	ce(c.Start())
	defer syscall.Kill(-c.Process.Pid, syscall.SIGKILL)
	time.Sleep(time.Second)
	pt("db started\n")

	db, err := sqlx.Open("postgres", fmt.Sprintf("postgres://localhost:%d/postgres?sslmode=disable", port))
	ce(err)
	defer db.Close()
	tx := db.MustBegin()
	_, err = tx.Exec(`
		CREATE TABLE entries (
			id bigserial primary key,
			transaction bigint,
			transaction_description text,
			date timestamp with time zone,
			account text[],
			currency text,
			amount numeric,
			description text
		)
		`,
	)
	ce(err)

	for _, view := range views {
		_, err := tx.Exec(view)
		ce(err)
	}

	stmt, err := tx.Prepare(pq.CopyIn("entries",
		"transaction", "transaction_description",
		"date", "account",
		"currency", "amount",
		"description",
	))
	if err != nil {
		panic(err)
	}
	for tid, transaction := range transactions {
		for _, entry := range transaction.Entries {
			if _, err := stmt.Exec(
				tid,
				transaction.Description,
				entry.Time,
				func() (ret pq.StringArray) {
					acc := entry.Account
					for acc != rootAccount {
						ret = append(ret, acc.Name)
						acc = acc.Parent
					}
					for i := len(ret)/2 - 1; i >= 0; i-- {
						j := len(ret) - 1 - i
						ret[i], ret[j] = ret[j], ret[i]
					}
					return
				}(),
				entry.Currency,
				entry.Amount.FloatString(3),
				entry.Description,
			); err != nil {
				panic(err)
			}
		}
	}
	if _, err := stmt.Exec(); err != nil {
		panic(err)
	}
	if err := stmt.Close(); err != nil {
		panic(err)
	}
	ce(tx.Commit())
	pt("data loaded\n")

	sigs := make(chan os.Signal)
	go func() {
		for {
			<-sigs
		}
	}()
	signal.Notify(sigs, os.Interrupt)

	psql := exec.Command("psql", fmt.Sprintf("postgres://localhost:%d/postgres", port))
	psql.Stdout = os.Stdout
	psql.Stdin = os.Stdin
	psql.Stderr = os.Stderr
	ce(psql.Run())
}

var views = []string{
	// props
	`
	create view props as
	select distinct on (date, transaction)
	date
	,transaction_description as desc
	,account[2] as kind
	from entries
	where account[1] = '支出'
	and account[2] in (
		'数码',
		'物品',
		'衣物服饰',
		'消耗品',
		'保健品',
		'书籍',
		'药物',
		'性用品'
	)
	order by date desc, transaction
	`,

	// consumables
	`
	create view consumables as
	select distinct on (date, transaction)
	date, transaction_description
	from entries
	where account[1] = '支出'
	and account[2] in (
		'饮食',
		'消耗品',
		'药物',
		'保健品'
	)
	order by date desc, transaction
	`,

	// yearly
	"create view yearly as" + intervalStat("date_trunc('year', date)"),

	// seasonally
	"create view seasonally as" + intervalStat("date_trunc('year', date) + interval '3 month' * (extract(month from date)::int / 3)"),

	// monthly
	"create view monthly as" + intervalStat("date_trunc('month', date)"),

	// weekly
	"create view weekly as" + intervalStat("date_trunc('week', date)"),

	// daily
	"create view daily as" + intervalStat("date_trunc('day', date)"),

	// this year expenses
	`
	create view this_year_expenses as
	select
	currency, sum(amount), account[2],
	jsonb_pretty(jsonb_agg(
			to_char(date, 'YYYY-MM-DD')
			|| ' '
			|| currency
			|| amount::numeric(10,2)
			|| ' '
			|| transaction_description
			order by amount desc, date desc
	))
	from entries
	where extract(year from date) = extract(year from now())
	and account[1] = '支出'
	group by account[2],currency
	order by sum desc
	`,

	// balance sheet
	`
	create view balance_sheet as
	select

	(
		select string_agg(currency || amount, ' ') from (
			select sum(amount) as amount, currency
			from entries
			where account[1] = '资产'
			group by currency
		) t0
		where amount <> 0
	)::text
	|| E'\n-----\n'
	|| (
		select string_agg(
			kind || ' ' || currency || amount, E'\n'
			order by amount desc
		)
		from (
			select sum(amount) as amount, currency, account[2] as kind
			from entries
			where account[1] = '资产'
			group by currency, account[2]
		) t0
		where amount <> 0
	)
	AS 资产

	,(
		select string_agg(currency || amount, ' ') from (
			select sum(amount) as amount, currency
			from entries
			where account[1] = '负债'
			group by currency
		) t0
		where amount <> 0
	)::text
	|| E'\n-----\n'
	|| (
		select string_agg(
			kind || ' ' || currency || amount, E'\n'
			order by amount asc
		)
		from (
			select sum(amount) as amount, currency, account[2] as kind
			from entries
			where account[1] = '负债'
			group by currency, account[2]
		) t0
		where amount <> 0
	)
	AS 负债

	,(
		select string_agg(currency || amount, ' ') from (
			select sum(amount) + (
				select sum(amount)
				from entries e
				where account[1] = '负债'
				and currency = entries.currency
			) as amount, currency
			from entries
			where account[1] = '资产'
			group by currency
		) t0
		where amount <> 0
	)::text
	AS 净资产

	,(
		select string_agg(currency || amount, ' ') from (
			select sum(amount) as amount, currency
			from entries
			where account[1] = '负债'
			and date < now() + interval '1 year'
			group by currency
		) t0
		where amount <> 0
	)::text
	|| E'\n-----\n'
	|| (
		select string_agg(
			kind || ' ' || currency || amount, E'\n'
			order by amount asc
		)
		from (
			select sum(amount) as amount, currency, account[2] as kind
			from entries
			where account[1] = '负债'
			and date < now() + interval '1 year'
			group by currency, account[2]
		) t0
		where amount <> 0
	)
	AS 一年负债

	,(
		select string_agg(currency || amount, ' ') from (
			select sum(amount) + (
				select sum(amount)
				from entries e
				where account[1] = '负债'
				and currency = entries.currency
				and date < now() + interval '1 year'
			) as amount, currency
			from entries
			where account[1] = '资产'
			group by currency
		) t0
		where amount <> 0
	)::text
	AS 一年净资产

	`,

	// net_asset_changes
	`
	create view net_asset_changes as
	select
	to_char(d, 'YYYY-MM') AS 月份,
	d - lag(d, 1) over (partition by c order by d asc) as 日数,
	net AS 净资产,
	c,
	net - lag(net, 1) over (partition by c order by d asc) as 变动
	from (
		select
		c,
		d,
		COALESCE((
			select sum(amount)
			from entries
			where account[1] = '资产'
			and currency = c
			and date < d
		), 0)
		+ COALESCE((
			select sum(amount)
			from entries
			where account[1] = '负债'
			and currency = c
			and date < d
		), 0) net
		from (
			select distinct date_trunc('month', date) as d , currency as c from entries
			where currency in ('￥', '$')
		) t0
		where d < now()
	) t1
	order by d desc
	`,

	//
}

func intervalStat(groupBy string) string {
	return `
	select
	to_char(` + groupBy + `, 'YYYY-MM-DD') as span,

	COALESCE(
		'支出' || currency || (sum(amount) filter (where account[1] = '支出'))::numeric(20,2)::text
		|| E'：\n'
		|| (
			select string_agg(
				currency || amount::numeric(20,2)::text || ' ' || account,
				E'\n'
				order by amount desc
			) from (
				select account[2] as account, currency, sum(amount) as amount
				from unnest(
					array_agg(id) filter (where account[1] = '支出')
				) as id
				join entries e2 using (id)
				group by account[2], currency
			) t0
		),
		'-'
	) || E'\n' as expenses,

	COALESCE(
		'收入' || currency || (sum(amount) filter (where account[1] = '收入'))::numeric(20,2)::text
		|| E'：\n'
		|| (
			select string_agg(
				currency || amount::numeric(20, 2)::text || ' ' || account,
				E'\n'
				order by amount asc
			) from (
				select account[2] as account, currency, sum(amount) as amount
				from unnest(
					array_agg(id) filter (where account[1] = '收入')
				) as id
				join entries e2 using (id)
				group by account[2], currency
			) t0
		),
		'-'
	) || E'\n' as income,

	COALESCE(
		'净资产' || currency || (
			-sum(amount) filter (where account[1] = '收入')
			-
			sum(amount) filter (where account[1] = '支出')
		)::numeric(20,2)::text,
		'-'
	) || E'\n' as net_income,

	COALESCE(
		E'资产：\n' || (
			select string_agg(
				currency
				|| amount::numeric(20, 2)::text || ' ' || account
				|| E'\n'
				|| '= 增' || pos_amount::numeric(20, 2)::text
				|| ' 减' || neg_amount::numeric(20, 2)::text,
				E'\n'
				order by amount desc
			) from (
				select account[2] as account, currency, sum(amount) as amount,
				COALESCE(sum(amount) filter (where amount >= 0), 0) as pos_amount,
				COALESCE(-sum(amount) filter (where amount < 0), 0) as neg_amount
				from unnest(
					array_agg(id) filter (where account[1] = '资产')
				) as id
				join entries e2 using (id)
				group by account[2], currency
			) t0
		),
		'-'
	) || E'\n' as equity,

	COALESCE(
		E'负债：\n' || (
			select string_agg(
				currency
				|| amount::numeric(20, 2)::text || ' ' || account
				|| E'\n'
				|| '= 还' || pos_amount::numeric(20, 2)::text
				|| ' 借' || neg_amount::numeric(20, 2)::text,
				E'\n'
				order by amount asc
			) from (
				select account[2] as account, currency, sum(amount) as amount,
				COALESCE(sum(amount) filter (where amount >= 0), 0) as pos_amount,
				COALESCE(-sum(amount) filter (where amount < 0), 0) as neg_amount
				from unnest(
					array_agg(id) filter (where account[1] = '负债')
				) as id
				join entries e2 using (id)
				group by account[2], currency
			) t0
		),
		'-'
	) || E'\n' as liability

	from
	entries

	group by ` + groupBy + `, currency
	order by span desc, currency asc
	`
}
<|endoftext|>


<commit_before>package stripe

import (
	"encoding/json"

	"github.com/stripe/stripe-go/form"
)

// SubscriptionStatus is the list of allowed values for the subscription's status.
type SubscriptionStatus string

const (
	SubscriptionStatusActive   SubscriptionStatus = "active"
	SubscriptionStatusAll      SubscriptionStatus = "all"
	SubscriptionStatusCanceled SubscriptionStatus = "canceled"
	SubscriptionStatusPastDue  SubscriptionStatus = "past_due"
	SubscriptionStatusTrialing SubscriptionStatus = "trialing"
	SubscriptionStatusUnpaid   SubscriptionStatus = "unpaid"
)

// SubscriptionBilling is the type of billing method for this subscription's invoices.
type SubscriptionBilling string

const (
	SubscriptionBillingChargeAutomatically SubscriptionBilling = "charge_automatically"
	SubscriptionBillingSendInvoice         SubscriptionBilling = "send_invoice"
)

// SubscriptionParams is the set of parameters that can be used when creating or updating a subscription.
// For more details see https://stripe.com/docs/api#create_subscription and https://stripe.com/docs/api#update_subscription.
type SubscriptionParams struct {
	Params                      `form:"*"`
	ApplicationFeePercent       *float64                   `form:"application_fee_percent"`
	Billing                     *string                    `form:"billing"`
	BillingCycleAnchor          *int64                     `form:"billing_cycle_anchor"`
	BillingCycleAnchorNow       *bool                      `form:"-"` // See custom AppendTo
	BillingCycleAnchorUnchanged *bool                      `form:"-"` // See custom AppendTo
	CancelAtPeriodEnd           *bool                      `form:"cancel_at_period_end"`
	Card                        *CardParams                `form:"card"`
	Coupon                      *string                    `form:"coupon"`
	Customer                    *string                    `form:"customer"`
	DaysUntilDue                *int64                     `form:"days_until_due"`
	Items                       []*SubscriptionItemsParams `form:"items,indexed"`
	OnBehalfOf                  *string                    `form:"on_behalf_of"`
	Plan                        *string                    `form:"plan"`
	Prorate                     *bool                      `form:"prorate"`
	ProrationDate               *int64                     `form:"proration_date"`
	Quantity                    *int64                     `form:"quantity"`
	Source                      *string                    `form:"source"`
	TaxPercent                  *float64                   `form:"tax_percent"`
	TrialEnd                    *int64                     `form:"trial_end"`
	TrialEndNow                 *bool                      `form:"-"` // See custom AppendTo
	TrialFromPlan               *bool                      `form:"trial_from_plan"`
	TrialPeriodDays             *int64                     `form:"trial_period_days"`
}

// SubscriptionCancelParams is the set of parameters that can be used when canceling a subscription.
// For more details see https://stripe.com/docs/api#cancel_subscription
type SubscriptionCancelParams struct {
	Params      `form:"*"`
	AtPeriodEnd *bool `form:"at_period_end"`
}

// AppendTo implements custom encoding logic for SubscriptionParams so that the special
// "now" value for billing_cycle_anchor and trial_end can be implemented
// (they're otherwise timestamps rather than strings).
func (p *SubscriptionParams) AppendTo(body *form.Values, keyParts []string) {
	if p.BillingCycleAnchorNow != nil {
		body.Add(form.FormatKey(append(keyParts, "billing_cycle_anchor")), "now")
	}

	if p.BillingCycleAnchorUnchanged != nil {
		body.Add(form.FormatKey(append(keyParts, "billing_cycle_anchor")), "unchanged")
	}

	if p.TrialEndNow != nil {
		body.Add(form.FormatKey(append(keyParts, "trial_end")), "now")
	}
}

// SubscriptionItemsParams is the set of parameters that can be used when creating or updating a subscription item on a subscription
// For more details see https://stripe.com/docs/api#create_subscription and https://stripe.com/docs/api#update_subscription.
type SubscriptionItemsParams struct {
	Params     `form:"*"`
	ClearUsage *bool   `form:"clear_usage"`
	Deleted    *bool   `form:"deleted"`
	ID         *string `form:"id"`
	Plan       *string `form:"plan"`
	Quantity   *int64  `form:"quantity"`
}

// SubscriptionListParams is the set of parameters that can be used when listing active subscriptions.
// For more details see https://stripe.com/docs/api#list_subscriptions.
type SubscriptionListParams struct {
	ListParams   `form:"*"`
	Billing      string            `form:"billing"`
	Created      int64             `form:"created"`
	CreatedRange *RangeQueryParams `form:"created"`
	Customer     string            `form:"customer"`
	Plan         string            `form:"plan"`
	Status       string            `form:"status"`
}

// Subscription is the resource representing a Stripe subscription.
// For more details see https://stripe.com/docs/api#subscriptions.
type Subscription struct {
	ApplicationFeePercent float64               `json:"application_fee_percent"`
	Billing               SubscriptionBilling   `json:"billing"`
	BillingCycleAnchor    int64                 `json:"billing_cycle_anchor"`
	CanceledAt            int64                 `json:"canceled_at"`
	Created               int64                 `json:"created"`
	CurrentPeriodEnd      int64                 `json:"current_period_end"`
	CurrentPeriodStart    int64                 `json:"current_period_start"`
	Customer              *Customer             `json:"customer"`
	DaysUntilDue          int64                 `json:"days_until_due"`
	Discount              *Discount             `json:"discount"`
	CancelAtPeriodEnd     bool                  `json:"cancel_at_period_end"`
	EndedAt               int64                 `json:"ended_at"`
	ID                    string                `json:"id"`
	Items                 *SubscriptionItemList `json:"items"`
	Metadata              map[string]string     `json:"metadata"`
	Plan                  *Plan                 `json:"plan"`
	Quantity              int64                 `json:"quantity"`
	Start                 int64                 `json:"start"`
	Status                SubscriptionStatus    `json:"status"`
	TaxPercent            float64               `json:"tax_percent"`
	TrialEnd              int64                 `json:"trial_end"`
	TrialStart            int64                 `json:"trial_start"`
}

// SubscriptionList is a list object for subscriptions.
type SubscriptionList struct {
	ListMeta
	Data []*Subscription `json:"data"`
}

// UnmarshalJSON handles deserialization of a Subscription.
// This custom unmarshaling is needed because the resulting
// property may be an id or the full struct if it was expanded.
func (s *Subscription) UnmarshalJSON(data []byte) error {
	type sub Subscription
	var ss sub
	err := json.Unmarshal(data, &ss)
	if err == nil {
		*s = Subscription(ss)
	} else {
		// the id is surrounded by "\" characters, so strip them
		s.ID = string(data[1 : len(data)-1])
	}

	return nil
}
<commit_msg>Only pass custom sub parameter if booleans are true<commit_after>package stripe

import (
	"encoding/json"

	"github.com/stripe/stripe-go/form"
)

// SubscriptionStatus is the list of allowed values for the subscription's status.
type SubscriptionStatus string

const (
	SubscriptionStatusActive   SubscriptionStatus = "active"
	SubscriptionStatusAll      SubscriptionStatus = "all"
	SubscriptionStatusCanceled SubscriptionStatus = "canceled"
	SubscriptionStatusPastDue  SubscriptionStatus = "past_due"
	SubscriptionStatusTrialing SubscriptionStatus = "trialing"
	SubscriptionStatusUnpaid   SubscriptionStatus = "unpaid"
)

// SubscriptionBilling is the type of billing method for this subscription's invoices.
type SubscriptionBilling string

const (
	SubscriptionBillingChargeAutomatically SubscriptionBilling = "charge_automatically"
	SubscriptionBillingSendInvoice         SubscriptionBilling = "send_invoice"
)

// SubscriptionParams is the set of parameters that can be used when creating or updating a subscription.
// For more details see https://stripe.com/docs/api#create_subscription and https://stripe.com/docs/api#update_subscription.
type SubscriptionParams struct {
	Params                      `form:"*"`
	ApplicationFeePercent       *float64                   `form:"application_fee_percent"`
	Billing                     *string                    `form:"billing"`
	BillingCycleAnchor          *int64                     `form:"billing_cycle_anchor"`
	BillingCycleAnchorNow       *bool                      `form:"-"` // See custom AppendTo
	BillingCycleAnchorUnchanged *bool                      `form:"-"` // See custom AppendTo
	CancelAtPeriodEnd           *bool                      `form:"cancel_at_period_end"`
	Card                        *CardParams                `form:"card"`
	Coupon                      *string                    `form:"coupon"`
	Customer                    *string                    `form:"customer"`
	DaysUntilDue                *int64                     `form:"days_until_due"`
	Items                       []*SubscriptionItemsParams `form:"items,indexed"`
	OnBehalfOf                  *string                    `form:"on_behalf_of"`
	Plan                        *string                    `form:"plan"`
	Prorate                     *bool                      `form:"prorate"`
	ProrationDate               *int64                     `form:"proration_date"`
	Quantity                    *int64                     `form:"quantity"`
	Source                      *string                    `form:"source"`
	TaxPercent                  *float64                   `form:"tax_percent"`
	TrialEnd                    *int64                     `form:"trial_end"`
	TrialEndNow                 *bool                      `form:"-"` // See custom AppendTo
	TrialFromPlan               *bool                      `form:"trial_from_plan"`
	TrialPeriodDays             *int64                     `form:"trial_period_days"`
}

// SubscriptionCancelParams is the set of parameters that can be used when canceling a subscription.
// For more details see https://stripe.com/docs/api#cancel_subscription
type SubscriptionCancelParams struct {
	Params      `form:"*"`
	AtPeriodEnd *bool `form:"at_period_end"`
}

// AppendTo implements custom encoding logic for SubscriptionParams so that the special
// "now" value for billing_cycle_anchor and trial_end can be implemented
// (they're otherwise timestamps rather than strings).
func (p *SubscriptionParams) AppendTo(body *form.Values, keyParts []string) {
	if BoolValue(p.BillingCycleAnchorNow) {
		body.Add(form.FormatKey(append(keyParts, "billing_cycle_anchor")), "now")
	}

	if BoolValue(p.BillingCycleAnchorUnchanged) {
		body.Add(form.FormatKey(append(keyParts, "billing_cycle_anchor")), "unchanged")
	}

	if BoolValue(p.TrialEndNow) {
		body.Add(form.FormatKey(append(keyParts, "trial_end")), "now")
	}
}

// SubscriptionItemsParams is the set of parameters that can be used when creating or updating a subscription item on a subscription
// For more details see https://stripe.com/docs/api#create_subscription and https://stripe.com/docs/api#update_subscription.
type SubscriptionItemsParams struct {
	Params     `form:"*"`
	ClearUsage *bool   `form:"clear_usage"`
	Deleted    *bool   `form:"deleted"`
	ID         *string `form:"id"`
	Plan       *string `form:"plan"`
	Quantity   *int64  `form:"quantity"`
}

// SubscriptionListParams is the set of parameters that can be used when listing active subscriptions.
// For more details see https://stripe.com/docs/api#list_subscriptions.
type SubscriptionListParams struct {
	ListParams   `form:"*"`
	Billing      string            `form:"billing"`
	Created      int64             `form:"created"`
	CreatedRange *RangeQueryParams `form:"created"`
	Customer     string            `form:"customer"`
	Plan         string            `form:"plan"`
	Status       string            `form:"status"`
}

// Subscription is the resource representing a Stripe subscription.
// For more details see https://stripe.com/docs/api#subscriptions.
type Subscription struct {
	ApplicationFeePercent float64               `json:"application_fee_percent"`
	Billing               SubscriptionBilling   `json:"billing"`
	BillingCycleAnchor    int64                 `json:"billing_cycle_anchor"`
	CanceledAt            int64                 `json:"canceled_at"`
	Created               int64                 `json:"created"`
	CurrentPeriodEnd      int64                 `json:"current_period_end"`
	CurrentPeriodStart    int64                 `json:"current_period_start"`
	Customer              *Customer             `json:"customer"`
	DaysUntilDue          int64                 `json:"days_until_due"`
	Discount              *Discount             `json:"discount"`
	CancelAtPeriodEnd     bool                  `json:"cancel_at_period_end"`
	EndedAt               int64                 `json:"ended_at"`
	ID                    string                `json:"id"`
	Items                 *SubscriptionItemList `json:"items"`
	Metadata              map[string]string     `json:"metadata"`
	Plan                  *Plan                 `json:"plan"`
	Quantity              int64                 `json:"quantity"`
	Start                 int64                 `json:"start"`
	Status                SubscriptionStatus    `json:"status"`
	TaxPercent            float64               `json:"tax_percent"`
	TrialEnd              int64                 `json:"trial_end"`
	TrialStart            int64                 `json:"trial_start"`
}

// SubscriptionList is a list object for subscriptions.
type SubscriptionList struct {
	ListMeta
	Data []*Subscription `json:"data"`
}

// UnmarshalJSON handles deserialization of a Subscription.
// This custom unmarshaling is needed because the resulting
// property may be an id or the full struct if it was expanded.
func (s *Subscription) UnmarshalJSON(data []byte) error {
	type sub Subscription
	var ss sub
	err := json.Unmarshal(data, &ss)
	if err == nil {
		*s = Subscription(ss)
	} else {
		// the id is surrounded by "\" characters, so strip them
		s.ID = string(data[1 : len(data)-1])
	}

	return nil
}
<|endoftext|>


<commit_before>package svg

import (
	"encoding/xml"
	"errors"
	"fmt"
	"io"
	"strconv"
	"strings"

	mt "github.com/rustyoz/Mtransform"
)

// DrawingInstructionParser allow getting segments and drawing
// instructions from them. All SVG elements should implement this
// interface.
type DrawingInstructionParser interface {
	ParseDrawingInstructions() (chan *DrawingInstruction, chan error)
}

// Tuple is an X,Y coordinate
type Tuple [2]float64

// Svg represents an SVG file containing at least a top level group or a
// number of Paths
type Svg struct {
	Title        string  `xml:"title"`
	Groups       []Group `xml:"g"`
	ViewBox      string  `xml:"viewBox,attr"`
	Elements     []DrawingInstructionParser
	Name         string
	Transform    *mt.Transform
	scale        float64
	instructions chan *DrawingInstruction
	errors       chan error
	segments     chan Segment
}

// Group represents an SVG group (usually located in a 'g' XML element)
type Group struct {
	ID              string
	Stroke          string
	StrokeWidth     int32
	Fill            string
	FillRule        string
	Elements        []DrawingInstructionParser
	TransformString string
	Transform       *mt.Transform // row, column
	Parent          *Group
	Owner           *Svg
	instructions    chan *DrawingInstruction
	errors          chan error
	segments        chan Segment
}

// ParseDrawingInstructions implements the DrawingInstructionParser interface
//
// This method makes it easier to get all the drawing instructions.
func (g *Group) ParseDrawingInstructions() (chan *DrawingInstruction, chan error) {
	g.instructions = make(chan *DrawingInstruction, 100)
	g.errors = make(chan error, 100)

	go func() {
		defer close(g.instructions)
		defer close(g.errors)
		for _, e := range g.Elements {
			instrs, errs := e.ParseDrawingInstructions()
			go func() {
				for er := range errs {
					g.errors <- er
				}
			}()
			for is := range instrs {
				g.instructions <- is
			}
		}
	}()

	return g.instructions, g.errors
}

// UnmarshalXML implements the encoding.xml.Unmarshaler interface
func (g *Group) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error {
	for _, attr := range start.Attr {
		switch attr.Name.Local {
		case "id":
			g.ID = attr.Value
		case "stroke":
			g.Stroke = attr.Value
		case "stroke-width":
			intValue, err := strconv.ParseInt(attr.Value, 10, 32)
			if err != nil {
				return err
			}
			g.StrokeWidth = int32(intValue)
		case "fill":
			g.Fill = attr.Value
		case "fill-rule":
			g.FillRule = attr.Value
		case "transform":
			g.TransformString = attr.Value
			t, err := parseTransform(g.TransformString)
			if err != nil {
				fmt.Println(err)
			}
			g.Transform = &t
		}
	}

	for {
		token, err := decoder.Token()
		if err != nil {
			return err
		}

		switch tok := token.(type) {
		case xml.StartElement:
			var elementStruct DrawingInstructionParser

			switch tok.Name.Local {
			case "g":
				elementStruct = &Group{Parent: g, Owner: g.Owner, Transform: mt.NewTransform()}
			case "rect":
				elementStruct = &Rect{group: g}
			case "circle":
				elementStruct = &Circle{group: g}
			case "path":
				elementStruct = &Path{group: g, StrokeWidth: float64(g.StrokeWidth), Stroke: &g.Stroke, Fill: &g.Fill}

			}

			if err = decoder.DecodeElement(elementStruct, &tok); err != nil {
				return fmt.Errorf("error decoding element of Group: %s", err)
			}
			g.Elements = append(g.Elements, elementStruct)

		case xml.EndElement:
			return nil
		}
	}
}

// ParseDrawingInstructions implements the DrawingInstructionParser interface
//
// This method makes it easier to get all the drawing instructions.
func (s *Svg) ParseDrawingInstructions() (chan *DrawingInstruction, chan error) {
	s.instructions = make(chan *DrawingInstruction, 100)
	s.errors = make(chan error, 100)

	go func() {
		var elecount int
		defer close(s.instructions)
		defer close(s.errors)
		for _, e := range s.Elements {
			elecount++
			instrs, errs := e.ParseDrawingInstructions()
			go func(count int) {
				for er := range errs {
					s.errors <- fmt.Errorf("error when parsing element nr. %d: %s", count, er)
				}
			}(elecount)

			for is := range instrs {
				s.instructions <- is
			}
		}

		for _, g := range s.Groups {
			instrs, errs := g.ParseDrawingInstructions()
			go func() {
				for er := range errs {
					g.errors <- er
				}
			}()
			for is := range instrs {
				s.instructions <- is
			}
		}
	}()

	return s.instructions, s.errors
}

// UnmarshalXML implements the encoding.xml.Unmarshaler interface
func (s *Svg) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error {
	for {
		for _, attr := range start.Attr {
			if attr.Name.Local == "viewBox" {
				s.ViewBox = attr.Value
				break
			}
		}

		token, err := decoder.Token()
		if err != nil {
			return err
		}

		switch tok := token.(type) {
		case xml.StartElement:
			var dip DrawingInstructionParser

			switch tok.Name.Local {
			case "g":
				g := &Group{Owner: s, Transform: mt.NewTransform()}
				if err = decoder.DecodeElement(g, &tok); err != nil {
					return fmt.Errorf("error decoding group element within SVG struct: %s", err)
				}
				s.Groups = append(s.Groups, *g)
				continue
			case "rect":
				dip = &Rect{}
			case "circle":
				dip = &Circle{}
			case "path":
				dip = &Path{}

			default:
				continue
			}

			if err = decoder.DecodeElement(dip, &tok); err != nil {
				return fmt.Errorf("error decoding element of SVG struct: %s", err)
			}

			s.Elements = append(s.Elements, dip)

		case xml.EndElement:
			if tok.Name.Local == "svg" {
				return nil
			}
		}
	}
}

// ParseSvg parses an SVG string into an SVG struct
func ParseSvg(str string, name string, scale float64) (*Svg, error) {
	var svg Svg
	svg.Name = name
	svg.Transform = mt.NewTransform()
	if scale > 0 {
		svg.Transform.Scale(scale, scale)
		svg.scale = scale
	}
	if scale < 0 {
		svg.Transform.Scale(1.0/-scale, 1.0/-scale)
		svg.scale = 1.0 / -scale
	}

	err := xml.Unmarshal([]byte(str), &svg)
	if err != nil {
		return nil, fmt.Errorf("ParseSvg Error: %v", err)
	}

	for i := range svg.Groups {
		svg.Groups[i].SetOwner(&svg)
		if svg.Groups[i].Transform == nil {
			svg.Groups[i].Transform = mt.NewTransform()
		}
	}
	return &svg, nil
}

// ParseSvgFromReader parses an SVG struct from an io.Reader
func ParseSvgFromReader(r io.Reader, name string, scale float64) (*Svg, error) {
	var svg Svg
	svg.Name = name
	svg.Transform = mt.NewTransform()
	if scale > 0 {
		svg.Transform.Scale(scale, scale)
		svg.scale = scale
	}
	if scale < 0 {
		svg.Transform.Scale(1.0/-scale, 1.0/-scale)
		svg.scale = 1.0 / -scale
	}

	if err := xml.NewDecoder(r).Decode(&svg); err != nil {
		return nil, fmt.Errorf("ParseSvg Error: %v", err)
	}

	for i := range svg.Groups {
		svg.Groups[i].SetOwner(&svg)
		if svg.Groups[i].Transform == nil {
			svg.Groups[i].Transform = mt.NewTransform()
		}
	}
	return &svg, nil
}

// ViewBoxValues returns all the numerical values in the viewBox
// attribute.
func (s *Svg) ViewBoxValues() ([]float64, error) {
	var vals []float64

	if s.ViewBox == "" {
		return vals, errors.New("viewBox attribute is empty")
	}

	split := strings.Split(s.ViewBox, " ")

	for _, val := range split {
		ival, err := strconv.ParseFloat(val, 64)
		if err != nil {
			return vals, err
		}
		vals = append(vals, ival)
	}

	return vals, nil
}

// SetOwner sets the owner of a SVG Group
func (g *Group) SetOwner(svg *Svg) {
	g.Owner = svg
	for _, gn := range g.Elements {
		switch gn.(type) {
		case *Group:
			gn.(*Group).Owner = g.Owner
			gn.(*Group).SetOwner(svg)
		case *Path:
			gn.(*Path).group = g
		}
	}
}
<commit_msg>fix race condition in err goroutines; fix send on wrong goroutine<commit_after>package svg

import (
	"encoding/xml"
	"errors"
	"fmt"
	"io"
	"strconv"
	"strings"
	"sync"

	mt "github.com/rustyoz/Mtransform"
)

// DrawingInstructionParser allow getting segments and drawing
// instructions from them. All SVG elements should implement this
// interface.
type DrawingInstructionParser interface {
	ParseDrawingInstructions() (chan *DrawingInstruction, chan error)
}

// Tuple is an X,Y coordinate
type Tuple [2]float64

// Svg represents an SVG file containing at least a top level group or a
// number of Paths
type Svg struct {
	Title        string  `xml:"title"`
	Groups       []Group `xml:"g"`
	ViewBox      string  `xml:"viewBox,attr"`
	Elements     []DrawingInstructionParser
	Name         string
	Transform    *mt.Transform
	scale        float64
	instructions chan *DrawingInstruction
	errors       chan error
	segments     chan Segment
}

// Group represents an SVG group (usually located in a 'g' XML element)
type Group struct {
	ID              string
	Stroke          string
	StrokeWidth     int32
	Fill            string
	FillRule        string
	Elements        []DrawingInstructionParser
	TransformString string
	Transform       *mt.Transform // row, column
	Parent          *Group
	Owner           *Svg
	instructions    chan *DrawingInstruction
	errors          chan error
	segments        chan Segment
}

// ParseDrawingInstructions implements the DrawingInstructionParser interface
//
// This method makes it easier to get all the drawing instructions.
func (g *Group) ParseDrawingInstructions() (chan *DrawingInstruction, chan error) {
	g.instructions = make(chan *DrawingInstruction, 100)
	g.errors = make(chan error, 100)

	errWg := &sync.WaitGroup{}

	go func() {
		defer close(g.instructions)
		defer func() { errWg.Wait(); close(g.errors) }()
		for _, e := range g.Elements {
			instrs, errs := e.ParseDrawingInstructions()
			errWg.Add(1)
			go func() {
				for er := range errs {
					g.errors <- er
				}
				errWg.Done()
			}()
			for is := range instrs {
				g.instructions <- is
			}
		}
	}()

	return g.instructions, g.errors
}

// UnmarshalXML implements the encoding.xml.Unmarshaler interface
func (g *Group) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error {
	for _, attr := range start.Attr {
		switch attr.Name.Local {
		case "id":
			g.ID = attr.Value
		case "stroke":
			g.Stroke = attr.Value
		case "stroke-width":
			intValue, err := strconv.ParseInt(attr.Value, 10, 32)
			if err != nil {
				return err
			}
			g.StrokeWidth = int32(intValue)
		case "fill":
			g.Fill = attr.Value
		case "fill-rule":
			g.FillRule = attr.Value
		case "transform":
			g.TransformString = attr.Value
			t, err := parseTransform(g.TransformString)
			if err != nil {
				fmt.Println(err)
			}
			g.Transform = &t
		}
	}

	for {
		token, err := decoder.Token()
		if err != nil {
			return err
		}

		switch tok := token.(type) {
		case xml.StartElement:
			var elementStruct DrawingInstructionParser

			switch tok.Name.Local {
			case "g":
				elementStruct = &Group{Parent: g, Owner: g.Owner, Transform: mt.NewTransform()}
			case "rect":
				elementStruct = &Rect{group: g}
			case "circle":
				elementStruct = &Circle{group: g}
			case "path":
				elementStruct = &Path{group: g, StrokeWidth: float64(g.StrokeWidth), Stroke: &g.Stroke, Fill: &g.Fill}

			}

			if err = decoder.DecodeElement(elementStruct, &tok); err != nil {
				return fmt.Errorf("error decoding element of Group: %s", err)
			}
			g.Elements = append(g.Elements, elementStruct)

		case xml.EndElement:
			return nil
		}
	}
}

// ParseDrawingInstructions implements the DrawingInstructionParser interface
//
// This method makes it easier to get all the drawing instructions.
func (s *Svg) ParseDrawingInstructions() (chan *DrawingInstruction, chan error) {
	s.instructions = make(chan *DrawingInstruction, 100)
	s.errors = make(chan error, 100)

	go func() {
		errWg := &sync.WaitGroup{}
		var elecount int
		defer close(s.instructions)
		defer func() { errWg.Wait(); close(s.errors) }()
		for _, e := range s.Elements {
			elecount++
			instrs, errs := e.ParseDrawingInstructions()
			errWg.Add(1)
			go func(count int) {
				for er := range errs {
					s.errors <- fmt.Errorf("error when parsing element nr. %d: %s", count, er)
				}
				errWg.Done()
			}(elecount)

			for is := range instrs {
				s.instructions <- is
			}
		}

		for _, g := range s.Groups {
			instrs, errs := g.ParseDrawingInstructions()
			errWg.Add(1)
			go func() {
				for er := range errs {
					s.errors <- er
				}
				errWg.Done()
			}()
			for is := range instrs {
				s.instructions <- is
			}
		}
	}()

	return s.instructions, s.errors
}

// UnmarshalXML implements the encoding.xml.Unmarshaler interface
func (s *Svg) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error {
	for {
		for _, attr := range start.Attr {
			if attr.Name.Local == "viewBox" {
				s.ViewBox = attr.Value
				break
			}
		}

		token, err := decoder.Token()
		if err != nil {
			return err
		}

		switch tok := token.(type) {
		case xml.StartElement:
			var dip DrawingInstructionParser

			switch tok.Name.Local {
			case "g":
				g := &Group{Owner: s, Transform: mt.NewTransform()}
				if err = decoder.DecodeElement(g, &tok); err != nil {
					return fmt.Errorf("error decoding group element within SVG struct: %s", err)
				}
				s.Groups = append(s.Groups, *g)
				continue
			case "rect":
				dip = &Rect{}
			case "circle":
				dip = &Circle{}
			case "path":
				dip = &Path{}

			default:
				continue
			}

			if err = decoder.DecodeElement(dip, &tok); err != nil {
				return fmt.Errorf("error decoding element of SVG struct: %s", err)
			}

			s.Elements = append(s.Elements, dip)

		case xml.EndElement:
			if tok.Name.Local == "svg" {
				return nil
			}
		}
	}
}

// ParseSvg parses an SVG string into an SVG struct
func ParseSvg(str string, name string, scale float64) (*Svg, error) {
	var svg Svg
	svg.Name = name
	svg.Transform = mt.NewTransform()
	if scale > 0 {
		svg.Transform.Scale(scale, scale)
		svg.scale = scale
	}
	if scale < 0 {
		svg.Transform.Scale(1.0/-scale, 1.0/-scale)
		svg.scale = 1.0 / -scale
	}

	err := xml.Unmarshal([]byte(str), &svg)
	if err != nil {
		return nil, fmt.Errorf("ParseSvg Error: %v", err)
	}

	for i := range svg.Groups {
		svg.Groups[i].SetOwner(&svg)
		if svg.Groups[i].Transform == nil {
			svg.Groups[i].Transform = mt.NewTransform()
		}
	}
	return &svg, nil
}

// ParseSvgFromReader parses an SVG struct from an io.Reader
func ParseSvgFromReader(r io.Reader, name string, scale float64) (*Svg, error) {
	var svg Svg
	svg.Name = name
	svg.Transform = mt.NewTransform()
	if scale > 0 {
		svg.Transform.Scale(scale, scale)
		svg.scale = scale
	}
	if scale < 0 {
		svg.Transform.Scale(1.0/-scale, 1.0/-scale)
		svg.scale = 1.0 / -scale
	}

	if err := xml.NewDecoder(r).Decode(&svg); err != nil {
		return nil, fmt.Errorf("ParseSvg Error: %v", err)
	}

	for i := range svg.Groups {
		svg.Groups[i].SetOwner(&svg)
		if svg.Groups[i].Transform == nil {
			svg.Groups[i].Transform = mt.NewTransform()
		}
	}
	return &svg, nil
}

// ViewBoxValues returns all the numerical values in the viewBox
// attribute.
func (s *Svg) ViewBoxValues() ([]float64, error) {
	var vals []float64

	if s.ViewBox == "" {
		return vals, errors.New("viewBox attribute is empty")
	}

	split := strings.Split(s.ViewBox, " ")

	for _, val := range split {
		ival, err := strconv.ParseFloat(val, 64)
		if err != nil {
			return vals, err
		}
		vals = append(vals, ival)
	}

	return vals, nil
}

// SetOwner sets the owner of a SVG Group
func (g *Group) SetOwner(svg *Svg) {
	g.Owner = svg
	for _, gn := range g.Elements {
		switch gn.(type) {
		case *Group:
			gn.(*Group).Owner = g.Owner
			gn.(*Group).SetOwner(svg)
		case *Path:
			gn.(*Path).group = g
		}
	}
}
<|endoftext|>


<commit_before>// Copyright 2016 Albert Nigmatzianov. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package id3v2

import (
	"errors"
	"io"
	"os"
)

var ErrNoFile = errors.New("tag was not initialized with file")

// Tag stores all information about opened tag.
type Tag struct {
	frames    map[string]Framer
	sequences map[string]*sequence

	defaultEncoding Encoding
	reader          io.Reader
	originalSize    int64
	version         byte
}

// AddFrame adds f to tag with appropriate id. If id is "" or f is nil,
// AddFrame will not add it to tag.
//
// If you want to add attached picture, comment or unsynchronised lyrics/text
// transcription frames, better use AddAttachedPicture, AddCommentFrame
// or AddUnsynchronisedLyricsFrame methods respectively.
func (tag *Tag) AddFrame(id string, f Framer) {
	if id == "" || f == nil {
		return
	}

	if mustFrameBeInSequence(id) {
		sequence := tag.sequences[id]
		if sequence == nil {
			sequence = getSequence()
		}
		sequence.AddFrame(f)
		tag.sequences[id] = sequence
	} else {
		tag.frames[id] = f
	}
}

// AddAttachedPicture adds the picture frame to tag.
func (tag *Tag) AddAttachedPicture(pf PictureFrame) {
	id := tag.CommonID("Attached picture")
	tag.AddFrame(id, pf)
}

// AddCommentFrame adds the comment frame to tag.
func (tag *Tag) AddCommentFrame(cf CommentFrame) {
	id := tag.CommonID("Comments")
	tag.AddFrame(id, cf)
}

// AddTextFrame creates the text frame with provided encoding and text
// and adds to tag.
func (tag *Tag) AddTextFrame(id string, encoding Encoding, text string) {
	tag.AddFrame(id, TextFrame{Encoding: encoding, Text: text})
}

// AddUnsynchronisedLyricsFrame adds the unsynchronised lyrics/text frame
// to tag.
func (tag *Tag) AddUnsynchronisedLyricsFrame(uslf UnsynchronisedLyricsFrame) {
	id := tag.CommonID("Unsynchronised lyrics/text transcription")
	tag.AddFrame(id, uslf)
}

// AddUserDefinedTextFrame adds the custom frame (TXXX) to tag.
func (tag *Tag) AddUserDefinedTextFrame(uf UserDefinedTextFrame) {
	id := tag.CommonID("User defined text information frame")
	tag.AddFrame(id, uf)
}

// AddUFIDFrame adds the unique file identifier frame (UFID) to tag.
func (tag *Tag) AddUFIDFrame(uf UFIDFrame) {
	id := tag.CommonID("Unique file identifier")
	tag.AddFrame(id, uf)
}

// CommonID returns ID3v2.3 or ID3v2.4 (in appropriate to version of Tag) frame ID
// from given description.
// For example, CommonID("Language") will return "TLAN".
// If it can't find the ID with given description, it returns the description.
//
// All descriptions you can find in file common_ids.go
// or in id3 documentation (for fourth version: http://id3.org/id3v2.4.0-frames;
// for third version: http://id3.org/id3v2.3.0#Declared_ID3v2_frames).
func (tag *Tag) CommonID(description string) string {
	var ids map[string]string
	if tag.version == 3 {
		ids = V23CommonIDs
	} else {
		ids = V24CommonIDs
	}
	if id, ok := ids[description]; ok {
		return id
	}
	return description
}

// AllFrames returns map, that contains all frames in tag, that could be parsed.
// The key of this map is an ID of frame and value is an array of frames.
func (tag *Tag) AllFrames() map[string][]Framer {
	frames := make(map[string][]Framer)

	for id, f := range tag.frames {
		frames[id] = []Framer{f}
	}
	for id, sequence := range tag.sequences {
		frames[id] = sequence.Frames()
	}

	return frames
}

// DeleteAllFrames deletes all frames in tag.
func (tag *Tag) DeleteAllFrames() {
	if tag.frames == nil || len(tag.frames) > 0 {
		tag.frames = make(map[string]Framer)
	}
	if tag.sequences == nil || len(tag.sequences) > 0 {
		for _, s := range tag.sequences {
			putSequence(s)
		}
		tag.sequences = make(map[string]*sequence)
	}
}

// DeleteFrames deletes frames in tag with given id.
func (tag *Tag) DeleteFrames(id string) {
	delete(tag.frames, id)
	if s, ok := tag.sequences[id]; ok {
		putSequence(s)
		delete(tag.sequences, id)
	}
}

// Reset deletes all frames in tag and parses rd considering opts.
func (tag *Tag) Reset(rd io.Reader, opts Options) error {
	tag.DeleteAllFrames()
	return tag.parse(rd, opts)
}

// GetFrames returns frames with corresponding id.
// It returns nil if there is no frames with given id.
func (tag *Tag) GetFrames(id string) []Framer {
	if f, exists := tag.frames[id]; exists {
		return []Framer{f}
	} else if s, exists := tag.sequences[id]; exists {
		return s.Frames()
	}
	return nil
}

// GetLastFrame returns last frame from slice, that is returned from GetFrames function.
// GetLastFrame is suitable for frames, that can be only one in whole tag.
// For example, for text frames.
func (tag *Tag) GetLastFrame(id string) Framer {
	// Avoid an allocation of slice in GetFrames,
	// if there is anyway one frame.
	if f, exists := tag.frames[id]; exists {
		return f
	}

	fs := tag.GetFrames(id)
	if len(fs) == 0 {
		return nil
	}
	return fs[len(fs)-1]
}

// GetTextFrame returns text frame with corresponding id.
func (tag *Tag) GetTextFrame(id string) TextFrame {
	f := tag.GetLastFrame(id)
	if f == nil {
		return TextFrame{}
	}
	tf := f.(TextFrame)
	return tf
}

// DefaultEncoding returns default encoding of tag.
// Default encoding is used in methods (e.g. SetArtist, SetAlbum ...) for
// setting text frames without the explicit providing of encoding.
func (tag *Tag) DefaultEncoding() Encoding {
	return tag.defaultEncoding
}

// SetDefaultEncoding sets default encoding for tag.
// Default encoding is used in methods (e.g. SetArtist, SetAlbum ...) for
// setting text frames without explicit providing encoding.
func (tag *Tag) SetDefaultEncoding(encoding Encoding) {
	tag.defaultEncoding = encoding
}

func (tag *Tag) setDefaultEncodingBasedOnVersion(version byte) {
	if version == 4 {
		tag.SetDefaultEncoding(EncodingUTF8)
	} else {
		tag.SetDefaultEncoding(EncodingISO)
	}
}

// Count returns the number of frames in tag.
func (tag *Tag) Count() int {
	n := len(tag.frames)
	for _, s := range tag.sequences {
		n += s.Count()
	}
	return n
}

// HasFrames checks if there is at least one frame in tag.
// It's much faster than tag.Count() > 0.
func (tag *Tag) HasFrames() bool {
	return len(tag.frames) > 0 || len(tag.sequences) > 0
}

func (tag *Tag) Title() string {
	return tag.GetTextFrame(tag.CommonID("Title")).Text
}

func (tag *Tag) SetTitle(title string) {
	tag.AddTextFrame(tag.CommonID("Title"), tag.DefaultEncoding(), title)
}

func (tag *Tag) Artist() string {
	return tag.GetTextFrame(tag.CommonID("Artist")).Text
}

func (tag *Tag) SetArtist(artist string) {
	tag.AddTextFrame(tag.CommonID("Artist"), tag.DefaultEncoding(), artist)
}

func (tag *Tag) Album() string {
	return tag.GetTextFrame(tag.CommonID("Album/Movie/Show title")).Text
}

func (tag *Tag) SetAlbum(album string) {
	tag.AddTextFrame(tag.CommonID("Album/Movie/Show title"), tag.DefaultEncoding(), album)
}

func (tag *Tag) Year() string {
	return tag.GetTextFrame(tag.CommonID("Year")).Text
}

func (tag *Tag) SetYear(year string) {
	tag.AddTextFrame(tag.CommonID("Year"), tag.DefaultEncoding(), year)
}

func (tag *Tag) Genre() string {
	return tag.GetTextFrame(tag.CommonID("Content type")).Text
}

func (tag *Tag) SetGenre(genre string) {
	tag.AddTextFrame(tag.CommonID("Content type"), tag.DefaultEncoding(), genre)
}

// iterateOverAllFrames iterates over every single frame in tag and calls
// f for them. It consumps no memory at all, unlike the tag.AllFrames().
// It returns error only if f returns error.
func (tag *Tag) iterateOverAllFrames(f func(id string, frame Framer) error) error {
	for id, frame := range tag.frames {
		if err := f(id, frame); err != nil {
			return err
		}
	}
	for id, sequence := range tag.sequences {
		for _, frame := range sequence.Frames() {
			if err := f(id, frame); err != nil {
				return err
			}
		}
	}
	return nil
}

// Size returns the size of tag (tag header + size of all frames) in bytes.
func (tag *Tag) Size() int {
	if !tag.HasFrames() {
		return 0
	}

	var n int
	n += tagHeaderSize // Add the size of tag header
	tag.iterateOverAllFrames(func(id string, f Framer) error {
		n += frameHeaderSize + f.Size() // Add the whole frame size
		return nil
	})

	return n
}

// Version returns current ID3v2 version of tag.
func (tag *Tag) Version() byte {
	return tag.version
}

// SetVersion sets given ID3v2 version to tag.
// If version is less than 3 or greater than 4, then this method will do nothing.
// If tag has some frames, which are deprecated or changed in given version,
// then to your notice you can delete, change or just stay them.
func (tag *Tag) SetVersion(version byte) {
	if version < 3 || version > 4 {
		return
	}
	tag.version = version
	tag.setDefaultEncodingBasedOnVersion(version)
}

// Save writes tag to the file, if tag was opened with a file.
// If there are no frames in tag, Save will write
// only music part without any ID3v2 information.
// If tag was initiliazed not with file, it returns ErrNoFile.
func (tag *Tag) Save() error {
	file, ok := tag.reader.(*os.File)
	if !ok {
		return ErrNoFile
	}

	// Get original file mode.
	originalFile := file
	originalStat, err := originalFile.Stat()
	if err != nil {
		return err
	}

	// Create a temp file for mp3 file, which will contain new tag.
	name := file.Name() + "-id3v2"
	newFile, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE, originalStat.Mode())
	if err != nil {
		return err
	}

	// Make sure we clean up the temp file if it's still around.
	// tempfileShouldBeRemoved created only for performance
	// improvement to prevent calling redundant Remove syscalls if file is moved
	// and is not need to be removed.
	tempfileShouldBeRemoved := true
	defer func() {
		if tempfileShouldBeRemoved {
			os.Remove(newFile.Name())
		}
	}()

	// Write tag in new file.
	tagSize, err := tag.WriteTo(newFile)
	if err != nil {
		return err
	}

	// Seek to a music part of original file.
	if _, err = originalFile.Seek(tag.originalSize, os.SEEK_SET); err != nil {
		return err
	}

	// Write to new file the music part.
	buf := getByteSlice(128 * 1024)
	defer putByteSlice(buf)
	if _, err = io.CopyBuffer(newFile, originalFile, buf); err != nil {
		return err
	}

	// Close files to allow replacing.
	newFile.Close()
	originalFile.Close()

	// Replace original file with new file.
	if err = os.Rename(newFile.Name(), originalFile.Name()); err != nil {
		return err
	}
	tempfileShouldBeRemoved = false

	// Set tag.reader to new file with original name.
	tag.reader, err = os.Open(originalFile.Name())
	if err != nil {
		return err
	}

	// Set tag.originalSize to new frames size.
	tag.originalSize = tagSize

	return nil
}

// WriteTo writes whole tag in w if there is at least one frame.
// It returns the number of bytes written and error during the write.
// It returns nil as error if the write was successful.
func (tag *Tag) WriteTo(w io.Writer) (n int64, err error) {
	if w == nil {
		return 0, errors.New("w is nil")
	}

	// Count size of frames.
	framesSize := tag.Size() - tagHeaderSize
	if framesSize <= 0 {
		return 0, nil
	}

	// Write tag header.
	bw := getBufWriter(w)
	defer putBufWriter(bw)
	writeTagHeader(bw, uint(framesSize), tag.version)

	// Write frames.
	synchSafe := tag.Version() == 4
	err = tag.iterateOverAllFrames(func(id string, f Framer) error {
		return writeFrame(bw, id, f, synchSafe)
	})
	if err != nil {
		bw.Flush()
		return bw.Written(), err
	}

	return bw.Written(), bw.Flush()
}

func writeTagHeader(bw *bufWriter, framesSize uint, version byte) {
	bw.Write(id3Identifier)
	bw.WriteByte(version)
	bw.WriteByte(0) // Revision
	bw.WriteByte(0) // Flags
	bw.WriteBytesSize(framesSize, true)
}

func writeFrame(bw *bufWriter, id string, frame Framer, synchSafe bool) error {
	writeFrameHeader(bw, id, uint(frame.Size()), synchSafe)
	_, err := frame.WriteTo(bw)
	return err
}

func writeFrameHeader(bw *bufWriter, id string, frameSize uint, synchSafe bool) {
	bw.WriteString(id)
	bw.WriteBytesSize(frameSize, synchSafe)
	bw.Write([]byte{0, 0}) // Flags
}

// Close closes tag's file, if tag was opened with a file.
// If tag was initiliazed not with file, it returns ErrNoFile.
func (tag *Tag) Close() error {
	file, ok := tag.reader.(*os.File)
	if !ok {
		return ErrNoFile
	}
	return file.Close()
}
<commit_msg>Shorten Add*Frame funcs<commit_after>// Copyright 2016 Albert Nigmatzianov. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package id3v2

import (
	"errors"
	"io"
	"os"
)

var ErrNoFile = errors.New("tag was not initialized with file")

// Tag stores all information about opened tag.
type Tag struct {
	frames    map[string]Framer
	sequences map[string]*sequence

	defaultEncoding Encoding
	reader          io.Reader
	originalSize    int64
	version         byte
}

// AddFrame adds f to tag with appropriate id. If id is "" or f is nil,
// AddFrame will not add it to tag.
//
// If you want to add attached picture, comment or unsynchronised lyrics/text
// transcription frames, better use AddAttachedPicture, AddCommentFrame
// or AddUnsynchronisedLyricsFrame methods respectively.
func (tag *Tag) AddFrame(id string, f Framer) {
	if id == "" || f == nil {
		return
	}

	if mustFrameBeInSequence(id) {
		sequence := tag.sequences[id]
		if sequence == nil {
			sequence = getSequence()
		}
		sequence.AddFrame(f)
		tag.sequences[id] = sequence
	} else {
		tag.frames[id] = f
	}
}

// AddAttachedPicture adds the picture frame to tag.
func (tag *Tag) AddAttachedPicture(pf PictureFrame) {
	tag.AddFrame(tag.CommonID("Attached picture"), pf)
}

// AddCommentFrame adds the comment frame to tag.
func (tag *Tag) AddCommentFrame(cf CommentFrame) {
	tag.AddFrame(tag.CommonID("Comments"), cf)
}

// AddTextFrame creates the text frame with provided encoding and text
// and adds to tag.
func (tag *Tag) AddTextFrame(id string, encoding Encoding, text string) {
	tag.AddFrame(id, TextFrame{Encoding: encoding, Text: text})
}

// AddUnsynchronisedLyricsFrame adds the unsynchronised lyrics/text frame
// to tag.
func (tag *Tag) AddUnsynchronisedLyricsFrame(uslf UnsynchronisedLyricsFrame) {
	tag.AddFrame(tag.CommonID("Unsynchronised lyrics/text transcription"), uslf)
}

// AddUserDefinedTextFrame adds the custom frame (TXXX) to tag.
func (tag *Tag) AddUserDefinedTextFrame(udtf UserDefinedTextFrame) {
	tag.AddFrame(tag.CommonID("User defined text information frame"), udtf)
}

// AddUFIDFrame adds the unique file identifier frame (UFID) to tag.
func (tag *Tag) AddUFIDFrame(ufid UFIDFrame) {
	tag.AddFrame(tag.CommonID("Unique file identifier"), ufid)
}

// CommonID returns ID3v2.3 or ID3v2.4 (in appropriate to version of Tag) frame ID
// from given description.
// For example, CommonID("Language") will return "TLAN".
// If it can't find the ID with given description, it returns the description.
//
// All descriptions you can find in file common_ids.go
// or in id3 documentation (for fourth version: http://id3.org/id3v2.4.0-frames;
// for third version: http://id3.org/id3v2.3.0#Declared_ID3v2_frames).
func (tag *Tag) CommonID(description string) string {
	var ids map[string]string
	if tag.version == 3 {
		ids = V23CommonIDs
	} else {
		ids = V24CommonIDs
	}
	if id, ok := ids[description]; ok {
		return id
	}
	return description
}

// AllFrames returns map, that contains all frames in tag, that could be parsed.
// The key of this map is an ID of frame and value is an array of frames.
func (tag *Tag) AllFrames() map[string][]Framer {
	frames := make(map[string][]Framer)

	for id, f := range tag.frames {
		frames[id] = []Framer{f}
	}
	for id, sequence := range tag.sequences {
		frames[id] = sequence.Frames()
	}

	return frames
}

// DeleteAllFrames deletes all frames in tag.
func (tag *Tag) DeleteAllFrames() {
	if tag.frames == nil || len(tag.frames) > 0 {
		tag.frames = make(map[string]Framer)
	}
	if tag.sequences == nil || len(tag.sequences) > 0 {
		for _, s := range tag.sequences {
			putSequence(s)
		}
		tag.sequences = make(map[string]*sequence)
	}
}

// DeleteFrames deletes frames in tag with given id.
func (tag *Tag) DeleteFrames(id string) {
	delete(tag.frames, id)
	if s, ok := tag.sequences[id]; ok {
		putSequence(s)
		delete(tag.sequences, id)
	}
}

// Reset deletes all frames in tag and parses rd considering opts.
func (tag *Tag) Reset(rd io.Reader, opts Options) error {
	tag.DeleteAllFrames()
	return tag.parse(rd, opts)
}

// GetFrames returns frames with corresponding id.
// It returns nil if there is no frames with given id.
func (tag *Tag) GetFrames(id string) []Framer {
	if f, exists := tag.frames[id]; exists {
		return []Framer{f}
	} else if s, exists := tag.sequences[id]; exists {
		return s.Frames()
	}
	return nil
}

// GetLastFrame returns last frame from slice, that is returned from GetFrames function.
// GetLastFrame is suitable for frames, that can be only one in whole tag.
// For example, for text frames.
func (tag *Tag) GetLastFrame(id string) Framer {
	// Avoid an allocation of slice in GetFrames,
	// if there is anyway one frame.
	if f, exists := tag.frames[id]; exists {
		return f
	}

	fs := tag.GetFrames(id)
	if len(fs) == 0 {
		return nil
	}
	return fs[len(fs)-1]
}

// GetTextFrame returns text frame with corresponding id.
func (tag *Tag) GetTextFrame(id string) TextFrame {
	f := tag.GetLastFrame(id)
	if f == nil {
		return TextFrame{}
	}
	tf := f.(TextFrame)
	return tf
}

// DefaultEncoding returns default encoding of tag.
// Default encoding is used in methods (e.g. SetArtist, SetAlbum ...) for
// setting text frames without the explicit providing of encoding.
func (tag *Tag) DefaultEncoding() Encoding {
	return tag.defaultEncoding
}

// SetDefaultEncoding sets default encoding for tag.
// Default encoding is used in methods (e.g. SetArtist, SetAlbum ...) for
// setting text frames without explicit providing encoding.
func (tag *Tag) SetDefaultEncoding(encoding Encoding) {
	tag.defaultEncoding = encoding
}

func (tag *Tag) setDefaultEncodingBasedOnVersion(version byte) {
	if version == 4 {
		tag.SetDefaultEncoding(EncodingUTF8)
	} else {
		tag.SetDefaultEncoding(EncodingISO)
	}
}

// Count returns the number of frames in tag.
func (tag *Tag) Count() int {
	n := len(tag.frames)
	for _, s := range tag.sequences {
		n += s.Count()
	}
	return n
}

// HasFrames checks if there is at least one frame in tag.
// It's much faster than tag.Count() > 0.
func (tag *Tag) HasFrames() bool {
	return len(tag.frames) > 0 || len(tag.sequences) > 0
}

func (tag *Tag) Title() string {
	return tag.GetTextFrame(tag.CommonID("Title")).Text
}

func (tag *Tag) SetTitle(title string) {
	tag.AddTextFrame(tag.CommonID("Title"), tag.DefaultEncoding(), title)
}

func (tag *Tag) Artist() string {
	return tag.GetTextFrame(tag.CommonID("Artist")).Text
}

func (tag *Tag) SetArtist(artist string) {
	tag.AddTextFrame(tag.CommonID("Artist"), tag.DefaultEncoding(), artist)
}

func (tag *Tag) Album() string {
	return tag.GetTextFrame(tag.CommonID("Album/Movie/Show title")).Text
}

func (tag *Tag) SetAlbum(album string) {
	tag.AddTextFrame(tag.CommonID("Album/Movie/Show title"), tag.DefaultEncoding(), album)
}

func (tag *Tag) Year() string {
	return tag.GetTextFrame(tag.CommonID("Year")).Text
}

func (tag *Tag) SetYear(year string) {
	tag.AddTextFrame(tag.CommonID("Year"), tag.DefaultEncoding(), year)
}

func (tag *Tag) Genre() string {
	return tag.GetTextFrame(tag.CommonID("Content type")).Text
}

func (tag *Tag) SetGenre(genre string) {
	tag.AddTextFrame(tag.CommonID("Content type"), tag.DefaultEncoding(), genre)
}

// iterateOverAllFrames iterates over every single frame in tag and calls
// f for them. It consumps no memory at all, unlike the tag.AllFrames().
// It returns error only if f returns error.
func (tag *Tag) iterateOverAllFrames(f func(id string, frame Framer) error) error {
	for id, frame := range tag.frames {
		if err := f(id, frame); err != nil {
			return err
		}
	}
	for id, sequence := range tag.sequences {
		for _, frame := range sequence.Frames() {
			if err := f(id, frame); err != nil {
				return err
			}
		}
	}
	return nil
}

// Size returns the size of tag (tag header + size of all frames) in bytes.
func (tag *Tag) Size() int {
	if !tag.HasFrames() {
		return 0
	}

	var n int
	n += tagHeaderSize // Add the size of tag header
	tag.iterateOverAllFrames(func(id string, f Framer) error {
		n += frameHeaderSize + f.Size() // Add the whole frame size
		return nil
	})

	return n
}

// Version returns current ID3v2 version of tag.
func (tag *Tag) Version() byte {
	return tag.version
}

// SetVersion sets given ID3v2 version to tag.
// If version is less than 3 or greater than 4, then this method will do nothing.
// If tag has some frames, which are deprecated or changed in given version,
// then to your notice you can delete, change or just stay them.
func (tag *Tag) SetVersion(version byte) {
	if version < 3 || version > 4 {
		return
	}
	tag.version = version
	tag.setDefaultEncodingBasedOnVersion(version)
}

// Save writes tag to the file, if tag was opened with a file.
// If there are no frames in tag, Save will write
// only music part without any ID3v2 information.
// If tag was initiliazed not with file, it returns ErrNoFile.
func (tag *Tag) Save() error {
	file, ok := tag.reader.(*os.File)
	if !ok {
		return ErrNoFile
	}

	// Get original file mode.
	originalFile := file
	originalStat, err := originalFile.Stat()
	if err != nil {
		return err
	}

	// Create a temp file for mp3 file, which will contain new tag.
	name := file.Name() + "-id3v2"
	newFile, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE, originalStat.Mode())
	if err != nil {
		return err
	}

	// Make sure we clean up the temp file if it's still around.
	// tempfileShouldBeRemoved created only for performance
	// improvement to prevent calling redundant Remove syscalls if file is moved
	// and is not need to be removed.
	tempfileShouldBeRemoved := true
	defer func() {
		if tempfileShouldBeRemoved {
			os.Remove(newFile.Name())
		}
	}()

	// Write tag in new file.
	tagSize, err := tag.WriteTo(newFile)
	if err != nil {
		return err
	}

	// Seek to a music part of original file.
	if _, err = originalFile.Seek(tag.originalSize, os.SEEK_SET); err != nil {
		return err
	}

	// Write to new file the music part.
	buf := getByteSlice(128 * 1024)
	defer putByteSlice(buf)
	if _, err = io.CopyBuffer(newFile, originalFile, buf); err != nil {
		return err
	}

	// Close files to allow replacing.
	newFile.Close()
	originalFile.Close()

	// Replace original file with new file.
	if err = os.Rename(newFile.Name(), originalFile.Name()); err != nil {
		return err
	}
	tempfileShouldBeRemoved = false

	// Set tag.reader to new file with original name.
	tag.reader, err = os.Open(originalFile.Name())
	if err != nil {
		return err
	}

	// Set tag.originalSize to new frames size.
	tag.originalSize = tagSize

	return nil
}

// WriteTo writes whole tag in w if there is at least one frame.
// It returns the number of bytes written and error during the write.
// It returns nil as error if the write was successful.
func (tag *Tag) WriteTo(w io.Writer) (n int64, err error) {
	if w == nil {
		return 0, errors.New("w is nil")
	}

	// Count size of frames.
	framesSize := tag.Size() - tagHeaderSize
	if framesSize <= 0 {
		return 0, nil
	}

	// Write tag header.
	bw := getBufWriter(w)
	defer putBufWriter(bw)
	writeTagHeader(bw, uint(framesSize), tag.version)

	// Write frames.
	synchSafe := tag.Version() == 4
	err = tag.iterateOverAllFrames(func(id string, f Framer) error {
		return writeFrame(bw, id, f, synchSafe)
	})
	if err != nil {
		bw.Flush()
		return bw.Written(), err
	}

	return bw.Written(), bw.Flush()
}

func writeTagHeader(bw *bufWriter, framesSize uint, version byte) {
	bw.Write(id3Identifier)
	bw.WriteByte(version)
	bw.WriteByte(0) // Revision
	bw.WriteByte(0) // Flags
	bw.WriteBytesSize(framesSize, true)
}

func writeFrame(bw *bufWriter, id string, frame Framer, synchSafe bool) error {
	writeFrameHeader(bw, id, uint(frame.Size()), synchSafe)
	_, err := frame.WriteTo(bw)
	return err
}

func writeFrameHeader(bw *bufWriter, id string, frameSize uint, synchSafe bool) {
	bw.WriteString(id)
	bw.WriteBytesSize(frameSize, synchSafe)
	bw.Write([]byte{0, 0}) // Flags
}

// Close closes tag's file, if tag was opened with a file.
// If tag was initiliazed not with file, it returns ErrNoFile.
func (tag *Tag) Close() error {
	file, ok := tag.reader.(*os.File)
	if !ok {
		return ErrNoFile
	}
	return file.Close()
}
<|endoftext|>