/* Copyright 2015 Google LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "bytes" "errors" "fmt" "io" "strconv" "strings" "unicode" "cloud.google.com/go/bigtable" ) // Parse a GC policy. Valid policies include // never // maxage = 5d // maxversions = 3 // maxage = 5d || maxversions = 3 // maxage=30d || (maxage=3d && maxversions=100) func parseGCPolicy(s string) (bigtable.GCPolicy, error) { if strings.TrimSpace(s) == "never" { return bigtable.NoGcPolicy(), nil } r := strings.NewReader(s) p, err := parsePolicyExpr(r) if err != nil { return nil, fmt.Errorf("invalid GC policy: %v", err) } tok, err := getToken(r) if err != nil { return nil, err } if tok != "" { return nil, fmt.Errorf("invalid GC policy: want end of input, got %q", tok) } return p, nil } // expr ::= term (op term)* // op ::= "and" | "or" | "&&" | "||" func parsePolicyExpr(r io.RuneScanner) (bigtable.GCPolicy, error) { policy, err := parsePolicyTerm(r) if err != nil { return nil, err } for { tok, err := getToken(r) if err != nil { return nil, err } var f func(...bigtable.GCPolicy) bigtable.GCPolicy switch tok { case "and", "&&": f = bigtable.IntersectionPolicy case "or", "||": f = bigtable.UnionPolicy default: ungetToken(tok) return policy, nil } p2, err := parsePolicyTerm(r) if err != nil { return nil, err } policy = f(policy, p2) } } // term ::= "maxage" "=" duration | "maxversions" "=" int | "(" policy ")" func parsePolicyTerm(r io.RuneScanner) (bigtable.GCPolicy, error) { tok, err := getToken(r) if err != nil { return nil, err } switch tok { case "": return nil, errors.New("empty GC policy term") case "maxage", "maxversions": if err := expectToken(r, "="); err != nil { return nil, err } tok2, err := getToken(r) if err != nil { return nil, err } if tok2 == "" { return nil, errors.New("expected a token after '='") } if tok == "maxage" { dur, err := parseDuration(tok2) if err != nil { return nil, err } return bigtable.MaxAgePolicy(dur), nil } n, err := strconv.ParseUint(tok2, 10, 16) if err != nil { return nil, err } return bigtable.MaxVersionsPolicy(int(n)), nil case "(": p, err := parsePolicyExpr(r) if err != nil { return nil, err } if err := expectToken(r, ")"); err != nil { return nil, err } return p, nil default: return nil, fmt.Errorf("unexpected token: %q", tok) } } func expectToken(r io.RuneScanner, want string) error { got, err := getToken(r) if err != nil { return err } if got != want { return fmt.Errorf("expected %q, saw %q", want, got) } return nil } const noToken = "_" // empty token is valid, so use "_" instead // If not noToken, getToken will return this instead of reading a new token // from the input. var ungotToken = noToken // getToken extracts the first token from the input. Valid tokens include // any sequence of letters and digits, and these symbols: &&, ||, =, ( and ). // getToken returns ("", nil) at end of input. func getToken(r io.RuneScanner) (string, error) { if ungotToken != noToken { t := ungotToken ungotToken = noToken return t, nil } var err error // Skip leading whitespace. c := ' ' for unicode.IsSpace(c) { c, _, err = r.ReadRune() if err == io.EOF { return "", nil } if err != nil { return "", err } } switch { case c == '=' || c == '(' || c == ')': return string(c), nil case c == '&' || c == '|': c2, _, err := r.ReadRune() if err != nil && err != io.EOF { return "", err } if c != c2 { return "", fmt.Errorf("expected %c%c", c, c) } return string([]rune{c, c}), nil case unicode.IsLetter(c) || unicode.IsDigit(c): // Collect an alphanumeric token. var b bytes.Buffer for unicode.IsLetter(c) || unicode.IsDigit(c) { b.WriteRune(c) c, _, err = r.ReadRune() if err == io.EOF { break } if err != nil { return "", err } } r.UnreadRune() return b.String(), nil default: return "", fmt.Errorf("bad rune %q", c) } } // "unget" a token so the next call to getToken will return it. func ungetToken(tok string) { if ungotToken != noToken { panic("ungetToken called twice") } ungotToken = tok }