| // Copyright 2015 The LUCI Authors. |
| // |
| // 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 pubsub |
| |
| import ( |
| "errors" |
| "fmt" |
| "strings" |
| "unicode" |
| ) |
| |
| func newResource(project, collection, name string) string { |
| return strings.Join([]string{"projects", project, collection, name}, "/") |
| } |
| |
| func splitResource(v string) []string { |
| return strings.Split(v, "/") |
| } |
| |
| // resourceProjectName returns the resource's project and name components. |
| func resourceProjectName(v string) (p, n string, err error) { |
| parts := splitResource(v) |
| if len(parts) != 4 { |
| err = errors.New("malformed resource") |
| return |
| } |
| p, n = parts[1], parts[3] |
| return |
| } |
| |
| // validateResource validates that a resource is well-formed. |
| // |
| // A resource is in the form: |
| // projects/<project>/<collection>/<value> |
| func validateResource(v, collection string) error { |
| // A resource must contain exactly three forward slashes. |
| parts := splitResource(v) |
| switch len(parts) { |
| case 0: |
| return errors.New("missing project component") |
| case 1: |
| return errors.New("missing project name") |
| case 2: |
| return errors.New("missing collection type") |
| case 3: |
| return errors.New("missing resource name") |
| case 4: |
| break |
| default: |
| return fmt.Errorf("too many components (%d) in resource name", len(parts)) |
| } |
| |
| switch { |
| case parts[0] != "projects": |
| return errors.New("first resource component must be 'projects'") |
| case parts[2] != collection: |
| return fmt.Errorf("third resource component must be '%s'", collection) |
| } |
| |
| // Validate the resource name. |
| if err := validateResourceName(parts[3]); err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| // validateResourceName validates a resource name. Resource naming is described |
| // in: https://cloud.google.com/pubsub/overview#names |
| // |
| // As of 'v1', a resource must: |
| // - start with a letter. |
| // - end with a lowercase letter or number. |
| // - contain only letters, numbers, dashes (-), underscores (_) periods (.), |
| // tildes (~), pluses (+), or percent signs (%). |
| // - be between 3 and 255 characters in length. |
| // - cannot begin with the string goog. |
| // |
| func validateResourceName(s string) error { |
| if l := len(s); l < 3 || l > 255 { |
| return fmt.Errorf("length (%d) must be between 3 and 255", l) |
| } |
| |
| if strings.HasPrefix(s, "goog") { |
| return errors.New("resource cannot begin with 'goog'") |
| } |
| |
| // Validate correctness. |
| for i, r := range s { |
| if r >= unicode.MaxASCII { |
| return fmt.Errorf("non-ASCII character found at index #%d", i) |
| } |
| |
| if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')) { |
| // Must begin with an ASCII letter? |
| if i == 0 { |
| return errors.New("pubsub: resource names must begin with a letter") |
| } |
| |
| // Is this a valid mid-resource value? |
| const validMidResourceRunes = "-_.~+%" |
| if !((r >= '0' && r <= '9') || strings.ContainsRune(validMidResourceRunes, r)) { |
| return fmt.Errorf("pubsub: invalid resource rune at %d: %c", i, r) |
| } |
| } |
| } |
| return nil |
| } |