| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187 |
- // Unless explicitly stated otherwise all files in this repository are licensed
- // under the Apache License Version 2.0.
- // This product includes software developed at Datadog (https://www.datadoghq.com/).
- // Copyright 2016-present Datadog, Inc.
- package obfuscate
- import (
- "bytes"
- "strings"
- )
- // redisTokenType specifies the token type returned by the tokenizer.
- type redisTokenType int
- const (
- // redisTokenCommand is a command token. For compound tokens, it is
- // only the first part up to a space.
- redisTokenCommand redisTokenType = iota
- // redisTokenArgument is an argument token.
- redisTokenArgument
- )
- // String implements fmt.Stringer.
- func (t redisTokenType) String() string {
- return map[redisTokenType]string{
- redisTokenCommand: "command",
- redisTokenArgument: "argument",
- }[t]
- }
- // redisTokenizer tokenizes a Redis command string. The string can be on
- // multiple lines. The tokenizer is capable of parsing quoted strings and escape
- // sequences inside them.
- type redisTokenizer struct {
- data []byte
- ch byte
- off int
- done bool
- state redisParseState
- }
- // redisParseState specifies the current state of the tokenizer.
- type redisParseState int
- const (
- // redisStateCommand specifies that we are about to parse a command.
- // It is usually the state at the beginning of the scan or after a
- // new line.
- redisStateCommand redisParseState = iota
- // redisStateArgument specifies that we are about to parse an argument
- // to a command or the rest of the tokens in a compound command.
- redisStateArgument
- )
- // newRedisTokenizer returns a new tokenizer for the given data.
- func newRedisTokenizer(data []byte) *redisTokenizer {
- return &redisTokenizer{
- data: bytes.TrimSpace(data),
- off: -1,
- state: redisStateCommand,
- }
- }
- // scan returns the next token, it's type and a bool. The boolean specifies if
- // the returned token was the last one.
- func (t *redisTokenizer) scan() (tok string, typ redisTokenType, done bool) {
- switch t.state {
- case redisStateCommand:
- return t.scanCommand()
- default:
- return t.scanArg()
- }
- }
- // next advances the scanner to the next character.
- func (t *redisTokenizer) next() {
- t.off++
- if t.off <= len(t.data)-1 {
- t.ch = t.data[t.off]
- return
- }
- t.done = true
- }
- // scanCommand scans a command from the buffer.
- func (t *redisTokenizer) scanCommand() (tok string, typ redisTokenType, done bool) {
- var (
- str strings.Builder
- started bool
- )
- for {
- t.next()
- if t.done {
- return str.String(), typ, t.done
- }
- switch t.ch {
- case ' ':
- if !started {
- // skip spaces preceding token
- t.skipSpace()
- break
- }
- // done scanning command
- t.state = redisStateArgument
- t.skipSpace()
- return str.String(), redisTokenCommand, t.done
- case '\n':
- return str.String(), redisTokenCommand, t.done
- default:
- str.WriteByte(t.ch)
- }
- started = true
- }
- }
- // scanArg scans an argument from the buffer.
- func (t *redisTokenizer) scanArg() (tok string, typ redisTokenType, done bool) {
- var (
- str strings.Builder
- quoted bool // in quoted string
- escape bool // escape sequence
- )
- for {
- t.next()
- if t.done {
- return str.String(), redisTokenArgument, t.done
- }
- switch t.ch {
- case '\\':
- str.WriteByte('\\')
- if !escape {
- // next character could be escaped
- escape = true
- continue
- }
- case '\n':
- if !quoted {
- // last argument, new command follows
- t.state = redisStateCommand
- return str.String(), redisTokenArgument, t.done
- }
- str.WriteByte('\n')
- case '"':
- str.WriteByte('"')
- if !escape {
- // this quote wasn't escaped, toggle quoted mode
- quoted = !quoted
- }
- case ' ':
- if !quoted {
- t.skipSpace()
- return str.String(), redisTokenArgument, t.done
- }
- str.WriteByte(' ')
- default:
- str.WriteByte(t.ch)
- }
- escape = false
- }
- }
- // unread is the reverse of next, unreading a character.
- func (t *redisTokenizer) unread() {
- if t.off < 1 {
- return
- }
- t.off--
- t.ch = t.data[t.off]
- }
- // skipSpace moves the cursor forward until it meets the last space
- // in a sequence of contiguous spaces.
- func (t *redisTokenizer) skipSpace() {
- for t.ch == ' ' || t.ch == '\t' || t.ch == '\r' && !t.done {
- t.next()
- }
- if t.ch == '\n' {
- // next token is a command
- t.state = redisStateCommand
- } else {
- // don't steal the first non-space character
- t.unread()
- }
- }
|