1365 lines
25 KiB
Go
1365 lines
25 KiB
Go
// Commands from https://redis.io/commands#string
|
|
|
|
package miniredis
|
|
|
|
import (
|
|
"math/big"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/alicebob/miniredis/v2/server"
|
|
)
|
|
|
|
// commandsString handles all string value operations.
|
|
func commandsString(m *Miniredis) {
|
|
m.srv.Register("APPEND", m.cmdAppend)
|
|
m.srv.Register("BITCOUNT", m.cmdBitcount)
|
|
m.srv.Register("BITOP", m.cmdBitop)
|
|
m.srv.Register("BITPOS", m.cmdBitpos)
|
|
m.srv.Register("DECRBY", m.cmdDecrby)
|
|
m.srv.Register("DECR", m.cmdDecr)
|
|
m.srv.Register("GETBIT", m.cmdGetbit)
|
|
m.srv.Register("GET", m.cmdGet)
|
|
m.srv.Register("GETEX", m.cmdGetex)
|
|
m.srv.Register("GETRANGE", m.cmdGetrange)
|
|
m.srv.Register("GETSET", m.cmdGetset)
|
|
m.srv.Register("GETDEL", m.cmdGetdel)
|
|
m.srv.Register("INCRBYFLOAT", m.cmdIncrbyfloat)
|
|
m.srv.Register("INCRBY", m.cmdIncrby)
|
|
m.srv.Register("INCR", m.cmdIncr)
|
|
m.srv.Register("MGET", m.cmdMget)
|
|
m.srv.Register("MSET", m.cmdMset)
|
|
m.srv.Register("MSETNX", m.cmdMsetnx)
|
|
m.srv.Register("PSETEX", m.cmdPsetex)
|
|
m.srv.Register("SETBIT", m.cmdSetbit)
|
|
m.srv.Register("SETEX", m.cmdSetex)
|
|
m.srv.Register("SET", m.cmdSet)
|
|
m.srv.Register("SETNX", m.cmdSetnx)
|
|
m.srv.Register("SETRANGE", m.cmdSetrange)
|
|
m.srv.Register("STRLEN", m.cmdStrlen)
|
|
}
|
|
|
|
// SET
|
|
func (m *Miniredis) cmdSet(c *server.Peer, cmd string, args []string) {
|
|
if len(args) < 2 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
var opts struct {
|
|
key string
|
|
value string
|
|
nx bool // set iff not exists
|
|
xx bool // set iff exists
|
|
keepttl bool // set keepttl
|
|
ttlSet bool
|
|
ttl time.Duration
|
|
get bool
|
|
}
|
|
|
|
opts.key, opts.value, args = args[0], args[1], args[2:]
|
|
for len(args) > 0 {
|
|
timeUnit := time.Second
|
|
switch arg := strings.ToUpper(args[0]); arg {
|
|
case "NX":
|
|
opts.nx = true
|
|
args = args[1:]
|
|
continue
|
|
case "XX":
|
|
opts.xx = true
|
|
args = args[1:]
|
|
continue
|
|
case "KEEPTTL":
|
|
opts.keepttl = true
|
|
args = args[1:]
|
|
continue
|
|
case "PX", "PXAT":
|
|
timeUnit = time.Millisecond
|
|
fallthrough
|
|
case "EX", "EXAT":
|
|
if len(args) < 2 {
|
|
setDirty(c)
|
|
c.WriteError(msgInvalidInt)
|
|
return
|
|
}
|
|
if opts.ttlSet {
|
|
// multiple ex/exat/px/pxat options set
|
|
setDirty(c)
|
|
c.WriteError(msgSyntaxError)
|
|
return
|
|
}
|
|
expire, err := strconv.Atoi(args[1])
|
|
if err != nil {
|
|
setDirty(c)
|
|
c.WriteError(msgInvalidInt)
|
|
return
|
|
}
|
|
if expire <= 0 {
|
|
setDirty(c)
|
|
c.WriteError(msgInvalidSETime)
|
|
return
|
|
}
|
|
|
|
if arg == "PXAT" || arg == "EXAT" {
|
|
opts.ttl = m.at(expire, timeUnit)
|
|
} else {
|
|
opts.ttl = time.Duration(expire) * timeUnit
|
|
}
|
|
opts.ttlSet = true
|
|
|
|
args = args[2:]
|
|
continue
|
|
case "GET":
|
|
opts.get = true
|
|
args = args[1:]
|
|
continue
|
|
default:
|
|
setDirty(c)
|
|
c.WriteError(msgSyntaxError)
|
|
return
|
|
}
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
readonly := false
|
|
if opts.nx {
|
|
if db.exists(opts.key) {
|
|
if opts.get {
|
|
// special case for SET NX GET
|
|
readonly = true
|
|
} else {
|
|
c.WriteNull()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
if opts.xx {
|
|
if !db.exists(opts.key) {
|
|
if opts.get {
|
|
// special case for SET XX GET
|
|
readonly = true
|
|
} else {
|
|
c.WriteNull()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
if opts.keepttl {
|
|
if val, ok := db.ttl[opts.key]; ok {
|
|
opts.ttl = val
|
|
}
|
|
}
|
|
if opts.get {
|
|
if t, ok := db.keys[opts.key]; ok && t != "string" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
}
|
|
|
|
old, existed := db.stringKeys[opts.key]
|
|
if !readonly {
|
|
db.del(opts.key, true) // be sure to remove existing values of other type keys.
|
|
// a vanilla SET clears the expire
|
|
if opts.ttl >= 0 { // EXAT/PXAT can expire right away
|
|
db.stringSet(opts.key, opts.value)
|
|
}
|
|
if opts.ttl != 0 {
|
|
db.ttl[opts.key] = opts.ttl
|
|
}
|
|
}
|
|
if opts.get {
|
|
if !existed {
|
|
c.WriteNull()
|
|
} else {
|
|
c.WriteBulk(old)
|
|
}
|
|
return
|
|
}
|
|
c.WriteOK()
|
|
})
|
|
}
|
|
|
|
// SETEX
|
|
func (m *Miniredis) cmdSetex(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 3 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
key := args[0]
|
|
ttl, err := strconv.Atoi(args[1])
|
|
if err != nil {
|
|
setDirty(c)
|
|
c.WriteError(msgInvalidInt)
|
|
return
|
|
}
|
|
if ttl <= 0 {
|
|
setDirty(c)
|
|
c.WriteError(msgInvalidSETEXTime)
|
|
return
|
|
}
|
|
value := args[2]
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
db.del(key, true) // Clear any existing keys.
|
|
db.stringSet(key, value)
|
|
db.ttl[key] = time.Duration(ttl) * time.Second
|
|
c.WriteOK()
|
|
})
|
|
}
|
|
|
|
// PSETEX
|
|
func (m *Miniredis) cmdPsetex(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 3 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
var opts struct {
|
|
key string
|
|
ttl int
|
|
value string
|
|
}
|
|
|
|
opts.key = args[0]
|
|
if ok := optInt(c, args[1], &opts.ttl); !ok {
|
|
return
|
|
}
|
|
if opts.ttl <= 0 {
|
|
setDirty(c)
|
|
c.WriteError(msgInvalidPSETEXTime)
|
|
return
|
|
}
|
|
opts.value = args[2]
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
db.del(opts.key, true) // Clear any existing keys.
|
|
db.stringSet(opts.key, opts.value)
|
|
db.ttl[opts.key] = time.Duration(opts.ttl) * time.Millisecond
|
|
c.WriteOK()
|
|
})
|
|
}
|
|
|
|
// SETNX
|
|
func (m *Miniredis) cmdSetnx(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 2 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
key, value := args[0], args[1]
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if _, ok := db.keys[key]; ok {
|
|
c.WriteInt(0)
|
|
return
|
|
}
|
|
|
|
db.stringSet(key, value)
|
|
c.WriteInt(1)
|
|
})
|
|
}
|
|
|
|
// MSET
|
|
func (m *Miniredis) cmdMset(c *server.Peer, cmd string, args []string) {
|
|
if len(args) < 2 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
if len(args)%2 != 0 {
|
|
setDirty(c)
|
|
// non-default error message
|
|
c.WriteError("ERR wrong number of arguments for MSET")
|
|
return
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
for len(args) > 0 {
|
|
key, value := args[0], args[1]
|
|
args = args[2:]
|
|
|
|
db.del(key, true) // clear TTL
|
|
db.stringSet(key, value)
|
|
}
|
|
c.WriteOK()
|
|
})
|
|
}
|
|
|
|
// MSETNX
|
|
func (m *Miniredis) cmdMsetnx(c *server.Peer, cmd string, args []string) {
|
|
if len(args) < 2 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
if len(args)%2 != 0 {
|
|
setDirty(c)
|
|
// non-default error message (yes, with 'MSET').
|
|
c.WriteError("ERR wrong number of arguments for MSET")
|
|
return
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
keys := map[string]string{}
|
|
existing := false
|
|
for len(args) > 0 {
|
|
key := args[0]
|
|
value := args[1]
|
|
args = args[2:]
|
|
keys[key] = value
|
|
if _, ok := db.keys[key]; ok {
|
|
existing = true
|
|
}
|
|
}
|
|
|
|
res := 0
|
|
if !existing {
|
|
res = 1
|
|
for k, v := range keys {
|
|
// Nothing to delete. That's the whole point.
|
|
db.stringSet(k, v)
|
|
}
|
|
}
|
|
c.WriteInt(res)
|
|
})
|
|
}
|
|
|
|
// GET
|
|
func (m *Miniredis) cmdGet(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 1 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
key := args[0]
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if !db.exists(key) {
|
|
c.WriteNull()
|
|
return
|
|
}
|
|
if db.t(key) != "string" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
|
|
c.WriteBulk(db.stringGet(key))
|
|
})
|
|
}
|
|
|
|
// GETEX
|
|
func (m *Miniredis) cmdGetex(c *server.Peer, cmd string, args []string) {
|
|
if len(args) < 1 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
var opts struct {
|
|
key string
|
|
ttl time.Duration
|
|
persist bool // remove existing TTL on the key.
|
|
}
|
|
|
|
opts.key, args = args[0], args[1:]
|
|
if len(args) > 0 {
|
|
timeUnit := time.Second
|
|
switch arg := strings.ToUpper(args[0]); arg {
|
|
case "PERSIST":
|
|
if len(args) > 1 {
|
|
setDirty(c)
|
|
c.WriteError(msgSyntaxError)
|
|
return
|
|
}
|
|
opts.persist = true
|
|
case "PX", "PXAT":
|
|
timeUnit = time.Millisecond
|
|
fallthrough
|
|
case "EX", "EXAT":
|
|
if len(args) != 2 {
|
|
setDirty(c)
|
|
c.WriteError(msgSyntaxError)
|
|
return
|
|
}
|
|
expire, err := strconv.Atoi(args[1])
|
|
if err != nil {
|
|
setDirty(c)
|
|
c.WriteError(msgInvalidInt)
|
|
return
|
|
}
|
|
if expire <= 0 {
|
|
setDirty(c)
|
|
c.WriteError(msgInvalidSETime)
|
|
return
|
|
}
|
|
|
|
if arg == "PXAT" || arg == "EXAT" {
|
|
opts.ttl = m.at(expire, timeUnit)
|
|
} else {
|
|
opts.ttl = time.Duration(expire) * timeUnit
|
|
}
|
|
default:
|
|
setDirty(c)
|
|
c.WriteError(msgSyntaxError)
|
|
return
|
|
}
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if !db.exists(opts.key) {
|
|
c.WriteNull()
|
|
return
|
|
}
|
|
switch {
|
|
case opts.persist:
|
|
delete(db.ttl, opts.key)
|
|
case opts.ttl != 0:
|
|
db.ttl[opts.key] = opts.ttl
|
|
}
|
|
|
|
if db.t(opts.key) != "string" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
|
|
c.WriteBulk(db.stringGet(opts.key))
|
|
})
|
|
}
|
|
|
|
// GETSET
|
|
func (m *Miniredis) cmdGetset(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 2 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
key, value := args[0], args[1]
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if t, ok := db.keys[key]; ok && t != "string" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
|
|
old, ok := db.stringKeys[key]
|
|
db.stringSet(key, value)
|
|
// a GETSET clears the ttl
|
|
delete(db.ttl, key)
|
|
|
|
if !ok {
|
|
c.WriteNull()
|
|
return
|
|
}
|
|
c.WriteBulk(old)
|
|
})
|
|
}
|
|
|
|
// GETDEL
|
|
func (m *Miniredis) cmdGetdel(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 1 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
key := args[0]
|
|
|
|
if !db.exists(key) {
|
|
c.WriteNull()
|
|
return
|
|
}
|
|
|
|
if db.t(key) != "string" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
|
|
v := db.stringGet(key)
|
|
db.del(key, true)
|
|
c.WriteBulk(v)
|
|
})
|
|
}
|
|
|
|
// MGET
|
|
func (m *Miniredis) cmdMget(c *server.Peer, cmd string, args []string) {
|
|
if len(args) < 1 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
c.WriteLen(len(args))
|
|
for _, k := range args {
|
|
if t, ok := db.keys[k]; !ok || t != "string" {
|
|
c.WriteNull()
|
|
continue
|
|
}
|
|
v, ok := db.stringKeys[k]
|
|
if !ok {
|
|
// Should not happen, we just checked keys[]
|
|
c.WriteNull()
|
|
continue
|
|
}
|
|
c.WriteBulk(v)
|
|
}
|
|
})
|
|
}
|
|
|
|
// INCR
|
|
func (m *Miniredis) cmdIncr(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 1 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
key := args[0]
|
|
if t, ok := db.keys[key]; ok && t != "string" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
v, err := db.stringIncr(key, +1)
|
|
if err != nil {
|
|
c.WriteError(err.Error())
|
|
return
|
|
}
|
|
// Don't touch TTL
|
|
c.WriteInt(v)
|
|
})
|
|
}
|
|
|
|
// INCRBY
|
|
func (m *Miniredis) cmdIncrby(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 2 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
var opts struct {
|
|
key string
|
|
delta int
|
|
}
|
|
opts.key = args[0]
|
|
if ok := optInt(c, args[1], &opts.delta); !ok {
|
|
return
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if t, ok := db.keys[opts.key]; ok && t != "string" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
|
|
v, err := db.stringIncr(opts.key, opts.delta)
|
|
if err != nil {
|
|
c.WriteError(err.Error())
|
|
return
|
|
}
|
|
// Don't touch TTL
|
|
c.WriteInt(v)
|
|
})
|
|
}
|
|
|
|
// INCRBYFLOAT
|
|
func (m *Miniredis) cmdIncrbyfloat(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 2 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
key := args[0]
|
|
delta, _, err := big.ParseFloat(args[1], 10, 128, 0)
|
|
if err != nil {
|
|
setDirty(c)
|
|
c.WriteError(msgInvalidFloat)
|
|
return
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if t, ok := db.keys[key]; ok && t != "string" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
|
|
v, err := db.stringIncrfloat(key, delta)
|
|
if err != nil {
|
|
c.WriteError(err.Error())
|
|
return
|
|
}
|
|
// Don't touch TTL
|
|
c.WriteBulk(formatBig(v))
|
|
})
|
|
}
|
|
|
|
// DECR
|
|
func (m *Miniredis) cmdDecr(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 1 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
key := args[0]
|
|
if t, ok := db.keys[key]; ok && t != "string" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
v, err := db.stringIncr(key, -1)
|
|
if err != nil {
|
|
c.WriteError(err.Error())
|
|
return
|
|
}
|
|
// Don't touch TTL
|
|
c.WriteInt(v)
|
|
})
|
|
}
|
|
|
|
// DECRBY
|
|
func (m *Miniredis) cmdDecrby(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 2 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
var opts struct {
|
|
key string
|
|
delta int
|
|
}
|
|
opts.key = args[0]
|
|
if ok := optInt(c, args[1], &opts.delta); !ok {
|
|
return
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if t, ok := db.keys[opts.key]; ok && t != "string" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
|
|
v, err := db.stringIncr(opts.key, -opts.delta)
|
|
if err != nil {
|
|
c.WriteError(err.Error())
|
|
return
|
|
}
|
|
// Don't touch TTL
|
|
c.WriteInt(v)
|
|
})
|
|
}
|
|
|
|
// STRLEN
|
|
func (m *Miniredis) cmdStrlen(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 1 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
key := args[0]
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if t, ok := db.keys[key]; ok && t != "string" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
|
|
c.WriteInt(len(db.stringKeys[key]))
|
|
})
|
|
}
|
|
|
|
// APPEND
|
|
func (m *Miniredis) cmdAppend(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 2 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
key, value := args[0], args[1]
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if t, ok := db.keys[key]; ok && t != "string" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
|
|
newValue := db.stringKeys[key] + value
|
|
db.stringSet(key, newValue)
|
|
|
|
c.WriteInt(len(newValue))
|
|
})
|
|
}
|
|
|
|
// GETRANGE
|
|
func (m *Miniredis) cmdGetrange(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 3 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
var opts struct {
|
|
key string
|
|
start int
|
|
end int
|
|
}
|
|
opts.key = args[0]
|
|
if ok := optInt(c, args[1], &opts.start); !ok {
|
|
return
|
|
}
|
|
if ok := optInt(c, args[2], &opts.end); !ok {
|
|
return
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if t, ok := db.keys[opts.key]; ok && t != "string" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
|
|
v := db.stringKeys[opts.key]
|
|
c.WriteBulk(withRange(v, opts.start, opts.end))
|
|
})
|
|
}
|
|
|
|
// SETRANGE
|
|
func (m *Miniredis) cmdSetrange(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 3 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
var opts struct {
|
|
key string
|
|
pos int
|
|
subst string
|
|
}
|
|
opts.key = args[0]
|
|
if ok := optInt(c, args[1], &opts.pos); !ok {
|
|
return
|
|
}
|
|
if opts.pos < 0 {
|
|
setDirty(c)
|
|
c.WriteError("ERR offset is out of range")
|
|
return
|
|
}
|
|
opts.subst = args[2]
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if t, ok := db.keys[opts.key]; ok && t != "string" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
|
|
v := []byte(db.stringKeys[opts.key])
|
|
end := opts.pos + len(opts.subst)
|
|
if len(v) < end {
|
|
newV := make([]byte, end)
|
|
copy(newV, v)
|
|
v = newV
|
|
}
|
|
copy(v[opts.pos:end], opts.subst)
|
|
db.stringSet(opts.key, string(v))
|
|
c.WriteInt(len(v))
|
|
})
|
|
}
|
|
|
|
// BITCOUNT
|
|
func (m *Miniredis) cmdBitcount(c *server.Peer, cmd string, args []string) {
|
|
if len(args) < 1 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
var opts struct {
|
|
useRange bool
|
|
start int
|
|
end int
|
|
key string
|
|
}
|
|
opts.key, args = args[0], args[1:]
|
|
if len(args) >= 2 {
|
|
opts.useRange = true
|
|
if ok := optInt(c, args[0], &opts.start); !ok {
|
|
return
|
|
}
|
|
if ok := optInt(c, args[1], &opts.end); !ok {
|
|
return
|
|
}
|
|
args = args[2:]
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if !db.exists(opts.key) {
|
|
c.WriteInt(0)
|
|
return
|
|
}
|
|
if db.t(opts.key) != "string" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
|
|
// Real redis only checks after it knows the key is there and a string.
|
|
if len(args) != 0 {
|
|
c.WriteError(msgSyntaxError)
|
|
return
|
|
}
|
|
|
|
v := db.stringKeys[opts.key]
|
|
if opts.useRange {
|
|
v = withRange(v, opts.start, opts.end)
|
|
}
|
|
|
|
c.WriteInt(countBits([]byte(v)))
|
|
})
|
|
}
|
|
|
|
// BITOP
|
|
func (m *Miniredis) cmdBitop(c *server.Peer, cmd string, args []string) {
|
|
if len(args) < 3 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
var opts struct {
|
|
op string
|
|
target string
|
|
input []string
|
|
}
|
|
opts.op = strings.ToUpper(args[0])
|
|
opts.target = args[1]
|
|
opts.input = args[2:]
|
|
|
|
// 'op' is tested when the transaction is executed.
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
switch opts.op {
|
|
case "AND", "OR", "XOR":
|
|
first := opts.input[0]
|
|
if t, ok := db.keys[first]; ok && t != "string" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
res := []byte(db.stringKeys[first])
|
|
for _, vk := range opts.input[1:] {
|
|
if t, ok := db.keys[vk]; ok && t != "string" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
v := db.stringKeys[vk]
|
|
cb := map[string]func(byte, byte) byte{
|
|
"AND": func(a, b byte) byte { return a & b },
|
|
"OR": func(a, b byte) byte { return a | b },
|
|
"XOR": func(a, b byte) byte { return a ^ b },
|
|
}[opts.op]
|
|
res = sliceBinOp(cb, res, []byte(v))
|
|
}
|
|
db.del(opts.target, false) // Keep TTL
|
|
if len(res) == 0 {
|
|
db.del(opts.target, true)
|
|
} else {
|
|
db.stringSet(opts.target, string(res))
|
|
}
|
|
c.WriteInt(len(res))
|
|
case "NOT":
|
|
// NOT only takes a single argument.
|
|
if len(opts.input) != 1 {
|
|
c.WriteError("ERR BITOP NOT must be called with a single source key.")
|
|
return
|
|
}
|
|
key := opts.input[0]
|
|
if t, ok := db.keys[key]; ok && t != "string" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
value := []byte(db.stringKeys[key])
|
|
for i := range value {
|
|
value[i] = ^value[i]
|
|
}
|
|
db.del(opts.target, false) // Keep TTL
|
|
if len(value) == 0 {
|
|
db.del(opts.target, true)
|
|
} else {
|
|
db.stringSet(opts.target, string(value))
|
|
}
|
|
c.WriteInt(len(value))
|
|
default:
|
|
c.WriteError(msgSyntaxError)
|
|
}
|
|
})
|
|
}
|
|
|
|
// BITPOS
|
|
func (m *Miniredis) cmdBitpos(c *server.Peer, cmd string, args []string) {
|
|
if len(args) < 2 || len(args) > 4 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
var opts struct {
|
|
Key string
|
|
Bit int
|
|
Start int
|
|
End int
|
|
WithEnd bool
|
|
}
|
|
|
|
opts.Key = args[0]
|
|
if ok := optInt(c, args[1], &opts.Bit); !ok {
|
|
return
|
|
}
|
|
if len(args) > 2 {
|
|
if ok := optInt(c, args[2], &opts.Start); !ok {
|
|
return
|
|
}
|
|
}
|
|
if len(args) > 3 {
|
|
if ok := optInt(c, args[3], &opts.End); !ok {
|
|
return
|
|
}
|
|
opts.WithEnd = true
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if t, ok := db.keys[opts.Key]; ok && t != "string" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
} else if !ok {
|
|
// non-existing key behaves differently
|
|
if opts.Bit == 0 {
|
|
c.WriteInt(0)
|
|
} else {
|
|
c.WriteInt(-1)
|
|
}
|
|
return
|
|
}
|
|
value := db.stringKeys[opts.Key]
|
|
start := opts.Start
|
|
end := opts.End
|
|
if start < 0 {
|
|
start += len(value)
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
}
|
|
if start > len(value) {
|
|
start = len(value)
|
|
}
|
|
|
|
if opts.WithEnd {
|
|
if end < 0 {
|
|
end += len(value)
|
|
}
|
|
if end < 0 {
|
|
end = 0
|
|
}
|
|
end++ // +1 for redis end semantics
|
|
if end > len(value) {
|
|
end = len(value)
|
|
}
|
|
} else {
|
|
end = len(value)
|
|
}
|
|
|
|
if start != 0 || opts.WithEnd {
|
|
if end < start {
|
|
value = ""
|
|
} else {
|
|
value = value[start:end]
|
|
}
|
|
}
|
|
pos := bitPos([]byte(value), opts.Bit == 1)
|
|
if pos >= 0 {
|
|
pos += start * 8
|
|
}
|
|
// Special case when looking for 0, but not when start and end are
|
|
// given.
|
|
if opts.Bit == 0 && pos == -1 && !opts.WithEnd && len(value) > 0 {
|
|
pos = start*8 + len(value)*8
|
|
}
|
|
c.WriteInt(pos)
|
|
})
|
|
}
|
|
|
|
// GETBIT
|
|
func (m *Miniredis) cmdGetbit(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 2 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
var opts struct {
|
|
key string
|
|
bit int
|
|
}
|
|
opts.key = args[0]
|
|
if ok := optIntErr(c, args[1], &opts.bit, "ERR bit offset is not an integer or out of range"); !ok {
|
|
return
|
|
}
|
|
if opts.bit < 0 {
|
|
setDirty(c)
|
|
c.WriteError("ERR bit offset is not an integer or out of range")
|
|
return
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if t, ok := db.keys[opts.key]; ok && t != "string" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
value := db.stringKeys[opts.key]
|
|
|
|
ourByteNr := opts.bit / 8
|
|
var ourByte byte
|
|
if ourByteNr > len(value)-1 {
|
|
ourByte = '\x00'
|
|
} else {
|
|
ourByte = value[ourByteNr]
|
|
}
|
|
res := 0
|
|
if toBits(ourByte)[opts.bit%8] {
|
|
res = 1
|
|
}
|
|
c.WriteInt(res)
|
|
})
|
|
}
|
|
|
|
// SETBIT
|
|
func (m *Miniredis) cmdSetbit(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 3 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
var opts struct {
|
|
key string
|
|
bit int
|
|
newBit int
|
|
}
|
|
opts.key = args[0]
|
|
if ok := optIntErr(c, args[1], &opts.bit, "ERR bit offset is not an integer or out of range"); !ok {
|
|
return
|
|
}
|
|
if opts.bit < 0 {
|
|
setDirty(c)
|
|
c.WriteError("ERR bit offset is not an integer or out of range")
|
|
return
|
|
}
|
|
if ok := optIntErr(c, args[2], &opts.newBit, "ERR bit is not an integer or out of range"); !ok {
|
|
return
|
|
}
|
|
if opts.newBit != 0 && opts.newBit != 1 {
|
|
setDirty(c)
|
|
c.WriteError("ERR bit is not an integer or out of range")
|
|
return
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if t, ok := db.keys[opts.key]; ok && t != "string" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
value := []byte(db.stringKeys[opts.key])
|
|
|
|
ourByteNr := opts.bit / 8
|
|
ourBitNr := opts.bit % 8
|
|
if ourByteNr > len(value)-1 {
|
|
// Too short. Expand.
|
|
newValue := make([]byte, ourByteNr+1)
|
|
copy(newValue, value)
|
|
value = newValue
|
|
}
|
|
old := 0
|
|
if toBits(value[ourByteNr])[ourBitNr] {
|
|
old = 1
|
|
}
|
|
if opts.newBit == 0 {
|
|
value[ourByteNr] &^= 1 << uint8(7-ourBitNr)
|
|
} else {
|
|
value[ourByteNr] |= 1 << uint8(7-ourBitNr)
|
|
}
|
|
db.stringSet(opts.key, string(value))
|
|
|
|
c.WriteInt(old)
|
|
})
|
|
}
|
|
|
|
// Redis range. both start and end can be negative.
|
|
func withRange(v string, start, end int) string {
|
|
s, e := redisRange(len(v), start, end, true /* string getrange symantics */)
|
|
return v[s:e]
|
|
}
|
|
|
|
func countBits(v []byte) int {
|
|
count := 0
|
|
for _, b := range []byte(v) {
|
|
for b > 0 {
|
|
count += int((b % uint8(2)))
|
|
b = b >> 1
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
// sliceBinOp applies an operator to all slice elements, with Redis string
|
|
// padding logic.
|
|
func sliceBinOp(f func(a, b byte) byte, a, b []byte) []byte {
|
|
maxl := len(a)
|
|
if len(b) > maxl {
|
|
maxl = len(b)
|
|
}
|
|
lA := make([]byte, maxl)
|
|
copy(lA, a)
|
|
lB := make([]byte, maxl)
|
|
copy(lB, b)
|
|
res := make([]byte, maxl)
|
|
for i := range res {
|
|
res[i] = f(lA[i], lB[i])
|
|
}
|
|
return res
|
|
}
|
|
|
|
// Return the number of the first bit set/unset.
|
|
func bitPos(s []byte, bit bool) int {
|
|
for i, b := range s {
|
|
for j, set := range toBits(b) {
|
|
if set == bit {
|
|
return i*8 + j
|
|
}
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// toBits changes a byte in 8 bools.
|
|
func toBits(s byte) [8]bool {
|
|
r := [8]bool{}
|
|
for i := range r {
|
|
if s&(uint8(1)<<uint8(7-i)) != 0 {
|
|
r[i] = true
|
|
}
|
|
}
|
|
return r
|
|
}
|