High severity7.5NVD Advisory· Published Apr 6, 2026· Updated Apr 27, 2026
CVE-2026-35172
CVE-2026-35172
Description
Distribution is a toolkit to pack, ship, store, and deliver container content. Prior to 3.1.0, distribution can restore read access in repo a after an explicit delete when storage.cache.blobdescriptor: redis and storage.delete.enabled: true are both enabled. The delete path clears the shared digest descriptor but leaves stale repo-scoped membership behind, so a later Stat or Get from repo b repopulates the shared descriptor and makes the deleted blob readable from repo a again. This vulnerability is fixed in 3.1.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/distribution/distribution/v3Go | < 3.1.0 | 3.1.0 |
github.com/distribution/distributionGo | <= 2.8.3 | — |
Affected products
1Patches
1078b0783f239Merge commit from fork
113 files changed · +37368 −2
go.mod+2 −0 modified@@ -8,6 +8,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 + github.com/alicebob/miniredis/v2 v2.35.0 github.com/aws/aws-sdk-go v1.55.5 github.com/bshuster-repo/logrus-logstash-hook v1.1.0 github.com/coreos/go-systemd/v22 v22.5.0 @@ -90,6 +91,7 @@ require ( github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect + github.com/yuin/gopher-lua v1.1.1 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 // indirect
go.sum+4 −0 modified@@ -50,6 +50,8 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapp github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1/go.mod h1:viRWSEhtMZqz1rhwmOVKkWl6SwmVowfL9O2YR5gI2PE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= +github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -271,6 +273,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
registry/storage/cache/redis/redis.go+10 −2 modified@@ -214,16 +214,24 @@ func (rsrbds *repositoryScopedRedisBlobDescriptorService) Clear(ctx context.Cont return err } + pool := rsrbds.upstream.pool + // Check membership to repository first - member, err := rsrbds.upstream.pool.SIsMember(ctx, rsrbds.repositoryBlobSetKey(rsrbds.repo), dgst.String()).Result() + member, err := pool.SIsMember(ctx, rsrbds.repositoryBlobSetKey(rsrbds.repo), dgst.String()).Result() if err != nil { return err } if !member { return distribution.ErrBlobUnknown } - return rsrbds.upstream.Clear(ctx, dgst) + pipe := pool.TxPipeline() + pipe.SRem(ctx, rsrbds.repositoryBlobSetKey(rsrbds.repo), dgst.String()) + pipe.Del(ctx, rsrbds.blobDescriptorHashKey(dgst)) + pipe.HDel(ctx, rsrbds.upstream.blobDescriptorHashKey(dgst), "digest", "size", "mediatype") + + _, err = pipe.Exec(ctx) + return err } func (rsrbds *repositoryScopedRedisBlobDescriptorService) SetDescriptor(ctx context.Context, dgst digest.Digest, desc v1.Descriptor) error {
registry/storage/cache/redis/redis_test.go+75 −0 modified@@ -6,7 +6,11 @@ import ( "os" "testing" + "github.com/alicebob/miniredis/v2" + "github.com/distribution/distribution/v3" "github.com/distribution/distribution/v3/registry/storage/cache/cachecheck" + "github.com/opencontainers/go-digest" + v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/redis/go-redis/v9" ) @@ -48,3 +52,74 @@ func TestRedisBlobDescriptorCacheProvider(t *testing.T) { cachecheck.CheckBlobDescriptorCache(t, NewRedisBlobDescriptorCacheProvider(pool)) } + +func TestRepositoryScopedClearRevokesRepositoryMembership(t *testing.T) { + ctx := context.Background() + + server, err := miniredis.Run() + if err != nil { + t.Fatalf("unexpected error starting miniredis: %v", err) + } + defer server.Close() + + pool := redis.NewClient(&redis.Options{Addr: server.Addr()}) + defer pool.Close() + + cache := &redisBlobDescriptorService{pool: pool} + repoA := &repositoryScopedRedisBlobDescriptorService{repo: "foo/repo-a", upstream: cache} + repoB := &repositoryScopedRedisBlobDescriptorService{repo: "foo/repo-b", upstream: cache} + + dgst := digest.FromString("stale-membership-regression") + desc := v1.Descriptor{ + Digest: dgst, + Size: 1337, + MediaType: "application/vnd.oci.image.layer.v1.tar", + } + + if err := repoA.SetDescriptor(ctx, dgst, desc); err != nil { + t.Fatalf("unexpected error setting descriptor for repo a: %v", err) + } + if err := repoB.SetDescriptor(ctx, dgst, desc); err != nil { + t.Fatalf("unexpected error setting descriptor for repo b: %v", err) + } + + if _, err := repoA.Stat(ctx, dgst); err != nil { + t.Fatalf("unexpected error statting descriptor for repo a before delete: %v", err) + } + + if err := repoA.Clear(ctx, dgst); err != nil { + t.Fatalf("unexpected error clearing descriptor for repo a: %v", err) + } + + if _, err := repoA.Stat(ctx, dgst); err != distribution.ErrBlobUnknown { + t.Fatalf("expected repo a stat after clear to return ErrBlobUnknown, got: %v", err) + } + + // Simulate a peer repository repopulating the shared descriptor after a backend miss. + if err := repoB.SetDescriptor(ctx, dgst, desc); err != nil { + t.Fatalf("unexpected error warming descriptor for repo b: %v", err) + } + + if _, err := repoB.Stat(ctx, dgst); err != nil { + t.Fatalf("unexpected error statting descriptor for repo b after warm: %v", err) + } + if _, err := repoA.Stat(ctx, dgst); err != distribution.ErrBlobUnknown { + t.Fatalf("expected repo a stat after peer warm to return ErrBlobUnknown, got: %v", err) + } + + member, err := pool.SIsMember(ctx, repoA.repositoryBlobSetKey(repoA.repo), dgst.String()).Result() + if err != nil { + t.Fatalf("unexpected error checking repo a membership: %v", err) + } + if member { + t.Fatal("expected repo a membership to be removed during clear") + } + + exists, err := pool.Exists(ctx, repoA.blobDescriptorHashKey(dgst)).Result() + if err != nil { + t.Fatalf("unexpected error checking repo a descriptor hash: %v", err) + } + if exists != 0 { + t.Fatal("expected repo a descriptor hash to be removed during clear") + } +}
vendor/github.com/alicebob/miniredis/v2/CHANGELOG.md+328 −0 added@@ -0,0 +1,328 @@ +## Changelog + + +## v2.35.0 + +- add Lua redis.setresp({2,3}) +- embed gopher-json package +- fix XAUTOCLAIM (thanks @kgunning) +- fix writeXpending (thanks @gnpaone) +- fix BLMOVE TTL special case +- constants for key types @alyssaruth + + +### v2.34.0 + +- fix ZINTERSTORE where target is one of the source sets +- added support for ZRank and ZRevRank with score (thanks Jeff Howell) +- fix MEMORY subcommand casing (thanks @joshaber) +- use streamCmp in Xtrim (thanks @daniel-cohere) + + +### v2.33.0 + +- minimum Go version is now 1.17 +- fix integer overflow (thanks @wszaranski) +- test against the last BSD redis (7.2.4) +- ignore 'redis.set_repl()' call (thanks @TingluoHuang) +- various build fixes (thanks @wszaranski) +- add StartAddrTLS function (thanks @agriffaut) +- support for the NOMKSTREAM option for XADD (thanks @Jahaja) +- return empty array for SRANDMEMBER on nonexistent key (thanks @WKBae) + + +### v2.32.1 + +- support for SINTERCARD (thanks @s-barr-fetch) +- support for EXPIRETIME and PEXPIRETIME (thanks @wszaranski) +- fix GEO* units to be case insensitive + + +### v2.31.1 + +- support COUNT in SCAN and ZSCAN (thanks @BarakSilverfort) +- support for OBJECT IDLETIME (thanks @nerd2) +- support for HRANDFIELD (thanks @sejin-P) + + +### v2.31.0 + +- support for MEMORY USAGE (thanks @davidroman0O) +- test against Redis 7.2.0 +- support for CLIENT SETNAME/GETNAME (thanks @mr-karan) +- fix very small numbers (thanks @zsh1995) +- use the same float-to-string logic real Redis uses + + +### v2.30.5 + +- support SMISMEMBER (thanks @sandyharvie) + + +### v2.30.4 + +- fix ZADD LT/LG (thanks @sejin-P) +- fix COPY (thanks @jerargus) +- quicker SPOP + + +### v2.30.3 + +- fix lua error_reply (thanks @pkierski) +- fix use of blocking functions in lua +- support for ZMSCORE (thanks @lsgndln) +- lua cache (thanks @tonyhb) + + +### v2.30.2 + +- support MINID in XADD (thanks @nathan-cormier) +- support BLMOVE (thanks @sevein) +- fix COMMAND (thanks @pje) +- fix 'XREAD ... $' on a non-existing stream + + +### v2.30.1 + +- support SET NX GET special case + + +### v2.30.0 + +- implement redis 7.0.x (from 6.X). Main changes: + - test against 7.0.7 + - update error messages + - support nx|xx|gt|lt options in [P]EXPIRE[AT] + - update how deleted items are processed in pending queues in streams + + +### v2.23.1 + +- resolve $ to latest ID in XREAD (thanks @josh-hook) +- handle disconnect in blocking functions (thanks @jgirtakovskis) +- fix type conversion bug in redisToLua (thanks Sandy Harvie) +- BRPOP{LPUSH} timeout can be float since 6.0 + + +### v2.23.0 + +- basic INFO support (thanks @kirill-a-belov) +- support COUNT in SSCAN (thanks @Abdi-dd) +- test and support Go 1.19 +- support LPOS (thanks @ianstarz) +- support XPENDING, XGROUP {CREATECONSUMER,DESTROY,DELCONSUMER}, XINFO {CONSUMERS,GROUPS}, XCLAIM (thanks @sandyharvie) + + +### v2.22.0 + +- set miniredis.DumpMaxLineLen to get more Dump() info (thanks @afjoseph) +- fix invalid resposne of COMMAND (thanks @zsh1995) +- fix possibility to generate duplicate IDs in XADD (thanks @readams) +- adds support for XAUTOCLAIM min-idle parameter (thanks @readams) + + +### v2.21.0 + +- support for GETEX (thanks @dntj) +- support for GT and LT in ZADD (thanks @lsgndln) +- support for XAUTOCLAIM (thanks @randall-fulton) + + +### v2.20.0 + +- back to support Go >= 1.14 (thanks @ajatprabha and @marcind) + + +### v2.19.0 + +- support for TYPE in SCAN (thanks @0xDiddi) +- update BITPOS (thanks @dirkm) +- fix a lua redis.call() return value (thanks @mpetronic) +- update ZRANGE (thanks @valdemarpereira) + + +### v2.18.0 + +- support for ZUNION (thanks @propan) +- support for COPY (thanks @matiasinsaurralde and @rockitbaby) +- support for LMOVE (thanks @btwear) + + +### v2.17.0 + +- added miniredis.RunT(t) + + +### v2.16.1 + +- fix ZINTERSTORE with sets (thanks @lingjl2010 and @okhowang) +- fix exclusive ranges in XRANGE (thanks @joseotoro) + + +### v2.16.0 + +- simplify some code (thanks @zonque) +- support for EXAT/PXAT in SET +- support for XTRIM (thanks @joseotoro) +- support for ZRANDMEMBER +- support for redis.log() in lua (thanks @dirkm) + + +### v2.15.2 + +- Fix race condition in blocking code (thanks @zonque and @robx) +- XREAD accepts '$' as ID (thanks @bradengroom) + + +### v2.15.1 + +- EVAL should cache the script (thanks @guoshimin) + + +### v2.15.0 + +- target redis 6.2 and added new args to various commands +- support for all hyperlog commands (thanks @ilbaktin) +- support for GETDEL (thanks @wszaranski) + + +### v2.14.5 + +- added XPENDING +- support for BLOCK option in XREAD and XREADGROUP + + +### v2.14.4 + +- fix BITPOS error (thanks @xiaoyuzdy) +- small fixes for XREAD, XACK, and XDEL. Mostly error cases. +- fix empty EXEC return type (thanks @ashanbrown) +- fix XDEL (thanks @svakili and @yvesf) +- fix FLUSHALL for streams (thanks @svakili) + + +### v2.14.3 + +- fix problem where Lua code didn't set the selected DB +- update to redis 6.0.10 (thanks @lazappa) + + +### v2.14.2 + +- update LUA dependency +- deal with (p)unsubscribe when there are no channels + + +### v2.14.1 + +- mod tidy + + +### v2.14.0 + +- support for HELLO and the RESP3 protocol +- KEEPTTL in SET (thanks @johnpena) + + +### v2.13.3 + +- support Go 1.14 and 1.15 +- update the `Check...()` methods +- support for XREAD (thanks @pieterlexis) + + +### v2.13.2 + +- Use SAN instead of CN in self signed cert for testing (thanks @johejo) +- Travis CI now tests against the most recent two versions of Go (thanks @johejo) +- changed unit and integration tests to compare raw payloads, not parsed payloads +- remove "redigo" dependency + + +### v2.13.1 + +- added HSTRLEN +- minimal support for ACL users in AUTH + + +### v2.13.0 + +- added RunTLS(...) +- added SetError(...) + + +### v2.12.0 + +- redis 6 +- Lua json update (thanks @gsmith85) +- CLUSTER commands (thanks @kratisto) +- fix TOUCH +- fix a shutdown race condition + + +### v2.11.4 + +- ZUNIONSTORE now supports standard set types (thanks @wshirey) + + +### v2.11.3 + +- support for TOUCH (thanks @cleroux) +- support for cluster and stream commands (thanks @kak-tus) + + +### v2.11.2 + +- make sure Lua code is executed concurrently +- add command GEORADIUSBYMEMBER (thanks @kyeett) + + +### v2.11.1 + +- globals protection for Lua code (thanks @vk-outreach) +- HSET update (thanks @carlgreen) +- fix BLPOP block on shutdown (thanks @Asalle) + + +### v2.11.0 + +- added XRANGE/XREVRANGE, XADD, and XLEN (thanks @skateinmars) +- added GEODIST +- improved precision for geohashes, closer to what real redis does +- use 128bit floats internally for INCRBYFLOAT and related (thanks @timnd) + + +### v2.10.1 + +- added m.Server() + + +### v2.10.0 + +- added UNLINK +- fix DEL zero-argument case +- cleanup some direct access commands +- added GEOADD, GEOPOS, GEORADIUS, and GEORADIUS_RO + + +### v2.9.1 + +- fix issue with ZRANGEBYLEX +- fix issue with BRPOPLPUSH and direct access + + +### v2.9.0 + +- proper versioned import of github.com/gomodule/redigo (thanks @yfei1) +- fix messages generated by PSUBSCRIBE +- optional internal seed (thanks @zikaeroh) + + +### v2.8.0 + +Proper `v2` in go.mod. + + +### older + +See https://github.com/alicebob/miniredis/releases for the full changelog
vendor/github.com/alicebob/miniredis/v2/check.go+63 −0 added@@ -0,0 +1,63 @@ +package miniredis + +import ( + "reflect" + "sort" +) + +// T is implemented by Testing.T +type T interface { + Helper() + Errorf(string, ...interface{}) +} + +// CheckGet does not call Errorf() iff there is a string key with the +// expected value. Normal use case is `m.CheckGet(t, "username", "theking")`. +func (m *Miniredis) CheckGet(t T, key, expected string) { + t.Helper() + + found, err := m.Get(key) + if err != nil { + t.Errorf("GET error, key %#v: %v", key, err) + return + } + if found != expected { + t.Errorf("GET error, key %#v: Expected %#v, got %#v", key, expected, found) + return + } +} + +// CheckList does not call Errorf() iff there is a list key with the +// expected values. +// Normal use case is `m.CheckGet(t, "favorite_colors", "red", "green", "infrared")`. +func (m *Miniredis) CheckList(t T, key string, expected ...string) { + t.Helper() + + found, err := m.List(key) + if err != nil { + t.Errorf("List error, key %#v: %v", key, err) + return + } + if !reflect.DeepEqual(expected, found) { + t.Errorf("List error, key %#v: Expected %#v, got %#v", key, expected, found) + return + } +} + +// CheckSet does not call Errorf() iff there is a set key with the +// expected values. +// Normal use case is `m.CheckSet(t, "visited", "Rome", "Stockholm", "Dublin")`. +func (m *Miniredis) CheckSet(t T, key string, expected ...string) { + t.Helper() + + found, err := m.Members(key) + if err != nil { + t.Errorf("Set error, key %#v: %v", key, err) + return + } + sort.Strings(expected) + if !reflect.DeepEqual(expected, found) { + t.Errorf("Set error, key %#v: Expected %#v, got %#v", key, expected, found) + return + } +}
vendor/github.com/alicebob/miniredis/v2/cmd_client.go+68 −0 added@@ -0,0 +1,68 @@ +package miniredis + +import ( + "fmt" + "strings" + + "github.com/alicebob/miniredis/v2/server" +) + +// commandsClient handles client operations. +func commandsClient(m *Miniredis) { + m.srv.Register("CLIENT", m.cmdClient) +} + +// CLIENT +func (m *Miniredis) cmdClient(c *server.Peer, cmd string, args []string) { + if len(args) == 0 { + setDirty(c) + c.WriteError("ERR wrong number of arguments for 'client' command") + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + switch cmd := strings.ToUpper(args[0]); cmd { + case "SETNAME": + m.cmdClientSetName(c, args[1:]) + case "GETNAME": + m.cmdClientGetName(c, args[1:]) + default: + setDirty(c) + c.WriteError(fmt.Sprintf("ERR unknown subcommand '%s'. Try CLIENT HELP.", cmd)) + } + }) +} + +// CLIENT SETNAME +func (m *Miniredis) cmdClientSetName(c *server.Peer, args []string) { + if len(args) != 1 { + setDirty(c) + c.WriteError("ERR wrong number of arguments for 'client setname' command") + return + } + + name := args[0] + if strings.ContainsAny(name, " \n") { + setDirty(c) + c.WriteError("ERR Client names cannot contain spaces, newlines or special characters.") + return + + } + c.ClientName = name + c.WriteOK() +} + +// CLIENT GETNAME +func (m *Miniredis) cmdClientGetName(c *server.Peer, args []string) { + if len(args) > 0 { + setDirty(c) + c.WriteError("ERR wrong number of arguments for 'client getname' command") + return + } + + if c.ClientName == "" { + c.WriteNull() + } else { + c.WriteBulk(c.ClientName) + } +}
vendor/github.com/alicebob/miniredis/v2/cmd_cluster.go+67 −0 added@@ -0,0 +1,67 @@ +// Commands from https://redis.io/commands#cluster + +package miniredis + +import ( + "fmt" + "strings" + + "github.com/alicebob/miniredis/v2/server" +) + +// commandsCluster handles some cluster operations. +func commandsCluster(m *Miniredis) { + m.srv.Register("CLUSTER", m.cmdCluster) +} + +func (m *Miniredis) cmdCluster(c *server.Peer, cmd string, args []string) { + if !m.handleAuth(c) { + return + } + + if len(args) < 1 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + switch strings.ToUpper(args[0]) { + case "SLOTS": + m.cmdClusterSlots(c, cmd, args) + case "KEYSLOT": + m.cmdClusterKeySlot(c, cmd, args) + case "NODES": + m.cmdClusterNodes(c, cmd, args) + default: + setDirty(c) + c.WriteError(fmt.Sprintf("ERR 'CLUSTER %s' not supported", strings.Join(args, " "))) + return + } +} + +// CLUSTER SLOTS +func (m *Miniredis) cmdClusterSlots(c *server.Peer, cmd string, args []string) { + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + c.WriteLen(1) + c.WriteLen(3) + c.WriteInt(0) + c.WriteInt(16383) + c.WriteLen(3) + c.WriteBulk(m.srv.Addr().IP.String()) + c.WriteInt(m.srv.Addr().Port) + c.WriteBulk("09dbe9720cda62f7865eabc5fd8857c5d2678366") + }) +} + +// CLUSTER KEYSLOT +func (m *Miniredis) cmdClusterKeySlot(c *server.Peer, cmd string, args []string) { + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + c.WriteInt(163) + }) +} + +// CLUSTER NODES +func (m *Miniredis) cmdClusterNodes(c *server.Peer, cmd string, args []string) { + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + c.WriteBulk("e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca 127.0.0.1:7000@7000 myself,master - 0 0 1 connected 0-16383") + }) +}
vendor/github.com/alicebob/miniredis/v2/cmd_command.go+14 −0 added@@ -0,0 +1,14 @@ +// Command 'COMMAND' from https://redis.io/commands#server + +package miniredis + +import "github.com/alicebob/miniredis/v2/server" + +func (m *Miniredis) cmdCommand(c *server.Peer, cmd string, args []string) { + // Got from redis 5.0.7 with + // echo 'COMMAND' | nc redis_addr redis_port + + res := "*200\r\n*6\r\n$12\r\nhincrbyfloat\r\n:4\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$10\r\nxreadgroup\r\n:-7\r\n*3\r\n+write\r\n+noscript\r\n+movablekeys\r\n:1\r\n:1\r\n:1\r\n*6\r\n$10\r\nsdiffstore\r\n:-3\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$8\r\nlastsave\r\n:1\r\n*2\r\n+random\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\nsetnx\r\n:3\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$8\r\nbzpopmax\r\n:-3\r\n*3\r\n+write\r\n+noscript\r\n+fast\r\n:1\r\n:-2\r\n:1\r\n*6\r\n$12\r\npunsubscribe\r\n:-1\r\n*4\r\n+pubsub\r\n+noscript\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\nxack\r\n:-4\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$10\r\npfselftest\r\n:1\r\n*1\r\n+admin\r\n:0\r\n:0\r\n:0\r\n*6\r\n$6\r\nsubstr\r\n:4\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$8\r\nsmembers\r\n:2\r\n*2\r\n+readonly\r\n+sort_for_script\r\n:1\r\n:1\r\n:1\r\n*6\r\n$11\r\nunsubscribe\r\n:-1\r\n*4\r\n+pubsub\r\n+noscript\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$11\r\nzinterstore\r\n:-4\r\n*3\r\n+write\r\n+denyoom\r\n+movablekeys\r\n:0\r\n:0\r\n:0\r\n*6\r\n$6\r\nstrlen\r\n:2\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\npfmerge\r\n:-2\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$9\r\nrandomkey\r\n:1\r\n*2\r\n+readonly\r\n+random\r\n:0\r\n:0\r\n:0\r\n*6\r\n$6\r\nlolwut\r\n:-1\r\n*1\r\n+readonly\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\nrpop\r\n:2\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nhkeys\r\n:2\r\n*2\r\n+readonly\r\n+sort_for_script\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nclient\r\n:-2\r\n*2\r\n+admin\r\n+noscript\r\n:0\r\n:0\r\n:0\r\n*6\r\n$6\r\nmodule\r\n:-2\r\n*2\r\n+admin\r\n+noscript\r\n:0\r\n:0\r\n:0\r\n*6\r\n$7\r\nslowlog\r\n:-2\r\n*2\r\n+admin\r\n+random\r\n:0\r\n:0\r\n:0\r\n*6\r\n$7\r\ngeohash\r\n:-2\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nlrange\r\n:4\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nping\r\n:-1\r\n*2\r\n+stale\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$8\r\nbitcount\r\n:-2\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\npubsub\r\n:-2\r\n*4\r\n+pubsub\r\n+random\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\nrole\r\n:1\r\n*3\r\n+noscript\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\nhget\r\n:3\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nobject\r\n:-2\r\n*2\r\n+readonly\r\n+random\r\n:2\r\n:2\r\n:1\r\n*6\r\n$9\r\nzrevrange\r\n:-4\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\nhincrby\r\n:4\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$9\r\nzlexcount\r\n:4\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nscard\r\n:2\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nappend\r\n:3\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\nhstrlen\r\n:3\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nconfig\r\n:-2\r\n*4\r\n+admin\r\n+noscript\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\nhset\r\n:-4\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$16\r\nzrevrangebyscore\r\n:-4\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nincr\r\n:2\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nsetbit\r\n:4\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:1\r\n:1\r\n*6\r\n$9\r\nrpoplpush\r\n:3\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:2\r\n:1\r\n*6\r\n$6\r\nxclaim\r\n:-6\r\n*3\r\n+write\r\n+random\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$11\r\nsinterstore\r\n:-3\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$7\r\npublish\r\n:3\r\n*4\r\n+pubsub\r\n+loading\r\n+stale\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\nhscan\r\n:-3\r\n*2\r\n+readonly\r\n+random\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nmulti\r\n:1\r\n*2\r\n+noscript\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$3\r\nset\r\n:-3\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nlpushx\r\n:-3\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$16\r\nzremrangebyscore\r\n:4\r\n*1\r\n+write\r\n:1\r\n:1\r\n:1\r\n*6\r\n$9\r\npexpireat\r\n:3\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nhdel\r\n:-3\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$12\r\nbgrewriteaof\r\n:1\r\n*2\r\n+admin\r\n+noscript\r\n:0\r\n:0\r\n:0\r\n*6\r\n$7\r\nmigrate\r\n:-6\r\n*3\r\n+write\r\n+random\r\n+movablekeys\r\n:0\r\n:0\r\n:0\r\n*6\r\n$9\r\nreplicaof\r\n:3\r\n*3\r\n+admin\r\n+noscript\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\ntouch\r\n:-2\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nxsetid\r\n:3\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nbitop\r\n:-4\r\n*2\r\n+write\r\n+denyoom\r\n:2\r\n:-1\r\n:1\r\n*6\r\n$6\r\nswapdb\r\n:3\r\n*2\r\n+write\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\nsdiff\r\n:-2\r\n*2\r\n+readonly\r\n+sort_for_script\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$6\r\nlindex\r\n:3\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nwait\r\n:3\r\n*1\r\n+noscript\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\nlrem\r\n:4\r\n*1\r\n+write\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nhsetnx\r\n:4\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$8\r\ngetrange\r\n:4\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nhlen\r\n:2\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\npost\r\n:-1\r\n*2\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$9\r\nsismember\r\n:3\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\nunwatch\r\n:1\r\n*2\r\n+noscript\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\nlpush\r\n:-3\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nscan\r\n:-2\r\n*2\r\n+readonly\r\n+random\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\nsmove\r\n:4\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:2\r\n:1\r\n*6\r\n$7\r\ncluster\r\n:-2\r\n*1\r\n+admin\r\n:0\r\n:0\r\n:0\r\n*6\r\n$6\r\nbgsave\r\n:-1\r\n*2\r\n+admin\r\n+noscript\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\ndump\r\n:2\r\n*2\r\n+readonly\r\n+random\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\nlatency\r\n:-2\r\n*4\r\n+admin\r\n+noscript\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$8\r\nbzpopmin\r\n:-3\r\n*3\r\n+write\r\n+noscript\r\n+fast\r\n:1\r\n:-2\r\n:1\r\n*6\r\n$6\r\ngetbit\r\n:3\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\nhgetall\r\n:2\r\n*2\r\n+readonly\r\n+random\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nrename\r\n:3\r\n*1\r\n+write\r\n:1\r\n:2\r\n:1\r\n*6\r\n$9\r\nsubscribe\r\n:-2\r\n*4\r\n+pubsub\r\n+noscript\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\nxdel\r\n:-3\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$15\r\nzremrangebyrank\r\n:4\r\n*1\r\n+write\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\ntype\r\n:2\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nscript\r\n:-2\r\n*1\r\n+noscript\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\nhmset\r\n:-4\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nsunion\r\n:-2\r\n*2\r\n+readonly\r\n+sort_for_script\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$4\r\nmget\r\n:-2\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$10\r\nbrpoplpush\r\n:4\r\n*3\r\n+write\r\n+denyoom\r\n+noscript\r\n:1\r\n:2\r\n:1\r\n*6\r\n$6\r\ngeoadd\r\n:-5\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\ndecrby\r\n:3\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\necho\r\n:2\r\n*1\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$6\r\ndbsize\r\n:1\r\n*2\r\n+readonly\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\nzcard\r\n:2\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nselect\r\n:2\r\n*2\r\n+loading\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\nsadd\r\n:-3\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nhost:\r\n:-1\r\n*2\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\nsscan\r\n:-3\r\n*2\r\n+readonly\r\n+random\r\n:1\r\n:1\r\n:1\r\n*6\r\n$12\r\ngeoradius_ro\r\n:-6\r\n*2\r\n+readonly\r\n+movablekeys\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\nmonitor\r\n:1\r\n*2\r\n+admin\r\n+noscript\r\n:0\r\n:0\r\n:0\r\n*6\r\n$14\r\nzremrangebylex\r\n:4\r\n*1\r\n+write\r\n:1\r\n:1\r\n:1\r\n*6\r\n$11\r\nsunionstore\r\n:-3\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$5\r\nzscan\r\n:-3\r\n*2\r\n+readonly\r\n+random\r\n:1\r\n:1\r\n:1\r\n*6\r\n$9\r\nreadwrite\r\n:1\r\n*1\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$6\r\nxgroup\r\n:-2\r\n*2\r\n+write\r\n+denyoom\r\n:2\r\n:2\r\n:1\r\n*6\r\n$5\r\nsetex\r\n:4\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nsave\r\n:1\r\n*2\r\n+admin\r\n+noscript\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\nhvals\r\n:2\r\n*2\r\n+readonly\r\n+sort_for_script\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nwatch\r\n:-2\r\n*2\r\n+noscript\r\n+fast\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$7\r\nhexists\r\n:3\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\ninfo\r\n:-1\r\n*3\r\n+random\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\npsync\r\n:3\r\n*3\r\n+readonly\r\n+admin\r\n+noscript\r\n:0\r\n:0\r\n:0\r\n*6\r\n$11\r\nzrangebylex\r\n:-4\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nzadd\r\n:-4\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nxlen\r\n:2\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nauth\r\n:2\r\n*4\r\n+noscript\r\n+loading\r\n+stale\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\nsrem\r\n:-3\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$9\r\ngeoradius\r\n:-6\r\n*2\r\n+write\r\n+movablekeys\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nexec\r\n:1\r\n*2\r\n+noscript\r\n+skip_monitor\r\n:0\r\n:0\r\n:0\r\n*6\r\n$7\r\npfcount\r\n:-2\r\n*1\r\n+readonly\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$7\r\nzpopmin\r\n:-2\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nmove\r\n:3\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nxtrim\r\n:-2\r\n*3\r\n+write\r\n+random\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nasking\r\n:1\r\n*1\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\npttl\r\n:2\r\n*3\r\n+readonly\r\n+random\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$11\r\nsrandmember\r\n:-2\r\n*2\r\n+readonly\r\n+random\r\n:1\r\n:1\r\n:1\r\n*6\r\n$8\r\nflushall\r\n:-1\r\n*1\r\n+write\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\nsort\r\n:-2\r\n*3\r\n+write\r\n+denyoom\r\n+movablekeys\r\n:1\r\n:1\r\n:1\r\n*6\r\n$3\r\ndel\r\n:-2\r\n*1\r\n+write\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$14\r\nrestore-asking\r\n:-4\r\n*3\r\n+write\r\n+denyoom\r\n+asking\r\n:1\r\n:1\r\n:1\r\n*6\r\n$10\r\npsubscribe\r\n:-2\r\n*4\r\n+pubsub\r\n+noscript\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\ndecr\r\n:2\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nincrby\r\n:3\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$14\r\nzrevrangebylex\r\n:-4\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$8\r\nbitfield\r\n:-2\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nexists\r\n:-2\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$8\r\nreplconf\r\n:-1\r\n*4\r\n+admin\r\n+noscript\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$7\r\nzincrby\r\n:4\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nblpop\r\n:-3\r\n*2\r\n+write\r\n+noscript\r\n:1\r\n:-2\r\n:1\r\n*6\r\n$4\r\nlpop\r\n:2\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$3\r\nttl\r\n:2\r\n*3\r\n+readonly\r\n+random\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nxread\r\n:-4\r\n*3\r\n+readonly\r\n+noscript\r\n+movablekeys\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nrpush\r\n:-3\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$8\r\nzrevrank\r\n:3\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$11\r\nincrbyfloat\r\n:3\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nbrpop\r\n:-3\r\n*2\r\n+write\r\n+noscript\r\n:1\r\n:-2\r\n:1\r\n*6\r\n$4\r\nxadd\r\n:-5\r\n*4\r\n+write\r\n+denyoom\r\n+random\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$8\r\nsetrange\r\n:4\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:1\r\n:1\r\n*6\r\n$17\r\ngeoradiusbymember\r\n:-5\r\n*2\r\n+write\r\n+movablekeys\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nunlink\r\n:-2\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$8\r\nexpireat\r\n:3\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\ndebug\r\n:-2\r\n*2\r\n+admin\r\n+noscript\r\n:0\r\n:0\r\n:0\r\n*6\r\n$20\r\ngeoradiusbymember_ro\r\n:-5\r\n*2\r\n+readonly\r\n+movablekeys\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nlset\r\n:4\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nzscore\r\n:3\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nllen\r\n:2\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\ntime\r\n:1\r\n*2\r\n+random\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$8\r\nshutdown\r\n:-1\r\n*4\r\n+admin\r\n+noscript\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$7\r\nevalsha\r\n:-3\r\n*2\r\n+noscript\r\n+movablekeys\r\n:0\r\n:0\r\n:0\r\n*6\r\n$6\r\nzcount\r\n:4\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nmemory\r\n:-2\r\n*2\r\n+readonly\r\n+random\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\nxinfo\r\n:-2\r\n*2\r\n+readonly\r\n+random\r\n:2\r\n:2\r\n:1\r\n*6\r\n$8\r\nxpending\r\n:-3\r\n*2\r\n+readonly\r\n+random\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\neval\r\n:-3\r\n*2\r\n+noscript\r\n+movablekeys\r\n:0\r\n:0\r\n:0\r\n*6\r\n$6\r\nxrange\r\n:-4\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\nrestore\r\n:-4\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\nzpopmax\r\n:-2\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nmset\r\n:-3\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:-1\r\n:2\r\n*6\r\n$4\r\nspop\r\n:-2\r\n*3\r\n+write\r\n+random\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nltrim\r\n:4\r\n*1\r\n+write\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nzrank\r\n:3\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$9\r\nxrevrange\r\n:-4\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$3\r\nget\r\n:2\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\nflushdb\r\n:-1\r\n*1\r\n+write\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\nhmget\r\n:-3\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nmsetnx\r\n:-3\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:-1\r\n:2\r\n*6\r\n$7\r\npersist\r\n:2\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$11\r\nzunionstore\r\n:-4\r\n*3\r\n+write\r\n+denyoom\r\n+movablekeys\r\n:0\r\n:0\r\n:0\r\n*6\r\n$7\r\ncommand\r\n:0\r\n*3\r\n+random\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$8\r\nrenamenx\r\n:3\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:2\r\n:1\r\n*6\r\n$6\r\nzrange\r\n:-4\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\npexpire\r\n:3\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nkeys\r\n:2\r\n*2\r\n+readonly\r\n+sort_for_script\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\nzrem\r\n:-3\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\npfadd\r\n:-2\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\npsetex\r\n:4\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:1\r\n:1\r\n*6\r\n$13\r\nzrangebyscore\r\n:-4\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nsync\r\n:1\r\n*3\r\n+readonly\r\n+admin\r\n+noscript\r\n:0\r\n:0\r\n:0\r\n*6\r\n$7\r\npfdebug\r\n:-3\r\n*1\r\n+write\r\n:0\r\n:0\r\n:0\r\n*6\r\n$7\r\ndiscard\r\n:1\r\n*2\r\n+noscript\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$8\r\nreadonly\r\n:1\r\n*1\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$7\r\ngeodist\r\n:-4\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\ngeopos\r\n:-2\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nbitpos\r\n:-3\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nsinter\r\n:-2\r\n*2\r\n+readonly\r\n+sort_for_script\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$6\r\ngetset\r\n:3\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\nslaveof\r\n:3\r\n*3\r\n+admin\r\n+noscript\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$6\r\nrpushx\r\n:-3\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\nlinsert\r\n:5\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nexpire\r\n:3\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n" + + c.WriteRaw(res) +}
vendor/github.com/alicebob/miniredis/v2/cmd_connection.go+285 −0 added@@ -0,0 +1,285 @@ +// Commands from https://redis.io/commands#connection + +package miniredis + +import ( + "fmt" + "strings" + + "github.com/alicebob/miniredis/v2/server" +) + +func commandsConnection(m *Miniredis) { + m.srv.Register("AUTH", m.cmdAuth) + m.srv.Register("ECHO", m.cmdEcho) + m.srv.Register("HELLO", m.cmdHello) + m.srv.Register("PING", m.cmdPing) + m.srv.Register("QUIT", m.cmdQuit) + m.srv.Register("SELECT", m.cmdSelect) + m.srv.Register("SWAPDB", m.cmdSwapdb) +} + +// PING +func (m *Miniredis) cmdPing(c *server.Peer, cmd string, args []string) { + if !m.handleAuth(c) { + return + } + + if len(args) > 1 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + + payload := "" + if len(args) > 0 { + payload = args[0] + } + + // PING is allowed in subscribed state + if sub := getCtx(c).subscriber; sub != nil { + c.Block(func(c *server.Writer) { + c.WriteLen(2) + c.WriteBulk("pong") + c.WriteBulk(payload) + }) + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + if payload == "" { + c.WriteInline("PONG") + return + } + c.WriteBulk(payload) + }) +} + +// AUTH +func (m *Miniredis) cmdAuth(c *server.Peer, cmd string, args []string) { + if len(args) < 1 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + + if len(args) > 2 { + c.WriteError(msgSyntaxError) + return + } + if m.checkPubsub(c, cmd) { + return + } + ctx := getCtx(c) + if ctx.nested { + c.WriteError(msgNotFromScripts(ctx.nestedSHA)) + return + } + + var opts = struct { + username string + password string + }{ + username: "default", + password: args[0], + } + if len(args) == 2 { + opts.username, opts.password = args[0], args[1] + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + if len(m.passwords) == 0 && opts.username == "default" { + c.WriteError("ERR AUTH <password> called without any password configured for the default user. Are you sure your configuration is correct?") + return + } + setPW, ok := m.passwords[opts.username] + if !ok { + c.WriteError("WRONGPASS invalid username-password pair") + return + } + if setPW != opts.password { + c.WriteError("WRONGPASS invalid username-password pair") + return + } + + ctx.authenticated = true + c.WriteOK() + }) +} + +// HELLO +func (m *Miniredis) cmdHello(c *server.Peer, cmd string, args []string) { + if len(args) < 1 { + c.WriteError(errWrongNumber(cmd)) + return + } + + var opts struct { + version int + username string + password string + } + + if ok := optIntErr(c, args[0], &opts.version, "ERR Protocol version is not an integer or out of range"); !ok { + return + } + args = args[1:] + + switch opts.version { + case 2, 3: + default: + c.WriteError("NOPROTO unsupported protocol version") + return + } + + var checkAuth bool + for len(args) > 0 { + switch strings.ToUpper(args[0]) { + case "AUTH": + if len(args) < 3 { + c.WriteError(fmt.Sprintf("ERR Syntax error in HELLO option '%s'", args[0])) + return + } + opts.username, opts.password, args = args[1], args[2], args[3:] + checkAuth = true + case "SETNAME": + if len(args) < 2 { + c.WriteError(fmt.Sprintf("ERR Syntax error in HELLO option '%s'", args[0])) + return + } + _, args = args[1], args[2:] + default: + c.WriteError(fmt.Sprintf("ERR Syntax error in HELLO option '%s'", args[0])) + return + } + } + + if len(m.passwords) == 0 && opts.username == "default" { + // redis ignores legacy "AUTH" if it's not enabled. + checkAuth = false + } + if checkAuth { + setPW, ok := m.passwords[opts.username] + if !ok { + c.WriteError("WRONGPASS invalid username-password pair") + return + } + if setPW != opts.password { + c.WriteError("WRONGPASS invalid username-password pair") + return + } + getCtx(c).authenticated = true + } + + c.Resp3 = opts.version == 3 + + c.WriteMapLen(7) + c.WriteBulk("server") + c.WriteBulk("miniredis") + c.WriteBulk("version") + c.WriteBulk("6.0.5") + c.WriteBulk("proto") + c.WriteInt(opts.version) + c.WriteBulk("id") + c.WriteInt(42) + c.WriteBulk("mode") + c.WriteBulk("standalone") + c.WriteBulk("role") + c.WriteBulk("master") + c.WriteBulk("modules") + c.WriteLen(0) +} + +// ECHO +func (m *Miniredis) cmdEcho(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 + } + + msg := args[0] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + c.WriteBulk(msg) + }) +} + +// SELECT +func (m *Miniredis) cmdSelect(c *server.Peer, cmd string, args []string) { + if len(args) != 1 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.isValidCMD(c, cmd) { + return + } + + var opts struct { + id int + } + if ok := optInt(c, args[0], &opts.id); !ok { + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + if opts.id < 0 { + c.WriteError(msgDBIndexOutOfRange) + setDirty(c) + return + } + + ctx.selectedDB = opts.id + c.WriteOK() + }) +} + +// SWAPDB +func (m *Miniredis) cmdSwapdb(c *server.Peer, cmd string, args []string) { + if len(args) != 2 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + + var opts struct { + id1 int + id2 int + } + + if ok := optIntErr(c, args[0], &opts.id1, "ERR invalid first DB index"); !ok { + return + } + if ok := optIntErr(c, args[1], &opts.id2, "ERR invalid second DB index"); !ok { + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + if opts.id1 < 0 || opts.id2 < 0 { + c.WriteError(msgDBIndexOutOfRange) + setDirty(c) + return + } + + m.swapDB(opts.id1, opts.id2) + + c.WriteOK() + }) +} + +// QUIT +func (m *Miniredis) cmdQuit(c *server.Peer, cmd string, args []string) { + // QUIT isn't transactionfied and accepts any arguments. + c.WriteOK() + c.Close() +}
vendor/github.com/alicebob/miniredis/v2/cmd_generic.go+813 −0 added@@ -0,0 +1,813 @@ +// Commands from https://redis.io/commands#generic + +package miniredis + +import ( + "errors" + "fmt" + "sort" + "strconv" + "strings" + "time" + + "github.com/alicebob/miniredis/v2/server" +) + +const ( + // expiretimeReplyNoExpiration is return value for EXPIRETIME and PEXPIRETIME if the key exists but has no associated expiration time + expiretimeReplyNoExpiration = -1 + // expiretimeReplyMissingKey is return value for EXPIRETIME and PEXPIRETIME if the key does not exist + expiretimeReplyMissingKey = -2 +) + +func inSeconds(t time.Time) int { + return int(t.Unix()) +} + +func inMilliSeconds(t time.Time) int { + return int(t.UnixMilli()) +} + +// commandsGeneric handles EXPIRE, TTL, PERSIST, &c. +func commandsGeneric(m *Miniredis) { + m.srv.Register("COPY", m.cmdCopy) + m.srv.Register("DEL", m.cmdDel) + // DUMP + m.srv.Register("EXISTS", m.cmdExists) + m.srv.Register("EXPIRE", makeCmdExpire(m, false, time.Second)) + m.srv.Register("EXPIREAT", makeCmdExpire(m, true, time.Second)) + m.srv.Register("EXPIRETIME", m.makeCmdExpireTime(inSeconds)) + m.srv.Register("PEXPIRETIME", m.makeCmdExpireTime(inMilliSeconds)) + m.srv.Register("KEYS", m.cmdKeys) + // MIGRATE + m.srv.Register("MOVE", m.cmdMove) + // OBJECT + m.srv.Register("PERSIST", m.cmdPersist) + m.srv.Register("PEXPIRE", makeCmdExpire(m, false, time.Millisecond)) + m.srv.Register("PEXPIREAT", makeCmdExpire(m, true, time.Millisecond)) + m.srv.Register("PTTL", m.cmdPTTL) + m.srv.Register("RANDOMKEY", m.cmdRandomkey) + m.srv.Register("RENAME", m.cmdRename) + m.srv.Register("RENAMENX", m.cmdRenamenx) + // RESTORE + m.srv.Register("TOUCH", m.cmdTouch) + m.srv.Register("TTL", m.cmdTTL) + m.srv.Register("TYPE", m.cmdType) + m.srv.Register("SCAN", m.cmdScan) + // SORT + m.srv.Register("UNLINK", m.cmdDel) +} + +type expireOpts struct { + key string + value int + nx bool + xx bool + gt bool + lt bool +} + +func expireParse(cmd string, args []string) (*expireOpts, error) { + var opts expireOpts + + opts.key = args[0] + if err := optIntSimple(args[1], &opts.value); err != nil { + return nil, err + } + args = args[2:] + for len(args) > 0 { + switch strings.ToLower(args[0]) { + case "nx": + opts.nx = true + case "xx": + opts.xx = true + case "gt": + opts.gt = true + case "lt": + opts.lt = true + default: + return nil, fmt.Errorf("ERR Unsupported option %s", args[0]) + } + args = args[1:] + } + if opts.gt && opts.lt { + return nil, errors.New("ERR GT and LT options at the same time are not compatible") + } + if opts.nx && (opts.xx || opts.gt || opts.lt) { + return nil, errors.New("ERR NX and XX, GT or LT options at the same time are not compatible") + } + return &opts, nil +} + +// generic expire command for EXPIRE, PEXPIRE, EXPIREAT, PEXPIREAT +// d is the time unit. If unix is set it'll be seen as a unixtimestamp and +// converted to a duration. +func makeCmdExpire(m *Miniredis, unix bool, d time.Duration) func(*server.Peer, string, []string) { + return func(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 + } + + opts, err := expireParse(cmd, args) + if err != nil { + setDirty(c) + c.WriteError(err.Error()) + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + // Key must be present. + if _, ok := db.keys[opts.key]; !ok { + c.WriteInt(0) + return + } + + oldTTL, ok := db.ttl[opts.key] + + var newTTL time.Duration + if unix { + newTTL = m.at(opts.value, d) + } else { + newTTL = time.Duration(opts.value) * d + } + + // > NX -- Set expiry only when the key has no expiry + if opts.nx && ok { + c.WriteInt(0) + return + } + // > XX -- Set expiry only when the key has an existing expiry + if opts.xx && !ok { + c.WriteInt(0) + return + } + // > GT -- Set expiry only when the new expiry is greater than current one + // (no exp == infinity) + if opts.gt && (!ok || newTTL <= oldTTL) { + c.WriteInt(0) + return + } + // > LT -- Set expiry only when the new expiry is less than current one + if opts.lt && ok && newTTL > oldTTL { + c.WriteInt(0) + return + } + db.ttl[opts.key] = newTTL + db.incr(opts.key) + db.checkTTL(opts.key) + c.WriteInt(1) + }) + } +} + +// makeCmdExpireTime creates server command function that returns the absolute Unix timestamp (since January 1, 1970) +// at which the given key will expire, in unit selected by time result strategy (e.g. seconds, milliseconds). +// For more information see redis documentation for [expiretime] and [pexpiretime]. +// +// [expiretime]: https://redis.io/commands/expiretime/ +// [pexpiretime]: https://redis.io/commands/pexpiretime/ +func (m *Miniredis) makeCmdExpireTime(timeResultStrategy func(time.Time) int) server.Cmd { + return func(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 _, ok := db.keys[key]; !ok { + c.WriteInt(expiretimeReplyMissingKey) + return + } + + ttl, ok := db.ttl[key] + if !ok { + c.WriteInt(expiretimeReplyNoExpiration) + return + } + + c.WriteInt(timeResultStrategy(m.effectiveNow().Add(ttl))) + }) + } +} + +// TOUCH +func (m *Miniredis) cmdTouch(c *server.Peer, cmd string, args []string) { + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + if len(args) == 0 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + count := 0 + for _, key := range args { + if db.exists(key) { + count++ + } + } + c.WriteInt(count) + }) +} + +// TTL +func (m *Miniredis) cmdTTL(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 _, ok := db.keys[key]; !ok { + // No such key + c.WriteInt(-2) + return + } + + v, ok := db.ttl[key] + if !ok { + // no expire value + c.WriteInt(-1) + return + } + c.WriteInt(int(v.Seconds())) + }) +} + +// PTTL +func (m *Miniredis) cmdPTTL(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 _, ok := db.keys[key]; !ok { + // no such key + c.WriteInt(-2) + return + } + + v, ok := db.ttl[key] + if !ok { + // no expire value + c.WriteInt(-1) + return + } + c.WriteInt(int(v.Nanoseconds() / 1000000)) + }) +} + +// PERSIST +func (m *Miniredis) cmdPersist(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 _, ok := db.keys[key]; !ok { + // no such key + c.WriteInt(0) + return + } + + if _, ok := db.ttl[key]; !ok { + // no expire value + c.WriteInt(0) + return + } + delete(db.ttl, key) + db.incr(key) + c.WriteInt(1) + }) +} + +// DEL and UNLINK +func (m *Miniredis) cmdDel(c *server.Peer, cmd string, args []string) { + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + if len(args) == 0 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + count := 0 + for _, key := range args { + if db.exists(key) { + count++ + } + db.del(key, true) // delete expire + } + c.WriteInt(count) + }) +} + +// TYPE +func (m *Miniredis) cmdType(c *server.Peer, cmd string, args []string) { + if len(args) != 1 { + setDirty(c) + c.WriteError("usage error") + 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) + + t, ok := db.keys[key] + if !ok { + c.WriteInline("none") + return + } + + c.WriteInline(t) + }) +} + +// EXISTS +func (m *Miniredis) cmdExists(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) + + found := 0 + for _, k := range args { + if db.exists(k) { + found++ + } + } + c.WriteInt(found) + }) +} + +// MOVE +func (m *Miniredis) cmdMove(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 + targetDB int + } + + opts.key = args[0] + opts.targetDB, _ = strconv.Atoi(args[1]) + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + if ctx.selectedDB == opts.targetDB { + c.WriteError("ERR source and destination objects are the same") + return + } + db := m.db(ctx.selectedDB) + targetDB := m.db(opts.targetDB) + + if !db.move(opts.key, targetDB) { + c.WriteInt(0) + return + } + c.WriteInt(1) + }) +} + +// KEYS +func (m *Miniredis) cmdKeys(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) + + keys, _ := matchKeys(db.allKeys(), key) + c.WriteLen(len(keys)) + for _, s := range keys { + c.WriteBulk(s) + } + }) +} + +// RANDOMKEY +func (m *Miniredis) cmdRandomkey(c *server.Peer, cmd string, args []string) { + if len(args) != 0 { + 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) + + if len(db.keys) == 0 { + c.WriteNull() + return + } + nr := m.randIntn(len(db.keys)) + for k := range db.keys { + if nr == 0 { + c.WriteBulk(k) + return + } + nr-- + } + }) +} + +// RENAME +func (m *Miniredis) cmdRename(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 + } + + opts := struct { + from string + to string + }{ + from: args[0], + to: args[1], + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if !db.exists(opts.from) { + c.WriteError(msgKeyNotFound) + return + } + + db.rename(opts.from, opts.to) + c.WriteOK() + }) +} + +// RENAMENX +func (m *Miniredis) cmdRenamenx(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 + } + + opts := struct { + from string + to string + }{ + from: args[0], + to: args[1], + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if !db.exists(opts.from) { + c.WriteError(msgKeyNotFound) + return + } + + if db.exists(opts.to) { + c.WriteInt(0) + return + } + + db.rename(opts.from, opts.to) + c.WriteInt(1) + }) +} + +type scanOpts struct { + cursor int + count int + withMatch bool + match string + withType bool + _type string +} + +func scanParse(cmd string, args []string) (*scanOpts, error) { + var opts scanOpts + if err := optIntSimple(args[0], &opts.cursor); err != nil { + return nil, errors.New(msgInvalidCursor) + } + args = args[1:] + + // MATCH, COUNT and TYPE options + for len(args) > 0 { + if strings.ToLower(args[0]) == "count" { + if len(args) < 2 { + return nil, errors.New(msgSyntaxError) + } + count, err := strconv.Atoi(args[1]) + if err != nil || count < 0 { + return nil, errors.New(msgInvalidInt) + } + if count == 0 { + return nil, errors.New(msgSyntaxError) + } + opts.count = count + args = args[2:] + continue + } + if strings.ToLower(args[0]) == "match" { + if len(args) < 2 { + return nil, errors.New(msgSyntaxError) + } + opts.withMatch = true + opts.match, args = args[1], args[2:] + continue + } + if strings.ToLower(args[0]) == "type" { + if len(args) < 2 { + return nil, errors.New(msgSyntaxError) + } + opts.withType = true + opts._type, args = strings.ToLower(args[1]), args[2:] + continue + } + return nil, errors.New(msgSyntaxError) + } + return &opts, nil +} + +// SCAN +func (m *Miniredis) cmdScan(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 + } + + opts, err := scanParse(cmd, args) + if err != nil { + setDirty(c) + c.WriteError(err.Error()) + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + // We return _all_ (matched) keys every time. + var keys []string + + if opts.withType { + keys = make([]string, 0) + for k, t := range db.keys { + // type must be given exactly; no pattern matching is performed + if t == opts._type { + keys = append(keys, k) + } + } + } else { + keys = db.allKeys() + } + + sort.Strings(keys) // To make things deterministic. + + if opts.withMatch { + keys, _ = matchKeys(keys, opts.match) + } + + low := opts.cursor + high := low + opts.count + // validate high is correct + if high > len(keys) || high == 0 { + high = len(keys) + } + if opts.cursor > high { + // invalid cursor + c.WriteLen(2) + c.WriteBulk("0") // no next cursor + c.WriteLen(0) // no elements + return + } + cursorValue := low + opts.count + if cursorValue >= len(keys) { + cursorValue = 0 // no next cursor + } + keys = keys[low:high] + + c.WriteLen(2) + c.WriteBulk(fmt.Sprintf("%d", cursorValue)) + c.WriteLen(len(keys)) + for _, k := range keys { + c.WriteBulk(k) + } + }) +} + +type copyOpts struct { + from string + to string + destinationDB int + replace bool +} + +func copyParse(cmd string, args []string) (*copyOpts, error) { + opts := copyOpts{ + destinationDB: -1, + } + + opts.from, opts.to, args = args[0], args[1], args[2:] + for len(args) > 0 { + switch strings.ToLower(args[0]) { + case "db": + if len(args) < 2 { + return nil, errors.New(msgSyntaxError) + } + if err := optIntSimple(args[1], &opts.destinationDB); err != nil { + return nil, err + } + if opts.destinationDB < 0 { + return nil, errors.New(msgDBIndexOutOfRange) + } + args = args[2:] + case "replace": + opts.replace = true + args = args[1:] + default: + return nil, errors.New(msgSyntaxError) + } + } + return &opts, nil +} + +// COPY +func (m *Miniredis) cmdCopy(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 + } + + opts, err := copyParse(cmd, args) + if err != nil { + setDirty(c) + c.WriteError(err.Error()) + return + } + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + fromDB, toDB := ctx.selectedDB, opts.destinationDB + if toDB == -1 { + toDB = fromDB + } + + if fromDB == toDB && opts.from == opts.to { + c.WriteError("ERR source and destination objects are the same") + return + } + + if !m.db(fromDB).exists(opts.from) { + c.WriteInt(0) + return + } + + if !opts.replace { + if m.db(toDB).exists(opts.to) { + c.WriteInt(0) + return + } + } + + m.copy(m.db(fromDB), opts.from, m.db(toDB), opts.to) + c.WriteInt(1) + }) +}
vendor/github.com/alicebob/miniredis/v2/cmd_geo.go+609 −0 added@@ -0,0 +1,609 @@ +// Commands from https://redis.io/commands#geo + +package miniredis + +import ( + "fmt" + "sort" + "strconv" + "strings" + + "github.com/alicebob/miniredis/v2/server" +) + +// commandsGeo handles GEOADD, GEORADIUS etc. +func commandsGeo(m *Miniredis) { + m.srv.Register("GEOADD", m.cmdGeoadd) + m.srv.Register("GEODIST", m.cmdGeodist) + m.srv.Register("GEOPOS", m.cmdGeopos) + m.srv.Register("GEORADIUS", m.cmdGeoradius) + m.srv.Register("GEORADIUS_RO", m.cmdGeoradius) + m.srv.Register("GEORADIUSBYMEMBER", m.cmdGeoradiusbymember) + m.srv.Register("GEORADIUSBYMEMBER_RO", m.cmdGeoradiusbymember) +} + +// GEOADD +func (m *Miniredis) cmdGeoadd(c *server.Peer, cmd string, args []string) { + if len(args) < 3 || len(args[1:])%3 != 0 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + key, args := args[0], args[1:] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if db.exists(key) && db.t(key) != keyTypeSortedSet { + c.WriteError(ErrWrongType.Error()) + return + } + + toSet := map[string]float64{} + for len(args) > 2 { + rawLong, rawLat, name := args[0], args[1], args[2] + args = args[3:] + longitude, err := strconv.ParseFloat(rawLong, 64) + if err != nil { + c.WriteError("ERR value is not a valid float") + return + } + latitude, err := strconv.ParseFloat(rawLat, 64) + if err != nil { + c.WriteError("ERR value is not a valid float") + return + } + + if latitude < -85.05112878 || + latitude > 85.05112878 || + longitude < -180 || + longitude > 180 { + c.WriteError(fmt.Sprintf("ERR invalid longitude,latitude pair %.6f,%.6f", longitude, latitude)) + return + } + + toSet[name] = float64(toGeohash(longitude, latitude)) + } + + set := 0 + for name, score := range toSet { + if db.ssetAdd(key, score, name) { + set++ + } + } + c.WriteInt(set) + }) +} + +// GEODIST +func (m *Miniredis) cmdGeodist(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, from, to, args := args[0], args[1], args[2], args[3:] + + 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) != keyTypeSortedSet { + c.WriteError(ErrWrongType.Error()) + return + } + + unit := "m" + if len(args) > 0 { + unit, args = args[0], args[1:] + } + if len(args) > 0 { + c.WriteError(msgSyntaxError) + return + } + + toMeter := parseUnit(unit) + if toMeter == 0 { + c.WriteError(msgUnsupportedUnit) + return + } + + members := db.sortedsetKeys[key] + fromD, okFrom := members.get(from) + toD, okTo := members.get(to) + if !okFrom || !okTo { + c.WriteNull() + return + } + + fromLo, fromLat := fromGeohash(uint64(fromD)) + toLo, toLat := fromGeohash(uint64(toD)) + + dist := distance(fromLat, fromLo, toLat, toLo) / toMeter + c.WriteBulk(fmt.Sprintf("%.4f", dist)) + }) +} + +// GEOPOS +func (m *Miniredis) cmdGeopos(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 := args[0], args[1:] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if db.exists(key) && db.t(key) != keyTypeSortedSet { + c.WriteError(ErrWrongType.Error()) + return + } + + c.WriteLen(len(args)) + for _, l := range args { + if !db.ssetExists(key, l) { + c.WriteLen(-1) + continue + } + score := db.ssetScore(key, l) + c.WriteLen(2) + long, lat := fromGeohash(uint64(score)) + c.WriteBulk(fmt.Sprintf("%f", long)) + c.WriteBulk(fmt.Sprintf("%f", lat)) + } + }) +} + +type geoDistance struct { + Name string + Score float64 + Distance float64 + Longitude float64 + Latitude float64 +} + +// GEORADIUS and GEORADIUS_RO +func (m *Miniredis) cmdGeoradius(c *server.Peer, cmd string, args []string) { + if len(args) < 5 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + key := args[0] + longitude, err := strconv.ParseFloat(args[1], 64) + if err != nil { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + latitude, err := strconv.ParseFloat(args[2], 64) + if err != nil { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + radius, err := strconv.ParseFloat(args[3], 64) + if err != nil || radius < 0 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + toMeter := parseUnit(args[4]) + if toMeter == 0 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + args = args[5:] + + var opts struct { + withDist bool + withCoord bool + direction direction // unsorted + count int + withStore bool + storeKey string + withStoredist bool + storedistKey string + } + for len(args) > 0 { + arg := args[0] + args = args[1:] + switch strings.ToUpper(arg) { + case "WITHCOORD": + opts.withCoord = true + case "WITHDIST": + opts.withDist = true + case "ASC": + opts.direction = asc + case "DESC": + opts.direction = desc + case "COUNT": + if len(args) == 0 { + setDirty(c) + c.WriteError("ERR syntax error") + return + } + n, err := strconv.Atoi(args[0]) + if err != nil { + setDirty(c) + c.WriteError(msgInvalidInt) + return + } + if n <= 0 { + setDirty(c) + c.WriteError("ERR COUNT must be > 0") + return + } + args = args[1:] + opts.count = n + case "STORE": + if len(args) == 0 { + setDirty(c) + c.WriteError("ERR syntax error") + return + } + opts.withStore = true + opts.storeKey = args[0] + args = args[1:] + case "STOREDIST": + if len(args) == 0 { + setDirty(c) + c.WriteError("ERR syntax error") + return + } + opts.withStoredist = true + opts.storedistKey = args[0] + args = args[1:] + default: + setDirty(c) + c.WriteError("ERR syntax error") + return + } + } + + if strings.ToUpper(cmd) == "GEORADIUS_RO" && (opts.withStore || opts.withStoredist) { + setDirty(c) + c.WriteError("ERR syntax error") + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + if (opts.withStore || opts.withStoredist) && (opts.withDist || opts.withCoord) { + c.WriteError("ERR STORE option in GEORADIUS is not compatible with WITHDIST, WITHHASH and WITHCOORDS options") + return + } + + db := m.db(ctx.selectedDB) + members := db.ssetElements(key) + + matches := withinRadius(members, longitude, latitude, radius*toMeter) + + // deal with ASC/DESC + if opts.direction != unsorted { + sort.Slice(matches, func(i, j int) bool { + if opts.direction == desc { + return matches[i].Distance > matches[j].Distance + } + return matches[i].Distance < matches[j].Distance + }) + } + + // deal with COUNT + if opts.count > 0 && len(matches) > opts.count { + matches = matches[:opts.count] + } + + // deal with "STORE x" + if opts.withStore { + db.del(opts.storeKey, true) + for _, member := range matches { + db.ssetAdd(opts.storeKey, member.Score, member.Name) + } + c.WriteInt(len(matches)) + return + } + + // deal with "STOREDIST x" + if opts.withStoredist { + db.del(opts.storedistKey, true) + for _, member := range matches { + db.ssetAdd(opts.storedistKey, member.Distance/toMeter, member.Name) + } + c.WriteInt(len(matches)) + return + } + + c.WriteLen(len(matches)) + for _, member := range matches { + if !opts.withDist && !opts.withCoord { + c.WriteBulk(member.Name) + continue + } + + len := 1 + if opts.withDist { + len++ + } + if opts.withCoord { + len++ + } + c.WriteLen(len) + c.WriteBulk(member.Name) + if opts.withDist { + c.WriteBulk(fmt.Sprintf("%.4f", member.Distance/toMeter)) + } + if opts.withCoord { + c.WriteLen(2) + c.WriteBulk(fmt.Sprintf("%f", member.Longitude)) + c.WriteBulk(fmt.Sprintf("%f", member.Latitude)) + } + } + }) +} + +// GEORADIUSBYMEMBER and GEORADIUSBYMEMBER_RO +func (m *Miniredis) cmdGeoradiusbymember(c *server.Peer, cmd string, args []string) { + if len(args) < 4 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + opts := struct { + key string + member string + radius float64 + toMeter float64 + + withDist bool + withCoord bool + direction direction // unsorted + count int + withStore bool + storeKey string + withStoredist bool + storedistKey string + }{ + key: args[0], + member: args[1], + } + + r, err := strconv.ParseFloat(args[2], 64) + if err != nil || r < 0 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + opts.radius = r + + opts.toMeter = parseUnit(args[3]) + if opts.toMeter == 0 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + args = args[4:] + + for len(args) > 0 { + arg := args[0] + args = args[1:] + switch strings.ToUpper(arg) { + case "WITHCOORD": + opts.withCoord = true + case "WITHDIST": + opts.withDist = true + case "ASC": + opts.direction = asc + case "DESC": + opts.direction = desc + case "COUNT": + if len(args) == 0 { + setDirty(c) + c.WriteError("ERR syntax error") + return + } + n, err := strconv.Atoi(args[0]) + if err != nil { + setDirty(c) + c.WriteError(msgInvalidInt) + return + } + if n <= 0 { + setDirty(c) + c.WriteError("ERR COUNT must be > 0") + return + } + args = args[1:] + opts.count = n + case "STORE": + if len(args) == 0 { + setDirty(c) + c.WriteError("ERR syntax error") + return + } + opts.withStore = true + opts.storeKey = args[0] + args = args[1:] + case "STOREDIST": + if len(args) == 0 { + setDirty(c) + c.WriteError("ERR syntax error") + return + } + opts.withStoredist = true + opts.storedistKey = args[0] + args = args[1:] + default: + setDirty(c) + c.WriteError("ERR syntax error") + return + } + } + + if strings.ToUpper(cmd) == "GEORADIUSBYMEMBER_RO" && (opts.withStore || opts.withStoredist) { + setDirty(c) + c.WriteError("ERR syntax error") + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + if (opts.withStore || opts.withStoredist) && (opts.withDist || opts.withCoord) { + c.WriteError("ERR STORE option in GEORADIUS is not compatible with WITHDIST, WITHHASH and WITHCOORDS options") + return + } + + db := m.db(ctx.selectedDB) + if !db.exists(opts.key) { + c.WriteNull() + return + } + + if db.t(opts.key) != keyTypeSortedSet { + c.WriteError(ErrWrongType.Error()) + return + } + + // get position of member + if !db.ssetExists(opts.key, opts.member) { + c.WriteError("ERR could not decode requested zset member") + return + } + score := db.ssetScore(opts.key, opts.member) + longitude, latitude := fromGeohash(uint64(score)) + + members := db.ssetElements(opts.key) + matches := withinRadius(members, longitude, latitude, opts.radius*opts.toMeter) + + // deal with ASC/DESC + if opts.direction != unsorted { + sort.Slice(matches, func(i, j int) bool { + if opts.direction == desc { + return matches[i].Distance > matches[j].Distance + } + return matches[i].Distance < matches[j].Distance + }) + } + + // deal with COUNT + if opts.count > 0 && len(matches) > opts.count { + matches = matches[:opts.count] + } + + // deal with "STORE x" + if opts.withStore { + db.del(opts.storeKey, true) + for _, member := range matches { + db.ssetAdd(opts.storeKey, member.Score, member.Name) + } + c.WriteInt(len(matches)) + return + } + + // deal with "STOREDIST x" + if opts.withStoredist { + db.del(opts.storedistKey, true) + for _, member := range matches { + db.ssetAdd(opts.storedistKey, member.Distance/opts.toMeter, member.Name) + } + c.WriteInt(len(matches)) + return + } + + c.WriteLen(len(matches)) + for _, member := range matches { + if !opts.withDist && !opts.withCoord { + c.WriteBulk(member.Name) + continue + } + + len := 1 + if opts.withDist { + len++ + } + if opts.withCoord { + len++ + } + c.WriteLen(len) + c.WriteBulk(member.Name) + if opts.withDist { + c.WriteBulk(fmt.Sprintf("%.4f", member.Distance/opts.toMeter)) + } + if opts.withCoord { + c.WriteLen(2) + c.WriteBulk(fmt.Sprintf("%f", member.Longitude)) + c.WriteBulk(fmt.Sprintf("%f", member.Latitude)) + } + } + }) +} + +func withinRadius(members []ssElem, longitude, latitude, radius float64) []geoDistance { + matches := []geoDistance{} + for _, el := range members { + elLo, elLat := fromGeohash(uint64(el.score)) + distanceInMeter := distance(latitude, longitude, elLat, elLo) + + if distanceInMeter <= radius { + matches = append(matches, geoDistance{ + Name: el.member, + Score: el.score, + Distance: distanceInMeter, + Longitude: elLo, + Latitude: elLat, + }) + } + } + return matches +} + +func parseUnit(u string) float64 { + switch strings.ToLower(u) { + case "m": + return 1 + case "km": + return 1000 + case "mi": + return 1609.34 + case "ft": + return 0.3048 + default: + return 0 + } +}
vendor/github.com/alicebob/miniredis/v2/cmd_hash.go+777 −0 added@@ -0,0 +1,777 @@ +// Commands from https://redis.io/commands#hash + +package miniredis + +import ( + "math/big" + "strconv" + "strings" + + "github.com/alicebob/miniredis/v2/server" +) + +// commandsHash handles all hash value operations. +func commandsHash(m *Miniredis) { + m.srv.Register("HDEL", m.cmdHdel) + m.srv.Register("HEXISTS", m.cmdHexists) + m.srv.Register("HGET", m.cmdHget) + m.srv.Register("HGETALL", m.cmdHgetall) + m.srv.Register("HINCRBY", m.cmdHincrby) + m.srv.Register("HINCRBYFLOAT", m.cmdHincrbyfloat) + m.srv.Register("HKEYS", m.cmdHkeys) + m.srv.Register("HLEN", m.cmdHlen) + m.srv.Register("HMGET", m.cmdHmget) + m.srv.Register("HMSET", m.cmdHmset) + m.srv.Register("HSET", m.cmdHset) + m.srv.Register("HSETNX", m.cmdHsetnx) + m.srv.Register("HSTRLEN", m.cmdHstrlen) + m.srv.Register("HVALS", m.cmdHvals) + m.srv.Register("HSCAN", m.cmdHscan) + m.srv.Register("HRANDFIELD", m.cmdHrandfield) +} + +// HSET +func (m *Miniredis) cmdHset(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, pairs := args[0], args[1:] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if len(pairs)%2 == 1 { + c.WriteError(errWrongNumber(cmd)) + return + } + + if t, ok := db.keys[key]; ok && t != keyTypeHash { + c.WriteError(msgWrongType) + return + } + + new := db.hashSet(key, pairs...) + c.WriteInt(new) + }) +} + +// HSETNX +func (m *Miniredis) cmdHsetnx(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 + } + + opts := struct { + key string + field string + value string + }{ + key: args[0], + field: args[1], + value: 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 != keyTypeHash { + c.WriteError(msgWrongType) + return + } + + if _, ok := db.hashKeys[opts.key]; !ok { + db.hashKeys[opts.key] = map[string]string{} + db.keys[opts.key] = keyTypeHash + } + _, ok := db.hashKeys[opts.key][opts.field] + if ok { + c.WriteInt(0) + return + } + db.hashKeys[opts.key][opts.field] = opts.value + db.incr(opts.key) + c.WriteInt(1) + }) +} + +// HMSET +func (m *Miniredis) cmdHmset(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 := args[0], args[1:] + if len(args)%2 != 0 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if t, ok := db.keys[key]; ok && t != keyTypeHash { + c.WriteError(msgWrongType) + return + } + + for len(args) > 0 { + field, value := args[0], args[1] + args = args[2:] + db.hashSet(key, field, value) + } + c.WriteOK() + }) +} + +// HGET +func (m *Miniredis) cmdHget(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, field := args[0], args[1] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + t, ok := db.keys[key] + if !ok { + c.WriteNull() + return + } + if t != keyTypeHash { + c.WriteError(msgWrongType) + return + } + value, ok := db.hashKeys[key][field] + if !ok { + c.WriteNull() + return + } + c.WriteBulk(value) + }) +} + +// HDEL +func (m *Miniredis) cmdHdel(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 + } + + opts := struct { + key string + fields []string + }{ + key: args[0], + fields: args[1:], + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + t, ok := db.keys[opts.key] + if !ok { + // No key is zero deleted + c.WriteInt(0) + return + } + if t != keyTypeHash { + c.WriteError(msgWrongType) + return + } + + deleted := 0 + for _, f := range opts.fields { + _, ok := db.hashKeys[opts.key][f] + if !ok { + continue + } + delete(db.hashKeys[opts.key], f) + deleted++ + } + c.WriteInt(deleted) + + // Nothing left. Remove the whole key. + if len(db.hashKeys[opts.key]) == 0 { + db.del(opts.key, true) + } + }) +} + +// HEXISTS +func (m *Miniredis) cmdHexists(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 + } + + opts := struct { + key string + field string + }{ + key: args[0], + field: args[1], + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + t, ok := db.keys[opts.key] + if !ok { + c.WriteInt(0) + return + } + if t != keyTypeHash { + c.WriteError(msgWrongType) + return + } + + if _, ok := db.hashKeys[opts.key][opts.field]; !ok { + c.WriteInt(0) + return + } + c.WriteInt(1) + }) +} + +// HGETALL +func (m *Miniredis) cmdHgetall(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) + + t, ok := db.keys[key] + if !ok { + c.WriteMapLen(0) + return + } + if t != keyTypeHash { + c.WriteError(msgWrongType) + return + } + + c.WriteMapLen(len(db.hashKeys[key])) + for _, k := range db.hashFields(key) { + c.WriteBulk(k) + c.WriteBulk(db.hashGet(key, k)) + } + }) +} + +// HKEYS +func (m *Miniredis) cmdHkeys(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.WriteLen(0) + return + } + if db.t(key) != keyTypeHash { + c.WriteError(msgWrongType) + return + } + + fields := db.hashFields(key) + c.WriteLen(len(fields)) + for _, f := range fields { + c.WriteBulk(f) + } + }) +} + +// HSTRLEN +func (m *Miniredis) cmdHstrlen(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 + } + + hash, key := args[0], args[1] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + t, ok := db.keys[hash] + if !ok { + c.WriteInt(0) + return + } + if t != keyTypeHash { + c.WriteError(msgWrongType) + return + } + + keys := db.hashKeys[hash] + c.WriteInt(len(keys[key])) + }) +} + +// HVALS +func (m *Miniredis) cmdHvals(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) + + t, ok := db.keys[key] + if !ok { + c.WriteLen(0) + return + } + if t != keyTypeHash { + c.WriteError(msgWrongType) + return + } + + vals := db.hashValues(key) + c.WriteLen(len(vals)) + for _, v := range vals { + c.WriteBulk(v) + } + }) +} + +// HLEN +func (m *Miniredis) cmdHlen(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) + + t, ok := db.keys[key] + if !ok { + c.WriteInt(0) + return + } + if t != keyTypeHash { + c.WriteError(msgWrongType) + return + } + + c.WriteInt(len(db.hashKeys[key])) + }) +} + +// HMGET +func (m *Miniredis) cmdHmget(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] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if t, ok := db.keys[key]; ok && t != keyTypeHash { + c.WriteError(msgWrongType) + return + } + + f, ok := db.hashKeys[key] + if !ok { + f = map[string]string{} + } + + c.WriteLen(len(args) - 1) + for _, k := range args[1:] { + v, ok := f[k] + if !ok { + c.WriteNull() + continue + } + c.WriteBulk(v) + } + }) +} + +// HINCRBY +func (m *Miniredis) cmdHincrby(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 + } + + opts := struct { + key string + field string + delta int + }{ + key: args[0], + field: args[1], + } + if ok := optInt(c, args[2], &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 != keyTypeHash { + c.WriteError(msgWrongType) + return + } + + v, err := db.hashIncr(opts.key, opts.field, opts.delta) + if err != nil { + c.WriteError(err.Error()) + return + } + c.WriteInt(v) + }) +} + +// HINCRBYFLOAT +func (m *Miniredis) cmdHincrbyfloat(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 + } + + opts := struct { + key string + field string + delta *big.Float + }{ + key: args[0], + field: args[1], + } + delta, _, err := big.ParseFloat(args[2], 10, 128, 0) + if err != nil { + setDirty(c) + c.WriteError(msgInvalidFloat) + return + } + opts.delta = delta + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if t, ok := db.keys[opts.key]; ok && t != keyTypeHash { + c.WriteError(msgWrongType) + return + } + + v, err := db.hashIncrfloat(opts.key, opts.field, opts.delta) + if err != nil { + c.WriteError(err.Error()) + return + } + c.WriteBulk(formatBig(v)) + }) +} + +// HSCAN +func (m *Miniredis) cmdHscan(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 + } + + opts := struct { + key string + cursor int + withMatch bool + match string + }{ + key: args[0], + } + if ok := optIntErr(c, args[1], &opts.cursor, msgInvalidCursor); !ok { + return + } + args = args[2:] + + // MATCH and COUNT options + for len(args) > 0 { + if strings.ToLower(args[0]) == "count" { + // we do nothing with count + if len(args) < 2 { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + _, err := strconv.Atoi(args[1]) + if err != nil { + setDirty(c) + c.WriteError(msgInvalidInt) + return + } + args = args[2:] + continue + } + if strings.ToLower(args[0]) == "match" { + if len(args) < 2 { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + opts.withMatch = true + opts.match, args = args[1], args[2:] + continue + } + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + // return _all_ (matched) keys every time + + if opts.cursor != 0 { + // Invalid cursor. + c.WriteLen(2) + c.WriteBulk("0") // no next cursor + c.WriteLen(0) // no elements + return + } + if db.exists(opts.key) && db.t(opts.key) != keyTypeHash { + c.WriteError(ErrWrongType.Error()) + return + } + + members := db.hashFields(opts.key) + if opts.withMatch { + members, _ = matchKeys(members, opts.match) + } + + c.WriteLen(2) + c.WriteBulk("0") // no next cursor + // HSCAN gives key, values. + c.WriteLen(len(members) * 2) + for _, k := range members { + c.WriteBulk(k) + c.WriteBulk(db.hashGet(opts.key, k)) + } + }) +} + +// HRANDFIELD +func (m *Miniredis) cmdHrandfield(c *server.Peer, cmd string, args []string) { + if len(args) > 3 || len(args) < 1 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + opts := struct { + key string + count int + countSet bool + withValues bool + }{ + key: args[0], + } + + if len(args) > 1 { + if ok := optIntErr(c, args[1], &opts.count, msgInvalidInt); !ok { + return + } + opts.countSet = true + } + + if len(args) == 3 { + if strings.ToLower(args[2]) == "withvalues" { + opts.withValues = true + } else { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + } + + withTx(m, c, func(peer *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + members := db.hashFields(opts.key) + m.shuffle(members) + + if !opts.countSet { + // > When called with just the key argument, return a random field from the + // hash value stored at key. + if len(members) == 0 { + peer.WriteNull() + return + } + peer.WriteBulk(members[0]) + return + } + + if len(members) > abs(opts.count) { + members = members[:abs(opts.count)] + } + switch { + case opts.count >= 0: + // if count is positive there can't be duplicates, and the length is restricted + case opts.count < 0: + // if count is negative there can be duplicates, but length will match + if len(members) > 0 { + for len(members) < -opts.count { + members = append(members, members[m.randIntn(len(members))]) + } + } + } + + if opts.withValues { + peer.WriteMapLen(len(members)) + for _, m := range members { + peer.WriteBulk(m) + peer.WriteBulk(db.hashGet(opts.key, m)) + } + return + } + peer.WriteLen(len(members)) + for _, m := range members { + peer.WriteBulk(m) + } + }) +} + +func abs(n int) int { + if n < 0 { + return -n + } + return n +}
vendor/github.com/alicebob/miniredis/v2/cmd_hll.go+95 −0 added@@ -0,0 +1,95 @@ +package miniredis + +import "github.com/alicebob/miniredis/v2/server" + +// commandsHll handles all hll related operations. +func commandsHll(m *Miniredis) { + m.srv.Register("PFADD", m.cmdPfadd) + m.srv.Register("PFCOUNT", m.cmdPfcount) + m.srv.Register("PFMERGE", m.cmdPfmerge) +} + +// PFADD +func (m *Miniredis) cmdPfadd(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, items := args[0], args[1:] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if db.exists(key) && db.t(key) != keyTypeHll { + c.WriteError(ErrNotValidHllValue.Error()) + return + } + + altered := db.hllAdd(key, items...) + c.WriteInt(altered) + }) +} + +// PFCOUNT +func (m *Miniredis) cmdPfcount(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 + } + + keys := args + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + count, err := db.hllCount(keys) + if err != nil { + c.WriteError(err.Error()) + return + } + + c.WriteInt(count) + }) +} + +// PFMERGE +func (m *Miniredis) cmdPfmerge(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 + } + + keys := args + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if err := db.hllMerge(keys); err != nil { + c.WriteError(err.Error()) + return + } + c.WriteOK() + }) +}
vendor/github.com/alicebob/miniredis/v2/cmd_info.go+40 −0 added@@ -0,0 +1,40 @@ +package miniredis + +import ( + "fmt" + + "github.com/alicebob/miniredis/v2/server" +) + +// Command 'INFO' from https://redis.io/commands/info/ +func (m *Miniredis) cmdInfo(c *server.Peer, cmd string, args []string) { + if !m.isValidCMD(c, cmd) { + return + } + + if len(args) > 1 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + const ( + clientsSectionName = "clients" + clientsSectionContent = "# Clients\nconnected_clients:%d\r\n" + ) + + var result string + + for _, key := range args { + if key != clientsSectionName { + setDirty(c) + c.WriteError(fmt.Sprintf("section (%s) is not supported", key)) + return + } + } + result = fmt.Sprintf(clientsSectionContent, m.Server().ClientsLen()) + + c.WriteBulk(result) + }) +}
vendor/github.com/alicebob/miniredis/v2/cmd_list.go+1060 −0 added@@ -0,0 +1,1060 @@ +// Commands from https://redis.io/commands#list + +package miniredis + +import ( + "strconv" + "strings" + "time" + + "github.com/alicebob/miniredis/v2/server" +) + +type leftright int + +const ( + left leftright = iota + right +) + +// commandsList handles list commands (mostly L*) +func commandsList(m *Miniredis) { + m.srv.Register("BLPOP", m.cmdBlpop) + m.srv.Register("BRPOP", m.cmdBrpop) + m.srv.Register("BRPOPLPUSH", m.cmdBrpoplpush) + m.srv.Register("LINDEX", m.cmdLindex) + m.srv.Register("LPOS", m.cmdLpos) + m.srv.Register("LINSERT", m.cmdLinsert) + m.srv.Register("LLEN", m.cmdLlen) + m.srv.Register("LPOP", m.cmdLpop) + m.srv.Register("LPUSH", m.cmdLpush) + m.srv.Register("LPUSHX", m.cmdLpushx) + m.srv.Register("LRANGE", m.cmdLrange) + m.srv.Register("LREM", m.cmdLrem) + m.srv.Register("LSET", m.cmdLset) + m.srv.Register("LTRIM", m.cmdLtrim) + m.srv.Register("RPOP", m.cmdRpop) + m.srv.Register("RPOPLPUSH", m.cmdRpoplpush) + m.srv.Register("RPUSH", m.cmdRpush) + m.srv.Register("RPUSHX", m.cmdRpushx) + m.srv.Register("LMOVE", m.cmdLmove) + m.srv.Register("BLMOVE", m.cmdBlmove) +} + +// BLPOP +func (m *Miniredis) cmdBlpop(c *server.Peer, cmd string, args []string) { + m.cmdBXpop(c, cmd, args, left) +} + +// BRPOP +func (m *Miniredis) cmdBrpop(c *server.Peer, cmd string, args []string) { + m.cmdBXpop(c, cmd, args, right) +} + +func (m *Miniredis) cmdBXpop(c *server.Peer, cmd string, args []string, lr leftright) { + 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 { + keys []string + timeout time.Duration + } + + if ok := optDuration(c, args[len(args)-1], &opts.timeout); !ok { + return + } + opts.keys = args[:len(args)-1] + + blocking( + m, + c, + opts.timeout, + func(c *server.Peer, ctx *connCtx) bool { + db := m.db(ctx.selectedDB) + for _, key := range opts.keys { + if !db.exists(key) { + continue + } + if db.t(key) != keyTypeList { + c.WriteError(msgWrongType) + return true + } + + if len(db.listKeys[key]) == 0 { + continue + } + c.WriteLen(2) + c.WriteBulk(key) + var v string + switch lr { + case left: + v = db.listLpop(key) + case right: + v = db.listPop(key) + } + c.WriteBulk(v) + return true + } + return false + }, + func(c *server.Peer) { + // timeout + c.WriteLen(-1) + }, + ) +} + +// LINDEX +func (m *Miniredis) cmdLindex(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, offsets := args[0], args[1] + + offset, err := strconv.Atoi(offsets) + if err != nil || offsets == "-0" { + setDirty(c) + c.WriteError(msgInvalidInt) + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + t, ok := db.keys[key] + if !ok { + // No such key + c.WriteNull() + return + } + if t != keyTypeList { + c.WriteError(msgWrongType) + return + } + + l := db.listKeys[key] + if offset < 0 { + offset = len(l) + offset + } + if offset < 0 || offset > len(l)-1 { + c.WriteNull() + return + } + c.WriteBulk(l[offset]) + }) +} + +// LPOS key element [RANK rank] [COUNT num-matches] [MAXLEN len] +func (m *Miniredis) cmdLpos(c *server.Peer, cmd string, args []string) { + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + if len(args) == 1 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + + // Extract options from arguments if present. + // + // Redis allows duplicate options and uses the last specified. + // `LPOS key term RANK 1 RANK 2` is effectively the same as + // `LPOS key term RANK 2` + if len(args)%2 == 1 { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + rank, count := 1, 1 // Default values + var maxlen int // Default value is the list length (see below) + var countSpecified, maxlenSpecified bool + if len(args) > 2 { + for i := 2; i < len(args); i++ { + if i%2 == 0 { + val := args[i+1] + var err error + switch strings.ToLower(args[i]) { + case "rank": + if rank, err = strconv.Atoi(val); err != nil { + setDirty(c) + c.WriteError(msgInvalidInt) + return + } + if rank == 0 { + setDirty(c) + c.WriteError(msgRankIsZero) + return + } + case "count": + countSpecified = true + if count, err = strconv.Atoi(val); err != nil || count < 0 { + setDirty(c) + c.WriteError(msgCountIsNegative) + return + } + case "maxlen": + maxlenSpecified = true + if maxlen, err = strconv.Atoi(val); err != nil || maxlen < 0 { + setDirty(c) + c.WriteError(msgMaxLengthIsNegative) + return + } + default: + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + } + } + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + key, element := args[0], args[1] + t, ok := db.keys[key] + if !ok { + // No such key + c.WriteNull() + return + } + if t != keyTypeList { + c.WriteError(msgWrongType) + return + } + l := db.listKeys[key] + + // RANK cannot be zero (see above). + // If RANK is positive search forward (left to right). + // If RANK is negative search backward (right to left). + // Iterator returns true to continue iterating. + iterate := func(iterator func(i int, e string) bool) { + comparisons := len(l) + // Only use max length if specified, not zero, and less than total length. + // When max length is specified, but is zero, this means "unlimited". + if maxlenSpecified && maxlen != 0 && maxlen < len(l) { + comparisons = maxlen + } + if rank > 0 { + for i := 0; i < comparisons; i++ { + if resume := iterator(i, l[i]); !resume { + return + } + } + } else if rank < 0 { + start := len(l) - 1 + end := len(l) - comparisons + for i := start; i >= end; i-- { + if resume := iterator(i, l[i]); !resume { + return + } + } + } + } + + var currentRank, currentCount int + vals := make([]int, 0, count) + iterate(func(i int, e string) bool { + if e == element { + currentRank++ + // Only collect values only after surpassing the absolute value of rank. + if rank > 0 && currentRank < rank { + return true + } + if rank < 0 && currentRank < -rank { + return true + } + vals = append(vals, i) + currentCount++ + if currentCount == count { + return false + } + } + return true + }) + + if !countSpecified && len(vals) == 0 { + c.WriteNull() + return + } + if !countSpecified && len(vals) == 1 { + c.WriteInt(vals[0]) + return + } + c.WriteLen(len(vals)) + for _, val := range vals { + c.WriteInt(val) + } + }) +} + +// LINSERT +func (m *Miniredis) cmdLinsert(c *server.Peer, cmd string, args []string) { + if len(args) != 4 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + key := args[0] + where := 0 + switch strings.ToLower(args[1]) { + case "before": + where = -1 + case "after": + where = +1 + default: + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + pivot := args[2] + value := args[3] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + t, ok := db.keys[key] + if !ok { + // No such key + c.WriteInt(0) + return + } + if t != keyTypeList { + c.WriteError(msgWrongType) + return + } + + l := db.listKeys[key] + for i, el := range l { + if el != pivot { + continue + } + + if where < 0 { + l = append(l[:i], append(listKey{value}, l[i:]...)...) + } else { + if i == len(l)-1 { + l = append(l, value) + } else { + l = append(l[:i+1], append(listKey{value}, l[i+1:]...)...) + } + } + db.listKeys[key] = l + db.incr(key) + c.WriteInt(len(l)) + return + } + c.WriteInt(-1) + }) +} + +// LLEN +func (m *Miniredis) cmdLlen(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) + + t, ok := db.keys[key] + if !ok { + // No such key. That's zero length. + c.WriteInt(0) + return + } + if t != keyTypeList { + c.WriteError(msgWrongType) + return + } + + c.WriteInt(len(db.listKeys[key])) + }) +} + +// LPOP +func (m *Miniredis) cmdLpop(c *server.Peer, cmd string, args []string) { + m.cmdXpop(c, cmd, args, left) +} + +// RPOP +func (m *Miniredis) cmdRpop(c *server.Peer, cmd string, args []string) { + m.cmdXpop(c, cmd, args, right) +} + +func (m *Miniredis) cmdXpop(c *server.Peer, cmd string, args []string, lr leftright) { + 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 + withCount bool + count int + } + + opts.key, args = args[0], args[1:] + if len(args) > 0 { + if ok := optInt(c, args[0], &opts.count); !ok { + return + } + if opts.count < 0 { + setDirty(c) + c.WriteError(msgOutOfRange) + return + } + opts.withCount = true + args = args[1:] + } + if len(args) > 0 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if !db.exists(opts.key) { + // non-existing key is fine + if opts.withCount && !c.Resp3 { + // zero-length list in this specific case. Looks like a redis bug to me. + c.WriteLen(-1) + return + } + c.WriteNull() + return + } + if db.t(opts.key) != keyTypeList { + c.WriteError(msgWrongType) + return + } + + if opts.withCount { + var popped []string + for opts.count > 0 && len(db.listKeys[opts.key]) > 0 { + switch lr { + case left: + popped = append(popped, db.listLpop(opts.key)) + case right: + popped = append(popped, db.listPop(opts.key)) + } + opts.count -= 1 + } + c.WriteStrings(popped) + return + } + + var elem string + switch lr { + case left: + elem = db.listLpop(opts.key) + case right: + elem = db.listPop(opts.key) + } + c.WriteBulk(elem) + }) +} + +// LPUSH +func (m *Miniredis) cmdLpush(c *server.Peer, cmd string, args []string) { + m.cmdXpush(c, cmd, args, left) +} + +// RPUSH +func (m *Miniredis) cmdRpush(c *server.Peer, cmd string, args []string) { + m.cmdXpush(c, cmd, args, right) +} + +func (m *Miniredis) cmdXpush(c *server.Peer, cmd string, args []string, lr leftright) { + if len(args) < 2 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + key, args := args[0], args[1:] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if db.exists(key) && db.t(key) != keyTypeList { + c.WriteError(msgWrongType) + return + } + + var newLen int + for _, value := range args { + switch lr { + case left: + newLen = db.listLpush(key, value) + case right: + newLen = db.listPush(key, value) + } + } + c.WriteInt(newLen) + }) +} + +// LPUSHX +func (m *Miniredis) cmdLpushx(c *server.Peer, cmd string, args []string) { + m.cmdXpushx(c, cmd, args, left) +} + +// RPUSHX +func (m *Miniredis) cmdRpushx(c *server.Peer, cmd string, args []string) { + m.cmdXpushx(c, cmd, args, right) +} + +func (m *Miniredis) cmdXpushx(c *server.Peer, cmd string, args []string, lr leftright) { + if len(args) < 2 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + key, args := args[0], args[1:] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if !db.exists(key) { + c.WriteInt(0) + return + } + if db.t(key) != keyTypeList { + c.WriteError(msgWrongType) + return + } + + var newLen int + for _, value := range args { + switch lr { + case left: + newLen = db.listLpush(key, value) + case right: + newLen = db.listPush(key, value) + } + } + c.WriteInt(newLen) + }) +} + +// LRANGE +func (m *Miniredis) cmdLrange(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 + } + + opts := struct { + key string + start int + end int + }{ + 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 != keyTypeList { + c.WriteError(msgWrongType) + return + } + + l := db.listKeys[opts.key] + if len(l) == 0 { + c.WriteLen(0) + return + } + + rs, re := redisRange(len(l), opts.start, opts.end, false) + c.WriteLen(re - rs) + for _, el := range l[rs:re] { + c.WriteBulk(el) + } + }) +} + +// LREM +func (m *Miniredis) cmdLrem(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 + count int + value string + } + opts.key = args[0] + if ok := optInt(c, args[1], &opts.count); !ok { + return + } + opts.value = 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) != keyTypeList { + c.WriteError(msgWrongType) + return + } + + l := db.listKeys[opts.key] + if opts.count < 0 { + reverseSlice(l) + } + deleted := 0 + newL := []string{} + toDelete := len(l) + if opts.count < 0 { + toDelete = -opts.count + } + if opts.count > 0 { + toDelete = opts.count + } + for _, el := range l { + if el == opts.value { + if toDelete > 0 { + deleted++ + toDelete-- + continue + } + } + newL = append(newL, el) + } + if opts.count < 0 { + reverseSlice(newL) + } + if len(newL) == 0 { + db.del(opts.key, true) + } else { + db.listKeys[opts.key] = newL + db.incr(opts.key) + } + + c.WriteInt(deleted) + }) +} + +// LSET +func (m *Miniredis) cmdLset(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 + index int + value string + } + opts.key = args[0] + if ok := optInt(c, args[1], &opts.index); !ok { + return + } + opts.value = args[2] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if !db.exists(opts.key) { + c.WriteError(msgKeyNotFound) + return + } + if db.t(opts.key) != keyTypeList { + c.WriteError(msgWrongType) + return + } + + l := db.listKeys[opts.key] + index := opts.index + if index < 0 { + index = len(l) + index + } + if index < 0 || index > len(l)-1 { + c.WriteError(msgOutOfRange) + return + } + l[index] = opts.value + db.incr(opts.key) + + c.WriteOK() + }) +} + +// LTRIM +func (m *Miniredis) cmdLtrim(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) + + t, ok := db.keys[opts.key] + if !ok { + c.WriteOK() + return + } + if t != keyTypeList { + c.WriteError(msgWrongType) + return + } + + l := db.listKeys[opts.key] + rs, re := redisRange(len(l), opts.start, opts.end, false) + l = l[rs:re] + if len(l) == 0 { + db.del(opts.key, true) + } else { + db.listKeys[opts.key] = l + db.incr(opts.key) + } + c.WriteOK() + }) +} + +// RPOPLPUSH +func (m *Miniredis) cmdRpoplpush(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 + } + + src, dst := args[0], args[1] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if !db.exists(src) { + c.WriteNull() + return + } + if db.t(src) != keyTypeList || (db.exists(dst) && db.t(dst) != keyTypeList) { + c.WriteError(msgWrongType) + return + } + elem := db.listPop(src) + db.listLpush(dst, elem) + c.WriteBulk(elem) + }) +} + +// BRPOPLPUSH +func (m *Miniredis) cmdBrpoplpush(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 { + src string + dst string + timeout time.Duration + } + opts.src = args[0] + opts.dst = args[1] + if ok := optDuration(c, args[2], &opts.timeout); !ok { + return + } + + blocking( + m, + c, + opts.timeout, + func(c *server.Peer, ctx *connCtx) bool { + db := m.db(ctx.selectedDB) + + if !db.exists(opts.src) { + return false + } + if db.t(opts.src) != keyTypeList || (db.exists(opts.dst) && db.t(opts.dst) != keyTypeList) { + c.WriteError(msgWrongType) + return true + } + if len(db.listKeys[opts.src]) == 0 { + return false + } + elem := db.listPop(opts.src) + db.listLpush(opts.dst, elem) + c.WriteBulk(elem) + return true + }, + func(c *server.Peer) { + // timeout + c.WriteLen(-1) + }, + ) +} + +// LMOVE +func (m *Miniredis) cmdLmove(c *server.Peer, cmd string, args []string) { + if len(args) != 4 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + opts := struct { + src string + dst string + srcDir string + dstDir string + }{ + src: args[0], + dst: args[1], + srcDir: strings.ToLower(args[2]), + dstDir: strings.ToLower(args[3]), + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if !db.exists(opts.src) { + c.WriteNull() + return + } + if db.t(opts.src) != keyTypeList || (db.exists(opts.dst) && db.t(opts.dst) != keyTypeList) { + c.WriteError(msgWrongType) + return + } + var elem string + switch opts.srcDir { + case "left": + elem = db.listLpop(opts.src) + case "right": + elem = db.listPop(opts.src) + default: + c.WriteError(msgSyntaxError) + return + } + + switch opts.dstDir { + case "left": + db.listLpush(opts.dst, elem) + case "right": + db.listPush(opts.dst, elem) + default: + c.WriteError(msgSyntaxError) + return + } + c.WriteBulk(elem) + }) +} + +// BLMOVE +func (m *Miniredis) cmdBlmove(c *server.Peer, cmd string, args []string) { + if len(args) != 5 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + opts := struct { + src string + dst string + srcDir string + dstDir string + timeout time.Duration + }{ + src: args[0], + dst: args[1], + srcDir: strings.ToLower(args[2]), + dstDir: strings.ToLower(args[3]), + } + if ok := optDuration(c, args[len(args)-1], &opts.timeout); !ok { + return + } + + blocking( + m, + c, + opts.timeout, + func(c *server.Peer, ctx *connCtx) bool { + db := m.db(ctx.selectedDB) + + if !db.exists(opts.src) { + return false + } + if db.t(opts.src) != keyTypeList || (db.exists(opts.dst) && db.t(opts.dst) != keyTypeList) { + c.WriteError(msgWrongType) + return true + } + + var ( + elem string + ttl = db.ttl[opts.src] // in case we empty the array (deletes the entry) + ) + switch opts.srcDir { + case "left": + elem = db.listLpop(opts.src) + case "right": + elem = db.listPop(opts.src) + default: + c.WriteError(msgSyntaxError) + return true + } + + switch opts.dstDir { + case "left": + db.listLpush(opts.dst, elem) + case "right": + db.listPush(opts.dst, elem) + default: + c.WriteError(msgSyntaxError) + return true + } + if ttl > 0 { + db.ttl[opts.dst] = ttl + } + + c.WriteBulk(elem) + return true + }, + func(c *server.Peer) { + // timeout + c.WriteLen(-1) + }, + ) +}
vendor/github.com/alicebob/miniredis/v2/cmd_object.go+58 −0 added@@ -0,0 +1,58 @@ +package miniredis + +import ( + "fmt" + "strings" + + "github.com/alicebob/miniredis/v2/server" +) + +// commandsObject handles all object operations. +func commandsObject(m *Miniredis) { + m.srv.Register("OBJECT", m.cmdObject) +} + +// OBJECT +func (m *Miniredis) cmdObject(c *server.Peer, cmd string, args []string) { + if len(args) == 0 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + switch sub := strings.ToLower(args[0]); sub { + case "idletime": + m.cmdObjectIdletime(c, args[1:]) + default: + setDirty(c) + c.WriteError(fmt.Sprintf(msgFObjectUsage, sub)) + } +} + +// OBJECT IDLETIME +func (m *Miniredis) cmdObjectIdletime(c *server.Peer, args []string) { + if len(args) != 1 { + setDirty(c) + c.WriteError(errWrongNumber("object|idletime")) + return + } + key := args[0] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + t, ok := db.lru[key] + if !ok { + c.WriteNull() + return + } + + c.WriteInt(int(db.master.effectiveNow().Sub(t).Seconds())) + }) +}
vendor/github.com/alicebob/miniredis/v2/cmd_pubsub.go+262 −0 added@@ -0,0 +1,262 @@ +// Commands from https://redis.io/commands#pubsub + +package miniredis + +import ( + "fmt" + "strings" + + "github.com/alicebob/miniredis/v2/server" +) + +// commandsPubsub handles all PUB/SUB operations. +func commandsPubsub(m *Miniredis) { + m.srv.Register("SUBSCRIBE", m.cmdSubscribe) + m.srv.Register("UNSUBSCRIBE", m.cmdUnsubscribe) + m.srv.Register("PSUBSCRIBE", m.cmdPsubscribe) + m.srv.Register("PUNSUBSCRIBE", m.cmdPunsubscribe) + m.srv.Register("PUBLISH", m.cmdPublish) + m.srv.Register("PUBSUB", m.cmdPubSub) +} + +// SUBSCRIBE +func (m *Miniredis) cmdSubscribe(c *server.Peer, cmd string, args []string) { + if len(args) < 1 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + ctx := getCtx(c) + if ctx.nested { + c.WriteError(msgNotFromScripts(ctx.nestedSHA)) + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + sub := m.subscribedState(c) + for _, channel := range args { + n := sub.Subscribe(channel) + c.Block(func(w *server.Writer) { + w.WritePushLen(3) + w.WriteBulk("subscribe") + w.WriteBulk(channel) + w.WriteInt(n) + }) + } + }) +} + +// UNSUBSCRIBE +func (m *Miniredis) cmdUnsubscribe(c *server.Peer, cmd string, args []string) { + if !m.handleAuth(c) { + return + } + ctx := getCtx(c) + if ctx.nested { + c.WriteError(msgNotFromScripts(ctx.nestedSHA)) + return + } + + channels := args + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + sub := m.subscribedState(c) + + if len(channels) == 0 { + channels = sub.Channels() + } + + // there is no de-duplication + for _, channel := range channels { + n := sub.Unsubscribe(channel) + c.Block(func(w *server.Writer) { + w.WritePushLen(3) + w.WriteBulk("unsubscribe") + w.WriteBulk(channel) + w.WriteInt(n) + }) + } + if len(channels) == 0 { + // special case: there is always a reply + c.Block(func(w *server.Writer) { + w.WritePushLen(3) + w.WriteBulk("unsubscribe") + w.WriteNull() + w.WriteInt(0) + }) + } + + if sub.Count() == 0 { + endSubscriber(m, c) + } + }) +} + +// PSUBSCRIBE +func (m *Miniredis) cmdPsubscribe(c *server.Peer, cmd string, args []string) { + if len(args) < 1 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + ctx := getCtx(c) + if ctx.nested { + c.WriteError(msgNotFromScripts(ctx.nestedSHA)) + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + sub := m.subscribedState(c) + for _, pat := range args { + n := sub.Psubscribe(pat) + c.Block(func(w *server.Writer) { + w.WritePushLen(3) + w.WriteBulk("psubscribe") + w.WriteBulk(pat) + w.WriteInt(n) + }) + } + }) +} + +// PUNSUBSCRIBE +func (m *Miniredis) cmdPunsubscribe(c *server.Peer, cmd string, args []string) { + if !m.handleAuth(c) { + return + } + ctx := getCtx(c) + if ctx.nested { + c.WriteError(msgNotFromScripts(ctx.nestedSHA)) + return + } + + patterns := args + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + sub := m.subscribedState(c) + + if len(patterns) == 0 { + patterns = sub.Patterns() + } + + // there is no de-duplication + for _, pat := range patterns { + n := sub.Punsubscribe(pat) + c.Block(func(w *server.Writer) { + w.WritePushLen(3) + w.WriteBulk("punsubscribe") + w.WriteBulk(pat) + w.WriteInt(n) + }) + } + if len(patterns) == 0 { + // special case: there is always a reply + c.Block(func(w *server.Writer) { + w.WritePushLen(3) + w.WriteBulk("punsubscribe") + w.WriteNull() + w.WriteInt(0) + }) + } + + if sub.Count() == 0 { + endSubscriber(m, c) + } + }) +} + +// PUBLISH +func (m *Miniredis) cmdPublish(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 + } + + channel, mesg := args[0], args[1] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + c.WriteInt(m.publish(channel, mesg)) + }) +} + +// PUBSUB +func (m *Miniredis) cmdPubSub(c *server.Peer, cmd string, args []string) { + if len(args) < 1 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + + if m.checkPubsub(c, cmd) { + return + } + + subcommand := strings.ToUpper(args[0]) + subargs := args[1:] + var argsOk bool + + switch subcommand { + case "CHANNELS": + argsOk = len(subargs) < 2 + case "NUMSUB": + argsOk = true + case "NUMPAT": + argsOk = len(subargs) == 0 + default: + setDirty(c) + c.WriteError(fmt.Sprintf(msgFPubsubUsageSimple, subcommand)) + return + } + + if !argsOk { + setDirty(c) + c.WriteError(fmt.Sprintf(msgFPubsubUsage, subcommand)) + return + } + + if !m.handleAuth(c) { + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + switch subcommand { + case "CHANNELS": + pat := "" + if len(subargs) == 1 { + pat = subargs[0] + } + + allsubs := m.allSubscribers() + channels := activeChannels(allsubs, pat) + + c.WriteLen(len(channels)) + for _, channel := range channels { + c.WriteBulk(channel) + } + + case "NUMSUB": + subs := m.allSubscribers() + c.WriteLen(len(subargs) * 2) + for _, channel := range subargs { + c.WriteBulk(channel) + c.WriteInt(countSubs(subs, channel)) + } + + case "NUMPAT": + c.WriteInt(countPsubs(m.allSubscribers())) + } + }) +}
vendor/github.com/alicebob/miniredis/v2/cmd_scripting.go+343 −0 added@@ -0,0 +1,343 @@ +package miniredis + +import ( + "crypto/sha1" + "encoding/hex" + "fmt" + "io" + "strconv" + "strings" + "sync" + + lua "github.com/yuin/gopher-lua" + "github.com/yuin/gopher-lua/parse" + + luajson "github.com/alicebob/miniredis/v2/gopher-json" + "github.com/alicebob/miniredis/v2/server" +) + +func commandsScripting(m *Miniredis) { + m.srv.Register("EVAL", m.cmdEval) + m.srv.Register("EVALSHA", m.cmdEvalsha) + m.srv.Register("SCRIPT", m.cmdScript) +} + +var ( + parsedScripts = sync.Map{} +) + +// Execute lua. Needs to run m.Lock()ed, from within withTx(). +// Returns true if the lua was OK (and hence should be cached). +func (m *Miniredis) runLuaScript(c *server.Peer, sha, script string, args []string) bool { + l := lua.NewState(lua.Options{SkipOpenLibs: true}) + defer l.Close() + + // Taken from the go-lua manual + for _, pair := range []struct { + n string + f lua.LGFunction + }{ + {lua.LoadLibName, lua.OpenPackage}, + {lua.BaseLibName, lua.OpenBase}, + {lua.CoroutineLibName, lua.OpenCoroutine}, + {lua.TabLibName, lua.OpenTable}, + {lua.StringLibName, lua.OpenString}, + {lua.MathLibName, lua.OpenMath}, + {lua.DebugLibName, lua.OpenDebug}, + } { + if err := l.CallByParam(lua.P{ + Fn: l.NewFunction(pair.f), + NRet: 0, + Protect: true, + }, lua.LString(pair.n)); err != nil { + panic(err) + } + } + + luajson.Preload(l) + requireGlobal(l, "cjson", "json") + + // set global variable KEYS + keysTable := l.NewTable() + keysS, args := args[0], args[1:] + keysLen, err := strconv.Atoi(keysS) + if err != nil { + c.WriteError(msgInvalidInt) + return false + } + if keysLen < 0 { + c.WriteError(msgNegativeKeysNumber) + return false + } + if keysLen > len(args) { + c.WriteError(msgInvalidKeysNumber) + return false + } + keys, args := args[:keysLen], args[keysLen:] + for i, k := range keys { + l.RawSet(keysTable, lua.LNumber(i+1), lua.LString(k)) + } + l.SetGlobal("KEYS", keysTable) + + argvTable := l.NewTable() + for i, a := range args { + l.RawSet(argvTable, lua.LNumber(i+1), lua.LString(a)) + } + l.SetGlobal("ARGV", argvTable) + + redisFuncs, redisConstants := mkLua(m.srv, c, sha) + // Register command handlers + l.Push(l.NewFunction(func(l *lua.LState) int { + mod := l.RegisterModule("redis", redisFuncs).(*lua.LTable) + for k, v := range redisConstants { + mod.RawSetString(k, v) + } + l.Push(mod) + return 1 + })) + + _ = doScript(l, protectGlobals) + + l.Push(lua.LString("redis")) + l.Call(1, 0) + + // lua can call redis.setresp(...), but it's tmp state. + oldresp := c.Resp3 + if err := doScript(l, script); err != nil { + c.WriteError(err.Error()) + return false + } + + luaToRedis(l, c, l.Get(1)) + c.Resp3 = oldresp + c.SwitchResp3 = nil + return true +} + +// doScript pre-compiles the given script into a Lua prototype, +// then executes the pre-compiled function against the given lua state. +// +// This is thread-safe. +func doScript(l *lua.LState, script string) error { + proto, err := compile(script) + if err != nil { + return fmt.Errorf(errLuaParseError(err)) + } + + lfunc := l.NewFunctionFromProto(proto) + l.Push(lfunc) + if err := l.PCall(0, lua.MultRet, nil); err != nil { + // ensure we wrap with the correct format. + return fmt.Errorf(errLuaParseError(err)) + } + + return nil +} + +func compile(script string) (*lua.FunctionProto, error) { + if val, ok := parsedScripts.Load(script); ok { + return val.(*lua.FunctionProto), nil + } + chunk, err := parse.Parse(strings.NewReader(script), "<string>") + if err != nil { + return nil, err + } + proto, err := lua.Compile(chunk, "") + if err != nil { + return nil, err + } + parsedScripts.Store(script, proto) + return proto, nil +} + +func (m *Miniredis) cmdEval(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 + } + ctx := getCtx(c) + if ctx.nested { + c.WriteError(msgNotFromScripts(ctx.nestedSHA)) + return + } + + script, args := args[0], args[1:] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + sha := sha1Hex(script) + ok := m.runLuaScript(c, sha, script, args) + if ok { + m.scripts[sha] = script + } + }) +} + +func (m *Miniredis) cmdEvalsha(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 + } + ctx := getCtx(c) + if ctx.nested { + c.WriteError(msgNotFromScripts(ctx.nestedSHA)) + return + } + + sha, args := args[0], args[1:] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + script, ok := m.scripts[sha] + if !ok { + c.WriteError(msgNoScriptFound) + return + } + + m.runLuaScript(c, sha, script, args) + }) +} + +func (m *Miniredis) cmdScript(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 + } + + ctx := getCtx(c) + if ctx.nested { + c.WriteError(msgNotFromScripts(ctx.nestedSHA)) + return + } + + var opts struct { + subcmd string + script string + } + + opts.subcmd, args = args[0], args[1:] + + switch strings.ToLower(opts.subcmd) { + case "load": + if len(args) != 1 { + setDirty(c) + c.WriteError(fmt.Sprintf(msgFScriptUsage, "LOAD")) + return + } + opts.script = args[0] + case "exists": + if len(args) == 0 { + setDirty(c) + c.WriteError(errWrongNumber("script|exists")) + return + } + case "flush": + if len(args) == 1 { + switch strings.ToUpper(args[0]) { + case "SYNC", "ASYNC": + args = args[1:] + default: + } + } + if len(args) != 0 { + setDirty(c) + c.WriteError(msgScriptFlush) + return + } + default: + setDirty(c) + c.WriteError(fmt.Sprintf(msgFScriptUsageSimple, strings.ToUpper(opts.subcmd))) + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + switch strings.ToLower(opts.subcmd) { + case "load": + if _, err := parse.Parse(strings.NewReader(opts.script), "user_script"); err != nil { + c.WriteError(errLuaParseError(err)) + return + } + sha := sha1Hex(opts.script) + m.scripts[sha] = opts.script + c.WriteBulk(sha) + case "exists": + c.WriteLen(len(args)) + for _, arg := range args { + if _, ok := m.scripts[arg]; ok { + c.WriteInt(1) + } else { + c.WriteInt(0) + } + } + case "flush": + m.scripts = map[string]string{} + c.WriteOK() + } + }) +} + +func sha1Hex(s string) string { + h := sha1.New() + io.WriteString(h, s) + return hex.EncodeToString(h.Sum(nil)) +} + +// requireGlobal imports module modName into the global namespace with the +// identifier id. panics if an error results from the function execution +func requireGlobal(l *lua.LState, id, modName string) { + if err := l.CallByParam(lua.P{ + Fn: l.GetGlobal("require"), + NRet: 1, + Protect: true, + }, lua.LString(modName)); err != nil { + panic(err) + } + mod := l.Get(-1) + l.Pop(1) + + l.SetGlobal(id, mod) +} + +// the following script protects globals +// it is based on: http://metalua.luaforge.net/src/lib/strict.lua.html +var protectGlobals = ` +local dbg=debug +local mt = {} +setmetatable(_G, mt) +mt.__newindex = function (t, n, v) + if dbg.getinfo(2) then + local w = dbg.getinfo(2, "S").what + if w ~= "C" then + error("Script attempted to create global variable '"..tostring(n).."'", 2) + end + end + rawset(t, n, v) +end +mt.__index = function (t, n) + if dbg.getinfo(2) and dbg.getinfo(2, "S").what ~= "C" then + error("Script attempted to access nonexistent global variable '"..tostring(n).."'", 2) + end + return rawget(t, n) +end +debug = nil + +`
vendor/github.com/alicebob/miniredis/v2/cmd_server.go+177 −0 added@@ -0,0 +1,177 @@ +// Commands from https://redis.io/commands#server + +package miniredis + +import ( + "fmt" + "strconv" + "strings" + + "github.com/alicebob/miniredis/v2/server" + "github.com/alicebob/miniredis/v2/size" +) + +func commandsServer(m *Miniredis) { + m.srv.Register("COMMAND", m.cmdCommand) + m.srv.Register("DBSIZE", m.cmdDbsize) + m.srv.Register("FLUSHALL", m.cmdFlushall) + m.srv.Register("FLUSHDB", m.cmdFlushdb) + m.srv.Register("INFO", m.cmdInfo) + m.srv.Register("TIME", m.cmdTime) + m.srv.Register("MEMORY", m.cmdMemory) +} + +// MEMORY +func (m *Miniredis) cmdMemory(c *server.Peer, cmd string, args []string) { + if len(args) == 0 { + 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) + + cmd, args := strings.ToLower(args[0]), args[1:] + switch cmd { + case "usage": + if len(args) < 1 { + setDirty(c) + c.WriteError(errWrongNumber("memory|usage")) + return + } + if len(args) > 1 { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + + var ( + value interface{} + ok bool + ) + switch db.keys[args[0]] { + case keyTypeString: + value, ok = db.stringKeys[args[0]] + case keyTypeSet: + value, ok = db.setKeys[args[0]] + case keyTypeHash: + value, ok = db.hashKeys[args[0]] + case keyTypeList: + value, ok = db.listKeys[args[0]] + case keyTypeHll: + value, ok = db.hllKeys[args[0]] + case keyTypeSortedSet: + value, ok = db.sortedsetKeys[args[0]] + case keyTypeStream: + value, ok = db.streamKeys[args[0]] + } + if !ok { + c.WriteNull() + return + } + c.WriteInt(size.Of(value)) + default: + c.WriteError(fmt.Sprintf(msgMemorySubcommand, strings.ToUpper(cmd))) + } + }) +} + +// DBSIZE +func (m *Miniredis) cmdDbsize(c *server.Peer, cmd string, args []string) { + if len(args) > 0 { + 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.WriteInt(len(db.keys)) + }) +} + +// FLUSHALL +func (m *Miniredis) cmdFlushall(c *server.Peer, cmd string, args []string) { + if len(args) > 0 && strings.ToLower(args[0]) == "async" { + args = args[1:] + } + if len(args) > 0 { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + m.flushAll() + c.WriteOK() + }) +} + +// FLUSHDB +func (m *Miniredis) cmdFlushdb(c *server.Peer, cmd string, args []string) { + if len(args) > 0 && strings.ToLower(args[0]) == "async" { + args = args[1:] + } + if len(args) > 0 { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + m.db(ctx.selectedDB).flush() + c.WriteOK() + }) +} + +// TIME +func (m *Miniredis) cmdTime(c *server.Peer, cmd string, args []string) { + if len(args) > 0 { + 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) { + now := m.effectiveNow() + nanos := now.UnixNano() + seconds := nanos / 1_000_000_000 + microseconds := (nanos / 1_000) % 1_000_000 + + c.WriteLen(2) + c.WriteBulk(strconv.FormatInt(seconds, 10)) + c.WriteBulk(strconv.FormatInt(microseconds, 10)) + }) +}
vendor/github.com/alicebob/miniredis/v2/cmd_set.go+836 −0 added@@ -0,0 +1,836 @@ +// Commands from https://redis.io/commands#set + +package miniredis + +import ( + "fmt" + "strconv" + "strings" + + "github.com/alicebob/miniredis/v2/server" +) + +// commandsSet handles all set value operations. +func commandsSet(m *Miniredis) { + m.srv.Register("SADD", m.cmdSadd) + m.srv.Register("SCARD", m.cmdScard) + m.srv.Register("SDIFF", m.cmdSdiff) + m.srv.Register("SDIFFSTORE", m.cmdSdiffstore) + m.srv.Register("SINTERCARD", m.cmdSintercard) + m.srv.Register("SINTER", m.cmdSinter) + m.srv.Register("SINTERSTORE", m.cmdSinterstore) + m.srv.Register("SISMEMBER", m.cmdSismember) + m.srv.Register("SMEMBERS", m.cmdSmembers) + m.srv.Register("SMISMEMBER", m.cmdSmismember) + m.srv.Register("SMOVE", m.cmdSmove) + m.srv.Register("SPOP", m.cmdSpop) + m.srv.Register("SRANDMEMBER", m.cmdSrandmember) + m.srv.Register("SREM", m.cmdSrem) + m.srv.Register("SUNION", m.cmdSunion) + m.srv.Register("SUNIONSTORE", m.cmdSunionstore) + m.srv.Register("SSCAN", m.cmdSscan) +} + +// SADD +func (m *Miniredis) cmdSadd(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, elems := args[0], args[1:] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if db.exists(key) && db.t(key) != keyTypeSet { + c.WriteError(ErrWrongType.Error()) + return + } + + added := db.setAdd(key, elems...) + c.WriteInt(added) + }) +} + +// SCARD +func (m *Miniredis) cmdScard(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.WriteInt(0) + return + } + + if db.t(key) != "set" { + c.WriteError(ErrWrongType.Error()) + return + } + + members := db.setMembers(key) + c.WriteInt(len(members)) + }) +} + +// SDIFF +func (m *Miniredis) cmdSdiff(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 + } + + keys := args + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + set, err := db.setDiff(keys) + if err != nil { + c.WriteError(err.Error()) + return + } + + c.WriteSetLen(len(set)) + for k := range set { + c.WriteBulk(k) + } + }) +} + +// SDIFFSTORE +func (m *Miniredis) cmdSdiffstore(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 + } + + dest, keys := args[0], args[1:] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + set, err := db.setDiff(keys) + if err != nil { + c.WriteError(err.Error()) + return + } + + db.del(dest, true) + db.setSet(dest, set) + c.WriteInt(len(set)) + }) +} + +// SINTER +func (m *Miniredis) cmdSinter(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 + } + + keys := args + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + set, err := db.setInter(keys) + if err != nil { + c.WriteError(err.Error()) + return + } + + c.WriteLen(len(set)) + for k := range set { + c.WriteBulk(k) + } + }) +} + +// SINTERSTORE +func (m *Miniredis) cmdSinterstore(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 + } + + dest, keys := args[0], args[1:] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + set, err := db.setInter(keys) + if err != nil { + c.WriteError(err.Error()) + return + } + + db.del(dest, true) + db.setSet(dest, set) + c.WriteInt(len(set)) + }) +} + +// SINTERCARD +func (m *Miniredis) cmdSintercard(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 + } + + opts := struct { + keys []string + limit int + }{} + + numKeys, err := strconv.Atoi(args[0]) + if err != nil { + setDirty(c) + c.WriteError("ERR numkeys should be greater than 0") + return + } + if numKeys < 1 { + setDirty(c) + c.WriteError("ERR numkeys should be greater than 0") + return + } + + args = args[1:] + if len(args) < numKeys { + setDirty(c) + c.WriteError("ERR Number of keys can't be greater than number of args") + return + } + opts.keys = args[:numKeys] + + args = args[numKeys:] + if len(args) == 2 && strings.ToLower(args[0]) == "limit" { + l, err := strconv.Atoi(args[1]) + if err != nil { + setDirty(c) + c.WriteError(msgInvalidInt) + return + } + if l < 0 { + setDirty(c) + c.WriteError(msgLimitIsNegative) + return + } + opts.limit = l + } else if len(args) > 0 { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + count, err := db.setIntercard(opts.keys, opts.limit) + if err != nil { + c.WriteError(err.Error()) + return + } + c.WriteInt(count) + }) +} + +// SISMEMBER +func (m *Miniredis) cmdSismember(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 !db.exists(key) { + c.WriteInt(0) + return + } + + if db.t(key) != "set" { + c.WriteError(ErrWrongType.Error()) + return + } + + if db.setIsMember(key, value) { + c.WriteInt(1) + return + } + c.WriteInt(0) + }) +} + +// SMEMBERS +func (m *Miniredis) cmdSmembers(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.WriteSetLen(0) + return + } + + if db.t(key) != "set" { + c.WriteError(ErrWrongType.Error()) + return + } + + members := db.setMembers(key) + + c.WriteSetLen(len(members)) + for _, elem := range members { + c.WriteBulk(elem) + } + }) +} + +// SMISMEMBER +func (m *Miniredis) cmdSmismember(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, values := args[0], args[1:] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if !db.exists(key) { + c.WriteLen(len(values)) + for range values { + c.WriteInt(0) + } + return + } + + if db.t(key) != "set" { + c.WriteError(ErrWrongType.Error()) + return + } + + c.WriteLen(len(values)) + for _, value := range values { + if db.setIsMember(key, value) { + c.WriteInt(1) + } else { + c.WriteInt(0) + } + } + return + }) +} + +// SMOVE +func (m *Miniredis) cmdSmove(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 + } + + src, dst, member := args[0], args[1], args[2] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if !db.exists(src) { + c.WriteInt(0) + return + } + + if db.t(src) != "set" { + c.WriteError(ErrWrongType.Error()) + return + } + + if db.exists(dst) && db.t(dst) != "set" { + c.WriteError(ErrWrongType.Error()) + return + } + + if !db.setIsMember(src, member) { + c.WriteInt(0) + return + } + db.setRem(src, member) + db.setAdd(dst, member) + c.WriteInt(1) + }) +} + +// SPOP +func (m *Miniredis) cmdSpop(c *server.Peer, cmd string, args []string) { + if len(args) == 0 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + opts := struct { + key string + withCount bool + count int + }{ + count: 1, + } + opts.key, args = args[0], args[1:] + + if len(args) > 0 { + v, err := strconv.Atoi(args[0]) + if err != nil { + setDirty(c) + c.WriteError(msgInvalidInt) + return + } + if v < 0 { + setDirty(c) + c.WriteError(msgOutOfRange) + return + } + opts.count = v + opts.withCount = true + args = args[1:] + } + if len(args) > 0 { + setDirty(c) + c.WriteError(msgInvalidInt) + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if !db.exists(opts.key) { + if !opts.withCount { + c.WriteNull() + return + } + c.WriteLen(0) + return + } + + if db.t(opts.key) != "set" { + c.WriteError(ErrWrongType.Error()) + return + } + + var deleted []string + members := db.setMembers(opts.key) + for i := 0; i < opts.count; i++ { + if len(members) == 0 { + break + } + i := m.randIntn(len(members)) + member := members[i] + members = delElem(members, i) + db.setRem(opts.key, member) + deleted = append(deleted, member) + } + // without `count` return a single value + if !opts.withCount { + if len(deleted) == 0 { + c.WriteNull() + return + } + c.WriteBulk(deleted[0]) + return + } + // with `count` return a list + c.WriteLen(len(deleted)) + for _, v := range deleted { + c.WriteBulk(v) + } + }) +} + +// SRANDMEMBER +func (m *Miniredis) cmdSrandmember(c *server.Peer, cmd string, args []string) { + if len(args) < 1 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if len(args) > 2 { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + key := args[0] + count := 0 + withCount := false + if len(args) == 2 { + var err error + count, err = strconv.Atoi(args[1]) + if err != nil { + setDirty(c) + c.WriteError(msgInvalidInt) + return + } + withCount = true + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if !db.exists(key) { + if withCount { + c.WriteLen(0) + return + } + c.WriteNull() + return + } + + if db.t(key) != "set" { + c.WriteError(ErrWrongType.Error()) + return + } + + members := db.setMembers(key) + if count < 0 { + // Non-unique elements is allowed with negative count. + c.WriteLen(-count) + for count != 0 { + member := members[m.randIntn(len(members))] + c.WriteBulk(member) + count++ + } + return + } + + // Must be unique elements. + m.shuffle(members) + if count > len(members) { + count = len(members) + } + if !withCount { + c.WriteBulk(members[0]) + return + } + c.WriteLen(count) + for i := range make([]struct{}, count) { + c.WriteBulk(members[i]) + } + }) +} + +// SREM +func (m *Miniredis) cmdSrem(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, fields := args[0], args[1:] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if !db.exists(key) { + c.WriteInt(0) + return + } + + if db.t(key) != "set" { + c.WriteError(ErrWrongType.Error()) + return + } + + c.WriteInt(db.setRem(key, fields...)) + }) +} + +// SUNION +func (m *Miniredis) cmdSunion(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 + } + + keys := args + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + set, err := db.setUnion(keys) + if err != nil { + c.WriteError(err.Error()) + return + } + + c.WriteLen(len(set)) + for k := range set { + c.WriteBulk(k) + } + }) +} + +// SUNIONSTORE +func (m *Miniredis) cmdSunionstore(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 + } + + dest, keys := args[0], args[1:] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + set, err := db.setUnion(keys) + if err != nil { + c.WriteError(err.Error()) + return + } + + db.del(dest, true) + db.setSet(dest, set) + c.WriteInt(len(set)) + }) +} + +// SSCAN +func (m *Miniredis) cmdSscan(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 int + cursor int + count int + withMatch bool + match string + } + + opts.key = args[0] + if ok := optIntErr(c, args[1], &opts.cursor, msgInvalidCursor); !ok { + return + } + args = args[2:] + + // MATCH and COUNT options + for len(args) > 0 { + if strings.ToLower(args[0]) == "count" { + if len(args) < 2 { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + count, err := strconv.Atoi(args[1]) + if err != nil || count < 0 { + setDirty(c) + c.WriteError(msgInvalidInt) + return + } + if count == 0 { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + opts.count = count + args = args[2:] + continue + } + if strings.ToLower(args[0]) == "match" { + if len(args) < 2 { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + opts.withMatch = true + opts.match = args[1] + args = args[2:] + continue + } + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + // return _all_ (matched) keys every time + if db.exists(opts.key) && db.t(opts.key) != "set" { + c.WriteError(ErrWrongType.Error()) + return + } + members := db.setMembers(opts.key) + if opts.withMatch { + members, _ = matchKeys(members, opts.match) + } + low := opts.cursor + high := low + opts.count + // validate high is correct + if high > len(members) || high == 0 { + high = len(members) + } + if opts.cursor > high { + // invalid cursor + c.WriteLen(2) + c.WriteBulk("0") // no next cursor + c.WriteLen(0) // no elements + return + } + cursorValue := low + opts.count + if cursorValue > len(members) { + cursorValue = 0 // no next cursor + } + members = members[low:high] + c.WriteLen(2) + c.WriteBulk(fmt.Sprintf("%d", cursorValue)) + c.WriteLen(len(members)) + for _, k := range members { + c.WriteBulk(k) + } + + }) +} + +func delElem(ls []string, i int) []string { + // this swap+truncate is faster but changes behaviour: + // ls[i] = ls[len(ls)-1] + // ls = ls[:len(ls)-1] + // so we do the dumb thing: + ls = append(ls[:i], ls[i+1:]...) + return ls +}
vendor/github.com/alicebob/miniredis/v2/cmd_sorted_set.go+2025 −0 added@@ -0,0 +1,2025 @@ +// Commands from https://redis.io/commands#sorted_set + +package miniredis + +import ( + "errors" + "fmt" + "math" + "sort" + "strconv" + "strings" + + "github.com/alicebob/miniredis/v2/server" +) + +// commandsSortedSet handles all sorted set operations. +func commandsSortedSet(m *Miniredis) { + m.srv.Register("ZADD", m.cmdZadd) + m.srv.Register("ZCARD", m.cmdZcard) + m.srv.Register("ZCOUNT", m.cmdZcount) + m.srv.Register("ZINCRBY", m.cmdZincrby) + m.srv.Register("ZINTER", m.makeCmdZinter(false)) + m.srv.Register("ZINTERSTORE", m.makeCmdZinter(true)) + m.srv.Register("ZLEXCOUNT", m.cmdZlexcount) + m.srv.Register("ZRANGE", m.cmdZrange) + m.srv.Register("ZRANGEBYLEX", m.makeCmdZrangebylex(false)) + m.srv.Register("ZRANGEBYSCORE", m.makeCmdZrangebyscore(false)) + m.srv.Register("ZRANK", m.makeCmdZrank(false)) + m.srv.Register("ZREM", m.cmdZrem) + m.srv.Register("ZREMRANGEBYLEX", m.cmdZremrangebylex) + m.srv.Register("ZREMRANGEBYRANK", m.cmdZremrangebyrank) + m.srv.Register("ZREMRANGEBYSCORE", m.cmdZremrangebyscore) + m.srv.Register("ZREVRANGE", m.cmdZrevrange) + m.srv.Register("ZREVRANGEBYLEX", m.makeCmdZrangebylex(true)) + m.srv.Register("ZREVRANGEBYSCORE", m.makeCmdZrangebyscore(true)) + m.srv.Register("ZREVRANK", m.makeCmdZrank(true)) + m.srv.Register("ZSCORE", m.cmdZscore) + m.srv.Register("ZMSCORE", m.cmdZMscore) + m.srv.Register("ZUNION", m.cmdZunion) + m.srv.Register("ZUNIONSTORE", m.cmdZunionstore) + m.srv.Register("ZSCAN", m.cmdZscan) + m.srv.Register("ZPOPMAX", m.cmdZpopmax(true)) + m.srv.Register("ZPOPMIN", m.cmdZpopmax(false)) + m.srv.Register("ZRANDMEMBER", m.cmdZrandmember) +} + +// ZADD +func (m *Miniredis) cmdZadd(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 + nx bool + xx bool + gt bool + lt bool + ch bool + incr bool + } + elems := map[string]float64{} + + opts.key = args[0] + args = args[1:] +outer: + for len(args) > 0 { + switch strings.ToUpper(args[0]) { + case "NX": + opts.nx = true + args = args[1:] + continue + case "XX": + opts.xx = true + args = args[1:] + continue + case "GT": + opts.gt = true + args = args[1:] + continue + case "LT": + opts.lt = true + args = args[1:] + continue + case "CH": + opts.ch = true + args = args[1:] + continue + case "INCR": + opts.incr = true + args = args[1:] + continue + default: + break outer + } + } + + if len(args) == 0 || len(args)%2 != 0 { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + for len(args) > 0 { + score, err := strconv.ParseFloat(args[0], 64) + if err != nil { + setDirty(c) + c.WriteError(msgInvalidFloat) + return + } + elems[args[1]] = score + args = args[2:] + } + + if opts.xx && opts.nx { + setDirty(c) + c.WriteError(msgXXandNX) + return + } + + if opts.gt && opts.lt || + opts.gt && opts.nx || + opts.lt && opts.nx { + setDirty(c) + c.WriteError(msgGTLTandNX) + return + } + + if opts.incr && len(elems) > 1 { + setDirty(c) + c.WriteError(msgSingleElementPair) + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if db.exists(opts.key) && db.t(opts.key) != keyTypeSortedSet { + c.WriteError(ErrWrongType.Error()) + return + } + + if opts.incr { + for member, delta := range elems { + if opts.nx && db.ssetExists(opts.key, member) { + c.WriteNull() + return + } + if opts.xx && !db.ssetExists(opts.key, member) { + c.WriteNull() + return + } + newScore := db.ssetIncrby(opts.key, member, delta) + c.WriteFloat(newScore) + } + return + } + + res := 0 + for member, score := range elems { + exists := db.ssetExists(opts.key, member) + if opts.nx && exists { + continue + } + if opts.xx && !exists { + continue + } + old := db.ssetScore(opts.key, member) + if opts.gt && exists && score <= old { + continue + } + if opts.lt && exists && score >= old { + continue + } + if db.ssetAdd(opts.key, score, member) { + res++ + } else { + if opts.ch && old != score { + // if 'CH' is specified, only count changed keys + res++ + } + } + } + c.WriteInt(res) + }) +} + +// ZCARD +func (m *Miniredis) cmdZcard(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.WriteInt(0) + return + } + + if db.t(key) != keyTypeSortedSet { + c.WriteError(ErrWrongType.Error()) + return + } + + c.WriteInt(db.ssetCard(key)) + }) +} + +// ZCOUNT +func (m *Miniredis) cmdZcount(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 + min float64 + minIncl bool + max float64 + maxIncl bool + } + err error + ) + + opts.key = args[0] + opts.min, opts.minIncl, err = parseFloatRange(args[1]) + if err != nil { + setDirty(c) + c.WriteError(msgInvalidMinMax) + return + } + opts.max, opts.maxIncl, err = parseFloatRange(args[2]) + if err != nil { + setDirty(c) + c.WriteError(msgInvalidMinMax) + return + } + + 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) != keyTypeSortedSet { + c.WriteError(ErrWrongType.Error()) + return + } + + members := db.ssetElements(opts.key) + members = withSSRange(members, opts.min, opts.minIncl, opts.max, opts.maxIncl) + c.WriteInt(len(members)) + }) +} + +// ZINCRBY +func (m *Miniredis) cmdZincrby(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 + delta float64 + member string + } + + opts.key = args[0] + d, err := strconv.ParseFloat(args[1], 64) + if err != nil { + setDirty(c) + c.WriteError(msgInvalidFloat) + return + } + opts.delta = d + opts.member = args[2] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if db.exists(opts.key) && db.t(opts.key) != keyTypeSortedSet { + c.WriteError(msgWrongType) + return + } + newScore := db.ssetIncrby(opts.key, opts.member, opts.delta) + c.WriteFloat(newScore) + }) +} + +// ZINTERSTORE and ZINTER +func (m *Miniredis) makeCmdZinter(store bool) func(c *server.Peer, cmd string, args []string) { + return func(c *server.Peer, cmd string, args []string) { + minArgs := 2 + if store { + minArgs++ + } + if len(args) < minArgs { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + var opts = struct { + Store bool // if true this is ZINTERSTORE + Destination string // only relevant if $store is true + Keys []string + Aggregate string + WithWeights bool + Weights []float64 + WithScores bool // only for ZINTER + }{ + Store: store, + Aggregate: "sum", + } + + if store { + opts.Destination = args[0] + args = args[1:] + } + numKeys, err := strconv.Atoi(args[0]) + if err != nil { + setDirty(c) + c.WriteError(msgInvalidInt) + return + } + args = args[1:] + if len(args) < numKeys { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + if numKeys <= 0 { + setDirty(c) + c.WriteError("ERR at least 1 input key is needed for ZUNIONSTORE/ZINTERSTORE") + return + } + opts.Keys = args[:numKeys] + args = args[numKeys:] + + for len(args) > 0 { + switch strings.ToLower(args[0]) { + case "weights": + if len(args) < numKeys+1 { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + for i := 0; i < numKeys; i++ { + f, err := strconv.ParseFloat(args[i+1], 64) + if err != nil { + setDirty(c) + c.WriteError("ERR weight value is not a float") + return + } + opts.Weights = append(opts.Weights, f) + } + opts.WithWeights = true + args = args[numKeys+1:] + case "aggregate": + if len(args) < 2 { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + aggregate := strings.ToLower(args[1]) + switch aggregate { + case "sum", "min", "max": + opts.Aggregate = aggregate + default: + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + args = args[2:] + case "withscores": + if store { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + opts.WithScores = true + args = args[1:] + default: + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + // We collect everything and remove all keys which turned out not to be + // present in every set. + sset := map[string]float64{} + counts := map[string]int{} + for i, key := range opts.Keys { + if !db.exists(key) { + continue + } + + var set map[string]float64 + switch db.t(key) { + case keyTypeSet: + set = map[string]float64{} + for elem := range db.setKeys[key] { + set[elem] = 1.0 + } + case keyTypeSortedSet: + set = db.sortedSet(key) + default: + c.WriteError(msgWrongType) + return + } + for member, score := range set { + if opts.WithWeights { + score *= opts.Weights[i] + } + counts[member]++ + old, ok := sset[member] + if !ok { + sset[member] = score + continue + } + switch opts.Aggregate { + default: + panic("Invalid aggregate") + case "sum": + sset[member] += score + case "min": + if score < old { + sset[member] = score + } + case "max": + if score > old { + sset[member] = score + } + } + } + } + for key, count := range counts { + if count != numKeys { + delete(sset, key) + } + } + + if opts.Store { + // ZINTERSTORE mode + db.del(opts.Destination, true) + db.ssetSet(opts.Destination, sset) + c.WriteInt(len(sset)) + return + } + // ZINTER mode + size := len(sset) + if opts.WithScores { + size *= 2 + } + c.WriteLen(size) + for _, l := range sortedKeys(sset) { + c.WriteBulk(l) + if opts.WithScores { + c.WriteFloat(sset[l]) + } + } + }) + } +} + +// ZLEXCOUNT +func (m *Miniredis) cmdZlexcount(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 + Min string + Max string + }{ + Key: args[0], + Min: args[1], + Max: args[2], + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + min, minIncl, minErr := parseLexrange(opts.Min) + max, maxIncl, maxErr := parseLexrange(opts.Max) + if minErr != nil || maxErr != nil { + c.WriteError(msgInvalidRangeItem) + return + } + + db := m.db(ctx.selectedDB) + + if !db.exists(opts.Key) { + c.WriteInt(0) + return + } + + if db.t(opts.Key) != keyTypeSortedSet { + c.WriteError(ErrWrongType.Error()) + return + } + + members := db.ssetMembers(opts.Key) + // Just key sort. If scores are not the same we don't care. + sort.Strings(members) + members = withLexRange(members, min, minIncl, max, maxIncl) + + c.WriteInt(len(members)) + }) +} + +// ZRANGE +func (m *Miniredis) cmdZrange(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 + Min string + Max string + WithScores bool + ByScore bool + ByLex bool + Reverse bool + WithLimit bool + Offset string + Count string + } + + opts.Key, opts.Min, opts.Max = args[0], args[1], args[2] + args = args[3:] + + for len(args) > 0 { + switch strings.ToLower(args[0]) { + case "byscore": + opts.ByScore = true + args = args[1:] + case "bylex": + opts.ByLex = true + args = args[1:] + case "rev": + opts.Reverse = true + args = args[1:] + case "limit": + opts.WithLimit = true + args = args[1:] + if len(args) < 2 { + c.WriteError(msgSyntaxError) + return + } + opts.Offset = args[0] + opts.Count = args[1] + args = args[2:] + case "withscores": + opts.WithScores = true + args = args[1:] + default: + c.WriteError(msgSyntaxError) + return + } + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + switch { + case opts.ByScore && opts.ByLex: + c.WriteError(msgSyntaxError) + case opts.ByScore: + runRangeByScore(m, c, ctx, optsRangeByScore{ + Key: opts.Key, + Min: opts.Min, + Max: opts.Max, + Reverse: opts.Reverse, + WithLimit: opts.WithLimit, + Offset: opts.Offset, + Count: opts.Count, + WithScores: opts.WithScores, + }) + case opts.ByLex: + runRangeByLex(m, c, ctx, optsRangeByLex{ + Key: opts.Key, + Min: opts.Min, + Max: opts.Max, + Reverse: opts.Reverse, + WithLimit: opts.WithLimit, + Offset: opts.Offset, + Count: opts.Count, + WithScores: opts.WithScores, + }) + default: + if opts.WithLimit { + c.WriteError(msgLimitCombination) + return + } + runRange(m, c, ctx, optsRange{ + Key: opts.Key, + Min: opts.Min, + Max: opts.Max, + Reverse: opts.Reverse, + WithScores: opts.WithScores, + }) + } + }) +} + +// ZREVRANGE +func (m *Miniredis) cmdZrevrange(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 = optsRange{ + Reverse: true, + Key: args[0], + Min: args[1], + Max: args[2], + } + args = args[3:] + + for len(args) > 0 { + switch strings.ToLower(args[0]) { + case "withscores": + opts.WithScores = true + args = args[1:] + default: + c.WriteError(msgSyntaxError) + return + } + } + + withTx(m, c, func(c *server.Peer, cctx *connCtx) { + runRange(m, c, cctx, opts) + }) +} + +// ZRANGEBYLEX and ZREVRANGEBYLEX +func (m *Miniredis) makeCmdZrangebylex(reverse bool) server.Cmd { + return func(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 + } + opts := optsRangeByLex{ + Reverse: reverse, + Key: args[0], + Min: args[1], + Max: args[2], + } + args = args[3:] + + for len(args) > 0 { + switch strings.ToLower(args[0]) { + case "limit": + opts.WithLimit = true + args = args[1:] + if len(args) < 2 { + c.WriteError(msgSyntaxError) + return + } + opts.Offset = args[0] + opts.Count = args[1] + args = args[2:] + continue + default: + // Syntax error + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + } + + withTx(m, c, func(c *server.Peer, cctx *connCtx) { + runRangeByLex(m, c, cctx, opts) + }) + } +} + +// ZRANGEBYSCORE and ZREVRANGEBYSCORE +func (m *Miniredis) makeCmdZrangebyscore(reverse bool) server.Cmd { + return func(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 = optsRangeByScore{ + Reverse: reverse, + Key: args[0], + Min: args[1], + Max: args[2], + } + args = args[3:] + + for len(args) > 0 { + if strings.ToLower(args[0]) == "limit" { + opts.WithLimit = true + args = args[1:] + if len(args) < 2 { + c.WriteError(msgSyntaxError) + return + } + opts.Offset = args[0] + opts.Count = args[1] + args = args[2:] + continue + } + if strings.ToLower(args[0]) == "withscores" { + opts.WithScores = true + args = args[1:] + continue + } + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + + withTx(m, c, func(c *server.Peer, cctx *connCtx) { + runRangeByScore(m, c, cctx, opts) + }) + } +} + +// ZRANK and ZREVRANK +func (m *Miniredis) makeCmdZrank(reverse bool) server.Cmd { + return func(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, member := args[0], args[1] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + withScore := false + if len(args) > 0 && strings.ToUpper(args[len(args)-1]) == "WITHSCORE" { + withScore = true + args = args[:len(args)-1] + } + + if len(args) > 2 { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + + if !db.exists(key) { + if withScore { + c.WriteLen(-1) + } else { + c.WriteNull() + } + return + } + + if db.t(key) != keyTypeSortedSet { + c.WriteError(ErrWrongType.Error()) + return + } + + direction := asc + if reverse { + direction = desc + } + rank, ok := db.ssetRank(key, member, direction) + if !ok { + if withScore { + c.WriteLen(-1) + } else { + c.WriteNull() + } + return + } + + if withScore { + c.WriteLen(2) + c.WriteInt(rank) + c.WriteFloat(db.ssetScore(key, member)) + } else { + c.WriteInt(rank) + } + }) + } +} + +// ZREM +func (m *Miniredis) cmdZrem(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, members := args[0], args[1:] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if !db.exists(key) { + c.WriteInt(0) + return + } + + if db.t(key) != keyTypeSortedSet { + c.WriteError(ErrWrongType.Error()) + return + } + + deleted := 0 + for _, member := range members { + if db.ssetRem(key, member) { + deleted++ + } + } + c.WriteInt(deleted) + }) +} + +// ZREMRANGEBYLEX +func (m *Miniredis) cmdZremrangebylex(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 + Min string + Max string + }{ + Key: args[0], + Min: args[1], + Max: args[2], + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + min, minIncl, minErr := parseLexrange(opts.Min) + max, maxIncl, maxErr := parseLexrange(opts.Max) + if minErr != nil || maxErr != nil { + c.WriteError(msgInvalidRangeItem) + return + } + + db := m.db(ctx.selectedDB) + + if !db.exists(opts.Key) { + c.WriteInt(0) + return + } + + if db.t(opts.Key) != keyTypeSortedSet { + c.WriteError(ErrWrongType.Error()) + return + } + + members := db.ssetMembers(opts.Key) + // Just key sort. If scores are not the same we don't care. + sort.Strings(members) + members = withLexRange(members, min, minIncl, max, maxIncl) + + for _, el := range members { + db.ssetRem(opts.Key, el) + } + c.WriteInt(len(members)) + }) +} + +// ZREMRANGEBYRANK +func (m *Miniredis) cmdZremrangebyrank(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 !db.exists(opts.key) { + c.WriteInt(0) + return + } + + if db.t(opts.key) != keyTypeSortedSet { + c.WriteError(ErrWrongType.Error()) + return + } + + members := db.ssetMembers(opts.key) + rs, re := redisRange(len(members), opts.start, opts.end, false) + for _, el := range members[rs:re] { + db.ssetRem(opts.key, el) + } + c.WriteInt(re - rs) + }) +} + +// ZREMRANGEBYSCORE +func (m *Miniredis) cmdZremrangebyscore(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 + min float64 + minIncl bool + max float64 + maxIncl bool + } + err error + ) + opts.key = args[0] + opts.min, opts.minIncl, err = parseFloatRange(args[1]) + if err != nil { + setDirty(c) + c.WriteError(msgInvalidMinMax) + return + } + opts.max, opts.maxIncl, err = parseFloatRange(args[2]) + if err != nil { + setDirty(c) + c.WriteError(msgInvalidMinMax) + return + } + + 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) != keyTypeSortedSet { + c.WriteError(ErrWrongType.Error()) + return + } + + members := db.ssetElements(opts.key) + members = withSSRange(members, opts.min, opts.minIncl, opts.max, opts.maxIncl) + + for _, el := range members { + db.ssetRem(opts.key, el.member) + } + c.WriteInt(len(members)) + }) +} + +// ZSCORE +func (m *Miniredis) cmdZscore(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, member := args[0], args[1] + + 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) != keyTypeSortedSet { + c.WriteError(ErrWrongType.Error()) + return + } + + if !db.ssetExists(key, member) { + c.WriteNull() + return + } + + c.WriteFloat(db.ssetScore(key, member)) + }) +} + +// ZMSCORE +func (m *Miniredis) cmdZMscore(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, members := args[0], args[1:] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if !db.exists(key) { + c.WriteLen(len(members)) + for range members { + c.WriteNull() + } + return + } + + if db.t(key) != keyTypeSortedSet { + c.WriteError(ErrWrongType.Error()) + return + } + + c.WriteLen(len(members)) + for _, member := range members { + if !db.ssetExists(key, member) { + c.WriteNull() + continue + } + c.WriteFloat(db.ssetScore(key, member)) + } + }) +} + +// parseFloatRange handles ZRANGEBYSCORE floats. They are inclusive unless the +// string starts with '(' +func parseFloatRange(s string) (float64, bool, error) { + if len(s) == 0 { + return 0, false, nil + } + inclusive := true + if s[0] == '(' { + s = s[1:] + inclusive = false + } + switch strings.ToLower(s) { + case "+inf": + return math.Inf(+1), true, nil + case "-inf": + return math.Inf(-1), true, nil + default: + f, err := strconv.ParseFloat(s, 64) + return f, inclusive, err + } +} + +// withSSRange limits a list of sorted set elements by the ZRANGEBYSCORE range +// logic. +func withSSRange(members ssElems, min float64, minIncl bool, max float64, maxIncl bool) ssElems { + gt := func(a, b float64) bool { return a > b } + gteq := func(a, b float64) bool { return a >= b } + + mincmp := gt + if minIncl { + mincmp = gteq + } + for i, m := range members { + if mincmp(m.score, min) { + members = members[i:] + goto checkmax + } + } + // all elements were smaller + return nil + +checkmax: + maxcmp := gteq + if maxIncl { + maxcmp = gt + } + for i, m := range members { + if maxcmp(m.score, max) { + members = members[:i] + break + } + } + + return members +} + +// withLexRange limits a list of sorted set elements. +func withLexRange(members []string, min string, minIncl bool, max string, maxIncl bool) []string { + if max == "-" || min == "+" { + return nil + } + if min != "-" { + found := false + if minIncl { + for i, m := range members { + if m >= min { + members = members[i:] + found = true + break + } + } + } else { + // Excluding min + for i, m := range members { + if m > min { + members = members[i:] + found = true + break + } + } + } + if !found { + return nil + } + } + if max != "+" { + if maxIncl { + for i, m := range members { + if m > max { + members = members[:i] + break + } + } + } else { + // Excluding max + for i, m := range members { + if m >= max { + members = members[:i] + break + } + } + } + } + return members +} + +// ZUNION +func (m *Miniredis) cmdZunion(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 + } + + numKeys, err := strconv.Atoi(args[0]) + if err != nil { + setDirty(c) + c.WriteError(msgInvalidInt) + return + } + args = args[1:] + if len(args) < numKeys { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + if numKeys <= 0 { + setDirty(c) + c.WriteError("ERR at least 1 input key is needed for ZUNION") + return + } + keys := args[:numKeys] + args = args[numKeys:] + + withScores := false + if len(args) > 0 && strings.ToUpper(args[len(args)-1]) == "WITHSCORES" { + withScores = true + args = args[:len(args)-1] + } + + opts := zunionOptions{ + Keys: keys, + WithWeights: false, + Weights: []float64{}, + Aggregate: "sum", + } + + if err := opts.parseArgs(args, numKeys); err != nil { + setDirty(c) + c.WriteError(err.Error()) + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + sset, err := executeZUnion(db, opts) + if err != nil { + c.WriteError(err.Error()) + return + } + + if withScores { + c.WriteLen(len(sset) * 2) + } else { + c.WriteLen(len(sset)) + } + for _, el := range sset.byScore(asc) { + c.WriteBulk(el.member) + if withScores { + c.WriteFloat(el.score) + } + } + }) +} + +// ZUNIONSTORE +func (m *Miniredis) cmdZunionstore(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 + } + + destination := args[0] + numKeys, err := strconv.Atoi(args[1]) + if err != nil { + setDirty(c) + c.WriteError(msgInvalidInt) + return + } + args = args[2:] + if len(args) < numKeys { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + if numKeys <= 0 { + setDirty(c) + c.WriteError("ERR at least 1 input key is needed for ZUNIONSTORE/ZINTERSTORE") + return + } + keys := args[:numKeys] + args = args[numKeys:] + + opts := zunionOptions{ + Keys: keys, + WithWeights: false, + Weights: []float64{}, + Aggregate: "sum", + } + + if err := opts.parseArgs(args, numKeys); err != nil { + setDirty(c) + c.WriteError(err.Error()) + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + deleteDest := true + for _, key := range keys { + if destination == key { + deleteDest = false + } + } + if deleteDest { + db.del(destination, true) + } + + sset, err := executeZUnion(db, opts) + if err != nil { + c.WriteError(err.Error()) + return + } + db.ssetSet(destination, sset) + c.WriteInt(sset.card()) + }) +} + +type zunionOptions struct { + Keys []string + WithWeights bool + Weights []float64 + Aggregate string +} + +func (opts *zunionOptions) parseArgs(args []string, numKeys int) error { + for len(args) > 0 { + switch strings.ToLower(args[0]) { + case "weights": + if len(args) < numKeys+1 { + return errors.New(msgSyntaxError) + } + for i := 0; i < numKeys; i++ { + f, err := strconv.ParseFloat(args[i+1], 64) + if err != nil { + return errors.New("ERR weight value is not a float") + } + opts.Weights = append(opts.Weights, f) + } + opts.WithWeights = true + args = args[numKeys+1:] + case "aggregate": + if len(args) < 2 { + return errors.New(msgSyntaxError) + } + opts.Aggregate = strings.ToLower(args[1]) + switch opts.Aggregate { + default: + return errors.New(msgSyntaxError) + case "sum", "min", "max": + } + args = args[2:] + default: + return errors.New(msgSyntaxError) + } + } + return nil +} + +func executeZUnion(db *RedisDB, opts zunionOptions) (sortedSet, error) { + sset := sortedSet{} + for i, key := range opts.Keys { + if !db.exists(key) { + continue + } + + var set map[string]float64 + switch db.t(key) { + case keyTypeSet: + set = map[string]float64{} + for elem := range db.setKeys[key] { + set[elem] = 1.0 + } + case keyTypeSortedSet: + set = db.sortedSet(key) + default: + return nil, errors.New(msgWrongType) + } + + for member, score := range set { + if opts.WithWeights { + score *= opts.Weights[i] + } + old, ok := sset[member] + if !ok { + sset[member] = score + continue + } + switch opts.Aggregate { + default: + panic("Invalid aggregate") + case "sum": + sset[member] += score + case "min": + if score < old { + sset[member] = score + } + case "max": + if score > old { + sset[member] = score + } + } + } + } + + return sset, nil +} + +// ZSCAN +func (m *Miniredis) cmdZscan(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 + cursor int + count int + withMatch bool + match string + } + + opts.key = args[0] + if ok := optIntErr(c, args[1], &opts.cursor, msgInvalidCursor); !ok { + return + } + args = args[2:] + // MATCH and COUNT options + for len(args) > 0 { + if strings.ToLower(args[0]) == "count" { + if len(args) < 2 { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + count, err := strconv.Atoi(args[1]) + if err != nil { + setDirty(c) + c.WriteError(msgInvalidInt) + return + } + if count <= 0 { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + opts.count = count + args = args[2:] + continue + } + if strings.ToLower(args[0]) == "match" { + if len(args) < 2 { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + opts.withMatch = true + opts.match = args[1] + args = args[2:] + continue + } + 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) && db.t(opts.key) != keyTypeSortedSet { + c.WriteError(ErrWrongType.Error()) + return + } + + members := db.ssetMembers(opts.key) + if opts.withMatch { + members, _ = matchKeys(members, opts.match) + } + + low := opts.cursor + high := low + opts.count + // validate high is correct + if high > len(members) || high == 0 { + high = len(members) + } + if opts.cursor > high { + // invalid cursor + c.WriteLen(2) + c.WriteBulk("0") // no next cursor + c.WriteLen(0) // no elements + return + } + cursorValue := low + opts.count + if cursorValue >= len(members) { + cursorValue = 0 // no next cursor + } + members = members[low:high] + + c.WriteLen(2) + c.WriteBulk(fmt.Sprintf("%d", cursorValue)) + // HSCAN gives key, values. + c.WriteLen(len(members) * 2) + for _, k := range members { + c.WriteBulk(k) + c.WriteFloat(db.ssetScore(opts.key, k)) + } + }) +} + +// ZPOPMAX and ZPOPMIN +func (m *Miniredis) cmdZpopmax(reverse bool) server.Cmd { + return func(c *server.Peer, cmd string, args []string) { + if len(args) < 1 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + + key := args[0] + count := 1 + var err error + if len(args) > 1 { + count, err = strconv.Atoi(args[1]) + if err != nil || count < 0 { + setDirty(c) + c.WriteError(msgInvalidRange) + return + } + } + + withScores := true + if len(args) > 2 { + c.WriteError(msgSyntaxError) + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if !db.exists(key) { + c.WriteLen(0) + return + } + + if db.t(key) != keyTypeSortedSet { + c.WriteError(ErrWrongType.Error()) + return + } + + members := db.ssetMembers(key) + if reverse { + reverseSlice(members) + } + rs, re := redisRange(len(members), 0, count-1, false) + if withScores { + c.WriteLen((re - rs) * 2) + } else { + c.WriteLen(re - rs) + } + for _, el := range members[rs:re] { + c.WriteBulk(el) + if withScores { + c.WriteFloat(db.ssetScore(key, el)) + } + db.ssetRem(key, el) + } + }) + } +} + +// ZRANDMEMBER +func (m *Miniredis) cmdZrandmember(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 + withCount bool + count int + withScores bool + } + + opts.key = args[0] + args = args[1:] + + if len(args) > 0 { + // can be negative + if ok := optInt(c, args[0], &opts.count); !ok { + return + } + opts.withCount = true + args = args[1:] + } + + if len(args) > 0 && strings.ToUpper(args[0]) == "WITHSCORES" { + opts.withScores = true + args = args[1:] + } + + if len(args) > 0 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if !db.exists(opts.key) { + if opts.withCount { + c.WriteLen(0) + } else { + c.WriteNull() + } + return + } + + if db.t(opts.key) != keyTypeSortedSet { + c.WriteError(ErrWrongType.Error()) + return + } + + if !opts.withCount { + member := db.ssetRandomMember(opts.key) + if member == "" { + c.WriteNull() + return + } + c.WriteBulk(member) + return + } + + var members []string + switch { + case opts.count == 0: + c.WriteStrings(nil) + return + case opts.count > 0: + allMembers := db.ssetMembers(opts.key) + db.master.shuffle(allMembers) + if len(allMembers) > opts.count { + allMembers = allMembers[:opts.count] + } + members = allMembers + case opts.count < 0: + for i := 0; i < -opts.count; i++ { + members = append(members, db.ssetRandomMember(opts.key)) + } + } + if opts.withScores { + c.WriteLen(len(members) * 2) + for _, m := range members { + c.WriteBulk(m) + c.WriteFloat(db.ssetScore(opts.key, m)) + } + return + } + c.WriteStrings(members) + }) +} + +type optsRange struct { + Key string + Min string + Max string + Reverse bool + WithScores bool +} + +func runRange(m *Miniredis, c *server.Peer, cctx *connCtx, opts optsRange) { + min, minErr := strconv.Atoi(opts.Min) + max, maxErr := strconv.Atoi(opts.Max) + if minErr != nil || maxErr != nil { + c.WriteError(msgInvalidInt) + return + } + + db := m.db(cctx.selectedDB) + + if !db.exists(opts.Key) { + c.WriteLen(0) + return + } + + if db.t(opts.Key) != keyTypeSortedSet { + c.WriteError(ErrWrongType.Error()) + return + } + + members := db.ssetMembers(opts.Key) + if opts.Reverse { + reverseSlice(members) + } + rs, re := redisRange(len(members), min, max, false) + if opts.WithScores { + c.WriteLen((re - rs) * 2) + } else { + c.WriteLen(re - rs) + } + for _, el := range members[rs:re] { + c.WriteBulk(el) + if opts.WithScores { + c.WriteFloat(db.ssetScore(opts.Key, el)) + } + } +} + +type optsRangeByScore struct { + Key string + Min string + Max string + Reverse bool + WithLimit bool + Offset string + Count string + WithScores bool +} + +func runRangeByScore(m *Miniredis, c *server.Peer, cctx *connCtx, opts optsRangeByScore) { + var limitOffset, limitCount int + var err error + if opts.WithLimit { + limitOffset, err = strconv.Atoi(opts.Offset) + if err != nil { + c.WriteError(msgInvalidInt) + return + } + limitCount, err = strconv.Atoi(opts.Count) + if err != nil { + c.WriteError(msgInvalidInt) + return + } + } + min, minIncl, minErr := parseFloatRange(opts.Min) + max, maxIncl, maxErr := parseFloatRange(opts.Max) + if minErr != nil || maxErr != nil { + c.WriteError(msgInvalidMinMax) + return + } + + db := m.db(cctx.selectedDB) + + if !db.exists(opts.Key) { + c.WriteLen(0) + return + } + + if db.t(opts.Key) != keyTypeSortedSet { + c.WriteError(ErrWrongType.Error()) + return + } + + members := db.ssetElements(opts.Key) + if opts.Reverse { + min, max = max, min + minIncl, maxIncl = maxIncl, minIncl + } + members = withSSRange(members, min, minIncl, max, maxIncl) + if opts.Reverse { + reverseElems(members) + } + + // Apply LIMIT ranges. That's <start> <elements>. Unlike RANGE. + if opts.WithLimit { + if limitOffset < 0 { + members = ssElems{} + } else { + if limitOffset < len(members) { + members = members[limitOffset:] + } else { + // out of range + members = ssElems{} + } + if limitCount >= 0 { + if len(members) > limitCount { + members = members[:limitCount] + } + } + } + } + + if opts.WithScores { + c.WriteLen(len(members) * 2) + } else { + c.WriteLen(len(members)) + } + for _, el := range members { + c.WriteBulk(el.member) + if opts.WithScores { + c.WriteFloat(el.score) + } + } +} + +type optsRangeByLex struct { + Key string + Min string + Max string + Reverse bool + WithLimit bool + Offset string + Count string + WithScores bool +} + +func runRangeByLex(m *Miniredis, c *server.Peer, cctx *connCtx, opts optsRangeByLex) { + var limitOffset, limitCount int + var err error + if opts.WithLimit { + limitOffset, err = strconv.Atoi(opts.Offset) + if err != nil { + c.WriteError(msgInvalidInt) + return + } + limitCount, err = strconv.Atoi(opts.Count) + if err != nil { + c.WriteError(msgInvalidInt) + return + } + } + min, minIncl, minErr := parseLexrange(opts.Min) + max, maxIncl, maxErr := parseLexrange(opts.Max) + if minErr != nil || maxErr != nil { + c.WriteError(msgInvalidRangeItem) + return + } + + db := m.db(cctx.selectedDB) + + if !db.exists(opts.Key) { + c.WriteLen(0) + return + } + + if db.t(opts.Key) != keyTypeSortedSet { + c.WriteError(ErrWrongType.Error()) + return + } + + members := db.ssetMembers(opts.Key) + // Just key sort. If scores are not the same we don't care. + sort.Strings(members) + if opts.Reverse { + min, max = max, min + minIncl, maxIncl = maxIncl, minIncl + } + members = withLexRange(members, min, minIncl, max, maxIncl) + if opts.Reverse { + reverseSlice(members) + } + + // Apply LIMIT ranges. That's <start> <elements>. Unlike RANGE. + if opts.WithLimit { + if limitOffset < 0 { + members = nil + } else { + if limitOffset < len(members) { + members = members[limitOffset:] + } else { + // out of range + members = nil + } + if limitCount >= 0 { + if len(members) > limitCount { + members = members[:limitCount] + } + } + } + } + + c.WriteLen(len(members)) + for _, el := range members { + c.WriteBulk(el) + } +} + +// optLexrange handles ZRANGE{,BYLEX} ranges. They start with '[', '(', or are +// '+' or '-'. +// Sets destValue and destInclusive. destValue can be '+' or '-'. +func parseLexrange(s string) (string, bool, error) { + if len(s) == 0 { + return "", false, errors.New(msgInvalidRangeItem) + } + + if s == "+" || s == "-" { + return s, false, nil + } + + switch s[0] { + case '(': + return s[1:], false, nil + case '[': + return s[1:], true, nil + default: + return "", false, errors.New(msgInvalidRangeItem) + } +} + +func sortedKeys(m map[string]float64) []string { + var keys []string + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +}
vendor/github.com/alicebob/miniredis/v2/cmd_stream.go+1812 −0 added@@ -0,0 +1,1812 @@ +// Commands from https://redis.io/commands#stream + +package miniredis + +import ( + "errors" + "fmt" + "sort" + "strconv" + "strings" + "time" + + "github.com/alicebob/miniredis/v2/server" +) + +// commandsStream handles all stream operations. +func commandsStream(m *Miniredis) { + m.srv.Register("XADD", m.cmdXadd) + m.srv.Register("XLEN", m.cmdXlen) + m.srv.Register("XREAD", m.cmdXread) + m.srv.Register("XRANGE", m.makeCmdXrange(false)) + m.srv.Register("XREVRANGE", m.makeCmdXrange(true)) + m.srv.Register("XGROUP", m.cmdXgroup) + m.srv.Register("XINFO", m.cmdXinfo) + m.srv.Register("XREADGROUP", m.cmdXreadgroup) + m.srv.Register("XACK", m.cmdXack) + m.srv.Register("XDEL", m.cmdXdel) + m.srv.Register("XPENDING", m.cmdXpending) + m.srv.Register("XTRIM", m.cmdXtrim) + m.srv.Register("XAUTOCLAIM", m.cmdXautoclaim) + m.srv.Register("XCLAIM", m.cmdXclaim) +} + +// XADD +func (m *Miniredis) cmdXadd(c *server.Peer, cmd string, args []string) { + if len(args) < 4 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + key, args := args[0], args[1:] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + maxlen := -1 + minID := "" + makeStream := true + if strings.ToLower(args[0]) == "nomkstream" { + args = args[1:] + makeStream = false + } + if strings.ToLower(args[0]) == "maxlen" { + args = args[1:] + // we don't treat "~" special + if args[0] == "~" { + args = args[1:] + } + n, err := strconv.Atoi(args[0]) + if err != nil { + c.WriteError(msgInvalidInt) + return + } + if n < 0 { + c.WriteError("ERR The MAXLEN argument must be >= 0.") + return + } + maxlen = n + args = args[1:] + } else if strings.ToLower(args[0]) == "minid" { + args = args[1:] + // we don't treat "~" special + if args[0] == "~" { + args = args[1:] + } + minID = args[0] + args = args[1:] + } + if len(args) < 1 { + c.WriteError(errWrongNumber(cmd)) + return + } + entryID, args := args[0], args[1:] + + // args must be composed of field/value pairs. + if len(args) == 0 || len(args)%2 != 0 { + c.WriteError("ERR wrong number of arguments for XADD") // non-default message + return + } + + var values []string + for len(args) > 0 { + values = append(values, args[0], args[1]) + args = args[2:] + } + + db := m.db(ctx.selectedDB) + s, err := db.stream(key) + if err != nil { + c.WriteError(err.Error()) + return + } + if s == nil { + if !makeStream { + c.WriteNull() + return + } + s, _ = db.newStream(key) + } + + newID, err := s.add(entryID, values, m.effectiveNow()) + if err != nil { + switch err { + case errInvalidEntryID: + c.WriteError(msgInvalidStreamID) + default: + c.WriteError(err.Error()) + } + return + } + if maxlen >= 0 { + s.trim(maxlen) + } + if minID != "" { + s.trimBefore(minID) + } + db.incr(key) + + c.WriteBulk(newID) + }) +} + +// XLEN +func (m *Miniredis) cmdXlen(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) + + s, err := db.stream(key) + if err != nil { + c.WriteError(err.Error()) + } + if s == nil { + // No such key. That's zero length. + c.WriteInt(0) + return + } + + c.WriteInt(len(s.entries)) + }) +} + +// XRANGE and XREVRANGE +func (m *Miniredis) makeCmdXrange(reverse bool) server.Cmd { + return func(c *server.Peer, cmd string, args []string) { + if len(args) < 3 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if len(args) == 4 || len(args) > 5 { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + opts := struct { + key string + startKey string + startExclusive bool + endKey string + endExclusive bool + }{ + key: args[0], + startKey: args[1], + endKey: args[2], + } + if strings.HasPrefix(opts.startKey, "(") { + opts.startExclusive = true + opts.startKey = opts.startKey[1:] + if opts.startKey == "-" || opts.startKey == "+" { + setDirty(c) + c.WriteError(msgInvalidStreamID) + return + } + } + if strings.HasPrefix(opts.endKey, "(") { + opts.endExclusive = true + opts.endKey = opts.endKey[1:] + if opts.endKey == "-" || opts.endKey == "+" { + setDirty(c) + c.WriteError(msgInvalidStreamID) + return + } + } + + countArg := "0" + if len(args) == 5 { + if strings.ToLower(args[3]) != "count" { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + countArg = args[4] + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + start, err := formatStreamRangeBound(opts.startKey, true, reverse) + if err != nil { + c.WriteError(msgInvalidStreamID) + return + } + end, err := formatStreamRangeBound(opts.endKey, false, reverse) + if err != nil { + c.WriteError(msgInvalidStreamID) + return + } + count, err := strconv.Atoi(countArg) + if err != nil { + c.WriteError(msgInvalidInt) + return + } + + db := m.db(ctx.selectedDB) + + if !db.exists(opts.key) { + c.WriteLen(0) + return + } + + if db.t(opts.key) != keyTypeStream { + c.WriteError(ErrWrongType.Error()) + return + } + + var entries = db.streamKeys[opts.key].entries + if reverse { + entries = reversedStreamEntries(entries) + } + if count == 0 { + count = len(entries) + } + + var returnedEntries []StreamEntry + for _, entry := range entries { + if len(returnedEntries) == count { + break + } + + if !reverse { + // Break if entry ID > end + if streamCmp(entry.ID, end) == 1 { + break + } + + // Continue if entry ID < start + if streamCmp(entry.ID, start) == -1 { + continue + } + } else { + // Break if entry iD < end + if streamCmp(entry.ID, end) == -1 { + break + } + + // Continue if entry ID > start. + if streamCmp(entry.ID, start) == 1 { + continue + } + } + + // Continue if start exclusive and entry ID == start + if opts.startExclusive && streamCmp(entry.ID, start) == 0 { + continue + } + // Continue if end exclusive and entry ID == end + if opts.endExclusive && streamCmp(entry.ID, end) == 0 { + continue + } + + returnedEntries = append(returnedEntries, entry) + } + + c.WriteLen(len(returnedEntries)) + for _, entry := range returnedEntries { + c.WriteLen(2) + c.WriteBulk(entry.ID) + c.WriteLen(len(entry.Values)) + for _, v := range entry.Values { + c.WriteBulk(v) + } + } + }) + } +} + +// XGROUP +func (m *Miniredis) cmdXgroup(c *server.Peer, cmd string, args []string) { + if len(args) == 0 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + subCmd, args := strings.ToLower(args[0]), args[1:] + switch subCmd { + case "create": + m.cmdXgroupCreate(c, cmd, args) + case "destroy": + m.cmdXgroupDestroy(c, cmd, args) + case "createconsumer": + m.cmdXgroupCreateconsumer(c, cmd, args) + case "delconsumer": + m.cmdXgroupDelconsumer(c, cmd, args) + case "help", + "setid": + err := fmt.Sprintf("ERR 'XGROUP %s' not supported", subCmd) + setDirty(c) + c.WriteError(err) + default: + setDirty(c) + c.WriteError(fmt.Sprintf( + "ERR unknown subcommand '%s'. Try XGROUP HELP.", + subCmd, + )) + } +} + +// XGROUP CREATE +func (m *Miniredis) cmdXgroupCreate(c *server.Peer, cmd string, args []string) { + if len(args) != 3 && len(args) != 4 { + setDirty(c) + c.WriteError(errWrongNumber("CREATE")) + return + } + stream, group, id := args[0], args[1], args[2] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + s, err := db.stream(stream) + if err != nil { + c.WriteError(err.Error()) + return + } + if s == nil && len(args) == 4 && strings.ToUpper(args[3]) == "MKSTREAM" { + if s, err = db.newStream(stream); err != nil { + c.WriteError(err.Error()) + return + } + } + if s == nil { + c.WriteError(msgXgroupKeyNotFound) + return + } + + if err := s.createGroup(group, id); err != nil { + c.WriteError(err.Error()) + return + } + + c.WriteOK() + }) +} + +// XGROUP DESTROY +func (m *Miniredis) cmdXgroupDestroy(c *server.Peer, cmd string, args []string) { + if len(args) != 2 { + setDirty(c) + c.WriteError(errWrongNumber("DESTROY")) + return + } + stream, groupName := args[0], args[1] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + s, err := db.stream(stream) + if err != nil { + c.WriteError(err.Error()) + return + } + if s == nil { + c.WriteError(msgXgroupKeyNotFound) + return + } + + if _, ok := s.groups[groupName]; !ok { + c.WriteInt(0) + return + } + delete(s.groups, groupName) + c.WriteInt(1) + }) +} + +// XGROUP CREATECONSUMER +func (m *Miniredis) cmdXgroupCreateconsumer(c *server.Peer, cmd string, args []string) { + if len(args) != 3 { + setDirty(c) + c.WriteError(errWrongNumber("CREATECONSUMER")) + return + } + key, groupName, consumerName := args[0], args[1], args[2] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + s, err := db.stream(key) + if err != nil { + c.WriteError(err.Error()) + return + } + if s == nil { + c.WriteError(msgXgroupKeyNotFound) + return + } + + g, ok := s.groups[groupName] + if !ok { + err := fmt.Sprintf("NOGROUP No such consumer group '%s' for key name '%s'", groupName, key) + c.WriteError(err) + return + } + + if _, ok = g.consumers[consumerName]; ok { + c.WriteInt(0) + return + } + g.consumers[consumerName] = &consumer{} + c.WriteInt(1) + }) +} + +// XGROUP DELCONSUMER +func (m *Miniredis) cmdXgroupDelconsumer(c *server.Peer, cmd string, args []string) { + if len(args) != 3 { + setDirty(c) + c.WriteError(errWrongNumber("DELCONSUMER")) + return + } + key, groupName, consumerName := args[0], args[1], args[2] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + s, err := db.stream(key) + if err != nil { + c.WriteError(err.Error()) + return + } + if s == nil { + c.WriteError(msgXgroupKeyNotFound) + return + } + + g, ok := s.groups[groupName] + if !ok { + err := fmt.Sprintf("NOGROUP No such consumer group '%s' for key name '%s'", groupName, key) + c.WriteError(err) + return + } + + consumer, ok := g.consumers[consumerName] + if !ok { + c.WriteInt(0) + return + } + defer delete(g.consumers, consumerName) + + if consumer.numPendingEntries > 0 { + newPending := make([]pendingEntry, 0) + for _, entry := range g.pending { + if entry.consumer != consumerName { + newPending = append(newPending, entry) + } + } + g.pending = newPending + } + c.WriteInt(consumer.numPendingEntries) + }) +} + +// XINFO +func (m *Miniredis) cmdXinfo(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 + } + + subCmd, args := strings.ToUpper(args[0]), args[1:] + switch subCmd { + case "STREAM": + m.cmdXinfoStream(c, args) + case "CONSUMERS": + m.cmdXinfoConsumers(c, args) + case "GROUPS": + m.cmdXinfoGroups(c, args) + case "HELP": + err := fmt.Sprintf("'XINFO %s' not supported", strings.Join(args, " ")) + setDirty(c) + c.WriteError(err) + default: + setDirty(c) + c.WriteError(fmt.Sprintf( + "ERR unknown subcommand or wrong number of arguments for '%s'. Try XINFO HELP.", + subCmd, + )) + } +} + +// XINFO STREAM +// Produces only part of full command output +func (m *Miniredis) cmdXinfoStream(c *server.Peer, args []string) { + if len(args) < 1 { + setDirty(c) + c.WriteError(errWrongNumber("STREAM")) + return + } + key := args[0] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + s, err := db.stream(key) + if err != nil { + c.WriteError(err.Error()) + return + } + if s == nil { + c.WriteError(msgKeyNotFound) + return + } + + c.WriteMapLen(1) + c.WriteBulk("length") + c.WriteInt(len(s.entries)) + }) +} + +// XINFO GROUPS +func (m *Miniredis) cmdXinfoGroups(c *server.Peer, args []string) { + if len(args) != 1 { + setDirty(c) + c.WriteError(errWrongNumber("GROUPS")) + return + } + key := args[0] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + s, err := db.stream(key) + if err != nil { + c.WriteError(err.Error()) + return + } + if s == nil { + c.WriteError(msgKeyNotFound) + return + } + + c.WriteLen(len(s.groups)) + for name, g := range s.groups { + c.WriteMapLen(6) + + c.WriteBulk("name") + c.WriteBulk(name) + c.WriteBulk("consumers") + c.WriteInt(len(g.consumers)) + c.WriteBulk("pending") + c.WriteInt(len(g.activePending())) + c.WriteBulk("last-delivered-id") + c.WriteBulk(g.lastID) + c.WriteBulk("entries-read") + c.WriteNull() + c.WriteBulk("lag") + c.WriteInt(len(g.stream.entries)) + } + }) +} + +// XINFO CONSUMERS +// Please note that this is only a partial implementation, for it does not +// return each consumer's "idle" value, which indicates "the number of +// milliseconds that have passed since the consumer last interacted with the +// server." +func (m *Miniredis) cmdXinfoConsumers(c *server.Peer, args []string) { + if len(args) != 2 { + setDirty(c) + c.WriteError(errWrongNumber("CONSUMERS")) + return + } + key, groupName := args[0], args[1] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + s, err := db.stream(key) + if err != nil { + c.WriteError(err.Error()) + return + } + if s == nil { + c.WriteError(msgKeyNotFound) + return + } + + g, ok := s.groups[groupName] + if !ok { + err := fmt.Sprintf("NOGROUP No such consumer group '%s' for key name '%s'", groupName, key) + c.WriteError(err) + return + } + + var consumerNames []string + for name := range g.consumers { + consumerNames = append(consumerNames, name) + } + sort.Strings(consumerNames) + + c.WriteLen(len(consumerNames)) + for _, name := range consumerNames { + cons := g.consumers[name] + + c.WriteMapLen(4) + c.WriteBulk("name") + c.WriteBulk(name) + c.WriteBulk("pending") + c.WriteInt(cons.numPendingEntries) + // TODO: these times aren't set for all commands + c.WriteBulk("idle") + c.WriteInt(m.sinceMilli(cons.lastSeen)) + c.WriteBulk("inactive") + c.WriteInt(m.sinceMilli(cons.lastSuccess)) + } + }) +} + +func (m *Miniredis) sinceMilli(t time.Time) int { + if t.IsZero() { + return -1 + } + return int(m.effectiveNow().Sub(t).Milliseconds()) +} + +// XREADGROUP +func (m *Miniredis) cmdXreadgroup(c *server.Peer, cmd string, args []string) { + // XREADGROUP GROUP group consumer STREAMS key ID + if len(args) < 6 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + var opts struct { + group string + consumer string + count int + noack bool + streams []string + ids []string + block bool + blockTimeout time.Duration + } + + if strings.ToUpper(args[0]) != "GROUP" { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + + opts.group, opts.consumer, args = args[1], args[2], args[3:] + + var err error +parsing: + for len(args) > 0 { + switch strings.ToUpper(args[0]) { + case "COUNT": + if len(args) < 2 { + err = errors.New(errWrongNumber(cmd)) + break parsing + } + + opts.count, err = strconv.Atoi(args[1]) + if err != nil { + break parsing + } + + args = args[2:] + case "BLOCK": + err = parseBlock(cmd, args, &opts.block, &opts.blockTimeout) + if err != nil { + break parsing + } + args = args[2:] + case "NOACK": + args = args[1:] + opts.noack = true + case "STREAMS": + args = args[1:] + + if len(args)%2 != 0 { + err = errors.New(msgXreadUnbalanced) + break parsing + } + + opts.streams, opts.ids = args[0:len(args)/2], args[len(args)/2:] + break parsing + default: + err = fmt.Errorf("ERR incorrect argument %s", args[0]) + break parsing + } + } + + if err != nil { + setDirty(c) + c.WriteError(err.Error()) + return + } + + if len(opts.streams) == 0 || len(opts.ids) == 0 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + + for _, id := range opts.ids { + if id != `>` { + opts.block = false + } + } + + if !opts.block { + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + res, err := xreadgroup( + db, + opts.group, + opts.consumer, + opts.noack, + opts.streams, + opts.ids, + opts.count, + m.effectiveNow(), + ) + if err != nil { + c.WriteError(err.Error()) + return + } + writeXread(c, opts.streams, res) + }) + return + } + + blocking( + m, + c, + opts.blockTimeout, + func(c *server.Peer, ctx *connCtx) bool { + if ctx.nested { + setDirty(c) + c.WriteError("ERR XREADGROUP command is not allowed with BLOCK option from scripts") + return false + } + + db := m.db(ctx.selectedDB) + res, err := xreadgroup( + db, + opts.group, + opts.consumer, + opts.noack, + opts.streams, + opts.ids, + opts.count, + m.effectiveNow(), + ) + if err != nil { + c.WriteError(err.Error()) + return true + } + if len(res) == 0 { + return false + } + writeXread(c, opts.streams, res) + return true + }, + func(c *server.Peer) { // timeout + c.WriteLen(-1) + }, + ) +} + +func xreadgroup( + db *RedisDB, + group, + consumer string, + noack bool, + streams []string, + ids []string, + count int, + now time.Time, +) (map[string][]StreamEntry, error) { + res := map[string][]StreamEntry{} + for i, key := range streams { + id := ids[i] + + g, err := db.streamGroup(key, group) + if err != nil { + return nil, err + } + if g == nil { + return nil, errXreadgroup(key, group) + } + + if _, err := parseStreamID(id); id != `>` && err != nil { + return nil, err + } + entries := g.readGroup(now, consumer, id, count, noack) + if id == `>` && len(entries) == 0 { + continue + } + + res[key] = entries + } + return res, nil +} + +// XACK +func (m *Miniredis) cmdXack(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, group, ids := args[0], args[1], args[2:] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + g, err := db.streamGroup(key, group) + if err != nil { + c.WriteError(err.Error()) + return + } + if g == nil { + c.WriteInt(0) + return + } + + cnt, err := g.ack(ids) + if err != nil { + c.WriteError(err.Error()) + return + } + c.WriteInt(cnt) + }) +} + +// XDEL +func (m *Miniredis) cmdXdel(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 + } + + stream, ids := args[0], args[1:] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + s, err := db.stream(stream) + if err != nil { + c.WriteError(err.Error()) + return + } + if s == nil { + c.WriteInt(0) + return + } + + n, err := s.delete(ids) + if err != nil { + c.WriteError(err.Error()) + return + } + db.incr(stream) + c.WriteInt(n) + }) +} + +// XREAD +func (m *Miniredis) cmdXread(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 { + count int + streams []string + ids []string + block bool + blockTimeout time.Duration + } + err error + ) + +parsing: + for len(args) > 0 { + switch strings.ToUpper(args[0]) { + case "COUNT": + if len(args) < 2 { + err = errors.New(errWrongNumber(cmd)) + break parsing + } + + opts.count, err = strconv.Atoi(args[1]) + if err != nil { + break parsing + } + args = args[2:] + case "BLOCK": + err = parseBlock(cmd, args, &opts.block, &opts.blockTimeout) + if err != nil { + break parsing + } + args = args[2:] + case "STREAMS": + args = args[1:] + + if len(args)%2 != 0 { + err = errors.New(msgXreadUnbalanced) + break parsing + } + + opts.streams, opts.ids = args[0:len(args)/2], args[len(args)/2:] + for i, id := range opts.ids { + if _, err := parseStreamID(id); id != `$` && err != nil { + setDirty(c) + c.WriteError(msgInvalidStreamID) + return + } else if id == "$" { + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(getCtx(c).selectedDB) + stream, ok := db.streamKeys[opts.streams[i]] + if ok { + opts.ids[i] = stream.lastID() + } else { + opts.ids[i] = "0-0" + } + }) + } + } + args = nil + break parsing + default: + err = fmt.Errorf("ERR incorrect argument %s", args[0]) + break parsing + } + } + if err != nil { + setDirty(c) + c.WriteError(err.Error()) + return + } + + if !opts.block { + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + res := xread(db, opts.streams, opts.ids, opts.count) + writeXread(c, opts.streams, res) + }) + return + } + blocking( + m, + c, + opts.blockTimeout, + func(c *server.Peer, ctx *connCtx) bool { + if ctx.nested { + setDirty(c) + c.WriteError("ERR XREAD command is not allowed with BLOCK option from scripts") + return false + } + + db := m.db(ctx.selectedDB) + res := xread(db, opts.streams, opts.ids, opts.count) + if len(res) == 0 { + return false + } + writeXread(c, opts.streams, res) + return true + }, + func(c *server.Peer) { // timeout + c.WriteLen(-1) + }, + ) +} + +func xread(db *RedisDB, streams []string, ids []string, count int) map[string][]StreamEntry { + res := map[string][]StreamEntry{} + for i := range streams { + stream := streams[i] + id := ids[i] + + var s, ok = db.streamKeys[stream] + if !ok { + continue + } + entries := s.entries + if len(entries) == 0 { + continue + } + + entryCount := count + if entryCount == 0 { + entryCount = len(entries) + } + + var returnedEntries []StreamEntry + for _, entry := range entries { + if len(returnedEntries) == entryCount { + break + } + if id == "$" { + id = s.lastID() + } + if streamCmp(entry.ID, id) <= 0 { + continue + } + returnedEntries = append(returnedEntries, entry) + } + if len(returnedEntries) > 0 { + res[stream] = returnedEntries + } + } + return res +} + +func writeXread(c *server.Peer, streams []string, res map[string][]StreamEntry) { + if len(res) == 0 { + c.WriteLen(-1) + return + } + c.WriteLen(len(res)) + for _, stream := range streams { + entries, ok := res[stream] + if !ok { + continue + } + c.WriteLen(2) + c.WriteBulk(stream) + c.WriteLen(len(entries)) + for _, entry := range entries { + c.WriteLen(2) + c.WriteBulk(entry.ID) + c.WriteLen(len(entry.Values)) + for _, v := range entry.Values { + c.WriteBulk(v) + } + } + } +} + +// XPENDING +func (m *Miniredis) cmdXpending(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 + group string + summary bool + idle time.Duration + start, end string + count int + consumer *string + } + + opts.key, opts.group, args = args[0], args[1], args[2:] + opts.summary = true + if len(args) >= 3 { + opts.summary = false + + if strings.ToUpper(args[0]) == "IDLE" { + idleMs, err := strconv.ParseInt(args[1], 10, 64) + if err != nil { + setDirty(c) + c.WriteError(msgInvalidInt) + return + } + opts.idle = time.Duration(idleMs) * time.Millisecond + + args = args[2:] + if len(args) < 3 { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + } + + var err error + opts.start, err = formatStreamRangeBound(args[0], true, false) + if err != nil { + setDirty(c) + c.WriteError(msgInvalidStreamID) + return + } + opts.end, err = formatStreamRangeBound(args[1], false, false) + if err != nil { + setDirty(c) + c.WriteError(msgInvalidStreamID) + return + } + opts.count, err = strconv.Atoi(args[2]) // negative is allowed + if err != nil { + setDirty(c) + c.WriteError(msgInvalidInt) + return + } + args = args[3:] + + if len(args) == 1 { + opts.consumer, args = &args[0], args[1:] + } + } + if len(args) != 0 { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + g, err := db.streamGroup(opts.key, opts.group) + if err != nil { + c.WriteError(err.Error()) + return + } + if g == nil { + c.WriteError(errReadgroup(opts.key, opts.group).Error()) + return + } + + if opts.summary { + writeXpendingSummary(c, *g) + return + } + writeXpending(m.effectiveNow(), c, *g, opts.idle, opts.start, opts.end, opts.count, opts.consumer) + }) +} + +func writeXpendingSummary(c *server.Peer, g streamGroup) { + pend := g.activePending() + if len(pend) == 0 { + c.WriteLen(4) + c.WriteInt(0) + c.WriteNull() + c.WriteNull() + c.WriteLen(-1) + return + } + + // format: + // - number of pending + // - smallest ID + // - highest ID + // - all consumers with > 0 pending items + c.WriteLen(4) + c.WriteInt(len(pend)) + c.WriteBulk(pend[0].id) + c.WriteBulk(pend[len(pend)-1].id) + cons := map[string]int{} + for id := range g.consumers { + cnt := g.pendingCount(id) + if cnt > 0 { + cons[id] = cnt + } + } + c.WriteLen(len(cons)) + var ids []string + for id := range cons { + ids = append(ids, id) + } + sort.Strings(ids) // be predicatable + for _, id := range ids { + c.WriteLen(2) + c.WriteBulk(id) + c.WriteBulk(strconv.Itoa(cons[id])) + } +} + +func writeXpending( + now time.Time, + c *server.Peer, + g streamGroup, + idle time.Duration, + start, + end string, + count int, + consumer *string, +) { + if len(g.pending) == 0 || count < 0 { + c.WriteLen(0) + return + } + + // format, list of: + // - message ID + // - consumer + // - milliseconds since delivery + // - delivery count + type entry struct { + id string + consumer string + millis int + count int + } + var res []entry + for _, p := range g.pending { + if len(res) >= count { + break + } + if consumer != nil && p.consumer != *consumer { + continue + } + if streamCmp(p.id, start) < 0 { + continue + } + if streamCmp(p.id, end) > 0 { + continue + } + timeSinceLastDelivery := now.Sub(p.lastDelivery) + if timeSinceLastDelivery >= idle { + res = append(res, entry{ + id: p.id, + consumer: p.consumer, + millis: int(timeSinceLastDelivery.Milliseconds()), + count: p.deliveryCount, + }) + } + } + c.WriteLen(len(res)) + for _, e := range res { + c.WriteLen(4) + c.WriteBulk(e.id) + c.WriteBulk(e.consumer) + c.WriteInt(e.millis) + c.WriteInt(e.count) + } +} + +// XTRIM +func (m *Miniredis) cmdXtrim(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 { + stream string + strategy string + maxLen int // for MAXLEN + threshold string // for MINID + withLimit bool // "LIMIT" + withExact bool // "=" + withNearly bool // "~" + } + + opts.stream, opts.strategy, args = args[0], strings.ToUpper(args[1]), args[2:] + + if opts.strategy != "MAXLEN" && opts.strategy != "MINID" { + setDirty(c) + c.WriteError(msgXtrimInvalidStrategy) + return + } + + // Ignore nearly exact trimming parameters. + switch args[0] { + case "=": + opts.withExact = true + args = args[1:] + case "~": + opts.withNearly = true + args = args[1:] + } + + switch opts.strategy { + case "MAXLEN": + maxLen, err := strconv.Atoi(args[0]) + if err != nil { + setDirty(c) + c.WriteError(msgXtrimInvalidMaxLen) + return + } + opts.maxLen = maxLen + case "MINID": + opts.threshold = args[0] + } + args = args[1:] + + if len(args) == 2 && strings.ToUpper(args[0]) == "LIMIT" { + // Ignore LIMIT. + opts.withLimit = true + if _, err := strconv.Atoi(args[1]); err != nil { + setDirty(c) + c.WriteError(msgInvalidInt) + return + } + + args = args[2:] + } + + if len(args) != 0 { + setDirty(c) + c.WriteError(fmt.Sprintf("ERR incorrect argument %s", args[0])) + return + } + + if opts.withLimit && !opts.withNearly { + setDirty(c) + c.WriteError(fmt.Sprintf(msgXtrimInvalidLimit)) + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + s, err := db.stream(opts.stream) + if err != nil { + setDirty(c) + c.WriteError(err.Error()) + return + } + if s == nil { + c.WriteInt(0) + return + } + + switch opts.strategy { + case "MAXLEN": + entriesBefore := len(s.entries) + s.trim(opts.maxLen) + c.WriteInt(entriesBefore - len(s.entries)) + case "MINID": + n := s.trimBefore(opts.threshold) + c.WriteInt(n) + } + }) +} + +// XAUTOCLAIM +func (m *Miniredis) cmdXautoclaim(c *server.Peer, cmd string, args []string) { + // XAUTOCLAIM key group consumer min-idle-time start + if len(args) < 5 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + var opts struct { + key string + group string + consumer string + minIdleTime time.Duration + start string + justId bool + count int + } + + opts.key, opts.group, opts.consumer = args[0], args[1], args[2] + n, err := strconv.Atoi(args[3]) + if err != nil { + setDirty(c) + c.WriteError("ERR Invalid min-idle-time argument for XAUTOCLAIM") + return + } + opts.minIdleTime = time.Millisecond * time.Duration(n) + + start_, err := formatStreamRangeBound(args[4], true, false) + if err != nil { + c.WriteError(msgInvalidStreamID) + return + } + opts.start = start_ + + args = args[5:] + + opts.count = 100 +parsing: + for len(args) > 0 { + switch strings.ToUpper(args[0]) { + case "COUNT": + if len(args) < 2 { + err = errors.New(errWrongNumber(cmd)) + break parsing + } + + opts.count, err = strconv.Atoi(args[1]) + if err != nil { + break parsing + } + + args = args[2:] + case "JUSTID": + args = args[1:] + opts.justId = true + default: + err = errors.New(msgSyntaxError) + break parsing + } + } + + if err != nil { + setDirty(c) + c.WriteError(err.Error()) + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + g, err := db.streamGroup(opts.key, opts.group) + if err != nil { + c.WriteError(err.Error()) + return + } + if g == nil { + c.WriteError(errReadgroup(opts.key, opts.group).Error()) + return + } + + nextCallId, entries := xautoclaim(m.effectiveNow(), *g, opts.minIdleTime, opts.start, opts.count, opts.consumer) + writeXautoclaim(c, nextCallId, entries, opts.justId) + }) +} + +func xautoclaim( + now time.Time, + g streamGroup, + minIdleTime time.Duration, + start string, + count int, + consumerID string, +) (string, []StreamEntry) { + nextCallId := "0-0" + if len(g.pending) == 0 || count < 0 { + return nextCallId, nil + } + + msgs := g.pendingAfterOrEqual(start) + var res []StreamEntry + for i, p := range msgs { + if minIdleTime > 0 && now.Before(p.lastDelivery.Add(minIdleTime)) { + continue + } + + prevConsumerID := p.consumer + if _, ok := g.consumers[consumerID]; !ok { + g.consumers[consumerID] = &consumer{} + } + p.consumer = consumerID + + _, entry := g.stream.get(p.id) + // not found. Weird? + if entry == nil { + // TODO: support third element of return from XAUTOCLAIM, which + // should delete entries not found in the PEL during XAUTOCLAIM. + // (Introduced in Redis 7.0) + continue + } + + p.deliveryCount += 1 + p.lastDelivery = now + + g.consumers[prevConsumerID].numPendingEntries-- + g.consumers[consumerID].numPendingEntries++ + + msgs[i] = p + res = append(res, *entry) + + if len(res) >= count { + if len(msgs) > i+1 { + nextCallId = msgs[i+1].id + } + break + } + } + return nextCallId, res +} + +func writeXautoclaim(c *server.Peer, nextCallId string, res []StreamEntry, justId bool) { + c.WriteLen(3) + c.WriteBulk(nextCallId) + c.WriteLen(len(res)) + for _, entry := range res { + if justId { + c.WriteBulk(entry.ID) + continue + } + + c.WriteLen(2) + c.WriteBulk(entry.ID) + c.WriteLen(len(entry.Values)) + for _, v := range entry.Values { + c.WriteBulk(v) + } + } + // TODO: see "Redis 7" note + c.WriteLen(0) +} + +// XCLAIM +func (m *Miniredis) cmdXclaim(c *server.Peer, cmd string, args []string) { + if len(args) < 5 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + var opts struct { + key string + groupName string + consumerName string + minIdleTime time.Duration + newLastDelivery time.Time + ids []string + retryCount *int + force bool + justId bool + } + + opts.key, opts.groupName, opts.consumerName = args[0], args[1], args[2] + + minIdleTimeMillis, err := strconv.Atoi(args[3]) + if err != nil { + setDirty(c) + c.WriteError("ERR Invalid min-idle-time argument for XCLAIM") + return + } + opts.minIdleTime = time.Millisecond * time.Duration(minIdleTimeMillis) + + opts.newLastDelivery = m.effectiveNow() + opts.ids = append(opts.ids, args[4]) + + args = args[5:] + for len(args) > 0 { + arg := strings.ToUpper(args[0]) + if arg == "IDLE" || + arg == "TIME" || + arg == "RETRYCOUNT" || + arg == "FORCE" || + arg == "JUSTID" { + break + } + opts.ids = append(opts.ids, arg) + args = args[1:] + } + + for len(args) > 0 { + arg := strings.ToUpper(args[0]) + switch arg { + case "IDLE": + idleMs, err := strconv.ParseInt(args[1], 10, 64) + if err != nil { + setDirty(c) + c.WriteError("ERR Invalid IDLE option argument for XCLAIM") + return + } + if idleMs < 0 { + idleMs = 0 + } + opts.newLastDelivery = m.effectiveNow().Add(time.Millisecond * time.Duration(-idleMs)) + args = args[2:] + case "TIME": + timeMs, err := strconv.ParseInt(args[1], 10, 64) + if err != nil { + setDirty(c) + c.WriteError("ERR Invalid TIME option argument for XCLAIM") + return + } + opts.newLastDelivery = time.UnixMilli(timeMs) + args = args[2:] + case "RETRYCOUNT": + retryCount, err := strconv.Atoi(args[1]) + if err != nil { + setDirty(c) + c.WriteError("ERR Invalid RETRYCOUNT option argument for XCLAIM") + return + } + opts.retryCount = &retryCount + args = args[2:] + case "FORCE": + opts.force = true + args = args[1:] + case "JUSTID": + opts.justId = true + args = args[1:] + default: + setDirty(c) + c.WriteError(fmt.Sprintf("ERR Unrecognized XCLAIM option '%s'", args[0])) + return + } + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + g, err := db.streamGroup(opts.key, opts.groupName) + if err != nil { + c.WriteError(err.Error()) + return + } + if g == nil { + c.WriteError(errReadgroup(opts.key, opts.groupName).Error()) + return + } + + claimedEntryIDs := m.xclaim(g, opts.consumerName, opts.minIdleTime, opts.newLastDelivery, opts.ids, opts.retryCount, opts.force) + writeXclaim(c, g.stream, claimedEntryIDs, opts.justId) + }) +} + +func (m *Miniredis) xclaim( + group *streamGroup, + consumerName string, + minIdleTime time.Duration, + newLastDelivery time.Time, + ids []string, + retryCount *int, + force bool, +) (claimedEntryIDs []string) { + for _, id := range ids { + pelPos, pelEntry := group.searchPending(id) + if pelEntry == nil { + group.setLastSeen(consumerName, m.effectiveNow()) + if !force { + continue + } + + if pelPos < len(group.pending) { + group.pending = append(group.pending[:pelPos+1], group.pending[pelPos:]...) + } else { + group.pending = append(group.pending, pendingEntry{}) + } + pelEntry = &group.pending[pelPos] + + *pelEntry = pendingEntry{ + id: id, + consumer: consumerName, + deliveryCount: 1, + } + group.setLastSuccess(consumerName, m.effectiveNow()) + } else { + group.consumers[pelEntry.consumer].numPendingEntries-- + pelEntry.consumer = consumerName + } + + if retryCount != nil { + pelEntry.deliveryCount = *retryCount + } else { + pelEntry.deliveryCount++ + } + pelEntry.lastDelivery = newLastDelivery + + // redis7: don't report entries which are deleted by now + if _, e := group.stream.get(id); e == nil { + continue + } + + claimedEntryIDs = append(claimedEntryIDs, id) + } + if len(claimedEntryIDs) == 0 { + group.setLastSeen(consumerName, m.effectiveNow()) + return + } + + if _, ok := group.consumers[consumerName]; !ok { + group.consumers[consumerName] = &consumer{} + } + consumer := group.consumers[consumerName] + consumer.numPendingEntries += len(claimedEntryIDs) + + group.setLastSuccess(consumerName, m.effectiveNow()) + return +} + +func writeXclaim(c *server.Peer, stream *streamKey, claimedEntryIDs []string, justId bool) { + c.WriteLen(len(claimedEntryIDs)) + for _, id := range claimedEntryIDs { + if justId { + c.WriteBulk(id) + continue + } + + _, entry := stream.get(id) + if entry == nil { + c.WriteNull() + continue + } + + c.WriteLen(2) + c.WriteBulk(entry.ID) + c.WriteStrings(entry.Values) + } +} + +func parseBlock(cmd string, args []string, block *bool, timeout *time.Duration) error { + if len(args) < 2 { + return errors.New(errWrongNumber(cmd)) + } + (*block) = true + ms, err := strconv.Atoi(args[1]) + if err != nil { + return errors.New(msgInvalidInt) + } + if ms < 0 { + return errors.New("ERR timeout is negative") + } + (*timeout) = time.Millisecond * time.Duration(ms) + return nil +}
vendor/github.com/alicebob/miniredis/v2/cmd_string.go+1364 −0 added@@ -0,0 +1,1364 @@ +// 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 != keyTypeString { + 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) != keyTypeString { + 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) != keyTypeString { + 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 != keyTypeString { + 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) != keyTypeString { + 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 != keyTypeString { + 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 != keyTypeString { + 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 != keyTypeString { + 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 != keyTypeString { + 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 != keyTypeString { + 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 != keyTypeString { + 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 != keyTypeString { + 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 != keyTypeString { + 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 != keyTypeString { + 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 != keyTypeString { + 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) != keyTypeString { + 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 != keyTypeString { + c.WriteError(msgWrongType) + return + } + res := []byte(db.stringKeys[first]) + for _, vk := range opts.input[1:] { + if t, ok := db.keys[vk]; ok && t != keyTypeString { + 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 != keyTypeString { + 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 != keyTypeString { + 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 != keyTypeString { + 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 != keyTypeString { + 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 +}
vendor/github.com/alicebob/miniredis/v2/cmd_transactions.go+179 −0 added@@ -0,0 +1,179 @@ +// Commands from https://redis.io/commands#transactions + +package miniredis + +import ( + "github.com/alicebob/miniredis/v2/server" +) + +// commandsTransaction handles MULTI &c. +func commandsTransaction(m *Miniredis) { + m.srv.Register("DISCARD", m.cmdDiscard) + m.srv.Register("EXEC", m.cmdExec) + m.srv.Register("MULTI", m.cmdMulti) + m.srv.Register("UNWATCH", m.cmdUnwatch) + m.srv.Register("WATCH", m.cmdWatch) +} + +// MULTI +func (m *Miniredis) cmdMulti(c *server.Peer, cmd string, args []string) { + if len(args) != 0 { + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + ctx := getCtx(c) + if ctx.nested { + c.WriteError(msgNotFromScripts(ctx.nestedSHA)) + return + } + if inTx(ctx) { + c.WriteError("ERR MULTI calls can not be nested") + return + } + + startTx(ctx) + + c.WriteOK() +} + +// EXEC +func (m *Miniredis) cmdExec(c *server.Peer, cmd string, args []string) { + if len(args) != 0 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + ctx := getCtx(c) + if ctx.nested { + c.WriteError(msgNotFromScripts(ctx.nestedSHA)) + return + } + if !inTx(ctx) { + c.WriteError("ERR EXEC without MULTI") + return + } + + if ctx.dirtyTransaction { + c.WriteError("EXECABORT Transaction discarded because of previous errors.") + // a failed EXEC finishes the tx + stopTx(ctx) + return + } + + m.Lock() + defer m.Unlock() + + // Check WATCHed keys. + for t, version := range ctx.watch { + if m.db(t.db).keyVersion[t.key] > version { + // Abort! Abort! + stopTx(ctx) + c.WriteLen(-1) + return + } + } + + c.WriteLen(len(ctx.transaction)) + for _, cb := range ctx.transaction { + cb(c, ctx) + } + // wake up anyone who waits on anything. + m.signal.Broadcast() + + stopTx(ctx) +} + +// DISCARD +func (m *Miniredis) cmdDiscard(c *server.Peer, cmd string, args []string) { + if len(args) != 0 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + ctx := getCtx(c) + if !inTx(ctx) { + c.WriteError("ERR DISCARD without MULTI") + return + } + + stopTx(ctx) + c.WriteOK() +} + +// WATCH +func (m *Miniredis) cmdWatch(c *server.Peer, cmd string, args []string) { + if len(args) == 0 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + ctx := getCtx(c) + if ctx.nested { + c.WriteError(msgNotFromScripts(ctx.nestedSHA)) + return + } + if inTx(ctx) { + c.WriteError("ERR WATCH in MULTI") + return + } + + m.Lock() + defer m.Unlock() + db := m.db(ctx.selectedDB) + + for _, key := range args { + watch(db, ctx, key) + } + c.WriteOK() +} + +// UNWATCH +func (m *Miniredis) cmdUnwatch(c *server.Peer, cmd string, args []string) { + if len(args) != 0 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + // Doesn't matter if UNWATCH is in a TX or not. Looks like a Redis bug to me. + unwatch(getCtx(c)) + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + // Do nothing if it's called in a transaction. + c.WriteOK() + }) +}
vendor/github.com/alicebob/miniredis/v2/db.go+790 −0 added@@ -0,0 +1,790 @@ +package miniredis + +import ( + "errors" + "fmt" + "math" + "math/big" + "sort" + "strconv" + "time" +) + +var ( + errInvalidEntryID = errors.New("stream ID is invalid") +) + +// exists also updates the lru +func (db *RedisDB) exists(k string) bool { + _, ok := db.keys[k] + if ok { + db.lru[k] = db.master.effectiveNow() + } + return ok +} + +// t gives the type of a key, or "" +func (db *RedisDB) t(k string) string { + return db.keys[k] +} + +// incr increases the version and the lru timestamp +func (db *RedisDB) incr(k string) { + db.lru[k] = db.master.effectiveNow() + db.keyVersion[k]++ +} + +// allKeys returns all keys. Sorted. +func (db *RedisDB) allKeys() []string { + res := make([]string, 0, len(db.keys)) + for k := range db.keys { + res = append(res, k) + } + sort.Strings(res) // To make things deterministic. + return res +} + +// flush removes all keys and values. +func (db *RedisDB) flush() { + db.keys = map[string]string{} + db.lru = map[string]time.Time{} + db.stringKeys = map[string]string{} + db.hashKeys = map[string]hashKey{} + db.listKeys = map[string]listKey{} + db.setKeys = map[string]setKey{} + db.hllKeys = map[string]*hll{} + db.sortedsetKeys = map[string]sortedSet{} + db.ttl = map[string]time.Duration{} + db.streamKeys = map[string]*streamKey{} +} + +// move something to another db. Will return ok. Or not. +func (db *RedisDB) move(key string, to *RedisDB) bool { + if _, ok := to.keys[key]; ok { + return false + } + + t, ok := db.keys[key] + if !ok { + return false + } + to.keys[key] = db.keys[key] + switch t { + case keyTypeString: + to.stringKeys[key] = db.stringKeys[key] + case keyTypeHash: + to.hashKeys[key] = db.hashKeys[key] + case keyTypeList: + to.listKeys[key] = db.listKeys[key] + case keyTypeSet: + to.setKeys[key] = db.setKeys[key] + case keyTypeSortedSet: + to.sortedsetKeys[key] = db.sortedsetKeys[key] + case keyTypeStream: + to.streamKeys[key] = db.streamKeys[key] + case keyTypeHll: + to.hllKeys[key] = db.hllKeys[key] + default: + panic("unhandled key type") + } + if v, ok := db.ttl[key]; ok { + to.ttl[key] = v + } + to.incr(key) + db.del(key, true) + return true +} + +func (db *RedisDB) rename(from, to string) { + db.del(to, true) + switch db.t(from) { + case keyTypeString: + db.stringKeys[to] = db.stringKeys[from] + case keyTypeHash: + db.hashKeys[to] = db.hashKeys[from] + case keyTypeList: + db.listKeys[to] = db.listKeys[from] + case keyTypeSet: + db.setKeys[to] = db.setKeys[from] + case keyTypeSortedSet: + db.sortedsetKeys[to] = db.sortedsetKeys[from] + case keyTypeStream: + db.streamKeys[to] = db.streamKeys[from] + case keyTypeHll: + db.hllKeys[to] = db.hllKeys[from] + default: + panic("missing case") + } + db.keys[to] = db.keys[from] + if v, ok := db.ttl[from]; ok { + db.ttl[to] = v + } + db.incr(to) + + db.del(from, true) +} + +func (db *RedisDB) del(k string, delTTL bool) { + if !db.exists(k) { + return + } + t := db.t(k) + delete(db.keys, k) + delete(db.lru, k) + db.keyVersion[k]++ + if delTTL { + delete(db.ttl, k) + } + switch t { + case keyTypeString: + delete(db.stringKeys, k) + case keyTypeHash: + delete(db.hashKeys, k) + case keyTypeList: + delete(db.listKeys, k) + case keyTypeSet: + delete(db.setKeys, k) + case keyTypeSortedSet: + delete(db.sortedsetKeys, k) + case keyTypeStream: + delete(db.streamKeys, k) + case keyTypeHll: + delete(db.hllKeys, k) + default: + panic("Unknown key type: " + t) + } +} + +// stringGet returns the string key or "" on error/nonexists. +func (db *RedisDB) stringGet(k string) string { + if t, ok := db.keys[k]; !ok || t != keyTypeString { + return "" + } + return db.stringKeys[k] +} + +// stringSet force set()s a key. Does not touch expire. +func (db *RedisDB) stringSet(k, v string) { + db.del(k, false) + db.keys[k] = keyTypeString + db.stringKeys[k] = v + db.incr(k) +} + +// change int key value +func (db *RedisDB) stringIncr(k string, delta int) (int, error) { + v := 0 + if sv, ok := db.stringKeys[k]; ok { + var err error + v, err = strconv.Atoi(sv) + if err != nil { + return 0, ErrIntValueError + } + } + + if delta > 0 { + if math.MaxInt-delta < v { + return 0, ErrIntValueOverflowError + } + } else { + if math.MinInt-delta > v { + return 0, ErrIntValueOverflowError + } + } + + v += delta + db.stringSet(k, strconv.Itoa(v)) + return v, nil +} + +// change float key value +func (db *RedisDB) stringIncrfloat(k string, delta *big.Float) (*big.Float, error) { + v := big.NewFloat(0.0) + v.SetPrec(128) + if sv, ok := db.stringKeys[k]; ok { + var err error + v, _, err = big.ParseFloat(sv, 10, 128, 0) + if err != nil { + return nil, ErrFloatValueError + } + } + v.Add(v, delta) + db.stringSet(k, formatBig(v)) + return v, nil +} + +// listLpush is 'left push', aka unshift. Returns the new length. +func (db *RedisDB) listLpush(k, v string) int { + l, ok := db.listKeys[k] + if !ok { + db.keys[k] = keyTypeList + } + l = append([]string{v}, l...) + db.listKeys[k] = l + db.incr(k) + return len(l) +} + +// 'left pop', aka shift. +func (db *RedisDB) listLpop(k string) string { + l := db.listKeys[k] + el := l[0] + l = l[1:] + if len(l) == 0 { + db.del(k, true) + } else { + db.listKeys[k] = l + } + db.incr(k) + return el +} + +func (db *RedisDB) listPush(k string, v ...string) int { + l, ok := db.listKeys[k] + if !ok { + db.keys[k] = keyTypeList + } + l = append(l, v...) + db.listKeys[k] = l + db.incr(k) + return len(l) +} + +func (db *RedisDB) listPop(k string) string { + l := db.listKeys[k] + el := l[len(l)-1] + l = l[:len(l)-1] + if len(l) == 0 { + db.del(k, true) + } else { + db.listKeys[k] = l + db.incr(k) + } + return el +} + +// setset replaces a whole set. +func (db *RedisDB) setSet(k string, set setKey) { + db.keys[k] = keyTypeSet + db.setKeys[k] = set + db.incr(k) +} + +// setadd adds members to a set. Returns nr of new keys. +func (db *RedisDB) setAdd(k string, elems ...string) int { + s, ok := db.setKeys[k] + if !ok { + s = setKey{} + db.keys[k] = keyTypeSet + } + added := 0 + for _, e := range elems { + if _, ok := s[e]; !ok { + added++ + } + s[e] = struct{}{} + } + db.setKeys[k] = s + db.incr(k) + return added +} + +// setrem removes members from a set. Returns nr of deleted keys. +func (db *RedisDB) setRem(k string, fields ...string) int { + s, ok := db.setKeys[k] + if !ok { + return 0 + } + removed := 0 + for _, f := range fields { + if _, ok := s[f]; ok { + removed++ + delete(s, f) + } + } + if len(s) == 0 { + db.del(k, true) + } else { + db.setKeys[k] = s + } + db.incr(k) + return removed +} + +// All members of a set. +func (db *RedisDB) setMembers(k string) []string { + set := db.setKeys[k] + members := make([]string, 0, len(set)) + for k := range set { + members = append(members, k) + } + sort.Strings(members) + return members +} + +// Is a SET value present? +func (db *RedisDB) setIsMember(k, v string) bool { + set, ok := db.setKeys[k] + if !ok { + return false + } + _, ok = set[v] + return ok +} + +// hashFields returns all (sorted) keys ('fields') for a hash key. +func (db *RedisDB) hashFields(k string) []string { + v := db.hashKeys[k] + var r []string + for k := range v { + r = append(r, k) + } + sort.Strings(r) + return r +} + +// hashValues returns all (sorted) values a hash key. +func (db *RedisDB) hashValues(k string) []string { + h := db.hashKeys[k] + var r []string + for _, v := range h { + r = append(r, v) + } + sort.Strings(r) + return r +} + +// hashGet a value +func (db *RedisDB) hashGet(key, field string) string { + return db.hashKeys[key][field] +} + +// hashSet returns the number of new keys +func (db *RedisDB) hashSet(k string, fv ...string) int { + if t, ok := db.keys[k]; ok && t != keyTypeHash { + db.del(k, true) + } + db.keys[k] = keyTypeHash + if _, ok := db.hashKeys[k]; !ok { + db.hashKeys[k] = map[string]string{} + } + new := 0 + for idx := 0; idx < len(fv)-1; idx = idx + 2 { + f, v := fv[idx], fv[idx+1] + _, ok := db.hashKeys[k][f] + db.hashKeys[k][f] = v + db.incr(k) + if !ok { + new++ + } + } + return new +} + +// hashIncr changes int key value +func (db *RedisDB) hashIncr(key, field string, delta int) (int, error) { + v := 0 + if h, ok := db.hashKeys[key]; ok { + if f, ok := h[field]; ok { + var err error + v, err = strconv.Atoi(f) + if err != nil { + return 0, ErrIntValueError + } + } + } + v += delta + db.hashSet(key, field, strconv.Itoa(v)) + return v, nil +} + +// hashIncrfloat changes float key value +func (db *RedisDB) hashIncrfloat(key, field string, delta *big.Float) (*big.Float, error) { + v := big.NewFloat(0.0) + v.SetPrec(128) + if h, ok := db.hashKeys[key]; ok { + if f, ok := h[field]; ok { + var err error + v, _, err = big.ParseFloat(f, 10, 128, 0) + if err != nil { + return nil, ErrFloatValueError + } + } + } + v.Add(v, delta) + db.hashSet(key, field, formatBig(v)) + return v, nil +} + +// sortedSet set returns a sortedSet as map +func (db *RedisDB) sortedSet(key string) map[string]float64 { + ss := db.sortedsetKeys[key] + return map[string]float64(ss) +} + +// ssetSet sets a complete sorted set. +func (db *RedisDB) ssetSet(key string, sset sortedSet) { + db.keys[key] = keyTypeSortedSet + db.incr(key) + db.sortedsetKeys[key] = sset +} + +// ssetAdd adds member to a sorted set. Returns whether this was a new member. +func (db *RedisDB) ssetAdd(key string, score float64, member string) bool { + ss, ok := db.sortedsetKeys[key] + if !ok { + ss = newSortedSet() + db.keys[key] = keyTypeSortedSet + } + _, ok = ss[member] + ss[member] = score + db.sortedsetKeys[key] = ss + db.incr(key) + return !ok +} + +// All members from a sorted set, ordered by score. +func (db *RedisDB) ssetMembers(key string) []string { + ss, ok := db.sortedsetKeys[key] + if !ok { + return nil + } + elems := ss.byScore(asc) + members := make([]string, 0, len(elems)) + for _, e := range elems { + members = append(members, e.member) + } + return members +} + +// All members+scores from a sorted set, ordered by score. +func (db *RedisDB) ssetElements(key string) ssElems { + ss, ok := db.sortedsetKeys[key] + if !ok { + return nil + } + return ss.byScore(asc) +} + +func (db *RedisDB) ssetRandomMember(key string) string { + elems := db.ssetElements(key) + if len(elems) == 0 { + return "" + } + return elems[db.master.randIntn(len(elems))].member +} + +// ssetCard is the sorted set cardinality. +func (db *RedisDB) ssetCard(key string) int { + ss := db.sortedsetKeys[key] + return ss.card() +} + +// ssetRank is the sorted set rank. +func (db *RedisDB) ssetRank(key, member string, d direction) (int, bool) { + ss := db.sortedsetKeys[key] + return ss.rankByScore(member, d) +} + +// ssetScore is sorted set score. +func (db *RedisDB) ssetScore(key, member string) float64 { + ss := db.sortedsetKeys[key] + return ss[member] +} + +// ssetMScore returns multiple scores of a list of members in a sorted set. +func (db *RedisDB) ssetMScore(key string, members []string) []float64 { + scores := make([]float64, 0, len(members)) + ss := db.sortedsetKeys[key] + for _, member := range members { + scores = append(scores, ss[member]) + } + return scores +} + +// ssetRem is sorted set key delete. +func (db *RedisDB) ssetRem(key, member string) bool { + ss := db.sortedsetKeys[key] + _, ok := ss[member] + delete(ss, member) + if len(ss) == 0 { + // Delete key on removal of last member + db.del(key, true) + } + return ok +} + +// ssetExists tells if a member exists in a sorted set. +func (db *RedisDB) ssetExists(key, member string) bool { + ss := db.sortedsetKeys[key] + _, ok := ss[member] + return ok +} + +// ssetIncrby changes float sorted set score. +func (db *RedisDB) ssetIncrby(k, m string, delta float64) float64 { + ss, ok := db.sortedsetKeys[k] + if !ok { + ss = newSortedSet() + db.keys[k] = keyTypeSortedSet + db.sortedsetKeys[k] = ss + } + + v, _ := ss.get(m) + v += delta + ss.set(v, m) + db.incr(k) + return v +} + +// setDiff implements the logic behind SDIFF* +func (db *RedisDB) setDiff(keys []string) (setKey, error) { + key := keys[0] + keys = keys[1:] + if db.exists(key) && db.t(key) != keyTypeSet { + return nil, ErrWrongType + } + s := setKey{} + for k := range db.setKeys[key] { + s[k] = struct{}{} + } + for _, sk := range keys { + if !db.exists(sk) { + continue + } + if db.t(sk) != keyTypeSet { + return nil, ErrWrongType + } + for e := range db.setKeys[sk] { + delete(s, e) + } + } + return s, nil +} + +// setInter implements the logic behind SINTER* +// len keys needs to be > 0 +func (db *RedisDB) setInter(keys []string) (setKey, error) { + // all keys must either not exist, or be of type "set". + for _, key := range keys { + if db.exists(key) && db.t(key) != keyTypeSet { + return nil, ErrWrongType + } + } + + key := keys[0] + keys = keys[1:] + if !db.exists(key) { + return nil, nil + } + if db.t(key) != keyTypeSet { + return nil, ErrWrongType + } + s := setKey{} + for k := range db.setKeys[key] { + s[k] = struct{}{} + } + for _, sk := range keys { + if !db.exists(sk) { + return setKey{}, nil + } + if db.t(sk) != keyTypeSet { + return nil, ErrWrongType + } + other := db.setKeys[sk] + for e := range s { + if _, ok := other[e]; ok { + continue + } + delete(s, e) + } + } + return s, nil +} + +// setIntercard implements the logic behind SINTER* +// len keys needs to be > 0 +func (db *RedisDB) setIntercard(keys []string, limit int) (int, error) { + // all keys must either not exist, or be of type "set". + allExist := true + for _, key := range keys { + exists := db.exists(key) + allExist = allExist && exists + if exists && db.t(key) != "set" { + return 0, ErrWrongType + } + } + + if !allExist { + return 0, nil + } + + smallestKey := keys[0] + smallestIdx := 0 + for i, key := range keys { + if len(db.setKeys[key]) < len(db.setKeys[smallestKey]) { + smallestKey = key + smallestIdx = i + } + } + keys[smallestIdx] = keys[len(keys)-1] + keys = keys[:len(keys)-1] + + count := 0 + for item := range db.setKeys[smallestKey] { + inIntersection := true + for _, key := range keys { + if _, ok := db.setKeys[key][item]; !ok { + inIntersection = false + break + } + } + if inIntersection { + count++ + if count == limit { + break + } + } + } + + return count, nil +} + +// setUnion implements the logic behind SUNION* +func (db *RedisDB) setUnion(keys []string) (setKey, error) { + key := keys[0] + keys = keys[1:] + if db.exists(key) && db.t(key) != "set" { + return nil, ErrWrongType + } + s := setKey{} + for k := range db.setKeys[key] { + s[k] = struct{}{} + } + for _, sk := range keys { + if !db.exists(sk) { + continue + } + if db.t(sk) != "set" { + return nil, ErrWrongType + } + for e := range db.setKeys[sk] { + s[e] = struct{}{} + } + } + return s, nil +} + +func (db *RedisDB) newStream(key string) (*streamKey, error) { + if s, err := db.stream(key); err != nil { + return nil, err + } else if s != nil { + return nil, fmt.Errorf("ErrAlreadyExists") + } + + db.keys[key] = keyTypeStream + s := newStreamKey() + db.streamKeys[key] = s + db.incr(key) + return s, nil +} + +// return existing stream, or nil. +func (db *RedisDB) stream(key string) (*streamKey, error) { + if db.exists(key) && db.t(key) != keyTypeStream { + return nil, ErrWrongType + } + + return db.streamKeys[key], nil +} + +// return existing stream group, or nil. +func (db *RedisDB) streamGroup(key, group string) (*streamGroup, error) { + s, err := db.stream(key) + if err != nil || s == nil { + return nil, err + } + return s.groups[group], nil +} + +// fastForward proceeds the current timestamp with duration, works as a time machine +func (db *RedisDB) fastForward(duration time.Duration) { + for _, key := range db.allKeys() { + if value, ok := db.ttl[key]; ok { + db.ttl[key] = value - duration + db.checkTTL(key) + } + } +} + +func (db *RedisDB) checkTTL(key string) { + if v, ok := db.ttl[key]; ok && v <= 0 { + db.del(key, true) + } +} + +// hllAdd adds members to a hll. Returns 1 if at least 1 if internal HyperLogLog was altered, otherwise 0 +func (db *RedisDB) hllAdd(k string, elems ...string) int { + s, ok := db.hllKeys[k] + if !ok { + s = newHll() + db.keys[k] = keyTypeHll + } + hllAltered := 0 + for _, e := range elems { + if s.Add([]byte(e)) { + hllAltered = 1 + } + } + db.hllKeys[k] = s + db.incr(k) + return hllAltered +} + +// hllCount estimates the amount of members added to hll by hllAdd. If called with several arguments, hllCount returns a sum of estimations +func (db *RedisDB) hllCount(keys []string) (int, error) { + countOverall := 0 + for _, key := range keys { + if db.exists(key) && db.t(key) != keyTypeHll { + return 0, ErrNotValidHllValue + } + if !db.exists(key) { + continue + } + countOverall += db.hllKeys[key].Count() + } + + return countOverall, nil +} + +// hllMerge merges all the hlls provided as keys to the first key. Creates a new hll in the first key if it contains nothing +func (db *RedisDB) hllMerge(keys []string) error { + for _, key := range keys { + if db.exists(key) && db.t(key) != keyTypeHll { + return ErrNotValidHllValue + } + } + + destKey := keys[0] + restKeys := keys[1:] + + var destHll *hll + if db.exists(destKey) { + destHll = db.hllKeys[destKey] + } else { + destHll = newHll() + } + + for _, key := range restKeys { + if !db.exists(key) { + continue + } + destHll.Merge(db.hllKeys[key]) + } + + db.hllKeys[destKey] = destHll + db.keys[destKey] = keyTypeHll + db.incr(destKey) + + return nil +}
vendor/github.com/alicebob/miniredis/v2/direct.go+824 −0 added@@ -0,0 +1,824 @@ +package miniredis + +// Commands to modify and query our databases directly. + +import ( + "errors" + "math/big" + "time" +) + +var ( + // ErrKeyNotFound is returned when a key doesn't exist. + ErrKeyNotFound = errors.New(msgKeyNotFound) + + // ErrWrongType when a key is not the right type. + ErrWrongType = errors.New(msgWrongType) + + // ErrNotValidHllValue when a key is not a valid HyperLogLog string value. + ErrNotValidHllValue = errors.New(msgNotValidHllValue) + + // ErrIntValueError can returned by INCRBY + ErrIntValueError = errors.New(msgInvalidInt) + + // ErrIntValueOverflowError can be returned by INCR, DECR, INCRBY, DECRBY + ErrIntValueOverflowError = errors.New(msgIntOverflow) + + // ErrFloatValueError can returned by INCRBYFLOAT + ErrFloatValueError = errors.New(msgInvalidFloat) +) + +// Select sets the DB id for all direct commands. +func (m *Miniredis) Select(i int) { + m.Lock() + defer m.Unlock() + m.selectedDB = i +} + +// Keys returns all keys from the selected database, sorted. +func (m *Miniredis) Keys() []string { + return m.DB(m.selectedDB).Keys() +} + +// Keys returns all keys, sorted. +func (db *RedisDB) Keys() []string { + db.master.Lock() + defer db.master.Unlock() + + return db.allKeys() +} + +// FlushAll removes all keys from all databases. +func (m *Miniredis) FlushAll() { + m.Lock() + defer m.Unlock() + defer m.signal.Broadcast() + + m.flushAll() +} + +func (m *Miniredis) flushAll() { + for _, db := range m.dbs { + db.flush() + } +} + +// FlushDB removes all keys from the selected database. +func (m *Miniredis) FlushDB() { + m.DB(m.selectedDB).FlushDB() +} + +// FlushDB removes all keys. +func (db *RedisDB) FlushDB() { + db.master.Lock() + defer db.master.Unlock() + defer db.master.signal.Broadcast() + + db.flush() +} + +// Get returns string keys added with SET. +func (m *Miniredis) Get(k string) (string, error) { + return m.DB(m.selectedDB).Get(k) +} + +// Get returns a string key. +func (db *RedisDB) Get(k string) (string, error) { + db.master.Lock() + defer db.master.Unlock() + + if !db.exists(k) { + return "", ErrKeyNotFound + } + if db.t(k) != keyTypeString { + return "", ErrWrongType + } + return db.stringGet(k), nil +} + +// Set sets a string key. Removes expire. +func (m *Miniredis) Set(k, v string) error { + return m.DB(m.selectedDB).Set(k, v) +} + +// Set sets a string key. Removes expire. +// Unlike redis the key can't be an existing non-string key. +func (db *RedisDB) Set(k, v string) error { + db.master.Lock() + defer db.master.Unlock() + defer db.master.signal.Broadcast() + + if db.exists(k) && db.t(k) != keyTypeString { + return ErrWrongType + } + db.del(k, true) // Remove expire + db.stringSet(k, v) + return nil +} + +// Incr changes a int string value by delta. +func (m *Miniredis) Incr(k string, delta int) (int, error) { + return m.DB(m.selectedDB).Incr(k, delta) +} + +// Incr changes a int string value by delta. +func (db *RedisDB) Incr(k string, delta int) (int, error) { + db.master.Lock() + defer db.master.Unlock() + defer db.master.signal.Broadcast() + + if db.exists(k) && db.t(k) != keyTypeString { + return 0, ErrWrongType + } + + return db.stringIncr(k, delta) +} + +// IncrByFloat increments the float value of a key by the given delta. +// is an alias for Miniredis.Incrfloat +func (m *Miniredis) IncrByFloat(k string, delta float64) (float64, error) { + return m.Incrfloat(k, delta) +} + +// Incrfloat changes a float string value by delta. +func (m *Miniredis) Incrfloat(k string, delta float64) (float64, error) { + return m.DB(m.selectedDB).Incrfloat(k, delta) +} + +// Incrfloat changes a float string value by delta. +func (db *RedisDB) Incrfloat(k string, delta float64) (float64, error) { + db.master.Lock() + defer db.master.Unlock() + defer db.master.signal.Broadcast() + + if db.exists(k) && db.t(k) != keyTypeString { + return 0, ErrWrongType + } + + v, err := db.stringIncrfloat(k, big.NewFloat(delta)) + if err != nil { + return 0, err + } + vf, _ := v.Float64() + return vf, nil +} + +// List returns the list k, or an error if it's not there or something else. +// This is the same as the Redis command `LRANGE 0 -1`, but you can do your own +// range-ing. +func (m *Miniredis) List(k string) ([]string, error) { + return m.DB(m.selectedDB).List(k) +} + +// List returns the list k, or an error if it's not there or something else. +// This is the same as the Redis command `LRANGE 0 -1`, but you can do your own +// range-ing. +func (db *RedisDB) List(k string) ([]string, error) { + db.master.Lock() + defer db.master.Unlock() + + if !db.exists(k) { + return nil, ErrKeyNotFound + } + if db.t(k) != keyTypeList { + return nil, ErrWrongType + } + return db.listKeys[k], nil +} + +// Lpush prepends one value to a list. Returns the new length. +func (m *Miniredis) Lpush(k, v string) (int, error) { + return m.DB(m.selectedDB).Lpush(k, v) +} + +// Lpush prepends one value to a list. Returns the new length. +func (db *RedisDB) Lpush(k, v string) (int, error) { + db.master.Lock() + defer db.master.Unlock() + defer db.master.signal.Broadcast() + + if db.exists(k) && db.t(k) != keyTypeList { + return 0, ErrWrongType + } + return db.listLpush(k, v), nil +} + +// Lpop removes and returns the last element in a list. +func (m *Miniredis) Lpop(k string) (string, error) { + return m.DB(m.selectedDB).Lpop(k) +} + +// Lpop removes and returns the last element in a list. +func (db *RedisDB) Lpop(k string) (string, error) { + db.master.Lock() + defer db.master.Unlock() + defer db.master.signal.Broadcast() + + if !db.exists(k) { + return "", ErrKeyNotFound + } + if db.t(k) != keyTypeList { + return "", ErrWrongType + } + return db.listLpop(k), nil +} + +// RPush appends one or multiple values to a list. Returns the new length. +// An alias for Push +func (m *Miniredis) RPush(k string, v ...string) (int, error) { + return m.Push(k, v...) +} + +// Push add element at the end. Returns the new length. +func (m *Miniredis) Push(k string, v ...string) (int, error) { + return m.DB(m.selectedDB).Push(k, v...) +} + +// Push add element at the end. Is called RPUSH in redis. Returns the new length. +func (db *RedisDB) Push(k string, v ...string) (int, error) { + db.master.Lock() + defer db.master.Unlock() + defer db.master.signal.Broadcast() + + if db.exists(k) && db.t(k) != keyTypeList { + return 0, ErrWrongType + } + return db.listPush(k, v...), nil +} + +// RPop is an alias for Pop +func (m *Miniredis) RPop(k string) (string, error) { + return m.Pop(k) +} + +// Pop removes and returns the last element. Is called RPOP in Redis. +func (m *Miniredis) Pop(k string) (string, error) { + return m.DB(m.selectedDB).Pop(k) +} + +// Pop removes and returns the last element. Is called RPOP in Redis. +func (db *RedisDB) Pop(k string) (string, error) { + db.master.Lock() + defer db.master.Unlock() + defer db.master.signal.Broadcast() + + if !db.exists(k) { + return "", ErrKeyNotFound + } + if db.t(k) != keyTypeList { + return "", ErrWrongType + } + + return db.listPop(k), nil +} + +// SAdd adds keys to a set. Returns the number of new keys. +// Alias for SetAdd +func (m *Miniredis) SAdd(k string, elems ...string) (int, error) { + return m.SetAdd(k, elems...) +} + +// SetAdd adds keys to a set. Returns the number of new keys. +func (m *Miniredis) SetAdd(k string, elems ...string) (int, error) { + return m.DB(m.selectedDB).SetAdd(k, elems...) +} + +// SetAdd adds keys to a set. Returns the number of new keys. +func (db *RedisDB) SetAdd(k string, elems ...string) (int, error) { + db.master.Lock() + defer db.master.Unlock() + defer db.master.signal.Broadcast() + + if db.exists(k) && db.t(k) != keyTypeSet { + return 0, ErrWrongType + } + return db.setAdd(k, elems...), nil +} + +// SMembers returns all keys in a set, sorted. +// Alias for Members. +func (m *Miniredis) SMembers(k string) ([]string, error) { + return m.Members(k) +} + +// Members returns all keys in a set, sorted. +func (m *Miniredis) Members(k string) ([]string, error) { + return m.DB(m.selectedDB).Members(k) +} + +// Members gives all set keys. Sorted. +func (db *RedisDB) Members(k string) ([]string, error) { + db.master.Lock() + defer db.master.Unlock() + + if !db.exists(k) { + return nil, ErrKeyNotFound + } + if db.t(k) != keyTypeSet { + return nil, ErrWrongType + } + return db.setMembers(k), nil +} + +// SIsMember tells if value is in the set. +// Alias for IsMember +func (m *Miniredis) SIsMember(k, v string) (bool, error) { + return m.IsMember(k, v) +} + +// IsMember tells if value is in the set. +func (m *Miniredis) IsMember(k, v string) (bool, error) { + return m.DB(m.selectedDB).IsMember(k, v) +} + +// IsMember tells if value is in the set. +func (db *RedisDB) IsMember(k, v string) (bool, error) { + db.master.Lock() + defer db.master.Unlock() + + if !db.exists(k) { + return false, ErrKeyNotFound + } + if db.t(k) != keyTypeSet { + return false, ErrWrongType + } + return db.setIsMember(k, v), nil +} + +// HKeys returns all (sorted) keys ('fields') for a hash key. +func (m *Miniredis) HKeys(k string) ([]string, error) { + return m.DB(m.selectedDB).HKeys(k) +} + +// HKeys returns all (sorted) keys ('fields') for a hash key. +func (db *RedisDB) HKeys(key string) ([]string, error) { + db.master.Lock() + defer db.master.Unlock() + + if !db.exists(key) { + return nil, ErrKeyNotFound + } + if db.t(key) != keyTypeHash { + return nil, ErrWrongType + } + return db.hashFields(key), nil +} + +// Del deletes a key and any expiration value. Returns whether there was a key. +func (m *Miniredis) Del(k string) bool { + return m.DB(m.selectedDB).Del(k) +} + +// Del deletes a key and any expiration value. Returns whether there was a key. +func (db *RedisDB) Del(k string) bool { + db.master.Lock() + defer db.master.Unlock() + defer db.master.signal.Broadcast() + + if !db.exists(k) { + return false + } + db.del(k, true) + return true +} + +// Unlink deletes a key and any expiration value. Returns where there was a key. +// It's exactly the same as Del() and is not async. It is here for the consistency. +func (m *Miniredis) Unlink(k string) bool { + return m.Del(k) +} + +// Unlink deletes a key and any expiration value. Returns where there was a key. +// It's exactly the same as Del() and is not async. It is here for the consistency. +func (db *RedisDB) Unlink(k string) bool { + return db.Del(k) +} + +// TTL is the left over time to live. As set via EXPIRE, PEXPIRE, EXPIREAT, +// PEXPIREAT. +// Note: this direct function returns 0 if there is no TTL set, unlike redis, +// which returns -1. +func (m *Miniredis) TTL(k string) time.Duration { + return m.DB(m.selectedDB).TTL(k) +} + +// TTL is the left over time to live. As set via EXPIRE, PEXPIRE, EXPIREAT, +// PEXPIREAT. +// 0 if not set. +func (db *RedisDB) TTL(k string) time.Duration { + db.master.Lock() + defer db.master.Unlock() + + return db.ttl[k] +} + +// SetTTL sets the TTL of a key. +func (m *Miniredis) SetTTL(k string, ttl time.Duration) { + m.DB(m.selectedDB).SetTTL(k, ttl) +} + +// SetTTL sets the time to live of a key. +func (db *RedisDB) SetTTL(k string, ttl time.Duration) { + db.master.Lock() + defer db.master.Unlock() + defer db.master.signal.Broadcast() + + db.ttl[k] = ttl + db.incr(k) +} + +// Type gives the type of a key, or "" +func (m *Miniredis) Type(k string) string { + return m.DB(m.selectedDB).Type(k) +} + +// Type gives the type of a key, or "" +func (db *RedisDB) Type(k string) string { + db.master.Lock() + defer db.master.Unlock() + + return db.t(k) +} + +// Exists tells whether a key exists. +func (m *Miniredis) Exists(k string) bool { + return m.DB(m.selectedDB).Exists(k) +} + +// Exists tells whether a key exists. +func (db *RedisDB) Exists(k string) bool { + db.master.Lock() + defer db.master.Unlock() + + return db.exists(k) +} + +// HGet returns hash keys added with HSET. +// This will return an empty string if the key is not set. Redis would return +// a nil. +// Returns empty string when the key is of a different type. +func (m *Miniredis) HGet(k, f string) string { + return m.DB(m.selectedDB).HGet(k, f) +} + +// HGet returns hash keys added with HSET. +// Returns empty string when the key is of a different type. +func (db *RedisDB) HGet(k, f string) string { + db.master.Lock() + defer db.master.Unlock() + + h, ok := db.hashKeys[k] + if !ok { + return "" + } + return h[f] +} + +// HSet sets hash keys. +// If there is another key by the same name it will be gone. +func (m *Miniredis) HSet(k string, fv ...string) { + m.DB(m.selectedDB).HSet(k, fv...) +} + +// HSet sets hash keys. +// If there is another key by the same name it will be gone. +func (db *RedisDB) HSet(k string, fv ...string) { + db.master.Lock() + defer db.master.Unlock() + defer db.master.signal.Broadcast() + + db.hashSet(k, fv...) +} + +// HDel deletes a hash key. +func (m *Miniredis) HDel(k, f string) { + m.DB(m.selectedDB).HDel(k, f) +} + +// HDel deletes a hash key. +func (db *RedisDB) HDel(k, f string) { + db.master.Lock() + defer db.master.Unlock() + defer db.master.signal.Broadcast() + + db.hdel(k, f) +} + +func (db *RedisDB) hdel(k, f string) { + if _, ok := db.hashKeys[k]; !ok { + return + } + delete(db.hashKeys[k], f) + db.incr(k) +} + +// HIncrBy increases the integer value of a hash field by delta (int). +func (m *Miniredis) HIncrBy(k, f string, delta int) (int, error) { + return m.HIncr(k, f, delta) +} + +// HIncr increases a key/field by delta (int). +func (m *Miniredis) HIncr(k, f string, delta int) (int, error) { + return m.DB(m.selectedDB).HIncr(k, f, delta) +} + +// HIncr increases a key/field by delta (int). +func (db *RedisDB) HIncr(k, f string, delta int) (int, error) { + db.master.Lock() + defer db.master.Unlock() + defer db.master.signal.Broadcast() + + return db.hashIncr(k, f, delta) +} + +// HIncrByFloat increases a key/field by delta (float). +func (m *Miniredis) HIncrByFloat(k, f string, delta float64) (float64, error) { + return m.HIncrfloat(k, f, delta) +} + +// HIncrfloat increases a key/field by delta (float). +func (m *Miniredis) HIncrfloat(k, f string, delta float64) (float64, error) { + return m.DB(m.selectedDB).HIncrfloat(k, f, delta) +} + +// HIncrfloat increases a key/field by delta (float). +func (db *RedisDB) HIncrfloat(k, f string, delta float64) (float64, error) { + db.master.Lock() + defer db.master.Unlock() + defer db.master.signal.Broadcast() + + v, err := db.hashIncrfloat(k, f, big.NewFloat(delta)) + if err != nil { + return 0, err + } + vf, _ := v.Float64() + return vf, nil +} + +// SRem removes fields from a set. Returns number of deleted fields. +func (m *Miniredis) SRem(k string, fields ...string) (int, error) { + return m.DB(m.selectedDB).SRem(k, fields...) +} + +// SRem removes fields from a set. Returns number of deleted fields. +func (db *RedisDB) SRem(k string, fields ...string) (int, error) { + db.master.Lock() + defer db.master.Unlock() + defer db.master.signal.Broadcast() + + if !db.exists(k) { + return 0, ErrKeyNotFound + } + if db.t(k) != keyTypeSet { + return 0, ErrWrongType + } + return db.setRem(k, fields...), nil +} + +// ZAdd adds a score,member to a sorted set. +func (m *Miniredis) ZAdd(k string, score float64, member string) (bool, error) { + return m.DB(m.selectedDB).ZAdd(k, score, member) +} + +// ZAdd adds a score,member to a sorted set. +func (db *RedisDB) ZAdd(k string, score float64, member string) (bool, error) { + db.master.Lock() + defer db.master.Unlock() + defer db.master.signal.Broadcast() + + if db.exists(k) && db.t(k) != keyTypeSortedSet { + return false, ErrWrongType + } + return db.ssetAdd(k, score, member), nil +} + +// ZMembers returns all members of a sorted set by score +func (m *Miniredis) ZMembers(k string) ([]string, error) { + return m.DB(m.selectedDB).ZMembers(k) +} + +// ZMembers returns all members of a sorted set by score +func (db *RedisDB) ZMembers(k string) ([]string, error) { + db.master.Lock() + defer db.master.Unlock() + + if !db.exists(k) { + return nil, ErrKeyNotFound + } + if db.t(k) != keyTypeSortedSet { + return nil, ErrWrongType + } + return db.ssetMembers(k), nil +} + +// SortedSet returns a raw string->float64 map. +func (m *Miniredis) SortedSet(k string) (map[string]float64, error) { + return m.DB(m.selectedDB).SortedSet(k) +} + +// SortedSet returns a raw string->float64 map. +func (db *RedisDB) SortedSet(k string) (map[string]float64, error) { + db.master.Lock() + defer db.master.Unlock() + + if !db.exists(k) { + return nil, ErrKeyNotFound + } + if db.t(k) != keyTypeSortedSet { + return nil, ErrWrongType + } + return db.sortedSet(k), nil +} + +// ZRem deletes a member. Returns whether the was a key. +func (m *Miniredis) ZRem(k, member string) (bool, error) { + return m.DB(m.selectedDB).ZRem(k, member) +} + +// ZRem deletes a member. Returns whether the was a key. +func (db *RedisDB) ZRem(k, member string) (bool, error) { + db.master.Lock() + defer db.master.Unlock() + defer db.master.signal.Broadcast() + + if !db.exists(k) { + return false, ErrKeyNotFound + } + if db.t(k) != keyTypeSortedSet { + return false, ErrWrongType + } + return db.ssetRem(k, member), nil +} + +// ZScore gives the score of a sorted set member. +func (m *Miniredis) ZScore(k, member string) (float64, error) { + return m.DB(m.selectedDB).ZScore(k, member) +} + +// ZScore gives the score of a sorted set member. +func (db *RedisDB) ZScore(k, member string) (float64, error) { + db.master.Lock() + defer db.master.Unlock() + + if !db.exists(k) { + return 0, ErrKeyNotFound + } + if db.t(k) != keyTypeSortedSet { + return 0, ErrWrongType + } + return db.ssetScore(k, member), nil +} + +// ZScore gives scores of a list of members in a sorted set. +func (m *Miniredis) ZMScore(k string, members ...string) ([]float64, error) { + return m.DB(m.selectedDB).ZMScore(k, members) +} + +func (db *RedisDB) ZMScore(k string, members []string) ([]float64, error) { + db.master.Lock() + defer db.master.Unlock() + + if !db.exists(k) { + return nil, ErrKeyNotFound + } + if db.t(k) != keyTypeSortedSet { + return nil, ErrWrongType + } + return db.ssetMScore(k, members), nil +} + +// XAdd adds an entry to a stream. `id` can be left empty or be '*'. +// If a value is given normal XADD rules apply. Values should be an even +// length. +func (m *Miniredis) XAdd(k string, id string, values []string) (string, error) { + return m.DB(m.selectedDB).XAdd(k, id, values) +} + +// XAdd adds an entry to a stream. `id` can be left empty or be '*'. +// If a value is given normal XADD rules apply. Values should be an even +// length. +func (db *RedisDB) XAdd(k string, id string, values []string) (string, error) { + db.master.Lock() + defer db.master.Unlock() + defer db.master.signal.Broadcast() + + s, err := db.stream(k) + if err != nil { + return "", err + } + if s == nil { + s, _ = db.newStream(k) + } + + return s.add(id, values, db.master.effectiveNow()) +} + +// Stream returns a slice of stream entries. Oldest first. +func (m *Miniredis) Stream(k string) ([]StreamEntry, error) { + return m.DB(m.selectedDB).Stream(k) +} + +// Stream returns a slice of stream entries. Oldest first. +func (db *RedisDB) Stream(key string) ([]StreamEntry, error) { + db.master.Lock() + defer db.master.Unlock() + + s, err := db.stream(key) + if err != nil { + return nil, err + } + if s == nil { + return nil, nil + } + return s.entries, nil +} + +// Publish a message to subscribers. Returns the number of receivers. +func (m *Miniredis) Publish(channel, message string) int { + m.Lock() + defer m.Unlock() + + return m.publish(channel, message) +} + +// PubSubChannels is "PUBSUB CHANNELS <pattern>". An empty pattern is fine +// (meaning all channels). +// Returned channels will be ordered alphabetically. +func (m *Miniredis) PubSubChannels(pattern string) []string { + m.Lock() + defer m.Unlock() + + return activeChannels(m.allSubscribers(), pattern) +} + +// PubSubNumSub is "PUBSUB NUMSUB [channels]". It returns all channels with their +// subscriber count. +func (m *Miniredis) PubSubNumSub(channels ...string) map[string]int { + m.Lock() + defer m.Unlock() + + subs := m.allSubscribers() + res := map[string]int{} + for _, channel := range channels { + res[channel] = countSubs(subs, channel) + } + return res +} + +// PubSubNumPat is "PUBSUB NUMPAT" +func (m *Miniredis) PubSubNumPat() int { + m.Lock() + defer m.Unlock() + + return countPsubs(m.allSubscribers()) +} + +// PfAdd adds keys to a hll. Returns the flag which equals to 1 if the inner hll value has been changed. +func (m *Miniredis) PfAdd(k string, elems ...string) (int, error) { + return m.DB(m.selectedDB).HllAdd(k, elems...) +} + +// HllAdd adds keys to a hll. Returns the flag which equals to true if the inner hll value has been changed. +func (db *RedisDB) HllAdd(k string, elems ...string) (int, error) { + db.master.Lock() + defer db.master.Unlock() + + if db.exists(k) && db.t(k) != keyTypeHll { + return 0, ErrWrongType + } + return db.hllAdd(k, elems...), nil +} + +// PfCount returns an estimation of the amount of elements previously added to a hll. +func (m *Miniredis) PfCount(keys ...string) (int, error) { + return m.DB(m.selectedDB).HllCount(keys...) +} + +// HllCount returns an estimation of the amount of elements previously added to a hll. +func (db *RedisDB) HllCount(keys ...string) (int, error) { + db.master.Lock() + defer db.master.Unlock() + + return db.hllCount(keys) +} + +// PfMerge merges all the input hlls into a hll under destKey key. +func (m *Miniredis) PfMerge(destKey string, sourceKeys ...string) error { + return m.DB(m.selectedDB).HllMerge(destKey, sourceKeys...) +} + +// HllMerge merges all the input hlls into a hll under destKey key. +func (db *RedisDB) HllMerge(destKey string, sourceKeys ...string) error { + db.master.Lock() + defer db.master.Unlock() + + return db.hllMerge(append([]string{destKey}, sourceKeys...)) +} + +// Copy a value. +// Needs the IDs of both the source and dest DBs (which can differ). +// Returns ErrKeyNotFound if src does not exist. +// Overwrites dest if it already exists (unlike the redis command, which needs a flag to allow that). +func (m *Miniredis) Copy(srcDB int, src string, destDB int, dest string) error { + return m.copy(m.DB(srcDB), src, m.DB(destDB), dest) +}
vendor/github.com/alicebob/miniredis/v2/fpconv/dtoa.go+286 −0 added@@ -0,0 +1,286 @@ +package fpconv + +import ( + "math" +) + +var ( + fracmask uint64 = 0x000FFFFFFFFFFFFF + expmask uint64 = 0x7FF0000000000000 + hiddenbit uint64 = 0x0010000000000000 + signmask uint64 = 0x8000000000000000 + expbias int64 = 1023 + 52 + zeros = []rune("0000000000000000000000") + + tens = []uint64{ + 10000000000000000000, + 1000000000000000000, + 100000000000000000, + 10000000000000000, + 1000000000000000, + 100000000000000, + 10000000000000, + 1000000000000, + 100000000000, + 10000000000, + 1000000000, + 100000000, + 10000000, + 1000000, + 100000, + 10000, + 1000, + 100, + 10, + 1} +) + +func absv(n int) int { + if n < 0 { + return -n + } + return n +} + +func minv(a, b int) int { + if a < b { + return a + } + return b +} + +func Dtoa(d float64) string { + var ( + dest [25]rune // Note C has 24, which is broken + digits [18]rune + + str_len int = 0 + neg = false + ) + + if get_dbits(d)&signmask != 0 { + dest[0] = '-' + str_len++ + neg = true + } + + if spec := filter_special(d, dest[str_len:]); spec != 0 { + return string(dest[:str_len+spec]) + } + + var ( + k int = 0 + ndigits int = grisu2(d, &digits, &k) + ) + + str_len += emit_digits(&digits, ndigits, dest[str_len:], k, neg) + return string(dest[:str_len]) +} + +func filter_special(fp float64, dest []rune) int { + if fp == 0.0 { + dest[0] = '0' + return 1 + } + + if math.IsNaN(fp) { + dest[0] = 'n' + dest[1] = 'a' + dest[2] = 'n' + return 3 + } + if math.IsInf(fp, 0) { + dest[0] = 'i' + dest[1] = 'n' + dest[2] = 'f' + return 3 + } + return 0 +} + +func grisu2(d float64, digits *[18]rune, K *int) int { + w := build_fp(d) + + lower, upper := get_normalized_boundaries(w) + + w = normalize(w) + + var k int64 + cp := find_cachedpow10(upper.exp, &k) + + w = multiply(w, cp) + upper = multiply(upper, cp) + lower = multiply(lower, cp) + + lower.frac++ + upper.frac-- + + *K = int(-k) + + return generate_digits(w, upper, lower, digits[:], K) +} + +func emit_digits(digits *[18]rune, ndigits int, dest []rune, K int, neg bool) int { + exp := int(absv(K + ndigits - 1)) + + /* write plain integer */ + if K >= 0 && (exp < (ndigits + 7)) { + copy(dest, digits[:ndigits]) + copy(dest[ndigits:], zeros[:K]) + + return ndigits + K + } + + /* write decimal w/o scientific notation */ + if K < 0 && (K > -7 || exp < 4) { + offset := int(ndigits - absv(K)) + /* fp < 1.0 -> write leading zero */ + if offset <= 0 { + offset = -offset + dest[0] = '0' + dest[1] = '.' + copy(dest[2:], zeros[:offset]) + copy(dest[offset+2:], digits[:ndigits]) + + return ndigits + 2 + offset + + /* fp > 1.0 */ + } else { + copy(dest, digits[:offset]) + dest[offset] = '.' + copy(dest[offset+1:], digits[offset:offset+ndigits-offset]) + + return ndigits + 1 + } + } + /* write decimal w/ scientific notation */ + l := 18 // was: 18-neg + if neg { + l-- + } + ndigits = minv(ndigits, l) + + var idx int = 0 + dest[idx] = digits[0] + idx++ + + if ndigits > 1 { + dest[idx] = '.' + idx++ + copy(dest[idx:], digits[+1:ndigits-1+1]) + idx += ndigits - 1 + } + + dest[idx] = 'e' + idx++ + + sign := '+' + if K+ndigits-1 < 0 { + sign = '-' + } + dest[idx] = sign + idx++ + + var cent rune = 0 + + if exp > 99 { + cent = rune(exp / 100) + dest[idx] = cent + '0' + idx++ + exp -= int(cent) * 100 + } + if exp > 9 { + dec := rune(exp / 10) + dest[idx] = dec + '0' + idx++ + exp -= int(dec) * 10 + } else if cent != 0 { + dest[idx] = '0' + idx++ + } + + dest[idx] = rune(exp%10) + '0' + idx++ + + return idx +} + +func generate_digits(fp, upper, lower Fp, digits []rune, K *int) int { + var ( + wfrac = uint64(upper.frac - fp.frac) + delta = uint64(upper.frac - lower.frac) + ) + + one := Fp{ + frac: 1 << -upper.exp, + exp: upper.exp, + } + + part1 := uint64(upper.frac >> -one.exp) + part2 := uint64(upper.frac & (one.frac - 1)) + + var ( + idx = 0 + kappa = 10 + index = 10 + ) + /* 1000000000 */ + for ; kappa > 0; index++ { + div := tens[index] + digit := part1 / div + + if digit != 0 || idx != 0 { + digits[idx] = rune(digit) + '0' + idx++ + } + + part1 -= digit * div + kappa-- + + tmp := (part1 << -one.exp) + part2 + if tmp <= delta { + *K += kappa + round_digit(digits, idx, delta, tmp, div<<-one.exp, wfrac) + + return idx + } + } + + /* 10 */ + index = 18 + for { + var unit uint64 = tens[index] + part2 *= 10 + delta *= 10 + kappa-- + + digit := part2 >> -one.exp + if digit != 0 || idx != 0 { + digits[idx] = rune(digit) + '0' + idx++ + } + + part2 &= uint64(one.frac) - 1 + if part2 < delta { + *K += kappa + round_digit(digits, idx, delta, part2, uint64(one.frac), wfrac*unit) + + return idx + } + + index-- + } +} + +func round_digit(digits []rune, + ndigits int, + delta uint64, + rem uint64, + kappa uint64, + frac uint64) { + for rem < frac && delta-rem >= kappa && + (rem+kappa < frac || frac-rem > rem+kappa-frac) { + digits[ndigits-1]-- + rem += kappa + } +}
vendor/github.com/alicebob/miniredis/v2/fpconv/fp.go+96 −0 added@@ -0,0 +1,96 @@ +package fpconv + +import ( + "math" +) + +type ( + Fp struct { + frac uint64 + exp int64 + } +) + +func build_fp(d float64) Fp { + bits := get_dbits(d) + + fp := Fp{ + frac: bits & fracmask, + exp: int64((bits & expmask) >> 52), + } + + if fp.exp != 0 { + fp.frac += hiddenbit + fp.exp -= expbias + } else { + fp.exp = -expbias + 1 + } + + return fp +} + +func normalize(fp Fp) Fp { + for (fp.frac & hiddenbit) == 0 { + fp.frac <<= 1 + fp.exp-- + } + + var shift int64 = 64 - 52 - 1 + fp.frac <<= shift + fp.exp -= shift + return fp +} + +func multiply(a, b Fp) Fp { + lomask := uint64(0x00000000FFFFFFFF) + + var ( + ah_bl = uint64((a.frac >> 32) * (b.frac & lomask)) + al_bh = uint64((a.frac & lomask) * (b.frac >> 32)) + al_bl = uint64((a.frac & lomask) * (b.frac & lomask)) + ah_bh = uint64((a.frac >> 32) * (b.frac >> 32)) + ) + + tmp := uint64((ah_bl & lomask) + (al_bh & lomask) + (al_bl >> 32)) + /* round up */ + tmp += uint64(1) << 31 + + return Fp{ + ah_bh + (ah_bl >> 32) + (al_bh >> 32) + (tmp >> 32), + a.exp + b.exp + 64, + } +} + +func get_dbits(d float64) uint64 { + return math.Float64bits(d) +} + +func get_normalized_boundaries(fp Fp) (Fp, Fp) { + upper := Fp{ + frac: (fp.frac << 1) + 1, + exp: fp.exp - 1, + } + for (upper.frac & (hiddenbit << 1)) == 0 { + upper.frac <<= 1 + upper.exp-- + } + + var u_shift int64 = 64 - 52 - 2 + + upper.frac <<= u_shift + upper.exp = upper.exp - u_shift + + l_shift := int64(1) + if fp.frac == hiddenbit { + l_shift = 2 + } + + lower := Fp{ + frac: (fp.frac << l_shift) - 1, + exp: fp.exp - l_shift, + } + + lower.frac <<= lower.exp - upper.exp + lower.exp = upper.exp + return lower, upper +}
vendor/github.com/alicebob/miniredis/v2/fpconv/LICENSE.txt+26 −0 added@@ -0,0 +1,26 @@ +This code is derived from the C code in redis-7.2.0/deps/fpconv/*, which has +this license: + +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +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, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE.
vendor/github.com/alicebob/miniredis/v2/fpconv/Makefile+6 −0 added@@ -0,0 +1,6 @@ +.PHONY: test fuzz +test: + go test + +fuzz: + go test -fuzz=Fuzz
vendor/github.com/alicebob/miniredis/v2/fpconv/powers.go+82 −0 added@@ -0,0 +1,82 @@ +package fpconv + +var ( + npowers int64 = 87 + steppowers int64 = 8 + firstpower int64 = -348 /* 10 ^ -348 */ + + expmax = -32 + expmin = -60 + + powers_ten = []Fp{ + {18054884314459144840, -1220}, {13451937075301367670, -1193}, + {10022474136428063862, -1166}, {14934650266808366570, -1140}, + {11127181549972568877, -1113}, {16580792590934885855, -1087}, + {12353653155963782858, -1060}, {18408377700990114895, -1034}, + {13715310171984221708, -1007}, {10218702384817765436, -980}, + {15227053142812498563, -954}, {11345038669416679861, -927}, + {16905424996341287883, -901}, {12595523146049147757, -874}, + {9384396036005875287, -847}, {13983839803942852151, -821}, + {10418772551374772303, -794}, {15525180923007089351, -768}, + {11567161174868858868, -741}, {17236413322193710309, -715}, + {12842128665889583758, -688}, {9568131466127621947, -661}, + {14257626930069360058, -635}, {10622759856335341974, -608}, + {15829145694278690180, -582}, {11793632577567316726, -555}, + {17573882009934360870, -529}, {13093562431584567480, -502}, + {9755464219737475723, -475}, {14536774485912137811, -449}, + {10830740992659433045, -422}, {16139061738043178685, -396}, + {12024538023802026127, -369}, {17917957937422433684, -343}, + {13349918974505688015, -316}, {9946464728195732843, -289}, + {14821387422376473014, -263}, {11042794154864902060, -236}, + {16455045573212060422, -210}, {12259964326927110867, -183}, + {18268770466636286478, -157}, {13611294676837538539, -130}, + {10141204801825835212, -103}, {15111572745182864684, -77}, + {11258999068426240000, -50}, {16777216000000000000, -24}, + {12500000000000000000, 3}, {9313225746154785156, 30}, + {13877787807814456755, 56}, {10339757656912845936, 83}, + {15407439555097886824, 109}, {11479437019748901445, 136}, + {17105694144590052135, 162}, {12744735289059618216, 189}, + {9495567745759798747, 216}, {14149498560666738074, 242}, + {10542197943230523224, 269}, {15709099088952724970, 295}, + {11704190886730495818, 322}, {17440603504673385349, 348}, + {12994262207056124023, 375}, {9681479787123295682, 402}, + {14426529090290212157, 428}, {10748601772107342003, 455}, + {16016664761464807395, 481}, {11933345169920330789, 508}, + {17782069995880619868, 534}, {13248674568444952270, 561}, + {9871031767461413346, 588}, {14708983551653345445, 614}, + {10959046745042015199, 641}, {16330252207878254650, 667}, + {12166986024289022870, 694}, {18130221999122236476, 720}, + {13508068024458167312, 747}, {10064294952495520794, 774}, + {14996968138956309548, 800}, {11173611982879273257, 827}, + {16649979327439178909, 853}, {12405201291620119593, 880}, + {9242595204427927429, 907}, {13772540099066387757, 933}, + {10261342003245940623, 960}, {15290591125556738113, 986}, + {11392378155556871081, 1013}, {16975966327722178521, 1039}, + {12648080533535911531, 1066}, + } +) + +func find_cachedpow10(exp int64, k *int64) Fp { + one_log_ten := 0.30102999566398114 + + approx := int64(float64(-(exp + npowers)) * one_log_ten) + idx := int((approx - firstpower) / steppowers) + + for { + current := int(exp + powers_ten[idx].exp + 64) + + if current < expmin { + idx++ + continue + } + + if current > expmax { + idx-- + continue + } + + *k = (firstpower + int64(idx)*steppowers) + + return powers_ten[idx] + } +}
vendor/github.com/alicebob/miniredis/v2/fpconv/README.md+3 −0 added@@ -0,0 +1,3 @@ +This is a translation of the actual C code in Redis (7.2) which does the float +-> string conversion. +Strconv does a close enough job, but we can use the exact same logic, so why not.
vendor/github.com/alicebob/miniredis/v2/geo.go+46 −0 added@@ -0,0 +1,46 @@ +package miniredis + +import ( + "math" + + "github.com/alicebob/miniredis/v2/geohash" +) + +func toGeohash(long, lat float64) uint64 { + return geohash.EncodeIntWithPrecision(lat, long, 52) +} + +func fromGeohash(score uint64) (float64, float64) { + lat, long := geohash.DecodeIntWithPrecision(score, 52) + return long, lat +} + +// haversin(θ) function +func hsin(theta float64) float64 { + return math.Pow(math.Sin(theta/2), 2) +} + +// distance function returns the distance (in meters) between two points of +// a given longitude and latitude relatively accurately (using a spherical +// approximation of the Earth) through the Haversin Distance Formula for +// great arc distance on a sphere with accuracy for small distances +// point coordinates are supplied in degrees and converted into rad. in the func +// distance returned is meters +// http://en.wikipedia.org/wiki/Haversine_formula +// Source: https://gist.github.com/cdipaolo/d3f8db3848278b49db68 +func distance(lat1, lon1, lat2, lon2 float64) float64 { + // convert to radians + // must cast radius as float to multiply later + var la1, lo1, la2, lo2 float64 + la1 = lat1 * math.Pi / 180 + lo1 = lon1 * math.Pi / 180 + la2 = lat2 * math.Pi / 180 + lo2 = lon2 * math.Pi / 180 + + earth := 6372797.560856 // Earth radius in METERS, according to src/geohash_helper.c + + // calculate + h := hsin(la2-la1) + math.Cos(la1)*math.Cos(la2)*hsin(lo2-lo1) + + return 2 * earth * math.Asin(math.Sqrt(h)) +}
vendor/github.com/alicebob/miniredis/v2/geohash/base32.go+44 −0 added@@ -0,0 +1,44 @@ +package geohash + +// encoding encapsulates an encoding defined by a given base32 alphabet. +type encoding struct { + encode string + decode [256]byte +} + +// newEncoding constructs a new encoding defined by the given alphabet, +// which must be a 32-byte string. +func newEncoding(encoder string) *encoding { + e := new(encoding) + e.encode = encoder + for i := 0; i < len(e.decode); i++ { + e.decode[i] = 0xff + } + for i := 0; i < len(encoder); i++ { + e.decode[encoder[i]] = byte(i) + } + return e +} + +// Decode string into bits of a 64-bit word. The string s may be at most 12 +// characters. +func (e *encoding) Decode(s string) uint64 { + x := uint64(0) + for i := 0; i < len(s); i++ { + x = (x << 5) | uint64(e.decode[s[i]]) + } + return x +} + +// Encode bits of 64-bit word into a string. +func (e *encoding) Encode(x uint64) string { + b := [12]byte{} + for i := 0; i < 12; i++ { + b[11-i] = e.encode[x&0x1f] + x >>= 5 + } + return string(b[:]) +} + +// Base32Encoding with the Geohash alphabet. +var base32encoding = newEncoding("0123456789bcdefghjkmnpqrstuvwxyz")
vendor/github.com/alicebob/miniredis/v2/geohash/geohash.go+269 −0 added@@ -0,0 +1,269 @@ +// Package geohash provides encoding and decoding of string and integer +// geohashes. +package geohash + +import ( + "math" +) + +const ( + ENC_LAT = 85.05112878 + ENC_LONG = 180.0 +) + +// Direction represents directions in the latitute/longitude space. +type Direction int + +// Cardinal and intercardinal directions +const ( + North Direction = iota + NorthEast + East + SouthEast + South + SouthWest + West + NorthWest +) + +// Encode the point (lat, lng) as a string geohash with the standard 12 +// characters of precision. +func Encode(lat, lng float64) string { + return EncodeWithPrecision(lat, lng, 12) +} + +// EncodeWithPrecision encodes the point (lat, lng) as a string geohash with +// the specified number of characters of precision (max 12). +func EncodeWithPrecision(lat, lng float64, chars uint) string { + bits := 5 * chars + inthash := EncodeIntWithPrecision(lat, lng, bits) + enc := base32encoding.Encode(inthash) + return enc[12-chars:] +} + +// encodeInt provides a Go implementation of integer geohash. This is the +// default implementation of EncodeInt, but optimized versions are provided +// for certain architectures. +func EncodeInt(lat, lng float64) uint64 { + latInt := encodeRange(lat, ENC_LAT) + lngInt := encodeRange(lng, ENC_LONG) + return interleave(latInt, lngInt) +} + +// EncodeIntWithPrecision encodes the point (lat, lng) to an integer with the +// specified number of bits. +func EncodeIntWithPrecision(lat, lng float64, bits uint) uint64 { + hash := EncodeInt(lat, lng) + return hash >> (64 - bits) +} + +// Box represents a rectangle in latitude/longitude space. +type Box struct { + MinLat float64 + MaxLat float64 + MinLng float64 + MaxLng float64 +} + +// Center returns the center of the box. +func (b Box) Center() (lat, lng float64) { + lat = (b.MinLat + b.MaxLat) / 2.0 + lng = (b.MinLng + b.MaxLng) / 2.0 + return +} + +// Contains decides whether (lat, lng) is contained in the box. The +// containment test is inclusive of the edges and corners. +func (b Box) Contains(lat, lng float64) bool { + return (b.MinLat <= lat && lat <= b.MaxLat && + b.MinLng <= lng && lng <= b.MaxLng) +} + +// errorWithPrecision returns the error range in latitude and longitude for in +// integer geohash with bits of precision. +func errorWithPrecision(bits uint) (latErr, lngErr float64) { + b := int(bits) + latBits := b / 2 + lngBits := b - latBits + latErr = math.Ldexp(180.0, -latBits) + lngErr = math.Ldexp(360.0, -lngBits) + return +} + +// BoundingBox returns the region encoded by the given string geohash. +func BoundingBox(hash string) Box { + bits := uint(5 * len(hash)) + inthash := base32encoding.Decode(hash) + return BoundingBoxIntWithPrecision(inthash, bits) +} + +// BoundingBoxIntWithPrecision returns the region encoded by the integer +// geohash with the specified precision. +func BoundingBoxIntWithPrecision(hash uint64, bits uint) Box { + fullHash := hash << (64 - bits) + latInt, lngInt := deinterleave(fullHash) + lat := decodeRange(latInt, ENC_LAT) + lng := decodeRange(lngInt, ENC_LONG) + latErr, lngErr := errorWithPrecision(bits) + return Box{ + MinLat: lat, + MaxLat: lat + latErr, + MinLng: lng, + MaxLng: lng + lngErr, + } +} + +// BoundingBoxInt returns the region encoded by the given 64-bit integer +// geohash. +func BoundingBoxInt(hash uint64) Box { + return BoundingBoxIntWithPrecision(hash, 64) +} + +// DecodeCenter decodes the string geohash to the central point of the bounding box. +func DecodeCenter(hash string) (lat, lng float64) { + box := BoundingBox(hash) + return box.Center() +} + +// DecodeIntWithPrecision decodes the provided integer geohash with bits of +// precision to a (lat, lng) point. +func DecodeIntWithPrecision(hash uint64, bits uint) (lat, lng float64) { + box := BoundingBoxIntWithPrecision(hash, bits) + return box.Center() +} + +// DecodeInt decodes the provided 64-bit integer geohash to a (lat, lng) point. +func DecodeInt(hash uint64) (lat, lng float64) { + return DecodeIntWithPrecision(hash, 64) +} + +// Neighbors returns a slice of geohash strings that correspond to the provided +// geohash's neighbors. +func Neighbors(hash string) []string { + box := BoundingBox(hash) + lat, lng := box.Center() + latDelta := box.MaxLat - box.MinLat + lngDelta := box.MaxLng - box.MinLng + precision := uint(len(hash)) + return []string{ + // N + EncodeWithPrecision(lat+latDelta, lng, precision), + // NE, + EncodeWithPrecision(lat+latDelta, lng+lngDelta, precision), + // E, + EncodeWithPrecision(lat, lng+lngDelta, precision), + // SE, + EncodeWithPrecision(lat-latDelta, lng+lngDelta, precision), + // S, + EncodeWithPrecision(lat-latDelta, lng, precision), + // SW, + EncodeWithPrecision(lat-latDelta, lng-lngDelta, precision), + // W, + EncodeWithPrecision(lat, lng-lngDelta, precision), + // NW + EncodeWithPrecision(lat+latDelta, lng-lngDelta, precision), + } +} + +// NeighborsInt returns a slice of uint64s that correspond to the provided hash's +// neighbors at 64-bit precision. +func NeighborsInt(hash uint64) []uint64 { + return NeighborsIntWithPrecision(hash, 64) +} + +// NeighborsIntWithPrecision returns a slice of uint64s that correspond to the +// provided hash's neighbors at the given precision. +func NeighborsIntWithPrecision(hash uint64, bits uint) []uint64 { + box := BoundingBoxIntWithPrecision(hash, bits) + lat, lng := box.Center() + latDelta := box.MaxLat - box.MinLat + lngDelta := box.MaxLng - box.MinLng + return []uint64{ + // N + EncodeIntWithPrecision(lat+latDelta, lng, bits), + // NE, + EncodeIntWithPrecision(lat+latDelta, lng+lngDelta, bits), + // E, + EncodeIntWithPrecision(lat, lng+lngDelta, bits), + // SE, + EncodeIntWithPrecision(lat-latDelta, lng+lngDelta, bits), + // S, + EncodeIntWithPrecision(lat-latDelta, lng, bits), + // SW, + EncodeIntWithPrecision(lat-latDelta, lng-lngDelta, bits), + // W, + EncodeIntWithPrecision(lat, lng-lngDelta, bits), + // NW + EncodeIntWithPrecision(lat+latDelta, lng-lngDelta, bits), + } +} + +// Neighbor returns a geohash string that corresponds to the provided +// geohash's neighbor in the provided direction +func Neighbor(hash string, direction Direction) string { + return Neighbors(hash)[direction] +} + +// NeighborInt returns a uint64 that corresponds to the provided hash's +// neighbor in the provided direction at 64-bit precision. +func NeighborInt(hash uint64, direction Direction) uint64 { + return NeighborsIntWithPrecision(hash, 64)[direction] +} + +// NeighborIntWithPrecision returns a uint64s that corresponds to the +// provided hash's neighbor in the provided direction at the given precision. +func NeighborIntWithPrecision(hash uint64, bits uint, direction Direction) uint64 { + return NeighborsIntWithPrecision(hash, bits)[direction] +} + +// precalculated for performance +var exp232 = math.Exp2(32) + +// Encode the position of x within the range -r to +r as a 32-bit integer. +func encodeRange(x, r float64) uint32 { + p := (x + r) / (2 * r) + return uint32(p * exp232) +} + +// Decode the 32-bit range encoding X back to a value in the range -r to +r. +func decodeRange(X uint32, r float64) float64 { + p := float64(X) / exp232 + x := 2*r*p - r + return x +} + +// Spread out the 32 bits of x into 64 bits, where the bits of x occupy even +// bit positions. +func spread(x uint32) uint64 { + X := uint64(x) + X = (X | (X << 16)) & 0x0000ffff0000ffff + X = (X | (X << 8)) & 0x00ff00ff00ff00ff + X = (X | (X << 4)) & 0x0f0f0f0f0f0f0f0f + X = (X | (X << 2)) & 0x3333333333333333 + X = (X | (X << 1)) & 0x5555555555555555 + return X +} + +// Interleave the bits of x and y. In the result, x and y occupy even and odd +// bitlevels, respectively. +func interleave(x, y uint32) uint64 { + return spread(x) | (spread(y) << 1) +} + +// Squash the even bitlevels of X into a 32-bit word. Odd bitlevels of X are +// ignored, and may take any value. +func squash(X uint64) uint32 { + X &= 0x5555555555555555 + X = (X | (X >> 1)) & 0x3333333333333333 + X = (X | (X >> 2)) & 0x0f0f0f0f0f0f0f0f + X = (X | (X >> 4)) & 0x00ff00ff00ff00ff + X = (X | (X >> 8)) & 0x0000ffff0000ffff + X = (X | (X >> 16)) & 0x00000000ffffffff + return uint32(X) +} + +// Deinterleave the bits of X into 32-bit words containing the even and odd +// bitlevels of X, respectively. +func deinterleave(X uint64) (uint32, uint32) { + return squash(X), squash(X >> 1) +}
vendor/github.com/alicebob/miniredis/v2/geohash/LICENSE+22 −0 added@@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Michael McLoughlin + +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. +
vendor/github.com/alicebob/miniredis/v2/geohash/README.md+2 −0 added@@ -0,0 +1,2 @@ +This is a (selected) copy of github.com/mmcloughlin/geohash with the latitude +range changed from 90 to ~85, to align with the algorithm use by Redis.
vendor/github.com/alicebob/miniredis/v2/.gitignore+6 −0 added@@ -0,0 +1,6 @@ +/integration/redis_src/ +/integration/dump.rdb +*.swp +/integration/nodes.conf +.idea/ +miniredis.iml
vendor/github.com/alicebob/miniredis/v2/gopher-json/json.go+189 −0 added@@ -0,0 +1,189 @@ +package json + +import ( + "encoding/json" + "errors" + + "github.com/yuin/gopher-lua" +) + +// Preload adds json to the given Lua state's package.preload table. After it +// has been preloaded, it can be loaded using require: +// +// local json = require("json") +func Preload(L *lua.LState) { + L.PreloadModule("json", Loader) +} + +// Loader is the module loader function. +func Loader(L *lua.LState) int { + t := L.NewTable() + L.SetFuncs(t, api) + L.Push(t) + return 1 +} + +var api = map[string]lua.LGFunction{ + "decode": apiDecode, + "encode": apiEncode, +} + +func apiDecode(L *lua.LState) int { + if L.GetTop() != 1 { + L.Error(lua.LString("bad argument #1 to decode"), 1) + return 0 + } + str := L.CheckString(1) + + value, err := Decode(L, []byte(str)) + if err != nil { + L.Push(lua.LNil) + L.Push(lua.LString(err.Error())) + return 2 + } + L.Push(value) + return 1 +} + +func apiEncode(L *lua.LState) int { + if L.GetTop() != 1 { + L.Error(lua.LString("bad argument #1 to encode"), 1) + return 0 + } + value := L.CheckAny(1) + + data, err := Encode(value) + if err != nil { + L.Push(lua.LNil) + L.Push(lua.LString(err.Error())) + return 2 + } + L.Push(lua.LString(string(data))) + return 1 +} + +var ( + errNested = errors.New("cannot encode recursively nested tables to JSON") + errSparseArray = errors.New("cannot encode sparse array") + errInvalidKeys = errors.New("cannot encode mixed or invalid key types") +) + +type invalidTypeError lua.LValueType + +func (i invalidTypeError) Error() string { + return `cannot encode ` + lua.LValueType(i).String() + ` to JSON` +} + +// Encode returns the JSON encoding of value. +func Encode(value lua.LValue) ([]byte, error) { + return json.Marshal(jsonValue{ + LValue: value, + visited: make(map[*lua.LTable]bool), + }) +} + +type jsonValue struct { + lua.LValue + visited map[*lua.LTable]bool +} + +func (j jsonValue) MarshalJSON() (data []byte, err error) { + switch converted := j.LValue.(type) { + case lua.LBool: + data, err = json.Marshal(bool(converted)) + case lua.LNumber: + data, err = json.Marshal(float64(converted)) + case *lua.LNilType: + data = []byte(`null`) + case lua.LString: + data, err = json.Marshal(string(converted)) + case *lua.LTable: + if j.visited[converted] { + return nil, errNested + } + j.visited[converted] = true + + key, value := converted.Next(lua.LNil) + + switch key.Type() { + case lua.LTNil: // empty table + data = []byte(`[]`) + case lua.LTNumber: + arr := make([]jsonValue, 0, converted.Len()) + expectedKey := lua.LNumber(1) + for key != lua.LNil { + if key.Type() != lua.LTNumber { + err = errInvalidKeys + return + } + if expectedKey != key { + err = errSparseArray + return + } + arr = append(arr, jsonValue{value, j.visited}) + expectedKey++ + key, value = converted.Next(key) + } + data, err = json.Marshal(arr) + case lua.LTString: + obj := make(map[string]jsonValue) + for key != lua.LNil { + if key.Type() != lua.LTString { + err = errInvalidKeys + return + } + obj[key.String()] = jsonValue{value, j.visited} + key, value = converted.Next(key) + } + data, err = json.Marshal(obj) + default: + err = errInvalidKeys + } + default: + err = invalidTypeError(j.LValue.Type()) + } + return +} + +// Decode converts the JSON encoded data to Lua values. +func Decode(L *lua.LState, data []byte) (lua.LValue, error) { + var value interface{} + err := json.Unmarshal(data, &value) + if err != nil { + return nil, err + } + return DecodeValue(L, value), nil +} + +// DecodeValue converts the value to a Lua value. +// +// This function only converts values that the encoding/json package decodes to. +// All other values will return lua.LNil. +func DecodeValue(L *lua.LState, value interface{}) lua.LValue { + switch converted := value.(type) { + case bool: + return lua.LBool(converted) + case float64: + return lua.LNumber(converted) + case string: + return lua.LString(converted) + case json.Number: + return lua.LString(converted) + case []interface{}: + arr := L.CreateTable(len(converted), 0) + for _, item := range converted { + arr.Append(DecodeValue(L, item)) + } + return arr + case map[string]interface{}: + tbl := L.CreateTable(0, len(converted)) + for key, item := range converted { + tbl.RawSetH(lua.LString(key), DecodeValue(L, item)) + } + return tbl + case nil: + return lua.LNil + } + + return lua.LNil +}
vendor/github.com/alicebob/miniredis/v2/gopher-json/LICENSE+24 −0 added@@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to <http://unlicense.org/>
vendor/github.com/alicebob/miniredis/v2/gopher-json/README.md+1 −0 added@@ -0,0 +1 @@ +Copied from https://github.com/layeh/gopher-json and https://github.com/alicebob/gopher-json
vendor/github.com/alicebob/miniredis/v2/hll.go+42 −0 added@@ -0,0 +1,42 @@ +package miniredis + +import ( + "github.com/alicebob/miniredis/v2/hyperloglog" +) + +type hll struct { + inner *hyperloglog.Sketch +} + +func newHll() *hll { + return &hll{ + inner: hyperloglog.New14(), + } +} + +// Add returns true if cardinality has been changed, or false otherwise. +func (h *hll) Add(item []byte) bool { + return h.inner.Insert(item) +} + +// Count returns the estimation of a set cardinality. +func (h *hll) Count() int { + return int(h.inner.Estimate()) +} + +// Merge merges the other hll into original one (not making a copy but doing this in place). +func (h *hll) Merge(other *hll) { + _ = h.inner.Merge(other.inner) +} + +// Bytes returns raw-bytes representation of hll data structure. +func (h *hll) Bytes() []byte { + dataBytes, _ := h.inner.MarshalBinary() + return dataBytes +} + +func (h *hll) copy() *hll { + return &hll{ + inner: h.inner.Clone(), + } +}
vendor/github.com/alicebob/miniredis/v2/hyperloglog/compressed.go+180 −0 added@@ -0,0 +1,180 @@ +package hyperloglog + +import "encoding/binary" + +// Original author of this file is github.com/clarkduvall/hyperloglog +type iterable interface { + decode(i int, last uint32) (uint32, int) + Len() int + Iter() *iterator +} + +type iterator struct { + i int + last uint32 + v iterable +} + +func (iter *iterator) Next() uint32 { + n, i := iter.v.decode(iter.i, iter.last) + iter.last = n + iter.i = i + return n +} + +func (iter *iterator) Peek() uint32 { + n, _ := iter.v.decode(iter.i, iter.last) + return n +} + +func (iter iterator) HasNext() bool { + return iter.i < iter.v.Len() +} + +type compressedList struct { + count uint32 + last uint32 + b variableLengthList +} + +func (v *compressedList) Clone() *compressedList { + if v == nil { + return nil + } + + newV := &compressedList{ + count: v.count, + last: v.last, + } + + newV.b = make(variableLengthList, len(v.b)) + copy(newV.b, v.b) + return newV +} + +func (v *compressedList) MarshalBinary() (data []byte, err error) { + // Marshal the variableLengthList + bdata, err := v.b.MarshalBinary() + if err != nil { + return nil, err + } + + // At least 4 bytes for the two fixed sized values plus the size of bdata. + data = make([]byte, 0, 4+4+len(bdata)) + + // Marshal the count and last values. + data = append(data, []byte{ + // Number of items in the list. + byte(v.count >> 24), + byte(v.count >> 16), + byte(v.count >> 8), + byte(v.count), + // The last item in the list. + byte(v.last >> 24), + byte(v.last >> 16), + byte(v.last >> 8), + byte(v.last), + }...) + + // Append the list + return append(data, bdata...), nil +} + +func (v *compressedList) UnmarshalBinary(data []byte) error { + if len(data) < 12 { + return ErrorTooShort + } + + // Set the count. + v.count, data = binary.BigEndian.Uint32(data[:4]), data[4:] + + // Set the last value. + v.last, data = binary.BigEndian.Uint32(data[:4]), data[4:] + + // Set the list. + sz, data := binary.BigEndian.Uint32(data[:4]), data[4:] + v.b = make([]uint8, sz) + if uint32(len(data)) < sz { + return ErrorTooShort + } + for i := uint32(0); i < sz; i++ { + v.b[i] = data[i] + } + return nil +} + +func newCompressedList() *compressedList { + v := &compressedList{} + v.b = make(variableLengthList, 0) + return v +} + +func (v *compressedList) Len() int { + return len(v.b) +} + +func (v *compressedList) decode(i int, last uint32) (uint32, int) { + n, i := v.b.decode(i, last) + return n + last, i +} + +func (v *compressedList) Append(x uint32) { + v.count++ + v.b = v.b.Append(x - v.last) + v.last = x +} + +func (v *compressedList) Iter() *iterator { + return &iterator{0, 0, v} +} + +type variableLengthList []uint8 + +func (v variableLengthList) MarshalBinary() (data []byte, err error) { + // 4 bytes for the size of the list, and a byte for each element in the + // list. + data = make([]byte, 0, 4+v.Len()) + + // Length of the list. We only need 32 bits because the size of the set + // couldn't exceed that on 32 bit architectures. + sz := v.Len() + data = append(data, []byte{ + byte(sz >> 24), + byte(sz >> 16), + byte(sz >> 8), + byte(sz), + }...) + + // Marshal each element in the list. + for i := 0; i < sz; i++ { + data = append(data, v[i]) + } + + return data, nil +} + +func (v variableLengthList) Len() int { + return len(v) +} + +func (v *variableLengthList) Iter() *iterator { + return &iterator{0, 0, v} +} + +func (v variableLengthList) decode(i int, last uint32) (uint32, int) { + var x uint32 + j := i + for ; v[j]&0x80 != 0; j++ { + x |= uint32(v[j]&0x7f) << (uint(j-i) * 7) + } + x |= uint32(v[j]) << (uint(j-i) * 7) + return x, j + 1 +} + +func (v variableLengthList) Append(x uint32) variableLengthList { + for x&0xffffff80 != 0 { + v = append(v, uint8((x&0x7f)|0x80)) + x >>= 7 + } + return append(v, uint8(x&0x7f)) +}
vendor/github.com/alicebob/miniredis/v2/hyperloglog/hyperloglog.go+424 −0 added@@ -0,0 +1,424 @@ +package hyperloglog + +import ( + "encoding/binary" + "errors" + "fmt" + "math" + "sort" +) + +const ( + capacity = uint8(16) + pp = uint8(25) + mp = uint32(1) << pp + version = 1 +) + +// Sketch is a HyperLogLog data-structure for the count-distinct problem, +// approximating the number of distinct elements in a multiset. +type Sketch struct { + p uint8 + b uint8 + m uint32 + alpha float64 + tmpSet set + sparseList *compressedList + regs *registers +} + +// New returns a HyperLogLog Sketch with 2^14 registers (precision 14) +func New() *Sketch { + return New14() +} + +// New14 returns a HyperLogLog Sketch with 2^14 registers (precision 14) +func New14() *Sketch { + sk, _ := newSketch(14, true) + return sk +} + +// New16 returns a HyperLogLog Sketch with 2^16 registers (precision 16) +func New16() *Sketch { + sk, _ := newSketch(16, true) + return sk +} + +// NewNoSparse returns a HyperLogLog Sketch with 2^14 registers (precision 14) +// that will not use a sparse representation +func NewNoSparse() *Sketch { + sk, _ := newSketch(14, false) + return sk +} + +// New16NoSparse returns a HyperLogLog Sketch with 2^16 registers (precision 16) +// that will not use a sparse representation +func New16NoSparse() *Sketch { + sk, _ := newSketch(16, false) + return sk +} + +// newSketch returns a HyperLogLog Sketch with 2^precision registers +func newSketch(precision uint8, sparse bool) (*Sketch, error) { + if precision < 4 || precision > 18 { + return nil, fmt.Errorf("p has to be >= 4 and <= 18") + } + m := uint32(math.Pow(2, float64(precision))) + s := &Sketch{ + m: m, + p: precision, + alpha: alpha(float64(m)), + } + if sparse { + s.tmpSet = set{} + s.sparseList = newCompressedList() + } else { + s.regs = newRegisters(m) + } + return s, nil +} + +func (sk *Sketch) sparse() bool { + return sk.sparseList != nil +} + +// Clone returns a deep copy of sk. +func (sk *Sketch) Clone() *Sketch { + return &Sketch{ + b: sk.b, + p: sk.p, + m: sk.m, + alpha: sk.alpha, + tmpSet: sk.tmpSet.Clone(), + sparseList: sk.sparseList.Clone(), + regs: sk.regs.clone(), + } +} + +// Converts to normal if the sparse list is too large. +func (sk *Sketch) maybeToNormal() { + if uint32(len(sk.tmpSet))*100 > sk.m { + sk.mergeSparse() + if uint32(sk.sparseList.Len()) > sk.m { + sk.toNormal() + } + } +} + +// Merge takes another Sketch and combines it with Sketch h. +// If Sketch h is using the sparse Sketch, it will be converted +// to the normal Sketch. +func (sk *Sketch) Merge(other *Sketch) error { + if other == nil { + // Nothing to do + return nil + } + cpOther := other.Clone() + + if sk.p != cpOther.p { + return errors.New("precisions must be equal") + } + + if sk.sparse() && other.sparse() { + for k := range other.tmpSet { + sk.tmpSet.add(k) + } + for iter := other.sparseList.Iter(); iter.HasNext(); { + sk.tmpSet.add(iter.Next()) + } + sk.maybeToNormal() + return nil + } + + if sk.sparse() { + sk.toNormal() + } + + if cpOther.sparse() { + for k := range cpOther.tmpSet { + i, r := decodeHash(k, cpOther.p, pp) + sk.insert(i, r) + } + + for iter := cpOther.sparseList.Iter(); iter.HasNext(); { + i, r := decodeHash(iter.Next(), cpOther.p, pp) + sk.insert(i, r) + } + } else { + if sk.b < cpOther.b { + sk.regs.rebase(cpOther.b - sk.b) + sk.b = cpOther.b + } else { + cpOther.regs.rebase(sk.b - cpOther.b) + cpOther.b = sk.b + } + + for i, v := range cpOther.regs.tailcuts { + v1 := v.get(0) + if v1 > sk.regs.get(uint32(i)*2) { + sk.regs.set(uint32(i)*2, v1) + } + v2 := v.get(1) + if v2 > sk.regs.get(1+uint32(i)*2) { + sk.regs.set(1+uint32(i)*2, v2) + } + } + } + return nil +} + +// Convert from sparse Sketch to dense Sketch. +func (sk *Sketch) toNormal() { + if len(sk.tmpSet) > 0 { + sk.mergeSparse() + } + + sk.regs = newRegisters(sk.m) + for iter := sk.sparseList.Iter(); iter.HasNext(); { + i, r := decodeHash(iter.Next(), sk.p, pp) + sk.insert(i, r) + } + + sk.tmpSet = nil + sk.sparseList = nil +} + +func (sk *Sketch) insert(i uint32, r uint8) bool { + changed := false + if r-sk.b >= capacity { + //overflow + db := sk.regs.min() + if db > 0 { + sk.b += db + sk.regs.rebase(db) + changed = true + } + } + if r > sk.b { + val := r - sk.b + if c1 := capacity - 1; c1 < val { + val = c1 + } + + if val > sk.regs.get(i) { + sk.regs.set(i, val) + changed = true + } + } + return changed +} + +// Insert adds element e to sketch +func (sk *Sketch) Insert(e []byte) bool { + x := hash(e) + return sk.InsertHash(x) +} + +// InsertHash adds hash x to sketch +func (sk *Sketch) InsertHash(x uint64) bool { + if sk.sparse() { + changed := sk.tmpSet.add(encodeHash(x, sk.p, pp)) + if !changed { + return false + } + if uint32(len(sk.tmpSet))*100 > sk.m/2 { + sk.mergeSparse() + if uint32(sk.sparseList.Len()) > sk.m/2 { + sk.toNormal() + } + } + return true + } else { + i, r := getPosVal(x, sk.p) + return sk.insert(uint32(i), r) + } +} + +// Estimate returns the cardinality of the Sketch +func (sk *Sketch) Estimate() uint64 { + if sk.sparse() { + sk.mergeSparse() + return uint64(linearCount(mp, mp-sk.sparseList.count)) + } + + sum, ez := sk.regs.sumAndZeros(sk.b) + m := float64(sk.m) + var est float64 + + var beta func(float64) float64 + if sk.p < 16 { + beta = beta14 + } else { + beta = beta16 + } + + if sk.b == 0 { + est = (sk.alpha * m * (m - ez) / (sum + beta(ez))) + } else { + est = (sk.alpha * m * m / sum) + } + + return uint64(est + 0.5) +} + +func (sk *Sketch) mergeSparse() { + if len(sk.tmpSet) == 0 { + return + } + + keys := make(uint64Slice, 0, len(sk.tmpSet)) + for k := range sk.tmpSet { + keys = append(keys, k) + } + sort.Sort(keys) + + newList := newCompressedList() + for iter, i := sk.sparseList.Iter(), 0; iter.HasNext() || i < len(keys); { + if !iter.HasNext() { + newList.Append(keys[i]) + i++ + continue + } + + if i >= len(keys) { + newList.Append(iter.Next()) + continue + } + + x1, x2 := iter.Peek(), keys[i] + if x1 == x2 { + newList.Append(iter.Next()) + i++ + } else if x1 > x2 { + newList.Append(x2) + i++ + } else { + newList.Append(iter.Next()) + } + } + + sk.sparseList = newList + sk.tmpSet = set{} +} + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (sk *Sketch) MarshalBinary() (data []byte, err error) { + // Marshal a version marker. + data = append(data, version) + // Marshal p. + data = append(data, sk.p) + // Marshal b + data = append(data, sk.b) + + if sk.sparse() { + // It's using the sparse Sketch. + data = append(data, byte(1)) + + // Add the tmp_set + tsdata, err := sk.tmpSet.MarshalBinary() + if err != nil { + return nil, err + } + data = append(data, tsdata...) + + // Add the sparse Sketch + sdata, err := sk.sparseList.MarshalBinary() + if err != nil { + return nil, err + } + return append(data, sdata...), nil + } + + // It's using the dense Sketch. + data = append(data, byte(0)) + + // Add the dense sketch Sketch. + sz := len(sk.regs.tailcuts) + data = append(data, []byte{ + byte(sz >> 24), + byte(sz >> 16), + byte(sz >> 8), + byte(sz), + }...) + + // Marshal each element in the list. + for i := 0; i < len(sk.regs.tailcuts); i++ { + data = append(data, byte(sk.regs.tailcuts[i])) + } + + return data, nil +} + +// ErrorTooShort is an error that UnmarshalBinary try to parse too short +// binary. +var ErrorTooShort = errors.New("too short binary") + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (sk *Sketch) UnmarshalBinary(data []byte) error { + if len(data) < 8 { + return ErrorTooShort + } + + // Unmarshal version. We may need this in the future if we make + // non-compatible changes. + _ = data[0] + + // Unmarshal p. + p := data[1] + + // Unmarshal b. + sk.b = data[2] + + // Determine if we need a sparse Sketch + sparse := data[3] == byte(1) + + // Make a newSketch Sketch if the precision doesn't match or if the Sketch was used + if sk.p != p || sk.regs != nil || len(sk.tmpSet) > 0 || (sk.sparseList != nil && sk.sparseList.Len() > 0) { + newh, err := newSketch(p, sparse) + if err != nil { + return err + } + newh.b = sk.b + *sk = *newh + } + + // h is now initialised with the correct p. We just need to fill the + // rest of the details out. + if sparse { + // Using the sparse Sketch. + + // Unmarshal the tmp_set. + tssz := binary.BigEndian.Uint32(data[4:8]) + sk.tmpSet = make(map[uint32]struct{}, tssz) + + // We need to unmarshal tssz values in total, and each value requires us + // to read 4 bytes. + tsLastByte := int((tssz * 4) + 8) + for i := 8; i < tsLastByte; i += 4 { + k := binary.BigEndian.Uint32(data[i : i+4]) + sk.tmpSet[k] = struct{}{} + } + + // Unmarshal the sparse Sketch. + return sk.sparseList.UnmarshalBinary(data[tsLastByte:]) + } + + // Using the dense Sketch. + sk.sparseList = nil + sk.tmpSet = nil + dsz := binary.BigEndian.Uint32(data[4:8]) + sk.regs = newRegisters(dsz * 2) + data = data[8:] + + for i, val := range data { + sk.regs.tailcuts[i] = reg(val) + if uint8(sk.regs.tailcuts[i]<<4>>4) > 0 { + sk.regs.nz-- + } + if uint8(sk.regs.tailcuts[i]>>4) > 0 { + sk.regs.nz-- + } + } + + return nil +}
vendor/github.com/alicebob/miniredis/v2/hyperloglog/LICENSE+21 −0 added@@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Axiom Inc. <seif@axiom.sh> + +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.
vendor/github.com/alicebob/miniredis/v2/hyperloglog/README.md+1 −0 added@@ -0,0 +1 @@ +This is a copy of github.com/axiomhq/hyperloglog. \ No newline at end of file
vendor/github.com/alicebob/miniredis/v2/hyperloglog/registers.go+114 −0 added@@ -0,0 +1,114 @@ +package hyperloglog + +import ( + "math" +) + +type reg uint8 +type tailcuts []reg + +type registers struct { + tailcuts + nz uint32 +} + +func (r *reg) set(offset, val uint8) bool { + var isZero bool + if offset == 0 { + isZero = *r < 16 + tmpVal := uint8((*r) << 4 >> 4) + *r = reg(tmpVal | (val << 4)) + } else { + isZero = *r&0x0f == 0 + tmpVal := uint8((*r) >> 4 << 4) + *r = reg(tmpVal | val) + } + return isZero +} + +func (r *reg) get(offset uint8) uint8 { + if offset == 0 { + return uint8((*r) >> 4) + } + return uint8((*r) << 4 >> 4) +} + +func newRegisters(size uint32) *registers { + return ®isters{ + tailcuts: make(tailcuts, size/2), + nz: size, + } +} + +func (rs *registers) clone() *registers { + if rs == nil { + return nil + } + tc := make([]reg, len(rs.tailcuts)) + copy(tc, rs.tailcuts) + return ®isters{ + tailcuts: tc, + nz: rs.nz, + } +} + +func (rs *registers) rebase(delta uint8) { + nz := uint32(len(rs.tailcuts)) * 2 + for i := range rs.tailcuts { + for j := uint8(0); j < 2; j++ { + val := rs.tailcuts[i].get(j) + if val >= delta { + rs.tailcuts[i].set(j, val-delta) + if val-delta > 0 { + nz-- + } + } + } + } + rs.nz = nz +} + +func (rs *registers) set(i uint32, val uint8) { + offset, index := uint8(i)&1, i/2 + if rs.tailcuts[index].set(offset, val) { + rs.nz-- + } +} + +func (rs *registers) get(i uint32) uint8 { + offset, index := uint8(i)&1, i/2 + return rs.tailcuts[index].get(offset) +} + +func (rs *registers) sumAndZeros(base uint8) (res, ez float64) { + for _, r := range rs.tailcuts { + for j := uint8(0); j < 2; j++ { + v := float64(base + r.get(j)) + if v == 0 { + ez++ + } + res += 1.0 / math.Pow(2.0, v) + } + } + rs.nz = uint32(ez) + return res, ez +} + +func (rs *registers) min() uint8 { + if rs.nz > 0 { + return 0 + } + min := uint8(math.MaxUint8) + for _, r := range rs.tailcuts { + if r == 0 || min == 0 { + return 0 + } + if val := uint8(r << 4 >> 4); val < min { + min = val + } + if val := uint8(r >> 4); val < min { + min = val + } + } + return min +}
vendor/github.com/alicebob/miniredis/v2/hyperloglog/sparse.go+92 −0 added@@ -0,0 +1,92 @@ +package hyperloglog + +import ( + "math/bits" +) + +func getIndex(k uint32, p, pp uint8) uint32 { + if k&1 == 1 { + return bextr32(k, 32-p, p) + } + return bextr32(k, pp-p+1, p) +} + +// Encode a hash to be used in the sparse representation. +func encodeHash(x uint64, p, pp uint8) uint32 { + idx := uint32(bextr(x, 64-pp, pp)) + if bextr(x, 64-pp, pp-p) == 0 { + zeros := bits.LeadingZeros64((bextr(x, 0, 64-pp)<<pp)|(1<<pp-1)) + 1 + return idx<<7 | uint32(zeros<<1) | 1 + } + return idx << 1 +} + +// Decode a hash from the sparse representation. +func decodeHash(k uint32, p, pp uint8) (uint32, uint8) { + var r uint8 + if k&1 == 1 { + r = uint8(bextr32(k, 1, 6)) + pp - p + } else { + // We can use the 64bit clz implementation and reduce the result + // by 32 to get a clz for a 32bit word. + r = uint8(bits.LeadingZeros64(uint64(k<<(32-pp+p-1))) - 31) // -32 + 1 + } + return getIndex(k, p, pp), r +} + +type set map[uint32]struct{} + +func (s set) add(v uint32) bool { + _, ok := s[v] + if ok { + return false + } + s[v] = struct{}{} + return true +} + +func (s set) Clone() set { + if s == nil { + return nil + } + + newS := make(map[uint32]struct{}, len(s)) + for k, v := range s { + newS[k] = v + } + return newS +} + +func (s set) MarshalBinary() (data []byte, err error) { + // 4 bytes for the size of the set, and 4 bytes for each key. + // list. + data = make([]byte, 0, 4+(4*len(s))) + + // Length of the set. We only need 32 bits because the size of the set + // couldn't exceed that on 32 bit architectures. + sl := len(s) + data = append(data, []byte{ + byte(sl >> 24), + byte(sl >> 16), + byte(sl >> 8), + byte(sl), + }...) + + // Marshal each element in the set. + for k := range s { + data = append(data, []byte{ + byte(k >> 24), + byte(k >> 16), + byte(k >> 8), + byte(k), + }...) + } + + return data, nil +} + +type uint64Slice []uint32 + +func (p uint64Slice) Len() int { return len(p) } +func (p uint64Slice) Less(i, j int) bool { return p[i] < p[j] } +func (p uint64Slice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
vendor/github.com/alicebob/miniredis/v2/hyperloglog/utils.go+69 −0 added@@ -0,0 +1,69 @@ +package hyperloglog + +import ( + "github.com/alicebob/miniredis/v2/metro" + "math" + "math/bits" +) + +var hash = hashFunc + +func beta14(ez float64) float64 { + zl := math.Log(ez + 1) + return -0.370393911*ez + + 0.070471823*zl + + 0.17393686*math.Pow(zl, 2) + + 0.16339839*math.Pow(zl, 3) + + -0.09237745*math.Pow(zl, 4) + + 0.03738027*math.Pow(zl, 5) + + -0.005384159*math.Pow(zl, 6) + + 0.00042419*math.Pow(zl, 7) +} + +func beta16(ez float64) float64 { + zl := math.Log(ez + 1) + return -0.37331876643753059*ez + + -1.41704077448122989*zl + + 0.40729184796612533*math.Pow(zl, 2) + + 1.56152033906584164*math.Pow(zl, 3) + + -0.99242233534286128*math.Pow(zl, 4) + + 0.26064681399483092*math.Pow(zl, 5) + + -0.03053811369682807*math.Pow(zl, 6) + + 0.00155770210179105*math.Pow(zl, 7) +} + +func alpha(m float64) float64 { + switch m { + case 16: + return 0.673 + case 32: + return 0.697 + case 64: + return 0.709 + } + return 0.7213 / (1 + 1.079/m) +} + +func getPosVal(x uint64, p uint8) (uint64, uint8) { + i := bextr(x, 64-p, p) // {x63,...,x64-p} + w := x<<p | 1<<(p-1) // {x63-p,...,x0} + rho := uint8(bits.LeadingZeros64(w)) + 1 + return i, rho +} + +func linearCount(m uint32, v uint32) float64 { + fm := float64(m) + return fm * math.Log(fm/float64(v)) +} + +func bextr(v uint64, start, length uint8) uint64 { + return (v >> start) & ((1 << length) - 1) +} + +func bextr32(v uint32, start, length uint8) uint32 { + return (v >> start) & ((1 << length) - 1) +} + +func hashFunc(e []byte) uint64 { + return metro.Hash64(e, 1337) +}
vendor/github.com/alicebob/miniredis/v2/keys.go+83 −0 added@@ -0,0 +1,83 @@ +package miniredis + +// Translate the 'KEYS' or 'PSUBSCRIBE' argument ('foo*', 'f??', &c.) into a regexp. + +import ( + "bytes" + "regexp" +) + +// patternRE compiles a glob to a regexp. Returns nil if the given +// pattern will never match anything. +// The general strategy is to sandwich all non-meta characters between \Q...\E. +func patternRE(k string) *regexp.Regexp { + re := bytes.Buffer{} + re.WriteString(`(?s)^\Q`) + for i := 0; i < len(k); i++ { + p := k[i] + switch p { + case '*': + re.WriteString(`\E.*\Q`) + case '?': + re.WriteString(`\E.\Q`) + case '[': + charClass := bytes.Buffer{} + i++ + for ; i < len(k); i++ { + if k[i] == ']' { + break + } + if k[i] == '\\' { + if i == len(k)-1 { + // Ends with a '\'. U-huh. + return nil + } + charClass.WriteByte(k[i]) + i++ + charClass.WriteByte(k[i]) + continue + } + charClass.WriteByte(k[i]) + } + if charClass.Len() == 0 { + // '[]' is valid in Redis, but matches nothing. + return nil + } + re.WriteString(`\E[`) + re.Write(charClass.Bytes()) + re.WriteString(`]\Q`) + + case '\\': + if i == len(k)-1 { + // Ends with a '\'. U-huh. + return nil + } + // Forget the \, keep the next char. + i++ + re.WriteByte(k[i]) + continue + default: + re.WriteByte(p) + } + } + re.WriteString(`\E$`) + return regexp.MustCompile(re.String()) +} + +// matchKeys filters only matching keys. +// The returned boolean is whether the match pattern was valid +func matchKeys(keys []string, match string) ([]string, bool) { + re := patternRE(match) + if re == nil { + // Special case: the given pattern won't match anything or is invalid. + return nil, false + } + var res []string + for _, k := range keys { + if !re.MatchString(k) { + continue + } + res = append(res, k) + } + return res, true +}
vendor/github.com/alicebob/miniredis/v2/LICENSE+21 −0 added@@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Harmen + +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.
vendor/github.com/alicebob/miniredis/v2/lua.go+281 −0 added@@ -0,0 +1,281 @@ +package miniredis + +import ( + "bufio" + "bytes" + "fmt" + "strings" + + lua "github.com/yuin/gopher-lua" + + "github.com/alicebob/miniredis/v2/server" +) + +var luaRedisConstants = map[string]lua.LValue{ + "LOG_DEBUG": lua.LNumber(0), + "LOG_VERBOSE": lua.LNumber(1), + "LOG_NOTICE": lua.LNumber(2), + "LOG_WARNING": lua.LNumber(3), +} + +func mkLua(srv *server.Server, c *server.Peer, sha string) (map[string]lua.LGFunction, map[string]lua.LValue) { + mkCall := func(failFast bool) func(l *lua.LState) int { + // one server.Ctx for a single Lua run + pCtx := &connCtx{} + if getCtx(c).authenticated { + pCtx.authenticated = true + } + pCtx.nested = true + pCtx.nestedSHA = sha + pCtx.selectedDB = getCtx(c).selectedDB + + return func(l *lua.LState) int { + top := l.GetTop() + if top == 0 { + l.Error(lua.LString(fmt.Sprintf("Please specify at least one argument for this redis lib call script: %s, &c.", sha)), 1) + return 0 + } + var args []string + for i := 1; i <= top; i++ { + switch a := l.Get(i).(type) { + case lua.LNumber: + args = append(args, a.String()) + case lua.LString: + args = append(args, string(a)) + default: + l.Error(lua.LString(fmt.Sprintf("Lua redis lib command arguments must be strings or integers script: %s, &c.", sha)), 1) + return 0 + } + } + if len(args) == 0 { + l.Error(lua.LString(msgNotFromScripts(sha)), 1) + return 0 + } + + buf := &bytes.Buffer{} + wr := bufio.NewWriter(buf) + peer := server.NewPeer(wr) + peer.Ctx = pCtx + srv.Dispatch(peer, args) + wr.Flush() + + res, err := server.ParseReply(bufio.NewReader(buf)) + if err != nil { + if failFast { + // call() mode + if strings.Contains(err.Error(), "ERR unknown command") { + l.Error(lua.LString(fmt.Sprintf("Unknown Redis command called from script script: %s, &c.", sha)), 1) + } else { + l.Error(lua.LString(err.Error()), 1) + } + return 0 + } + // pcall() mode + l.Push(lua.LNil) + return 1 + } + + if res == nil { + l.Push(lua.LFalse) + } else { + switch r := res.(type) { + case int64: + l.Push(lua.LNumber(r)) + case int: + l.Push(lua.LNumber(r)) + case []uint8: + l.Push(lua.LString(string(r))) + case []interface{}: + l.Push(redisToLua(l, r)) + case server.Simple: + l.Push(luaStatusReply(string(r))) + case string: + l.Push(lua.LString(r)) + case error: + l.Error(lua.LString(r.Error()), 1) + return 0 + default: + panic(fmt.Sprintf("type not handled (%T)", r)) + } + } + return 1 + } + } + + return map[string]lua.LGFunction{ + "call": mkCall(true), + "pcall": mkCall(false), + "error_reply": func(l *lua.LState) int { + v := l.Get(1) + msg, ok := v.(lua.LString) + if !ok { + l.Error(lua.LString("wrong number or type of arguments"), 1) + return 0 + } + res := &lua.LTable{} + parts := strings.SplitN(msg.String(), " ", 2) + // '-' at the beginging will be added as a part of error response + if parts[0] != "" && parts[0][0] == '-' { + parts[0] = parts[0][1:] + } + var final_msg string + if len(parts) == 2 { + final_msg = fmt.Sprintf("%s %s", parts[0], parts[1]) + } else { + final_msg = fmt.Sprintf("ERR %s", parts[0]) + } + res.RawSetString("err", lua.LString(final_msg)) + l.Push(res) + return 1 + }, + "log": func(l *lua.LState) int { + level := l.CheckInt(1) + msg := l.CheckString(2) + _, _ = level, msg + // do nothing by default. To see logs uncomment: + // fmt.Printf("%v: %v", level, msg) + return 0 + }, + "status_reply": func(l *lua.LState) int { + v := l.Get(1) + msg, ok := v.(lua.LString) + if !ok { + l.Error(lua.LString("wrong number or type of arguments"), 1) + return 0 + } + res := luaStatusReply(string(msg)) + l.Push(res) + return 1 + }, + "sha1hex": func(l *lua.LState) int { + top := l.GetTop() + if top != 1 { + l.Error(lua.LString("wrong number of arguments"), 1) + return 0 + } + msg := lua.LVAsString(l.Get(1)) + l.Push(lua.LString(sha1Hex(msg))) + return 1 + }, + "replicate_commands": func(l *lua.LState) int { + // always succeeds since 7.0.0 + l.Push(lua.LTrue) + return 1 + }, + "set_repl": func(l *lua.LState) int { + top := l.GetTop() + if top != 1 { + l.Error(lua.LString("wrong number of arguments"), 1) + return 0 + } + // ignored + return 1 + }, + "setresp": func(l *lua.LState) int { + level := l.CheckInt(1) + toresp3 := false + switch level { + case 2: + toresp3 = false + case 3: + toresp3 = true + default: + l.Error(lua.LString("RESP version must be 2 or 3"), 1) + return 0 + } + c.SwitchResp3 = &toresp3 + return 0 + }, + }, luaRedisConstants +} + +func luaToRedis(l *lua.LState, c *server.Peer, value lua.LValue) { + if value == nil { + c.WriteNull() + return + } + + switch t := value.(type) { + case *lua.LNilType: + c.WriteNull() + case lua.LBool: + if lua.LVAsBool(value) { + c.WriteInt(1) + } else { + c.WriteNull() + } + case lua.LNumber: + c.WriteInt(int(lua.LVAsNumber(value))) + case lua.LString: + s := lua.LVAsString(value) + c.WriteBulk(s) + case *lua.LTable: + // special case for tables with an 'err' or 'ok' field + // note: according to the docs this only counts when 'err' or 'ok' is + // the only field. + if s := t.RawGetString("err"); s.Type() != lua.LTNil { + c.WriteError(s.String()) + return + } + if s := t.RawGetString("ok"); s.Type() != lua.LTNil { + c.WriteInline(s.String()) + return + } + + result := []lua.LValue{} + for j := 1; true; j++ { + val := l.GetTable(value, lua.LNumber(j)) + if val == nil { + result = append(result, val) + continue + } + + if val.Type() == lua.LTNil { + break + } + + result = append(result, val) + } + + c.WriteLen(len(result)) + for _, r := range result { + luaToRedis(l, c, r) + } + default: + panic(fmt.Sprintf("wat: %T", t)) + } +} + +func redisToLua(l *lua.LState, res []interface{}) *lua.LTable { + rettb := l.NewTable() + for _, e := range res { + var v lua.LValue + if e == nil { + v = lua.LFalse + } else { + switch et := e.(type) { + case int: + v = lua.LNumber(et) + case int64: + v = lua.LNumber(et) + case []uint8: + v = lua.LString(string(et)) + case []interface{}: + v = redisToLua(l, et) + case string: + v = lua.LString(et) + default: + // TODO: oops? + v = lua.LString(e.(string)) + } + } + l.RawSet(rettb, lua.LNumber(rettb.Len()+1), v) + } + return rettb +} + +func luaStatusReply(msg string) *lua.LTable { + tab := &lua.LTable{} + tab.RawSetString("ok", lua.LString(msg)) + return tab +}
vendor/github.com/alicebob/miniredis/v2/Makefile+33 −0 added@@ -0,0 +1,33 @@ +.PHONY: test +test: ### Run unit tests + go test ./... + +.PHONY: testrace +testrace: ### Run unit tests with race detector + go test -race ./... + +.PHONY: int +int: ### Run integration tests (doesn't download redis server) + ${MAKE} -C integration int + +.PHONY: ci +ci: ### Run full tests suite (including download and compilation of proper redis server) + ${MAKE} test + ${MAKE} -C integration redis_src/redis-server int + ${MAKE} testrace + +.PHONY: clean +clean: ### Clean integration test files and remove compiled redis from integration/redis_src + ${MAKE} -C integration clean + +.PHONY: help +help: +ifeq ($(UNAME), Linux) + @grep -P '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \ + awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' +else + @# this is not tested, but prepared in advance for you, Mac drivers + @awk -F ':.*###' '$$0 ~ FS {printf "%15s%s\n", $$1 ":", $$2}' \ + $(MAKEFILE_LIST) | grep -v '@awk' | sort +endif +
vendor/github.com/alicebob/miniredis/v2/metro/LICENSE+24 −0 added@@ -0,0 +1,24 @@ +This package is a mechanical translation of the reference C++ code for +MetroHash, available at https://github.com/jandrewrogers/MetroHash + +The MIT License (MIT) + +Copyright (c) 2016 Damian Gryski + +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.
vendor/github.com/alicebob/miniredis/v2/metro/metro64.go+87 −0 added@@ -0,0 +1,87 @@ +package metro + +import "encoding/binary" + +func Hash64(buffer []byte, seed uint64) uint64 { + + const ( + k0 = 0xD6D018F5 + k1 = 0xA2AA033B + k2 = 0x62992FC1 + k3 = 0x30BC5B29 + ) + + ptr := buffer + + hash := (seed + k2) * k0 + + if len(ptr) >= 32 { + v := [4]uint64{hash, hash, hash, hash} + + for len(ptr) >= 32 { + v[0] += binary.LittleEndian.Uint64(ptr[:8]) * k0 + v[0] = rotate_right(v[0], 29) + v[2] + v[1] += binary.LittleEndian.Uint64(ptr[8:16]) * k1 + v[1] = rotate_right(v[1], 29) + v[3] + v[2] += binary.LittleEndian.Uint64(ptr[16:24]) * k2 + v[2] = rotate_right(v[2], 29) + v[0] + v[3] += binary.LittleEndian.Uint64(ptr[24:32]) * k3 + v[3] = rotate_right(v[3], 29) + v[1] + ptr = ptr[32:] + } + + v[2] ^= rotate_right(((v[0]+v[3])*k0)+v[1], 37) * k1 + v[3] ^= rotate_right(((v[1]+v[2])*k1)+v[0], 37) * k0 + v[0] ^= rotate_right(((v[0]+v[2])*k0)+v[3], 37) * k1 + v[1] ^= rotate_right(((v[1]+v[3])*k1)+v[2], 37) * k0 + hash += v[0] ^ v[1] + } + + if len(ptr) >= 16 { + v0 := hash + (binary.LittleEndian.Uint64(ptr[:8]) * k2) + v0 = rotate_right(v0, 29) * k3 + v1 := hash + (binary.LittleEndian.Uint64(ptr[8:16]) * k2) + v1 = rotate_right(v1, 29) * k3 + v0 ^= rotate_right(v0*k0, 21) + v1 + v1 ^= rotate_right(v1*k3, 21) + v0 + hash += v1 + ptr = ptr[16:] + } + + if len(ptr) >= 8 { + hash += binary.LittleEndian.Uint64(ptr[:8]) * k3 + ptr = ptr[8:] + hash ^= rotate_right(hash, 55) * k1 + } + + if len(ptr) >= 4 { + hash += uint64(binary.LittleEndian.Uint32(ptr[:4])) * k3 + hash ^= rotate_right(hash, 26) * k1 + ptr = ptr[4:] + } + + if len(ptr) >= 2 { + hash += uint64(binary.LittleEndian.Uint16(ptr[:2])) * k3 + ptr = ptr[2:] + hash ^= rotate_right(hash, 48) * k1 + } + + if len(ptr) >= 1 { + hash += uint64(ptr[0]) * k3 + hash ^= rotate_right(hash, 37) * k1 + } + + hash ^= rotate_right(hash, 28) + hash *= k0 + hash ^= rotate_right(hash, 29) + + return hash +} + +func Hash64Str(buffer string, seed uint64) uint64 { + return Hash64([]byte(buffer), seed) +} + +func rotate_right(v uint64, k uint) uint64 { + return (v >> k) | (v << (64 - k)) +}
vendor/github.com/alicebob/miniredis/v2/metro/README.md+1 −0 added@@ -0,0 +1 @@ +This is a partial copy of github.com/dgryski/go-metro. \ No newline at end of file
vendor/github.com/alicebob/miniredis/v2/miniredis.go+759 −0 added@@ -0,0 +1,759 @@ +// Package miniredis is a pure Go Redis test server, for use in Go unittests. +// There are no dependencies on system binaries, and every server you start +// will be empty. +// +// import "github.com/alicebob/miniredis/v2" +// +// Start a server with `s := miniredis.RunT(t)`, it'll be shutdown via a t.Cleanup(). +// Or do everything manual: `s, err := miniredis.Run(); defer s.Close()` +// +// Point your Redis client to `s.Addr()` or `s.Host(), s.Port()`. +// +// Set keys directly via s.Set(...) and similar commands, or use a Redis client. +// +// For direct use you can select a Redis database with either `s.Select(12); +// s.Get("foo")` or `s.DB(12).Get("foo")`. +package miniredis + +import ( + "context" + "crypto/tls" + "fmt" + "math/rand" + "strconv" + "strings" + "sync" + "time" + + "github.com/alicebob/miniredis/v2/proto" + "github.com/alicebob/miniredis/v2/server" +) + +var DumpMaxLineLen = 60 + +type hashKey map[string]string +type listKey []string +type setKey map[string]struct{} + +// RedisDB holds a single (numbered) Redis database. +type RedisDB struct { + master *Miniredis // pointer to the lock in Miniredis + id int // db id + keys map[string]string // Master map of keys with their type + stringKeys map[string]string // GET/SET &c. keys + hashKeys map[string]hashKey // MGET/MSET &c. keys + listKeys map[string]listKey // LPUSH &c. keys + setKeys map[string]setKey // SADD &c. keys + hllKeys map[string]*hll // PFADD &c. keys + sortedsetKeys map[string]sortedSet // ZADD &c. keys + streamKeys map[string]*streamKey // XADD &c. keys + ttl map[string]time.Duration // effective TTL values + lru map[string]time.Time // last recently used ( read or written to ) + keyVersion map[string]uint // used to watch values +} + +// Miniredis is a Redis server implementation. +type Miniredis struct { + sync.Mutex + srv *server.Server + port int + passwords map[string]string // username password + dbs map[int]*RedisDB + selectedDB int // DB id used in the direct Get(), Set() &c. + scripts map[string]string // sha1 -> lua src + signal *sync.Cond + now time.Time // time.Now() if not set. + subscribers map[*Subscriber]struct{} + rand *rand.Rand + Ctx context.Context + CtxCancel context.CancelFunc +} + +type txCmd func(*server.Peer, *connCtx) + +// database id + key combo +type dbKey struct { + db int + key string +} + +// connCtx has all state for a single connection. +// (this struct was named before context.Context existed) +type connCtx struct { + selectedDB int // selected DB + authenticated bool // auth enabled and a valid AUTH seen + transaction []txCmd // transaction callbacks. Or nil. + dirtyTransaction bool // any error during QUEUEing + watch map[dbKey]uint // WATCHed keys + subscriber *Subscriber // client is in PUBSUB mode if not nil + nested bool // this is called via Lua + nestedSHA string // set to the SHA of the nesting function +} + +// NewMiniRedis makes a new, non-started, Miniredis object. +func NewMiniRedis() *Miniredis { + m := Miniredis{ + dbs: map[int]*RedisDB{}, + scripts: map[string]string{}, + subscribers: map[*Subscriber]struct{}{}, + } + m.Ctx, m.CtxCancel = context.WithCancel(context.Background()) + m.signal = sync.NewCond(&m) + return &m +} + +func newRedisDB(id int, m *Miniredis) RedisDB { + return RedisDB{ + id: id, + master: m, + keys: map[string]string{}, + lru: map[string]time.Time{}, + stringKeys: map[string]string{}, + hashKeys: map[string]hashKey{}, + listKeys: map[string]listKey{}, + setKeys: map[string]setKey{}, + hllKeys: map[string]*hll{}, + sortedsetKeys: map[string]sortedSet{}, + streamKeys: map[string]*streamKey{}, + ttl: map[string]time.Duration{}, + keyVersion: map[string]uint{}, + } +} + +// Run creates and Start()s a Miniredis. +func Run() (*Miniredis, error) { + m := NewMiniRedis() + return m, m.Start() +} + +// Run creates and Start()s a Miniredis, TLS version. +func RunTLS(cfg *tls.Config) (*Miniredis, error) { + m := NewMiniRedis() + return m, m.StartTLS(cfg) +} + +// Tester is a minimal version of a testing.T +type Tester interface { + Fatalf(string, ...interface{}) + Cleanup(func()) + Logf(format string, args ...interface{}) +} + +// RunT start a new miniredis, pass it a testing.T. It also registers the cleanup after your test is done. +func RunT(t Tester) *Miniredis { + m := NewMiniRedis() + if err := m.Start(); err != nil { + t.Fatalf("could not start miniredis: %s", err) + // not reached + } + t.Cleanup(m.Close) + return m +} + +func runWithClient(t Tester) (*Miniredis, *proto.Client) { + m := RunT(t) + + c, err := proto.Dial(m.Addr()) + if err != nil { + t.Fatalf("could not connect to miniredis: %s", err) + } + t.Cleanup(func() { + if err = c.Close(); err != nil { + t.Logf("error closing connection to miniredis: %s", err) + } + }) + + return m, c +} + +// Start starts a server. It listens on a random port on localhost. See also +// Addr(). +func (m *Miniredis) Start() error { + s, err := server.NewServer(fmt.Sprintf("127.0.0.1:%d", m.port)) + if err != nil { + return err + } + return m.start(s) +} + +// Start starts a server, TLS version. +func (m *Miniredis) StartTLS(cfg *tls.Config) error { + s, err := server.NewServerTLS(fmt.Sprintf("127.0.0.1:%d", m.port), cfg) + if err != nil { + return err + } + return m.start(s) +} + +// StartAddr runs miniredis with a given addr. Examples: "127.0.0.1:6379", +// ":6379", or "127.0.0.1:0" +func (m *Miniredis) StartAddr(addr string) error { + s, err := server.NewServer(addr) + if err != nil { + return err + } + return m.start(s) +} + +// StartAddrTLS runs miniredis with a given addr, TLS version. +func (m *Miniredis) StartAddrTLS(addr string, cfg *tls.Config) error { + s, err := server.NewServerTLS(addr, cfg) + if err != nil { + return err + } + return m.start(s) +} + +func (m *Miniredis) start(s *server.Server) error { + m.Lock() + defer m.Unlock() + m.srv = s + m.port = s.Addr().Port + + commandsConnection(m) + commandsGeneric(m) + commandsServer(m) + commandsString(m) + commandsHash(m) + commandsList(m) + commandsPubsub(m) + commandsSet(m) + commandsSortedSet(m) + commandsStream(m) + commandsTransaction(m) + commandsScripting(m) + commandsGeo(m) + commandsCluster(m) + commandsHll(m) + commandsClient(m) + commandsObject(m) + + return nil +} + +// Restart restarts a Close()d server on the same port. Values will be +// preserved. +func (m *Miniredis) Restart() error { + return m.Start() +} + +// Close shuts down a Miniredis. +func (m *Miniredis) Close() { + m.Lock() + + if m.srv == nil { + m.Unlock() + return + } + srv := m.srv + m.srv = nil + m.CtxCancel() + m.Unlock() + + // the OnDisconnect callbacks can lock m, so run Close() outside the lock. + srv.Close() + +} + +// RequireAuth makes every connection need to AUTH first. This is the old 'AUTH [password] command. +// Remove it by setting an empty string. +func (m *Miniredis) RequireAuth(pw string) { + m.RequireUserAuth("default", pw) +} + +// Add a username/password, for use with 'AUTH [username] [password]'. +// There are currently no access controls for commands implemented. +// Disable access for the user with an empty password. +func (m *Miniredis) RequireUserAuth(username, pw string) { + m.Lock() + defer m.Unlock() + if m.passwords == nil { + m.passwords = map[string]string{} + } + if pw == "" { + delete(m.passwords, username) + return + } + m.passwords[username] = pw +} + +// DB returns a DB by ID. +func (m *Miniredis) DB(i int) *RedisDB { + m.Lock() + defer m.Unlock() + return m.db(i) +} + +// get DB. No locks! +func (m *Miniredis) db(i int) *RedisDB { + if db, ok := m.dbs[i]; ok { + return db + } + db := newRedisDB(i, m) // main miniredis has our mutex. + m.dbs[i] = &db + return &db +} + +// SwapDB swaps DBs by IDs. +func (m *Miniredis) SwapDB(i, j int) bool { + m.Lock() + defer m.Unlock() + return m.swapDB(i, j) +} + +// swap DB. No locks! +func (m *Miniredis) swapDB(i, j int) bool { + db1 := m.db(i) + db2 := m.db(j) + + db1.id = j + db2.id = i + + m.dbs[i] = db2 + m.dbs[j] = db1 + + return true +} + +// Addr returns '127.0.0.1:12345'. Can be given to a Dial(). See also Host() +// and Port(), which return the same things. +func (m *Miniredis) Addr() string { + m.Lock() + defer m.Unlock() + return m.srv.Addr().String() +} + +// Host returns the host part of Addr(). +func (m *Miniredis) Host() string { + m.Lock() + defer m.Unlock() + return m.srv.Addr().IP.String() +} + +// Port returns the (random) port part of Addr(). +func (m *Miniredis) Port() string { + m.Lock() + defer m.Unlock() + return strconv.Itoa(m.srv.Addr().Port) +} + +// CommandCount returns the number of processed commands. +func (m *Miniredis) CommandCount() int { + m.Lock() + defer m.Unlock() + return int(m.srv.TotalCommands()) +} + +// CurrentConnectionCount returns the number of currently connected clients. +func (m *Miniredis) CurrentConnectionCount() int { + m.Lock() + defer m.Unlock() + return m.srv.ClientsLen() +} + +// TotalConnectionCount returns the number of client connections since server start. +func (m *Miniredis) TotalConnectionCount() int { + m.Lock() + defer m.Unlock() + return int(m.srv.TotalConnections()) +} + +// FastForward decreases all TTLs by the given duration. All TTLs <= 0 will be +// expired. +func (m *Miniredis) FastForward(duration time.Duration) { + m.Lock() + defer m.Unlock() + for _, db := range m.dbs { + db.fastForward(duration) + } +} + +// Server returns the underlying server to allow custom commands to be implemented +func (m *Miniredis) Server() *server.Server { + return m.srv +} + +// Dump returns a text version of the selected DB, usable for debugging. +// +// Dump limits the maximum length of each key:value to "DumpMaxLineLen" characters. +// To increase that, call something like: +// +// miniredis.DumpMaxLineLen = 1024 +// mr, _ = miniredis.Run() +// mr.Dump() +func (m *Miniredis) Dump() string { + m.Lock() + defer m.Unlock() + + var ( + maxLen = DumpMaxLineLen + indent = " " + db = m.db(m.selectedDB) + r = "" + v = func(s string) string { + suffix := "" + if len(s) > maxLen { + suffix = fmt.Sprintf("...(%d)", len(s)) + s = s[:maxLen-len(suffix)] + } + return fmt.Sprintf("%q%s", s, suffix) + } + ) + + for _, k := range db.allKeys() { + r += fmt.Sprintf("- %s\n", k) + t := db.t(k) + switch t { + case keyTypeString: + r += fmt.Sprintf("%s%s\n", indent, v(db.stringKeys[k])) + case keyTypeHash: + for _, hk := range db.hashFields(k) { + r += fmt.Sprintf("%s%s: %s\n", indent, hk, v(db.hashGet(k, hk))) + } + case keyTypeList: + for _, lk := range db.listKeys[k] { + r += fmt.Sprintf("%s%s\n", indent, v(lk)) + } + case keyTypeSet: + for _, mk := range db.setMembers(k) { + r += fmt.Sprintf("%s%s\n", indent, v(mk)) + } + case keyTypeSortedSet: + for _, el := range db.ssetElements(k) { + r += fmt.Sprintf("%s%f: %s\n", indent, el.score, v(el.member)) + } + case keyTypeStream: + for _, entry := range db.streamKeys[k].entries { + r += fmt.Sprintf("%s%s\n", indent, entry.ID) + ev := entry.Values + for i := 0; i < len(ev)/2; i++ { + r += fmt.Sprintf("%s%s%s: %s\n", indent, indent, v(ev[2*i]), v(ev[2*i+1])) + } + } + case keyTypeHll: + for _, entry := range db.hllKeys { + r += fmt.Sprintf("%s%s\n", indent, v(string(entry.Bytes()))) + } + default: + r += fmt.Sprintf("%s(a %s, fixme!)\n", indent, t) + } + } + return r +} + +// SetTime sets the time against which EXPIREAT values are compared, and the +// time used in stream entry IDs. Will use time.Now() if this is not set. +func (m *Miniredis) SetTime(t time.Time) { + m.Lock() + defer m.Unlock() + m.now = t +} + +// make every command return this message. For example: +// +// LOADING Redis is loading the dataset in memory +// MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'. +// +// Clear it with an empty string. Don't add newlines. +func (m *Miniredis) SetError(msg string) { + cb := server.Hook(nil) + if msg != "" { + cb = func(c *server.Peer, cmd string, args ...string) bool { + c.WriteError(msg) + return true + } + } + m.srv.SetPreHook(cb) +} + +// isValidCMD returns true if command is valid and can be executed. +func (m *Miniredis) isValidCMD(c *server.Peer, cmd string) bool { + if !m.handleAuth(c) { + return false + } + if m.checkPubsub(c, cmd) { + return false + } + + return true +} + +// handleAuth returns false if connection has no access. It sends the reply. +func (m *Miniredis) handleAuth(c *server.Peer) bool { + if getCtx(c).nested { + return true + } + + m.Lock() + defer m.Unlock() + if len(m.passwords) == 0 { + return true + } + if !getCtx(c).authenticated { + c.WriteError("NOAUTH Authentication required.") + return false + } + return true +} + +// handlePubsub sends an error to the user if the connection is in PUBSUB mode. +// It'll return true if it did. +func (m *Miniredis) checkPubsub(c *server.Peer, cmd string) bool { + if getCtx(c).nested { + return false + } + + m.Lock() + defer m.Unlock() + + ctx := getCtx(c) + if ctx.subscriber == nil { + return false + } + + prefix := "ERR " + if strings.ToLower(cmd) == "exec" { + prefix = "EXECABORT Transaction discarded because of: " + } + c.WriteError(fmt.Sprintf( + "%sCan't execute '%s': only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT are allowed in this context", + prefix, + strings.ToLower(cmd), + )) + return true +} + +func getCtx(c *server.Peer) *connCtx { + if c.Ctx == nil { + c.Ctx = &connCtx{} + } + return c.Ctx.(*connCtx) +} + +func startTx(ctx *connCtx) { + ctx.transaction = []txCmd{} + ctx.dirtyTransaction = false +} + +func stopTx(ctx *connCtx) { + ctx.transaction = nil + unwatch(ctx) +} + +func inTx(ctx *connCtx) bool { + return ctx.transaction != nil +} + +func addTxCmd(ctx *connCtx, cb txCmd) { + ctx.transaction = append(ctx.transaction, cb) +} + +func watch(db *RedisDB, ctx *connCtx, key string) { + if ctx.watch == nil { + ctx.watch = map[dbKey]uint{} + } + ctx.watch[dbKey{db: db.id, key: key}] = db.keyVersion[key] // Can be 0. +} + +func unwatch(ctx *connCtx) { + ctx.watch = nil +} + +// setDirty can be called even when not in an tx. Is an no-op then. +func setDirty(c *server.Peer) { + if c.Ctx == nil { + // No transaction. Not relevant. + return + } + getCtx(c).dirtyTransaction = true +} + +func (m *Miniredis) addSubscriber(s *Subscriber) { + m.subscribers[s] = struct{}{} +} + +// closes and remove the subscriber. +func (m *Miniredis) removeSubscriber(s *Subscriber) { + _, ok := m.subscribers[s] + delete(m.subscribers, s) + if ok { + s.Close() + } +} + +func (m *Miniredis) publish(c, msg string) int { + n := 0 + for s := range m.subscribers { + n += s.Publish(c, msg) + } + return n +} + +// enter 'subscribed state', or return the existing one. +func (m *Miniredis) subscribedState(c *server.Peer) *Subscriber { + ctx := getCtx(c) + sub := ctx.subscriber + if sub != nil { + return sub + } + + sub = newSubscriber() + m.addSubscriber(sub) + + c.OnDisconnect(func() { + m.Lock() + m.removeSubscriber(sub) + m.Unlock() + }) + + ctx.subscriber = sub + + go monitorPublish(c, sub.publish) + go monitorPpublish(c, sub.ppublish) + + return sub +} + +// whenever the p?sub count drops to 0 subscribed state should be stopped, and +// all redis commands are allowed again. +func endSubscriber(m *Miniredis, c *server.Peer) { + ctx := getCtx(c) + if sub := ctx.subscriber; sub != nil { + m.removeSubscriber(sub) // will Close() the sub + } + ctx.subscriber = nil +} + +// Start a new pubsub subscriber. It can (un) subscribe to channels and +// patterns, and has a channel to get published messages. Close it with +// Close(). +// Does not close itself when there are no subscriptions left. +func (m *Miniredis) NewSubscriber() *Subscriber { + sub := newSubscriber() + + m.Lock() + m.addSubscriber(sub) + m.Unlock() + + return sub +} + +func (m *Miniredis) allSubscribers() []*Subscriber { + var subs []*Subscriber + for s := range m.subscribers { + subs = append(subs, s) + } + return subs +} + +func (m *Miniredis) Seed(seed int) { + m.Lock() + defer m.Unlock() + + // m.rand is not safe for concurrent use. + m.rand = rand.New(rand.NewSource(int64(seed))) +} + +func (m *Miniredis) randIntn(n int) int { + if m.rand == nil { + return rand.Intn(n) + } + return m.rand.Intn(n) +} + +// shuffle shuffles a list of strings. Kinda. +func (m *Miniredis) shuffle(l []string) { + for range l { + i := m.randIntn(len(l)) + j := m.randIntn(len(l)) + l[i], l[j] = l[j], l[i] + } +} + +func (m *Miniredis) effectiveNow() time.Time { + if !m.now.IsZero() { + return m.now + } + return time.Now().UTC() +} + +// convert a unixtimestamp to a duration, to use an absolute time as TTL. +// d can be either time.Second or time.Millisecond. +func (m *Miniredis) at(i int, d time.Duration) time.Duration { + var ts time.Time + switch d { + case time.Millisecond: + ts = time.Unix(int64(i/1000), 1000000*int64(i%1000)) + case time.Second: + ts = time.Unix(int64(i), 0) + default: + panic("invalid time unit (d). Fixme!") + } + now := m.effectiveNow() + return ts.Sub(now) +} + +// copy does not mind if dst already exists. +func (m *Miniredis) copy( + srcDB *RedisDB, src string, + destDB *RedisDB, dst string, +) error { + if !srcDB.exists(src) { + return ErrKeyNotFound + } + + switch srcDB.t(src) { + case keyTypeString: + destDB.stringKeys[dst] = srcDB.stringKeys[src] + case keyTypeHash: + destDB.hashKeys[dst] = copyHashKey(srcDB.hashKeys[src]) + case keyTypeList: + destDB.listKeys[dst] = copyListKey(srcDB.listKeys[src]) + case keyTypeSet: + destDB.setKeys[dst] = copySetKey(srcDB.setKeys[src]) + case keyTypeSortedSet: + destDB.sortedsetKeys[dst] = copySortedSet(srcDB.sortedsetKeys[src]) + case keyTypeStream: + destDB.streamKeys[dst] = srcDB.streamKeys[src].copy() + case keyTypeHll: + destDB.hllKeys[dst] = srcDB.hllKeys[src].copy() + default: + panic("missing case") + } + destDB.keys[dst] = srcDB.keys[src] + destDB.incr(dst) + if v, ok := srcDB.ttl[src]; ok { + destDB.ttl[dst] = v + } + return nil +} + +func copyHashKey(orig hashKey) hashKey { + cpy := hashKey{} + for k, v := range orig { + cpy[k] = v + } + return cpy +} + +func copyListKey(orig listKey) listKey { + cpy := make(listKey, len(orig)) + copy(cpy, orig) + return cpy +} + +func copySetKey(orig setKey) setKey { + cpy := setKey{} + for k, v := range orig { + cpy[k] = v + } + return cpy +} + +func copySortedSet(orig sortedSet) sortedSet { + cpy := sortedSet{} + for k, v := range orig { + cpy[k] = v + } + return cpy +}
vendor/github.com/alicebob/miniredis/v2/opts.go+60 −0 added@@ -0,0 +1,60 @@ +package miniredis + +import ( + "errors" + "math" + "strconv" + "time" + + "github.com/alicebob/miniredis/v2/server" +) + +// optInt parses an int option in a command. +// Writes "invalid integer" error to c if it's not a valid integer. Returns +// whether or not things were okay. +func optInt(c *server.Peer, src string, dest *int) bool { + return optIntErr(c, src, dest, msgInvalidInt) +} + +func optIntErr(c *server.Peer, src string, dest *int, errMsg string) bool { + n, err := strconv.Atoi(src) + if err != nil { + setDirty(c) + c.WriteError(errMsg) + return false + } + *dest = n + return true +} + +// optIntSimple sets dest or returns an error +func optIntSimple(src string, dest *int) error { + n, err := strconv.Atoi(src) + if err != nil { + return errors.New(msgInvalidInt) + } + *dest = n + return nil +} + +func optDuration(c *server.Peer, src string, dest *time.Duration) bool { + n, err := strconv.ParseFloat(src, 64) + if err != nil { + setDirty(c) + c.WriteError(msgInvalidTimeout) + return false + } + if n < 0 { + setDirty(c) + c.WriteError(msgTimeoutNegative) + return false + } + if math.IsInf(n, 0) { + setDirty(c) + c.WriteError(msgTimeoutIsOutOfRange) + return false + } + + *dest = time.Duration(n*1_000_000) * time.Microsecond + return true +}
vendor/github.com/alicebob/miniredis/v2/proto/client.go+60 −0 added@@ -0,0 +1,60 @@ +package proto + +import ( + "bufio" + "crypto/tls" + "net" +) + +type Client struct { + c net.Conn + r *bufio.Reader +} + +func Dial(addr string) (*Client, error) { + c, err := net.Dial("tcp", addr) + if err != nil { + return nil, err + } + + return &Client{ + c: c, + r: bufio.NewReader(c), + }, nil +} + +func DialTLS(addr string, cfg *tls.Config) (*Client, error) { + c, err := tls.Dial("tcp", addr, cfg) + if err != nil { + return nil, err + } + + return &Client{ + c: c, + r: bufio.NewReader(c), + }, nil +} + +func (c *Client) Close() error { + return c.c.Close() +} + +func (c *Client) Do(cmd ...string) (string, error) { + if err := Write(c.c, cmd); err != nil { + return "", err + } + return Read(c.r) +} + +func (c *Client) Read() (string, error) { + return Read(c.r) +} + +// Do() + ReadStrings() +func (c *Client) DoStrings(cmd ...string) ([]string, error) { + res, err := c.Do(cmd...) + if err != nil { + return nil, err + } + return ReadStrings(res) +}
vendor/github.com/alicebob/miniredis/v2/proto/Makefile+2 −0 added@@ -0,0 +1,2 @@ +test: + go test
vendor/github.com/alicebob/miniredis/v2/proto/proto.go+288 −0 added@@ -0,0 +1,288 @@ +package proto + +import ( + "bufio" + "errors" + "fmt" + "io" + "strconv" + "strings" +) + +var ( + ErrProtocol = errors.New("unsupported protocol") + ErrUnexpected = errors.New("not what you asked for") +) + +func readLine(r *bufio.Reader) (string, error) { + line, err := r.ReadString('\n') + if err != nil { + return "", err + } + if len(line) < 3 { + return "", ErrProtocol + } + return line, nil +} + +// Read an array, with all elements are the raw redis commands +// Also reads sets and maps. +func ReadArray(b string) ([]string, error) { + r := bufio.NewReader(strings.NewReader(b)) + line, err := readLine(r) + if err != nil { + return nil, err + } + + elems := 0 + switch line[0] { + default: + return nil, ErrUnexpected + case '*', '>', '~': + // *: array + // >: push data + // ~: set + length, err := strconv.Atoi(line[1 : len(line)-2]) + if err != nil { + return nil, err + } + elems = length + case '%': + // we also read maps. + length, err := strconv.Atoi(line[1 : len(line)-2]) + if err != nil { + return nil, err + } + elems = length * 2 + } + + var res []string + for i := 0; i < elems; i++ { + next, err := Read(r) + if err != nil { + return nil, err + } + res = append(res, next) + } + return res, nil +} + +func ReadString(b string) (string, error) { + r := bufio.NewReader(strings.NewReader(b)) + line, err := readLine(r) + if err != nil { + return "", err + } + + switch line[0] { + default: + return "", ErrUnexpected + case '$': + // bulk strings are: `$5\r\nhello\r\n` + length, err := strconv.Atoi(line[1 : len(line)-2]) + if err != nil { + return "", err + } + if length < 0 { + // -1 is a nil response + return line, nil + } + var ( + buf = make([]byte, length+2) + pos = 0 + ) + for pos < length+2 { + n, err := r.Read(buf[pos:]) + if err != nil { + return "", err + } + pos += n + } + return string(buf[:len(buf)-2]), nil + } +} + +func readInline(b string) (string, error) { + if len(b) < 3 { + return "", ErrUnexpected + } + return b[1 : len(b)-2], nil +} + +func ReadError(b string) (string, error) { + if len(b) < 1 { + return "", ErrUnexpected + } + + switch b[0] { + default: + return "", ErrUnexpected + case '-': + return readInline(b) + } +} + +func ReadStrings(b string) ([]string, error) { + elems, err := ReadArray(b) + if err != nil { + return nil, err + } + var res []string + for _, e := range elems { + s, err := ReadString(e) + if err != nil { + return nil, err + } + res = append(res, s) + } + return res, nil +} + +// Read a single command, returning it raw. Used to read replies from redis. +// Understands RESP3 proto. +func Read(r *bufio.Reader) (string, error) { + line, err := readLine(r) + if err != nil { + return "", err + } + + switch line[0] { + default: + return "", ErrProtocol + case '+', '-', ':', ',', '_': + // +: inline string + // -: errors + // :: integer + // ,: float + // _: null + // Simple line based replies. + return line, nil + case '$': + // bulk strings are: `$5\r\nhello\r\n` + length, err := strconv.Atoi(line[1 : len(line)-2]) + if err != nil { + return "", err + } + if length < 0 { + // -1 is a nil response + return line, nil + } + var ( + buf = make([]byte, length+2) + pos = 0 + ) + for pos < length+2 { + n, err := r.Read(buf[pos:]) + if err != nil { + return "", err + } + pos += n + } + return line + string(buf), nil + case '*', '>', '~': + // arrays are: `*6\r\n...` + // pushdata is: `>6\r\n...` + // sets are: `~6\r\n...` + length, err := strconv.Atoi(line[1 : len(line)-2]) + if err != nil { + return "", err + } + for i := 0; i < length; i++ { + next, err := Read(r) + if err != nil { + return "", err + } + line += next + } + return line, nil + case '%': + // maps are: `%3\r\n...` + length, err := strconv.Atoi(line[1 : len(line)-2]) + if err != nil { + return "", err + } + for i := 0; i < length*2; i++ { + next, err := Read(r) + if err != nil { + return "", err + } + line += next + } + return line, nil + } +} + +// Write a command in RESP3 proto. Used to write commands to redis. +// Currently only supports string arrays. +func Write(w io.Writer, cmd []string) error { + if _, err := fmt.Fprintf(w, "*%d\r\n", len(cmd)); err != nil { + return err + } + for _, c := range cmd { + if _, err := fmt.Fprintf(w, "$%d\r\n%s\r\n", len(c), c); err != nil { + return err + } + } + return nil +} + +// Parse into interfaces. `b` must contain exactly a single command (which can be nested). +func Parse(b string) (interface{}, error) { + if len(b) < 1 { + return nil, ErrUnexpected + } + + switch b[0] { + default: + return "", ErrProtocol + case '+': + return readInline(b) + case '-': + e, err := readInline(b) + if err != nil { + return nil, err + } + return errors.New(e), nil + case ':': + e, err := readInline(b) + if err != nil { + return nil, err + } + return strconv.Atoi(e) + case '$': + return ReadString(b) + case '*': + elems, err := ReadArray(b) + if err != nil { + return nil, err + } + var res []interface{} + for _, elem := range elems { + e, err := Parse(elem) + if err != nil { + return nil, err + } + res = append(res, e) + } + return res, nil + case '%': + elems, err := ReadArray(b) + if err != nil { + return nil, err + } + var res = map[interface{}]interface{}{} + for len(elems) > 1 { + key, err := Parse(elems[0]) + if err != nil { + return nil, err + } + value, err := Parse(elems[1]) + if err != nil { + return nil, err + } + res[key] = value + elems = elems[2:] + } + return res, nil + } +}
vendor/github.com/alicebob/miniredis/v2/proto/types.go+102 −0 added@@ -0,0 +1,102 @@ +package proto + +import ( + "fmt" + "strings" +) + +// Byte-safe string +func String(s string) string { + return fmt.Sprintf("$%d\r\n%s\r\n", len(s), s) +} + +// Inline string +func Inline(s string) string { + return inline('+', s) +} + +// Error +func Error(s string) string { + return inline('-', s) +} + +func inline(r rune, s string) string { + return fmt.Sprintf("%s%s\r\n", string(r), s) +} + +// Int +func Int(n int) string { + return fmt.Sprintf(":%d\r\n", n) +} + +// Float +func Float(n float64) string { + return fmt.Sprintf(",%g\r\n", n) +} + +const ( + Nil = "$-1\r\n" + NilResp3 = "_\r\n" + NilList = "*-1\r\n" +) + +// Array assembles the args in a list. Args should be raw redis commands. +// Example: Array(String("foo"), String("bar")) +func Array(args ...string) string { + return fmt.Sprintf("*%d\r\n", len(args)) + strings.Join(args, "") +} + +// Push assembles the args for push-data. Args should be raw redis commands. +// Example: Push(String("foo"), String("bar")) +func Push(args ...string) string { + return fmt.Sprintf(">%d\r\n", len(args)) + strings.Join(args, "") +} + +// Strings is a helper to build 1 dimensional string arrays. +func Strings(args ...string) string { + var strings []string + for _, a := range args { + strings = append(strings, String(a)) + } + return Array(strings...) +} + +// Ints is a helper to build 1 dimensional int arrays. +func Ints(args ...int) string { + var ints []string + for _, a := range args { + ints = append(ints, Int(a)) + } + return Array(ints...) +} + +// Map assembles the args in a map. Args should be raw redis commands. +// Must be an even number of arguments. +// Example: Map(String("foo"), String("bar")) +func Map(args ...string) string { + return fmt.Sprintf("%%%d\r\n", len(args)/2) + strings.Join(args, "") +} + +// StringMap is is a wrapper to get a map of (bulk)strings. +func StringMap(args ...string) string { + var strings []string + for _, a := range args { + strings = append(strings, String(a)) + } + return Map(strings...) +} + +// Set assembles the args in a map. Args should be raw redis commands. +// Example: Set(String("foo"), String("bar")) +func Set(args ...string) string { + return fmt.Sprintf("~%d\r\n", len(args)) + strings.Join(args, "") +} + +// StringSet is is a wrapper to get a set of (bulk)strings. +func StringSet(args ...string) string { + var strings []string + for _, a := range args { + strings = append(strings, String(a)) + } + return Set(strings...) +}
vendor/github.com/alicebob/miniredis/v2/pubsub.go+240 −0 added@@ -0,0 +1,240 @@ +package miniredis + +import ( + "regexp" + "sort" + "sync" + + "github.com/alicebob/miniredis/v2/server" +) + +// PubsubMessage is what gets broadcasted over pubsub channels. +type PubsubMessage struct { + Channel string + Message string +} + +type PubsubPmessage struct { + Pattern string + Channel string + Message string +} + +// Subscriber has the (p)subscriptions. +type Subscriber struct { + publish chan PubsubMessage + ppublish chan PubsubPmessage + channels map[string]struct{} + patterns map[string]*regexp.Regexp + mu sync.Mutex +} + +// Make a new subscriber. The channel is not buffered, so you will need to keep +// reading using Messages(). Use Close() when done, or unsubscribe. +func newSubscriber() *Subscriber { + return &Subscriber{ + publish: make(chan PubsubMessage), + ppublish: make(chan PubsubPmessage), + channels: map[string]struct{}{}, + patterns: map[string]*regexp.Regexp{}, + } +} + +// Close the listening channel +func (s *Subscriber) Close() { + close(s.publish) + close(s.ppublish) +} + +// Count the total number of channels and patterns +func (s *Subscriber) Count() int { + s.mu.Lock() + defer s.mu.Unlock() + return s.count() +} + +func (s *Subscriber) count() int { + return len(s.channels) + len(s.patterns) +} + +// Subscribe to a channel. Returns the total number of (p)subscriptions after +// subscribing. +func (s *Subscriber) Subscribe(c string) int { + s.mu.Lock() + defer s.mu.Unlock() + + s.channels[c] = struct{}{} + return s.count() +} + +// Unsubscribe a channel. Returns the total number of (p)subscriptions after +// unsubscribing. +func (s *Subscriber) Unsubscribe(c string) int { + s.mu.Lock() + defer s.mu.Unlock() + + delete(s.channels, c) + return s.count() +} + +// Subscribe to a pattern. Returns the total number of (p)subscriptions after +// subscribing. +func (s *Subscriber) Psubscribe(pat string) int { + s.mu.Lock() + defer s.mu.Unlock() + + s.patterns[pat] = patternRE(pat) + return s.count() +} + +// Unsubscribe a pattern. Returns the total number of (p)subscriptions after +// unsubscribing. +func (s *Subscriber) Punsubscribe(pat string) int { + s.mu.Lock() + defer s.mu.Unlock() + + delete(s.patterns, pat) + return s.count() +} + +// List all subscribed channels, in alphabetical order +func (s *Subscriber) Channels() []string { + s.mu.Lock() + defer s.mu.Unlock() + + var cs []string + for c := range s.channels { + cs = append(cs, c) + } + sort.Strings(cs) + return cs +} + +// List all subscribed patterns, in alphabetical order +func (s *Subscriber) Patterns() []string { + s.mu.Lock() + defer s.mu.Unlock() + + var ps []string + for p := range s.patterns { + ps = append(ps, p) + } + sort.Strings(ps) + return ps +} + +// Publish a message. Will return return how often we sent the message (can be +// a match for a subscription and for a psubscription. +func (s *Subscriber) Publish(c, msg string) int { + s.mu.Lock() + defer s.mu.Unlock() + + found := 0 + +subs: + for sub := range s.channels { + if sub == c { + s.publish <- PubsubMessage{c, msg} + found++ + break subs + } + } + +pats: + for orig, pat := range s.patterns { + if pat != nil && pat.MatchString(c) { + s.ppublish <- PubsubPmessage{orig, c, msg} + found++ + break pats + } + } + + return found +} + +// The channel to read messages for this subscriber. Only for messages matching +// a SUBSCRIBE. +func (s *Subscriber) Messages() <-chan PubsubMessage { + return s.publish +} + +// The channel to read messages for this subscriber. Only for messages matching +// a PSUBSCRIBE. +func (s *Subscriber) Pmessages() <-chan PubsubPmessage { + return s.ppublish +} + +// List all pubsub channels. If `pat` isn't empty channels names must match the +// pattern. Channels are returned alphabetically. +func activeChannels(subs []*Subscriber, pat string) []string { + channels := map[string]struct{}{} + for _, s := range subs { + for c := range s.channels { + channels[c] = struct{}{} + } + } + + var cpat *regexp.Regexp + if pat != "" { + cpat = patternRE(pat) + } + + var cs []string + for k := range channels { + if cpat != nil && !cpat.MatchString(k) { + continue + } + cs = append(cs, k) + } + sort.Strings(cs) + return cs +} + +// Count all subscribed (not psubscribed) clients for the given channel +// pattern. Channels are returned alphabetically. +func countSubs(subs []*Subscriber, channel string) int { + n := 0 + for _, p := range subs { + for c := range p.channels { + if c == channel { + n++ + break + } + } + } + return n +} + +// Count the total of all client psubscriptions. +func countPsubs(subs []*Subscriber) int { + n := 0 + for _, p := range subs { + n += len(p.patterns) + } + return n +} + +func monitorPublish(conn *server.Peer, msgs <-chan PubsubMessage) { + for msg := range msgs { + conn.Block(func(c *server.Writer) { + c.WritePushLen(3) + c.WriteBulk("message") + c.WriteBulk(msg.Channel) + c.WriteBulk(msg.Message) + c.Flush() + }) + } +} + +func monitorPpublish(conn *server.Peer, msgs <-chan PubsubPmessage) { + for msg := range msgs { + conn.Block(func(c *server.Writer) { + c.WritePushLen(4) + c.WriteBulk("pmessage") + c.WriteBulk(msg.Pattern) + c.WriteBulk(msg.Channel) + c.WriteBulk(msg.Message) + c.Flush() + }) + } +}
vendor/github.com/alicebob/miniredis/v2/README.md+342 −0 added@@ -0,0 +1,342 @@ +# Miniredis + +Pure Go Redis test server, used in Go unittests. + + +## + +Sometimes you want to test code which uses Redis, without making it a full-blown +integration test. +Miniredis implements (parts of) the Redis server, to be used in unittests. It +enables a simple, cheap, in-memory, Redis replacement, with a real TCP interface. Think of it as the Redis version of `net/http/httptest`. + +It saves you from using mock code, and since the redis server lives in the +test process you can query for values directly, without going through the server +stack. + +There are no dependencies on external binaries, so you can easily integrate it in automated build processes. + +Be sure to import v2: +``` +import "github.com/alicebob/miniredis/v2" +``` + +## Commands + +Implemented commands: + + - Connection (complete) + - AUTH -- see RequireAuth() + - ECHO + - HELLO -- see RequireUserAuth() + - PING + - SELECT + - SWAPDB + - QUIT + - Key + - COPY + - DEL + - EXISTS + - EXPIRE + - EXPIREAT + - EXPIRETIME + - KEYS + - MOVE + - PERSIST + - PEXPIRE + - PEXPIREAT + - PEXPIRETIME + - PTTL + - RANDOMKEY -- see m.Seed(...) + - RENAME + - RENAMENX + - SCAN + - TOUCH + - TTL + - TYPE + - UNLINK + - Transactions (complete) + - DISCARD + - EXEC + - MULTI + - UNWATCH + - WATCH + - Server + - DBSIZE + - FLUSHALL + - FLUSHDB + - TIME -- returns time.Now() or value set by SetTime() + - COMMAND -- partly + - INFO -- partly, returns only "clients" section with one field "connected_clients" + - String keys (complete) + - APPEND + - BITCOUNT + - BITOP + - BITPOS + - DECR + - DECRBY + - GET + - GETBIT + - GETRANGE + - GETSET + - GETDEL + - GETEX + - INCR + - INCRBY + - INCRBYFLOAT + - MGET + - MSET + - MSETNX + - PSETEX + - SET + - SETBIT + - SETEX + - SETNX + - SETRANGE + - STRLEN + - Hash keys (complete) + - HDEL + - HEXISTS + - HGET + - HGETALL + - HINCRBY + - HINCRBYFLOAT + - HKEYS + - HLEN + - HMGET + - HMSET + - HRANDFIELD + - HSET + - HSETNX + - HSTRLEN + - HVALS + - HSCAN + - List keys (complete) + - BLPOP + - BRPOP + - BRPOPLPUSH + - LINDEX + - LINSERT + - LLEN + - LPOP + - LPUSH + - LPUSHX + - LRANGE + - LREM + - LSET + - LTRIM + - RPOP + - RPOPLPUSH + - RPUSH + - RPUSHX + - LMOVE + - BLMOVE + - Pub/Sub (complete) + - PSUBSCRIBE + - PUBLISH + - PUBSUB + - PUNSUBSCRIBE + - SUBSCRIBE + - UNSUBSCRIBE + - Set keys (complete) + - SADD + - SCARD + - SDIFF + - SDIFFSTORE + - SINTER + - SINTERSTORE + - SINTERCARD + - SISMEMBER + - SMEMBERS + - SMISMEMBER + - SMOVE + - SPOP -- see m.Seed(...) + - SRANDMEMBER -- see m.Seed(...) + - SREM + - SSCAN + - SUNION + - SUNIONSTORE + - Sorted Set keys (complete) + - ZADD + - ZCARD + - ZCOUNT + - ZINCRBY + - ZINTER + - ZINTERSTORE + - ZLEXCOUNT + - ZPOPMIN + - ZPOPMAX + - ZRANDMEMBER + - ZRANGE + - ZRANGEBYLEX + - ZRANGEBYSCORE + - ZRANK + - ZREM + - ZREMRANGEBYLEX + - ZREMRANGEBYRANK + - ZREMRANGEBYSCORE + - ZREVRANGE + - ZREVRANGEBYLEX + - ZREVRANGEBYSCORE + - ZREVRANK + - ZSCORE + - ZUNION + - ZUNIONSTORE + - ZSCAN + - Stream keys + - XACK + - XADD + - XAUTOCLAIM + - XCLAIM + - XDEL + - XGROUP CREATE + - XGROUP CREATECONSUMER + - XGROUP DESTROY + - XGROUP DELCONSUMER + - XINFO STREAM -- partly + - XINFO GROUPS + - XINFO CONSUMERS -- partly + - XLEN + - XRANGE + - XREAD + - XREADGROUP + - XREVRANGE + - XPENDING + - XTRIM + - Scripting + - EVAL + - EVALSHA + - SCRIPT LOAD + - SCRIPT EXISTS + - SCRIPT FLUSH + - GEO + - GEOADD + - GEODIST + - ~~GEOHASH~~ + - GEOPOS + - GEORADIUS + - GEORADIUS_RO + - GEORADIUSBYMEMBER + - GEORADIUSBYMEMBER_RO + - Cluster + - CLUSTER SLOTS + - CLUSTER KEYSLOT + - CLUSTER NODES + - HyperLogLog (complete) + - PFADD + - PFCOUNT + - PFMERGE + + +## TTLs, key expiration, and time + +Since miniredis is intended to be used in unittests TTLs don't decrease +automatically. You can use `TTL()` to get the TTL (as a time.Duration) of a +key. It will return 0 when no TTL is set. + +`m.FastForward(d)` can be used to decrement all TTLs. All TTLs which become <= +0 will be removed. + +EXPIREAT and PEXPIREAT values will be +converted to a duration. For that you can either set m.SetTime(t) to use that +time as the base for the (P)EXPIREAT conversion, or don't call SetTime(), in +which case time.Now() will be used. + +SetTime() also sets the value returned by TIME, which defaults to time.Now(). +It is not updated by FastForward, only by SetTime. + +## Randomness and Seed() + +Miniredis will use `math/rand`'s global RNG for randomness unless a seed is +provided by calling `m.Seed(...)`. If a seed is provided, then miniredis will +use its own RNG based on that seed. + +Commands which use randomness are: RANDOMKEY, SPOP, and SRANDMEMBER. + +## Example + +``` Go + +import ( + ... + "github.com/alicebob/miniredis/v2" + ... +) + +func TestSomething(t *testing.T) { + s := miniredis.RunT(t) + + // Optionally set some keys your code expects: + s.Set("foo", "bar") + s.HSet("some", "other", "key") + + // Run your code and see if it behaves. + // An example using the redigo library from "github.com/gomodule/redigo/redis": + c, err := redis.Dial("tcp", s.Addr()) + _, err = c.Do("SET", "foo", "bar") + + // Optionally check values in redis... + if got, err := s.Get("foo"); err != nil || got != "bar" { + t.Error("'foo' has the wrong value") + } + // ... or use a helper for that: + s.CheckGet(t, "foo", "bar") + + // TTL and expiration: + s.Set("foo", "bar") + s.SetTTL("foo", 10*time.Second) + s.FastForward(11 * time.Second) + if s.Exists("foo") { + t.Fatal("'foo' should not have existed anymore") + } +} +``` + +## Not supported + +Commands which will probably not be implemented: + + - CLUSTER (all) + - ~~CLUSTER *~~ + - ~~READONLY~~ + - ~~READWRITE~~ + - Key + - ~~DUMP~~ + - ~~MIGRATE~~ + - ~~OBJECT~~ + - ~~RESTORE~~ + - ~~WAIT~~ + - Scripting + - ~~FCALL / FCALL_RO *~~ + - ~~FUNCTION *~~ + - ~~SCRIPT DEBUG~~ + - ~~SCRIPT KILL~~ + - Server + - ~~BGSAVE~~ + - ~~BGWRITEAOF~~ + - ~~CLIENT *~~ + - ~~CONFIG *~~ + - ~~DEBUG *~~ + - ~~LASTSAVE~~ + - ~~MONITOR~~ + - ~~ROLE~~ + - ~~SAVE~~ + - ~~SHUTDOWN~~ + - ~~SLAVEOF~~ + - ~~SLOWLOG~~ + - ~~SYNC~~ + + +## &c. + +Integration tests are run against Redis 7.2.4. The [./integration](./integration/) subdir +compares miniredis against a real redis instance. + +The Redis 6 RESP3 protocol is supported. If there are problems, please open +an issue. + +If you want to test Redis Sentinel have a look at [minisentinel](https://github.com/Bose/minisentinel). + +A changelog is kept at [CHANGELOG.md](https://github.com/alicebob/miniredis/blob/master/CHANGELOG.md). + +[](https://pkg.go.dev/github.com/alicebob/miniredis/v2)
vendor/github.com/alicebob/miniredis/v2/redis.go+264 −0 added@@ -0,0 +1,264 @@ +package miniredis + +import ( + "context" + "fmt" + "math" + "math/big" + "strings" + "sync" + "time" + + "github.com/alicebob/miniredis/v2/server" +) + +const ( + keyTypeString = "string" + keyTypeHash = "hash" + keyTypeList = "list" + keyTypeSet = "set" + keyTypeHll = "hll" + keyTypeSortedSet = "zset" + keyTypeStream = "stream" +) + +const ( + msgWrongType = "WRONGTYPE Operation against a key holding the wrong kind of value" + msgNotValidHllValue = "WRONGTYPE Key is not a valid HyperLogLog string value." + msgInvalidInt = "ERR value is not an integer or out of range" + msgIntOverflow = "ERR increment or decrement would overflow" + msgInvalidFloat = "ERR value is not a valid float" + msgInvalidMinMax = "ERR min or max is not a float" + msgInvalidRangeItem = "ERR min or max not valid string range item" + msgInvalidTimeout = "ERR timeout is not a float or out of range" + msgInvalidRange = "ERR value is out of range, must be positive" + msgSyntaxError = "ERR syntax error" + msgKeyNotFound = "ERR no such key" + msgOutOfRange = "ERR index out of range" + msgInvalidCursor = "ERR invalid cursor" + msgXXandNX = "ERR XX and NX options at the same time are not compatible" + msgTimeoutNegative = "ERR timeout is negative" + msgTimeoutIsOutOfRange = "ERR timeout is out of range" + msgInvalidSETime = "ERR invalid expire time in set" + msgInvalidSETEXTime = "ERR invalid expire time in setex" + msgInvalidPSETEXTime = "ERR invalid expire time in psetex" + msgInvalidKeysNumber = "ERR Number of keys can't be greater than number of args" + msgNegativeKeysNumber = "ERR Number of keys can't be negative" + msgFScriptUsage = "ERR unknown subcommand or wrong number of arguments for '%s'. Try SCRIPT HELP." + msgFScriptUsageSimple = "ERR unknown subcommand '%s'. Try SCRIPT HELP." + msgFPubsubUsage = "ERR unknown subcommand or wrong number of arguments for '%s'. Try PUBSUB HELP." + msgFPubsubUsageSimple = "ERR unknown subcommand '%s'. Try PUBSUB HELP." + msgFObjectUsage = "ERR unknown subcommand '%s'. Try OBJECT HELP." + msgScriptFlush = "ERR SCRIPT FLUSH only support SYNC|ASYNC option" + msgSingleElementPair = "ERR INCR option supports a single increment-element pair" + msgGTLTandNX = "ERR GT, LT, and/or NX options at the same time are not compatible" + msgInvalidStreamID = "ERR Invalid stream ID specified as stream command argument" + msgStreamIDTooSmall = "ERR The ID specified in XADD is equal or smaller than the target stream top item" + msgStreamIDZero = "ERR The ID specified in XADD must be greater than 0-0" + msgNoScriptFound = "NOSCRIPT No matching script. Please use EVAL." + msgUnsupportedUnit = "ERR unsupported unit provided. please use M, KM, FT, MI" + msgXreadUnbalanced = "ERR Unbalanced 'xread' list of streams: for each stream key an ID or '$' must be specified." + msgXgroupKeyNotFound = "ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically." + msgXtrimInvalidStrategy = "ERR unsupported XTRIM strategy. Please use MAXLEN, MINID" + msgXtrimInvalidMaxLen = "ERR value is not an integer or out of range" + msgXtrimInvalidLimit = "ERR syntax error, LIMIT cannot be used without the special ~ option" + msgDBIndexOutOfRange = "ERR DB index is out of range" + msgLimitCombination = "ERR syntax error, LIMIT is only supported in combination with either BYSCORE or BYLEX" + msgRankIsZero = "ERR RANK can't be zero: use 1 to start from the first match, 2 from the second ... or use negative to start from the end of the list" + msgCountIsNegative = "ERR COUNT can't be negative" + msgMaxLengthIsNegative = "ERR MAXLEN can't be negative" + msgLimitIsNegative = "ERR LIMIT can't be negative" + msgMemorySubcommand = "ERR unknown subcommand '%s'. Try MEMORY HELP." +) + +func errWrongNumber(cmd string) string { + return fmt.Sprintf("ERR wrong number of arguments for '%s' command", strings.ToLower(cmd)) +} + +func errLuaParseError(err error) string { + return fmt.Sprintf("ERR Error compiling script (new function): %s", err.Error()) +} + +func errReadgroup(key, group string) error { + return fmt.Errorf("NOGROUP No such key '%s' or consumer group '%s'", key, group) +} + +func errXreadgroup(key, group string) error { + return fmt.Errorf("NOGROUP No such key '%s' or consumer group '%s' in XREADGROUP with GROUP option", key, group) +} + +func msgNotFromScripts(sha string) string { + return fmt.Sprintf("This Redis command is not allowed from script script: %s, &c", sha) +} + +// withTx wraps the non-argument-checking part of command handling code in +// transaction logic. +func withTx( + m *Miniredis, + c *server.Peer, + cb txCmd, +) { + ctx := getCtx(c) + + if ctx.nested { + // this is a call via Lua's .call(). It's already locked. + cb(c, ctx) + m.signal.Broadcast() + return + } + + if inTx(ctx) { + addTxCmd(ctx, cb) + c.WriteInline("QUEUED") + return + } + m.Lock() + cb(c, ctx) + // done, wake up anyone who waits on anything. + m.signal.Broadcast() + m.Unlock() +} + +// blockCmd is executed returns whether it is done +type blockCmd func(*server.Peer, *connCtx) bool + +// blocking keeps trying a command until the callback returns true. Calls +// onTimeout after the timeout (or when we call this in a transaction). +func blocking( + m *Miniredis, + c *server.Peer, + timeout time.Duration, + cb blockCmd, + onTimeout func(*server.Peer), +) { + var ( + ctx = getCtx(c) + ) + if inTx(ctx) { + addTxCmd(ctx, func(c *server.Peer, ctx *connCtx) { + if !cb(c, ctx) { + onTimeout(c) + } + }) + c.WriteInline("QUEUED") + return + } + + localCtx, cancel := context.WithCancel(m.Ctx) + defer cancel() + timedOut := false + if timeout != 0 { + go setCondTimer(localCtx, m.signal, &timedOut, timeout) + } + go func() { + <-localCtx.Done() + m.signal.Broadcast() // main loop might miss this signal + }() + + if !ctx.nested { + // this is a call via Lua's .call(). It's already locked. + m.Lock() + defer m.Unlock() + } + for { + if c.Closed() { + return + } + + if m.Ctx.Err() != nil { + return + } + + done := cb(c, ctx) + if done { + return + } + + if timedOut { + onTimeout(c) + return + } + + m.signal.Wait() + } +} + +func setCondTimer(ctx context.Context, sig *sync.Cond, timedOut *bool, timeout time.Duration) { + dl := time.NewTimer(timeout) + defer dl.Stop() + select { + case <-dl.C: + sig.L.Lock() // for timedOut + *timedOut = true + sig.Broadcast() // main loop might miss this signal + sig.L.Unlock() + case <-ctx.Done(): + } +} + +// formatBig formats a float the way redis does +func formatBig(v *big.Float) string { + // Format with %f and strip trailing 0s. + if v.IsInf() { + return "inf" + } + // if math.IsInf(v, -1) { + // return "-inf" + // } + return stripZeros(fmt.Sprintf("%.17f", v)) +} + +func stripZeros(sv string) string { + for strings.Contains(sv, ".") { + if sv[len(sv)-1] != '0' { + break + } + // Remove trailing 0s. + sv = sv[:len(sv)-1] + // Ends with a '.'. + if sv[len(sv)-1] == '.' { + sv = sv[:len(sv)-1] + break + } + } + return sv +} + +// redisRange gives Go offsets for something l long with start/end in +// Redis semantics. Both start and end can be negative. +// Used for string range and list range things. +// The results can be used as: v[start:end] +// Note that GETRANGE (on a string key) never returns an empty string when end +// is a large negative number. +func redisRange(l, start, end int, stringSymantics bool) (int, int) { + if start < 0 { + start = l + start + if start < 0 { + start = 0 + } + } + if start > l { + start = l + } + + if end < 0 { + end = l + end + if end < 0 { + end = -1 + if stringSymantics { + end = 0 + } + } + } + if end < math.MaxInt32 { + end++ // end argument is inclusive in Redis. + } + if end > l { + end = l + } + + if end < start { + return 0, 0 + } + return start, end +}
vendor/github.com/alicebob/miniredis/v2/server/Makefile+9 −0 added@@ -0,0 +1,9 @@ +.PHONY: all build test + +all: build test + +build: + go build + +test: + go test
vendor/github.com/alicebob/miniredis/v2/server/proto.go+157 −0 added@@ -0,0 +1,157 @@ +package server + +import ( + "bufio" + "errors" + "strconv" +) + +type Simple string + +// ErrProtocol is the general error for unexpected input +var ErrProtocol = errors.New("invalid request") + +// client always sends arrays with bulk strings +func readArray(rd *bufio.Reader) ([]string, error) { + line, err := rd.ReadString('\n') + if err != nil { + return nil, err + } + if len(line) < 3 { + return nil, ErrProtocol + } + + switch line[0] { + default: + return nil, ErrProtocol + case '*': + l, err := strconv.Atoi(line[1 : len(line)-2]) + if err != nil { + return nil, err + } + // l can be -1 + var fields []string + for ; l > 0; l-- { + s, err := readString(rd) + if err != nil { + return nil, err + } + fields = append(fields, s) + } + return fields, nil + } +} + +func readString(rd *bufio.Reader) (string, error) { + line, err := rd.ReadString('\n') + if err != nil { + return "", err + } + if len(line) < 3 { + return "", ErrProtocol + } + + switch line[0] { + default: + return "", ErrProtocol + case '+', '-', ':': + // +: simple string + // -: errors + // :: integer + // Simple line based replies. + return string(line[1 : len(line)-2]), nil + case '$': + // bulk strings are: `$5\r\nhello\r\n` + length, err := strconv.Atoi(line[1 : len(line)-2]) + if err != nil { + return "", err + } + if length < 0 { + // -1 is a nil response + return "", nil + } + var ( + buf = make([]byte, length+2) + pos = 0 + ) + for pos < length+2 { + n, err := rd.Read(buf[pos:]) + if err != nil { + return "", err + } + pos += n + } + return string(buf[:length]), nil + } +} + +// parse a reply +func ParseReply(rd *bufio.Reader) (interface{}, error) { + line, err := rd.ReadString('\n') + if err != nil { + return nil, err + } + if len(line) < 3 { + return nil, ErrProtocol + } + + switch line[0] { + default: + return nil, ErrProtocol + case '+': + // +: simple string + return Simple(line[1 : len(line)-2]), nil + case '-': + // -: errors + return nil, errors.New(string(line[1 : len(line)-2])) + case ':': + // :: integer + v := line[1 : len(line)-2] + if v == "" { + return 0, nil + } + n, err := strconv.Atoi(v) + if err != nil { + return nil, ErrProtocol + } + return n, nil + case '$': + // bulk strings are: `$5\r\nhello\r\n` + length, err := strconv.Atoi(line[1 : len(line)-2]) + if err != nil { + return "", err + } + if length < 0 { + // -1 is a nil response + return nil, nil + } + var ( + buf = make([]byte, length+2) + pos = 0 + ) + for pos < length+2 { + n, err := rd.Read(buf[pos:]) + if err != nil { + return "", err + } + pos += n + } + return string(buf[:length]), nil + case '*': + // array + l, err := strconv.Atoi(line[1 : len(line)-2]) + if err != nil { + return nil, ErrProtocol + } + // l can be -1 + var fields []interface{} + for ; l > 0; l-- { + s, err := ParseReply(rd) + if err != nil { + return nil, err + } + fields = append(fields, s) + } + return fields, nil + } +}
vendor/github.com/alicebob/miniredis/v2/server/server.go+490 −0 added@@ -0,0 +1,490 @@ +package server + +import ( + "bufio" + "crypto/tls" + "fmt" + "net" + "strings" + "sync" + "unicode" + + "github.com/alicebob/miniredis/v2/fpconv" +) + +func errUnknownCommand(cmd string, args []string) string { + s := fmt.Sprintf("ERR unknown command `%s`, with args beginning with: ", cmd) + if len(args) > 20 { + args = args[:20] + } + for _, a := range args { + s += fmt.Sprintf("`%s`, ", a) + } + return s +} + +// Cmd is what Register expects +type Cmd func(c *Peer, cmd string, args []string) + +type DisconnectHandler func(c *Peer) + +// Hook is can be added to run before every cmd. Return true if the command is done. +type Hook func(*Peer, string, ...string) bool + +// Server is a simple redis server +type Server struct { + l net.Listener + cmds map[string]Cmd + preHook Hook + peers map[net.Conn]struct{} + mu sync.Mutex + wg sync.WaitGroup + infoConns int + infoCmds int +} + +// NewServer makes a server listening on addr. Close with .Close(). +func NewServer(addr string) (*Server, error) { + l, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + return newServer(l), nil +} + +func NewServerTLS(addr string, cfg *tls.Config) (*Server, error) { + l, err := tls.Listen("tcp", addr, cfg) + if err != nil { + return nil, err + } + return newServer(l), nil +} + +func newServer(l net.Listener) *Server { + s := Server{ + cmds: map[string]Cmd{}, + peers: map[net.Conn]struct{}{}, + l: l, + } + + s.wg.Add(1) + go func() { + defer s.wg.Done() + s.serve(l) + + s.mu.Lock() + for c := range s.peers { + c.Close() + } + s.mu.Unlock() + }() + return &s +} + +// (un)set a hook which is ran before every call. It returns true if the command is done. +func (s *Server) SetPreHook(h Hook) { + s.mu.Lock() + s.preHook = h + s.mu.Unlock() +} + +func (s *Server) serve(l net.Listener) { + for { + conn, err := l.Accept() + if err != nil { + return + } + s.ServeConn(conn) + } +} + +// ServeConn handles a net.Conn. Nice with net.Pipe() +func (s *Server) ServeConn(conn net.Conn) { + s.wg.Add(1) + s.mu.Lock() + s.peers[conn] = struct{}{} + s.infoConns++ + s.mu.Unlock() + + go func() { + defer s.wg.Done() + defer conn.Close() + + s.servePeer(conn) + + s.mu.Lock() + delete(s.peers, conn) + s.mu.Unlock() + }() +} + +// Addr has the net.Addr struct +func (s *Server) Addr() *net.TCPAddr { + s.mu.Lock() + defer s.mu.Unlock() + if s.l == nil { + return nil + } + return s.l.Addr().(*net.TCPAddr) +} + +// Close a server started with NewServer. It will wait until all clients are +// closed. +func (s *Server) Close() { + s.mu.Lock() + if s.l != nil { + s.l.Close() + } + s.l = nil + s.mu.Unlock() + + s.wg.Wait() +} + +// Register a command. It can't have been registered before. Safe to call on a +// running server. +func (s *Server) Register(cmd string, f Cmd) error { + s.mu.Lock() + defer s.mu.Unlock() + cmd = strings.ToUpper(cmd) + if _, ok := s.cmds[cmd]; ok { + return fmt.Errorf("command already registered: %s", cmd) + } + s.cmds[cmd] = f + return nil +} + +func (s *Server) servePeer(c net.Conn) { + r := bufio.NewReader(c) + peer := &Peer{ + w: bufio.NewWriter(c), + } + + defer func() { + for _, f := range peer.onDisconnect { + f() + } + }() + + readCh := make(chan []string) + + go func() { + defer close(readCh) + + for { + args, err := readArray(r) + if err != nil { + peer.Close() + return + } + + readCh <- args + } + }() + + for args := range readCh { + s.Dispatch(peer, args) + peer.Flush() + + if peer.Closed() { + c.Close() + } + } +} + +func (s *Server) Dispatch(c *Peer, args []string) { + cmd, args := args[0], args[1:] + cmdUp := strings.ToUpper(cmd) + s.mu.Lock() + h := s.preHook + s.mu.Unlock() + if h != nil { + if h(c, cmdUp, args...) { + return + } + } + + s.mu.Lock() + cb, ok := s.cmds[cmdUp] + s.mu.Unlock() + if !ok { + c.WriteError(errUnknownCommand(cmd, args)) + return + } + + s.mu.Lock() + s.infoCmds++ + s.mu.Unlock() + cb(c, cmdUp, args) + if c.SwitchResp3 != nil { + c.Resp3 = *c.SwitchResp3 + c.SwitchResp3 = nil + } +} + +// TotalCommands is total (known) commands since this the server started +func (s *Server) TotalCommands() int { + s.mu.Lock() + defer s.mu.Unlock() + return s.infoCmds +} + +// ClientsLen gives the number of connected clients right now +func (s *Server) ClientsLen() int { + s.mu.Lock() + defer s.mu.Unlock() + return len(s.peers) +} + +// TotalConnections give the number of clients connected since the server +// started, including the currently connected ones +func (s *Server) TotalConnections() int { + s.mu.Lock() + defer s.mu.Unlock() + return s.infoConns +} + +// Peer is a client connected to the server +type Peer struct { + w *bufio.Writer + closed bool + Resp3 bool + SwitchResp3 *bool // we'll switch to this version _after_ the command + Ctx interface{} // anything goes, server won't touch this + onDisconnect []func() // list of callbacks + mu sync.Mutex // for Block() + ClientName string // client name set by CLIENT SETNAME +} + +func NewPeer(w *bufio.Writer) *Peer { + return &Peer{ + w: w, + } +} + +// Flush the write buffer. Called automatically after every redis command +func (c *Peer) Flush() { + c.mu.Lock() + defer c.mu.Unlock() + c.w.Flush() +} + +// Close the client connection after the current command is done. +func (c *Peer) Close() { + c.mu.Lock() + defer c.mu.Unlock() + c.closed = true +} + +// Return true if the peer connection closed. +func (c *Peer) Closed() bool { + c.mu.Lock() + defer c.mu.Unlock() + return c.closed +} + +// Register a function to execute on disconnect. There can be multiple +// functions registered. +func (c *Peer) OnDisconnect(f func()) { + c.onDisconnect = append(c.onDisconnect, f) +} + +// issue multiple calls, guarded with a mutex +func (c *Peer) Block(f func(*Writer)) { + c.mu.Lock() + defer c.mu.Unlock() + f(&Writer{c.w, c.Resp3}) +} + +// WriteError writes a redis 'Error' +func (c *Peer) WriteError(e string) { + c.Block(func(w *Writer) { + w.WriteError(e) + }) +} + +// WriteInline writes a redis inline string +func (c *Peer) WriteInline(s string) { + c.Block(func(w *Writer) { + w.WriteInline(s) + }) +} + +// WriteOK write the inline string `OK` +func (c *Peer) WriteOK() { + c.WriteInline("OK") +} + +// WriteBulk writes a bulk string +func (c *Peer) WriteBulk(s string) { + c.Block(func(w *Writer) { + w.WriteBulk(s) + }) +} + +// WriteNull writes a redis Null element +func (c *Peer) WriteNull() { + c.Block(func(w *Writer) { + w.WriteNull() + }) +} + +// WriteLen starts an array with the given length +func (c *Peer) WriteLen(n int) { + c.Block(func(w *Writer) { + w.WriteLen(n) + }) +} + +// WriteMapLen starts a map with the given length (number of keys) +func (c *Peer) WriteMapLen(n int) { + c.Block(func(w *Writer) { + w.WriteMapLen(n) + }) +} + +// WriteSetLen starts a set with the given length (number of elements) +func (c *Peer) WriteSetLen(n int) { + c.Block(func(w *Writer) { + w.WriteSetLen(n) + }) +} + +// WritePushLen starts a push-data array with the given length +func (c *Peer) WritePushLen(n int) { + c.Block(func(w *Writer) { + w.WritePushLen(n) + }) +} + +// WriteInt writes an integer +func (c *Peer) WriteInt(n int) { + c.Block(func(w *Writer) { + w.WriteInt(n) + }) +} + +// WriteFloat writes a float +func (c *Peer) WriteFloat(n float64) { + c.Block(func(w *Writer) { + w.WriteFloat(n) + }) +} + +// WriteRaw writes a raw redis response +func (c *Peer) WriteRaw(s string) { + c.Block(func(w *Writer) { + w.WriteRaw(s) + }) +} + +// WriteStrings is a helper to (bulk)write a string list +func (c *Peer) WriteStrings(strs []string) { + c.Block(func(w *Writer) { + w.WriteStrings(strs) + }) +} + +func toInline(s string) string { + return strings.Map(func(r rune) rune { + if unicode.IsSpace(r) { + return ' ' + } + return r + }, s) +} + +// A Writer is given to the callback in Block() +type Writer struct { + w *bufio.Writer + resp3 bool +} + +// WriteError writes a redis 'Error' +func (w *Writer) WriteError(e string) { + fmt.Fprintf(w.w, "-%s\r\n", toInline(e)) +} + +func (w *Writer) WriteLen(n int) { + fmt.Fprintf(w.w, "*%d\r\n", n) +} + +func (w *Writer) WriteMapLen(n int) { + if w.resp3 { + fmt.Fprintf(w.w, "%%%d\r\n", n) + return + } + w.WriteLen(n * 2) +} + +func (w *Writer) WriteSetLen(n int) { + if w.resp3 { + fmt.Fprintf(w.w, "~%d\r\n", n) + return + } + w.WriteLen(n) +} + +func (w *Writer) WritePushLen(n int) { + if w.resp3 { + fmt.Fprintf(w.w, ">%d\r\n", n) + return + } + w.WriteLen(n) +} + +// WriteBulk writes a bulk string +func (w *Writer) WriteBulk(s string) { + fmt.Fprintf(w.w, "$%d\r\n%s\r\n", len(s), s) +} + +// WriteStrings writes a list of strings (bulk) +func (w *Writer) WriteStrings(strs []string) { + w.WriteLen(len(strs)) + for _, s := range strs { + w.WriteBulk(s) + } +} + +// WriteInt writes an integer +func (w *Writer) WriteInt(n int) { + fmt.Fprintf(w.w, ":%d\r\n", n) +} + +// WriteFloat writes a float +func (w *Writer) WriteFloat(n float64) { + if w.resp3 { + fmt.Fprintf(w.w, ",%s\r\n", formatFloat(n)) + return + } + w.WriteBulk(formatFloat(n)) +} + +// WriteNull writes a redis Null element +func (w *Writer) WriteNull() { + if w.resp3 { + fmt.Fprint(w.w, "_\r\n") + return + } + fmt.Fprintf(w.w, "$-1\r\n") +} + +// WriteInline writes a redis inline string +func (w *Writer) WriteInline(s string) { + fmt.Fprintf(w.w, "+%s\r\n", toInline(s)) +} + +// WriteRaw writes a raw redis response +func (w *Writer) WriteRaw(s string) { + fmt.Fprint(w.w, s) +} + +func (w *Writer) Flush() { + w.w.Flush() +} + +// formatFloat formats a float the way redis does. +// Redis uses a method called "grisu2", which we ported from C. +func formatFloat(v float64) string { + return fpconv.Dtoa(v) +}
vendor/github.com/alicebob/miniredis/v2/size/readme.md+2 −0 added@@ -0,0 +1,2 @@ + +Credits to DmitriyVTitov on his package https://github.com/DmitriyVTitov/size
vendor/github.com/alicebob/miniredis/v2/size/size.go+138 −0 added@@ -0,0 +1,138 @@ +package size + +import ( + "reflect" + "unsafe" +) + +// Of returns the size of 'v' in bytes. +// If there is an error during calculation, Of returns -1. +func Of(v interface{}) int { + // Cache with every visited pointer so we don't count two pointers + // to the same memory twice. + cache := make(map[uintptr]bool) + return sizeOf(reflect.Indirect(reflect.ValueOf(v)), cache) +} + +// sizeOf returns the number of bytes the actual data represented by v occupies in memory. +// If there is an error, sizeOf returns -1. +func sizeOf(v reflect.Value, cache map[uintptr]bool) int { + switch v.Kind() { + + case reflect.Array: + sum := 0 + for i := 0; i < v.Len(); i++ { + s := sizeOf(v.Index(i), cache) + if s < 0 { + return -1 + } + sum += s + } + + return sum + (v.Cap()-v.Len())*int(v.Type().Elem().Size()) + + case reflect.Slice: + // return 0 if this node has been visited already + if cache[v.Pointer()] { + return 0 + } + cache[v.Pointer()] = true + + sum := 0 + for i := 0; i < v.Len(); i++ { + s := sizeOf(v.Index(i), cache) + if s < 0 { + return -1 + } + sum += s + } + + sum += (v.Cap() - v.Len()) * int(v.Type().Elem().Size()) + + return sum + int(v.Type().Size()) + + case reflect.Struct: + sum := 0 + for i, n := 0, v.NumField(); i < n; i++ { + s := sizeOf(v.Field(i), cache) + if s < 0 { + return -1 + } + sum += s + } + + // Look for struct padding. + padding := int(v.Type().Size()) + for i, n := 0, v.NumField(); i < n; i++ { + padding -= int(v.Field(i).Type().Size()) + } + + return sum + padding + + case reflect.String: + s := v.String() + hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) + if cache[hdr.Data] { + return int(v.Type().Size()) + } + cache[hdr.Data] = true + return len(s) + int(v.Type().Size()) + + case reflect.Ptr: + // return Ptr size if this node has been visited already (infinite recursion) + if cache[v.Pointer()] { + return int(v.Type().Size()) + } + cache[v.Pointer()] = true + if v.IsNil() { + return int(reflect.New(v.Type()).Type().Size()) + } + s := sizeOf(reflect.Indirect(v), cache) + if s < 0 { + return -1 + } + return s + int(v.Type().Size()) + + case reflect.Bool, + reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Int, reflect.Uint, + reflect.Chan, + reflect.Uintptr, + reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128, + reflect.Func: + return int(v.Type().Size()) + + case reflect.Map: + // return 0 if this node has been visited already (infinite recursion) + if cache[v.Pointer()] { + return 0 + } + cache[v.Pointer()] = true + sum := 0 + keys := v.MapKeys() + for i := range keys { + val := v.MapIndex(keys[i]) + // calculate size of key and value separately + sv := sizeOf(val, cache) + if sv < 0 { + return -1 + } + sum += sv + sk := sizeOf(keys[i], cache) + if sk < 0 { + return -1 + } + sum += sk + } + // Include overhead due to unused map buckets. 10.79 comes + // from https://golang.org/src/runtime/map.go. + return sum + int(v.Type().Size()) + int(float64(len(keys))*10.79) + + case reflect.Interface: + return sizeOf(v.Elem(), cache) + int(v.Type().Size()) + + } + + return -1 +}
vendor/github.com/alicebob/miniredis/v2/sorted_set.go+98 −0 added@@ -0,0 +1,98 @@ +package miniredis + +// The most KISS way to implement a sorted set. Luckily we don't care about +// performance that much. + +import ( + "sort" +) + +type direction int + +const ( + unsorted direction = iota + asc + desc +) + +type sortedSet map[string]float64 + +type ssElem struct { + score float64 + member string +} +type ssElems []ssElem + +type byScore ssElems + +func (sse byScore) Len() int { return len(sse) } +func (sse byScore) Swap(i, j int) { sse[i], sse[j] = sse[j], sse[i] } +func (sse byScore) Less(i, j int) bool { + if sse[i].score != sse[j].score { + return sse[i].score < sse[j].score + } + return sse[i].member < sse[j].member +} + +func newSortedSet() sortedSet { + return sortedSet{} +} + +func (ss *sortedSet) card() int { + return len(*ss) +} + +func (ss *sortedSet) set(score float64, member string) { + (*ss)[member] = score +} + +func (ss *sortedSet) get(member string) (float64, bool) { + v, ok := (*ss)[member] + return v, ok +} + +// elems gives the list of ssElem, ready to sort. +func (ss *sortedSet) elems() ssElems { + elems := make(ssElems, 0, len(*ss)) + for e, s := range *ss { + elems = append(elems, ssElem{s, e}) + } + return elems +} + +func (ss *sortedSet) byScore(d direction) ssElems { + elems := ss.elems() + sort.Sort(byScore(elems)) + if d == desc { + reverseElems(elems) + } + return ssElems(elems) +} + +// rankByScore gives the (0-based) index of member, or returns false. +func (ss *sortedSet) rankByScore(member string, d direction) (int, bool) { + if _, ok := (*ss)[member]; !ok { + return 0, false + } + for i, e := range ss.byScore(d) { + if e.member == member { + return i, true + } + } + // Can't happen + return 0, false +} + +func reverseSlice(o []string) { + for i := range make([]struct{}, len(o)/2) { + other := len(o) - 1 - i + o[i], o[other] = o[other], o[i] + } +} + +func reverseElems(o ssElems) { + for i := range make([]struct{}, len(o)/2) { + other := len(o) - 1 - i + o[i], o[other] = o[other], o[i] + } +}
vendor/github.com/alicebob/miniredis/v2/stream.go+507 −0 added@@ -0,0 +1,507 @@ +// Basic stream implementation. + +package miniredis + +import ( + "errors" + "fmt" + "math" + "sort" + "strconv" + "strings" + "sync" + "time" +) + +// a Stream is a list of entries, lowest ID (oldest) first, and all "groups". +type streamKey struct { + entries []StreamEntry + groups map[string]*streamGroup + lastAllocatedID string + mu sync.Mutex +} + +// a StreamEntry is an entry in a stream. The ID is always of the form +// "123-123". +// Values is an ordered list of key-value pairs. +type StreamEntry struct { + ID string + Values []string +} + +type streamGroup struct { + stream *streamKey + lastID string + pending []pendingEntry + consumers map[string]*consumer +} + +type consumer struct { + numPendingEntries int + // these timestamps aren't tracked perfectly + lastSeen time.Time // "idle" XINFO key + lastSuccess time.Time // "inactive" XINFO key +} + +type pendingEntry struct { + id string + consumer string + deliveryCount int + lastDelivery time.Time +} + +func newStreamKey() *streamKey { + return &streamKey{ + groups: map[string]*streamGroup{}, + } +} + +// generateID doesn't lock the mutex +func (s *streamKey) generateID(now time.Time) string { + ts := uint64(now.UnixNano()) / 1_000_000 + + next := fmt.Sprintf("%d-%d", ts, 0) + if s.lastAllocatedID != "" && streamCmp(s.lastAllocatedID, next) >= 0 { + last, _ := parseStreamID(s.lastAllocatedID) + next = fmt.Sprintf("%d-%d", last[0], last[1]+1) + } + + lastID := s.lastIDUnlocked() + if streamCmp(lastID, next) >= 0 { + last, _ := parseStreamID(lastID) + next = fmt.Sprintf("%d-%d", last[0], last[1]+1) + } + + s.lastAllocatedID = next + return next +} + +// lastID locks the mutex +func (s *streamKey) lastID() string { + s.mu.Lock() + defer s.mu.Unlock() + + return s.lastIDUnlocked() +} + +// lastID doesn't lock the mutex +func (s *streamKey) lastIDUnlocked() string { + if len(s.entries) == 0 { + return "0-0" + } + + return s.entries[len(s.entries)-1].ID +} + +func (s *streamKey) copy() *streamKey { + s.mu.Lock() + defer s.mu.Unlock() + + cpy := &streamKey{ + entries: s.entries, + } + groups := map[string]*streamGroup{} + for k, v := range s.groups { + gr := v.copy() + gr.stream = cpy + groups[k] = gr + } + cpy.groups = groups + return cpy +} + +func parseStreamID(id string) ([2]uint64, error) { + var ( + res [2]uint64 + err error + ) + parts := strings.SplitN(id, "-", 2) + res[0], err = strconv.ParseUint(parts[0], 10, 64) + if err != nil { + return res, errors.New(msgInvalidStreamID) + } + if len(parts) == 2 { + res[1], err = strconv.ParseUint(parts[1], 10, 64) + if err != nil { + return res, errors.New(msgInvalidStreamID) + } + } + return res, nil +} + +// compares two stream IDs (of the full format: "123-123"). Returns: -1, 0, 1 +// The given IDs should be valid stream IDs. +func streamCmp(a, b string) int { + ap, _ := parseStreamID(a) + bp, _ := parseStreamID(b) + + switch { + case ap[0] < bp[0]: + return -1 + case ap[0] > bp[0]: + return 1 + case ap[1] < bp[1]: + return -1 + case ap[1] > bp[1]: + return 1 + default: + return 0 + } +} + +// formatStreamID makes a full id ("42-42") out of a partial one ("42") +func formatStreamID(id string) (string, error) { + var ts [2]uint64 + parts := strings.SplitN(id, "-", 2) + + if len(parts) > 0 { + p, err := strconv.ParseUint(parts[0], 10, 64) + if err != nil { + return "", errInvalidEntryID + } + ts[0] = p + } + if len(parts) > 1 { + p, err := strconv.ParseUint(parts[1], 10, 64) + if err != nil { + return "", errInvalidEntryID + } + ts[1] = p + } + return fmt.Sprintf("%d-%d", ts[0], ts[1]), nil +} + +func formatStreamRangeBound(id string, start bool, reverse bool) (string, error) { + if id == "-" { + return "0-0", nil + } + + if id == "+" { + return fmt.Sprintf("%d-%d", uint64(math.MaxUint64), uint64(math.MaxUint64)), nil + } + + if id == "0" { + return "0-0", nil + } + + parts := strings.Split(id, "-") + if len(parts) == 2 { + return formatStreamID(id) + } + + // Incomplete IDs case + ts, err := strconv.ParseUint(parts[0], 10, 64) + if err != nil { + return "", errInvalidEntryID + } + + if (!start && !reverse) || (start && reverse) { + return fmt.Sprintf("%d-%d", ts, uint64(math.MaxUint64)), nil + } + + return fmt.Sprintf("%d-%d", ts, 0), nil +} + +func reversedStreamEntries(o []StreamEntry) []StreamEntry { + newStream := make([]StreamEntry, len(o)) + for i, e := range o { + newStream[len(o)-i-1] = e + } + return newStream +} + +func (s *streamKey) createGroup(group, id string) error { + s.mu.Lock() + defer s.mu.Unlock() + + if _, ok := s.groups[group]; ok { + return errors.New("BUSYGROUP Consumer Group name already exists") + } + + if id == "$" { + id = s.lastIDUnlocked() + } + s.groups[group] = &streamGroup{ + stream: s, + lastID: id, + consumers: map[string]*consumer{}, + } + return nil +} + +// streamAdd adds an entry to a stream. Returns the new entry ID. +// If id is empty or "*" the ID will be generated automatically. +// `values` should have an even length. +func (s *streamKey) add(entryID string, values []string, now time.Time) (string, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if entryID == "" || entryID == "*" { + entryID = s.generateID(now) + } + + entryID, err := formatStreamID(entryID) + if err != nil { + return "", err + } + if entryID == "0-0" { + return "", errors.New(msgStreamIDZero) + } + if streamCmp(s.lastIDUnlocked(), entryID) != -1 { + return "", errors.New(msgStreamIDTooSmall) + } + + s.entries = append(s.entries, StreamEntry{ + ID: entryID, + Values: values, + }) + return entryID, nil +} + +func (s *streamKey) trim(n int) { + s.mu.Lock() + defer s.mu.Unlock() + + if len(s.entries) > n { + s.entries = s.entries[len(s.entries)-n:] + } +} + +// trimBefore deletes entries with an id less than the provided id +// and returns the number of entries deleted +func (s *streamKey) trimBefore(id string) int { + s.mu.Lock() + var delete []string + for _, entry := range s.entries { + if streamCmp(entry.ID, id) < 0 { + delete = append(delete, entry.ID) + } else { + break + } + } + s.mu.Unlock() + s.delete(delete) + return len(delete) +} + +// all entries after "id" +func (s *streamKey) after(id string) []StreamEntry { + s.mu.Lock() + defer s.mu.Unlock() + + pos := sort.Search(len(s.entries), func(i int) bool { + return streamCmp(id, s.entries[i].ID) < 0 + }) + return s.entries[pos:] +} + +// get a stream entry by ID +// Also returns the position in the entries slice, if found. +func (s *streamKey) get(id string) (int, *StreamEntry) { + s.mu.Lock() + defer s.mu.Unlock() + + pos := sort.Search(len(s.entries), func(i int) bool { + return streamCmp(id, s.entries[i].ID) <= 0 + }) + if len(s.entries) <= pos || s.entries[pos].ID != id { + return 0, nil + } + return pos, &s.entries[pos] +} + +func (g *streamGroup) readGroup( + now time.Time, + consumerID, + id string, + count int, + noack bool, +) []StreamEntry { + if id == ">" { + // undelivered messages + msgs := g.stream.after(g.lastID) + if len(msgs) == 0 { + return nil + } + + if count > 0 && len(msgs) > count { + msgs = msgs[:count] + } + + if !noack { + shouldAppend := len(g.pending) == 0 + for _, msg := range msgs { + if !shouldAppend { + shouldAppend = streamCmp(msg.ID, g.pending[len(g.pending)-1].id) == 1 + } + + var entry *pendingEntry + if shouldAppend { + g.pending = append(g.pending, pendingEntry{}) + entry = &g.pending[len(g.pending)-1] + } else { + var pos int + pos, entry = g.searchPending(msg.ID) + if entry == nil { + g.pending = append(g.pending[:pos+1], g.pending[pos:]...) + entry = &g.pending[pos] + } else { + g.consumers[entry.consumer].numPendingEntries-- + } + } + + *entry = pendingEntry{ + id: msg.ID, + consumer: consumerID, + deliveryCount: 1, + lastDelivery: now, + } + } + } + if _, ok := g.consumers[consumerID]; !ok { + g.consumers[consumerID] = &consumer{} + } + g.consumers[consumerID].numPendingEntries += len(msgs) + g.lastID = msgs[len(msgs)-1].ID + return msgs + } + + // re-deliver messages from the pending list. + // con := gr.consumers[consumerID] + msgs := g.pendingAfter(id) + var res []StreamEntry + for i, p := range msgs { + if p.consumer != consumerID { + continue + } + _, entry := g.stream.get(p.id) + // not found. Weird? + if entry == nil { + continue + } + p.deliveryCount += 1 + p.lastDelivery = now + msgs[i] = p + res = append(res, *entry) + } + return res +} + +func (g *streamGroup) searchPending(id string) (int, *pendingEntry) { + pos := sort.Search(len(g.pending), func(i int) bool { + return streamCmp(id, g.pending[i].id) <= 0 + }) + if pos >= len(g.pending) || g.pending[pos].id != id { + return pos, nil + } + return pos, &g.pending[pos] +} + +func (g *streamGroup) ack(ids []string) (int, error) { + count := 0 + for _, id := range ids { + if _, err := parseStreamID(id); err != nil { + return 0, errors.New(msgInvalidStreamID) + } + + pos, entry := g.searchPending(id) + if entry == nil { + continue + } + + consumer := g.consumers[entry.consumer] + consumer.numPendingEntries-- + + g.pending = append(g.pending[:pos], g.pending[pos+1:]...) + // don't count deleted entries + if _, e := g.stream.get(id); e == nil { + continue + } + count++ + } + return count, nil +} + +func (s *streamKey) delete(ids []string) (int, error) { + count := 0 + for _, id := range ids { + if _, err := parseStreamID(id); err != nil { + return 0, errors.New(msgInvalidStreamID) + } + + i, entry := s.get(id) + if entry == nil { + continue + } + + s.entries = append(s.entries[:i], s.entries[i+1:]...) + count++ + } + return count, nil +} + +func (g *streamGroup) pendingAfterOrEqual(id string) []pendingEntry { + pos := sort.Search(len(g.pending), func(i int) bool { + return streamCmp(id, g.pending[i].id) <= 0 + }) + return g.pending[pos:] +} + +func (g *streamGroup) pendingAfter(id string) []pendingEntry { + pos := sort.Search(len(g.pending), func(i int) bool { + return streamCmp(id, g.pending[i].id) < 0 + }) + return g.pending[pos:] +} + +func (g *streamGroup) pendingCount(consumer string) int { + n := 0 + for _, p := range g.activePending() { + if p.consumer == consumer { + n++ + } + } + return n +} + +// pending entries without the entries deleted from the group +func (g *streamGroup) activePending() []pendingEntry { + var pe []pendingEntry + for _, p := range g.pending { + // drop deleted ones + if _, e := g.stream.get(p.id); e == nil { + continue + } + p := p + pe = append(pe, p) + } + return pe +} + +func (g *streamGroup) copy() *streamGroup { + cns := map[string]*consumer{} + for k, v := range g.consumers { + c := *v + cns[k] = &c + } + return &streamGroup{ + // don't copy stream + lastID: g.lastID, + pending: g.pending, + consumers: cns, + } +} + +func (g *streamGroup) setLastSeen(c string, t time.Time) { + cons, ok := g.consumers[c] + if !ok { + cons = &consumer{} + } + cons.lastSeen = t + g.consumers[c] = cons +} + +func (g *streamGroup) setLastSuccess(c string, t time.Time) { + g.setLastSeen(c, t) + g.consumers[c].lastSuccess = t +}
vendor/github.com/yuin/gopher-lua/alloc.go+79 −0 addedvendor/github.com/yuin/gopher-lua/ast/ast.go+29 −0 addedvendor/github.com/yuin/gopher-lua/ast/expr.go+138 −0 addedvendor/github.com/yuin/gopher-lua/ast/misc.go+17 −0 addedvendor/github.com/yuin/gopher-lua/ast/stmt.go+107 −0 addedvendor/github.com/yuin/gopher-lua/ast/token.go+22 −0 addedvendor/github.com/yuin/gopher-lua/auxlib.go+465 −0 addedvendor/github.com/yuin/gopher-lua/baselib.go+597 −0 addedvendor/github.com/yuin/gopher-lua/channellib.go+184 −0 addedvendor/github.com/yuin/gopher-lua/compile.go+1869 −0 addedvendor/github.com/yuin/gopher-lua/config.go+43 −0 addedvendor/github.com/yuin/gopher-lua/coroutinelib.go+112 −0 addedvendor/github.com/yuin/gopher-lua/debuglib.go+173 −0 addedvendor/github.com/yuin/gopher-lua/function.go+193 −0 addedvendor/github.com/yuin/gopher-lua/.gitignore+1 −0 added@@ -0,0 +1 @@ +.idea
vendor/github.com/yuin/gopher-lua/iolib.go+749 −0 addedvendor/github.com/yuin/gopher-lua/LICENSE+21 −0 added@@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Yusuke Inuzuka + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.
vendor/github.com/yuin/gopher-lua/linit.go+54 −0 addedvendor/github.com/yuin/gopher-lua/loadlib.go+128 −0 addedvendor/github.com/yuin/gopher-lua/Makefile+10 −0 added@@ -0,0 +1,10 @@ +.PHONY: build test glua + +build: + ./_tools/go-inline *.go && go fmt . && go build + +glua: *.go pm/*.go cmd/glua/glua.go + ./_tools/go-inline *.go && go fmt . && go build cmd/glua/glua.go + +test: + ./_tools/go-inline *.go && go fmt . && go test
vendor/github.com/yuin/gopher-lua/mathlib.go+231 −0 addedvendor/github.com/yuin/gopher-lua/opcode.go+371 −0 addedvendor/github.com/yuin/gopher-lua/oslib.go+236 −0 addedvendor/github.com/yuin/gopher-lua/package.go+7 −0 addedvendor/github.com/yuin/gopher-lua/parse/lexer.go+549 −0 addedvendor/github.com/yuin/gopher-lua/parse/Makefile+7 −0 addedvendor/github.com/yuin/gopher-lua/parse/parser.go+1385 −0 addedvendor/github.com/yuin/gopher-lua/parse/parser.go.y+535 −0 addedvendor/github.com/yuin/gopher-lua/pm/pm.go+638 −0 addedvendor/github.com/yuin/gopher-lua/README.rst+0 −0 addedvendor/github.com/yuin/gopher-lua/_state.go+2093 −0 addedvendor/github.com/yuin/gopher-lua/state.go+2306 −0 addedvendor/github.com/yuin/gopher-lua/stringlib.go+448 −0 addedvendor/github.com/yuin/gopher-lua/table.go+387 −0 addedvendor/github.com/yuin/gopher-lua/tablelib.go+100 −0 addedvendor/github.com/yuin/gopher-lua/utils.go+265 −0 addedvendor/github.com/yuin/gopher-lua/value.go+215 −0 addedvendor/github.com/yuin/gopher-lua/_vm.go+1049 −0 addedvendor/github.com/yuin/gopher-lua/vm.go+2465 −0 addedvendor/modules.txt+17 −0 modified
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
39- Rocky Linux launches opt-in security repository for urgent fixesHelp Net Security · May 15, 2026
- Fragnesia (CVE-2026-46300): Frequently asked questions about new Linux Kernel XFRM ESP-in-TCP privilege escalationTenable Blog · May 14, 2026
- Browser Run: now running on Cloudflare Containers, it’s faster and more scalableCloudflare Blog · May 13, 2026
- GemStuffer Abuses 150+ RubyGems to Exfiltrate Scraped U.K. Council Portal DataThe Hacker News · May 13, 2026
- [GUEST DIARY] Tearing apart website fraud to see how it works., (Wed, May 13th)SANS Internet Storm Center · May 13, 2026
- Fedora Hummingbird brings the container security model to a Linux host OSHelp Net Security · May 12, 2026
- The State of Ransomware – Q1 2026Check Point Research · May 11, 2026
- Dirty Frag (CVE-2026-43284, CVE-2026-43500): Frequently asked questions about this Linux kernel privilege escalation vulnerability chainTenable Blog · May 8, 2026
- Another Universal Linux Local Privilege Escalation (LPE) Vulnerability: Dirty Frag, (Fri, May 8th)SANS Internet Storm Center · May 8, 2026
- 'Dirty Frag' Linux flaw one-ups CopyFail with no patches and public root exploitThe Register Security · May 8, 2026
- Massive AI investment scam network spans 15,500 domainsMalwarebytes Labs · May 7, 2026
- How Cloudflare responded to the “Copy Fail” Linux vulnerabilityCloudflare Blog · May 7, 2026
- Google Chrome’s silent 4GB AI download problem [updated]Malwarebytes Labs · May 6, 2026
- Attackers adopt JavaScript runtime Bun to spread NWHStealerMalwarebytes Labs · May 6, 2026
- Insights into the clustering and reuse of phone numbers in scam emailsCisco Talos Intelligence · May 6, 2026
- Attackers are cashing in on fresh 'CopyFail' Linux flawThe Register Security · May 5, 2026
- Meta adds proof-based security to encrypted backupsHelp Net Security · May 5, 2026
- Trellix Source Code Repository BreachedSecurityWeek · May 4, 2026
- Hugging Face, ClawHub Abused for Malware DistributionSecurityWeek · May 1, 2026
- Copy Fail (CVE-2026-31431): Frequently asked questions about Linux kernel privilege escalation vulnerabilityTenable Blog · Apr 30, 2026
- Post-quantum encryption for Cloudflare IPsec is generally availableCloudflare Blog · Apr 30, 2026
- Nine-year-old Linux kernel flaw enables reliable local privilege escalation (CVE-2026-31431)Help Net Security · Apr 30, 2026
- EtherRAT Distribution Spoofing Administrative Tools via GitHub FacadesThe Hacker News · Apr 30, 2026
- VECT: Ransomware by design, Wiper by accidentCheck Point Research · Apr 28, 2026
- Medical and utility tech companies admit digital breakinsThe Register Security · Apr 27, 2026
- TeamPCP Supply Chain Campaign: Update 008 - 26-Day Pause Ends with Three Concurrent Compromises (Checkmarx KICS, Bitwarden CLI Cascade, xinference PyPI), CanisterSprawl npm Worm Identified, and Tier 1 Coverage Returns, (Mon, Apr 27th)SANS Internet Storm Center · Apr 27, 2026
- How cyberattacks on companies affect everyoneMalwarebytes Labs · Apr 23, 2026
- Hypersonic Supply Chain Attacks: One Solution That Didn’t Need to Know the PayloadSentinelOne Labs · Apr 22, 2026
- Malicious trading website drops malware that hands your browser to attackersMalwarebytes Labs · Apr 22, 2026
- Moving past bots vs. humansCloudflare Blog · Apr 21, 2026
- Orchestrating AI Code Review at scaleCloudflare Blog · Apr 20, 2026
- DFIR Report – The Gentlemen & SystemBC: A Sneak Peek Behind the ProxyCheck Point Research · Apr 20, 2026
- Unweight: how we compressed an LLM 22% without sacrificing qualityCloudflare Blog · Apr 17, 2026
- Redirects for AI Training enforces canonical contentCloudflare Blog · Apr 17, 2026
- Introducing Flagship: feature flags built for the age of AICloudflare Blog · Apr 17, 2026
- Unlocking foundational visibility for cyber-physical systems with OT vulnerability managementTenable Blog · Apr 15, 2026
- Securing the Software Supply Chain: How SentinelOne’s AI EDR Autonomously Blocked the CPU-Z Watering Hole Cyber AttackSentinelOne Labs · Apr 14, 2026
- Operation TrueChaos: 0-Day Exploitation Against Southeast Asian Government TargetsCheck Point Research · Mar 31, 2026
- Siemens Ruggedcom RoxCISA Alerts