xframe/vendor/github.com/alicebob/miniredis/v2/cmd_string.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
}