package src import ( "bufio" "bytes" "context" "fmt" "io" "log" "net" "regexp" "strconv" ) func adapt(ctx context.Context, config Config, conn net.Conn) error { reader := bufio.NewReader(conn) for ctx.Err() == nil { if err := adaptOne(ctx, config, conn, reader); err != nil { return err } } return io.EOF } func adaptOne(ctx context.Context, config Config, conn io.Writer, reader *bufio.Reader) error { raw, err := readMessage(reader) if err != nil { return err } else if len(raw) == 0 { return nil } log.Printf("routing: %q", raw) return config.WithConn(parseHashKey(raw), func(forwardConn net.Conn) error { if _, err := io.Copy(forwardConn, bytes.NewReader(raw)); err != nil { return err } return readMessageTo(conn, bufio.NewReader(forwardConn)) }) } func readMessage(reader *bufio.Reader) ([]byte, error) { w := bytes.NewBuffer(nil) err := readMessageTo(w, reader) return w.Bytes(), err } func readMessageTo(w io.Writer, reader *bufio.Reader) error { w2 := bufio.NewWriter(w) defer w2.Flush() err := _readMessageTo(w2, reader) if err != nil { return err } return w2.Flush() } func _readMessageTo(w io.Writer, reader *bufio.Reader) error { firstLine, _, err := reader.ReadLine() if err != nil { return err } firstLine = bytes.TrimSuffix(firstLine, []byte("\r\n")) if len(firstLine) == 0 { return nil } fmt.Fprintf(w, "%s\r\n", firstLine) switch firstLine[0] { case '+': // simple string, like +OK return nil case '-': // simple error, like -message return nil case ':': // number, like /[+-][0-9]+/ return err case '$': // long string, like $-1 for nil, like $LEN\r\nSTRING\r\n if firstLine[1] == '-' { return nil } firstLine = bytes.TrimPrefix(firstLine[1:], []byte("+")) n, err := strconv.Atoi(string(firstLine)) if err != nil { return fmt.Errorf("num not a num: %q: %w", firstLine, err) } nextLine := make([]byte, n+2) nAt := 0 for nAt < n+2 { nMore, err := reader.Read(nextLine[nAt:]) if err != nil { return fmt.Errorf("couldnt read %v more/%v bytes for long string: %w", n+2-nAt, n+2, err) } nAt += nMore } fmt.Fprintf(w, "%s", nextLine) return err case '*', '~', '%': // *=array ~=set %=map, like *-1 for nil, like *4 for [1,2,3,4] n, err := strconv.Atoi(string(firstLine[1:])) if err != nil { return fmt.Errorf("arr not a num: %q: %w", firstLine, err) } else if n == -1 { return nil } if isMap := firstLine[0] == '%'; isMap { n *= 2 } for i := 0; i < n; i++ { err := _readMessageTo(w, reader) if err != nil { return err } } return nil case '_': // nil return nil case '#': // boolean, like #t or #f return nil case ',': // double return nil } log.Fatalf("not impl: %q", firstLine) return nil } var alpha = regexp.MustCompile(`[a-zA-Z]`) var parts = regexp.MustCompile(`(.*?)\r\n*`) func parseHashKey(raw []byte) string { matches := parts.FindAllSubmatch(raw, 5) if len(matches) == 0 { return "" } for i := range matches { if isTheCommand := alpha.Match(matches[i][0]); isTheCommand { for j := i + 1; j < len(matches); j++ { if matches[j][0][0] == byte('$') { continue } return string(matches[j][1]) } return string(matches[i][1]) } } return "" }