launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/charm/url.go (about)

     1  // Copyright 2011, 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package charm
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"regexp"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"labix.org/v2/mgo/bson"
    14  )
    15  
    16  // A charm URL represents charm locations such as:
    17  //
    18  //     cs:~joe/oneiric/wordpress
    19  //     cs:oneiric/wordpress-42
    20  //     local:oneiric/wordpress
    21  //
    22  type URL struct {
    23  	Schema   string // "cs" or "local"
    24  	User     string // "joe"
    25  	Series   string // "oneiric"
    26  	Name     string // "wordpress"
    27  	Revision int    // -1 if unset, N otherwise
    28  }
    29  
    30  var (
    31  	validUser   = regexp.MustCompile("^[a-z0-9][a-zA-Z0-9+.-]+$")
    32  	validSeries = regexp.MustCompile("^[a-z]+([a-z-]+[a-z])?$")
    33  	validName   = regexp.MustCompile("^[a-z][a-z0-9]*(-[a-z0-9]*[a-z][a-z0-9]*)*$")
    34  )
    35  
    36  // IsValidUser returns whether user is a valid username in charm URLs.
    37  func IsValidUser(user string) bool {
    38  	return validUser.MatchString(user)
    39  }
    40  
    41  // IsValidSeries returns whether series is a valid series in charm URLs.
    42  func IsValidSeries(series string) bool {
    43  	return validSeries.MatchString(series)
    44  }
    45  
    46  // IsValidName returns whether name is a valid charm name.
    47  func IsValidName(name string) bool {
    48  	return validName.MatchString(name)
    49  }
    50  
    51  // WithRevision returns a URL equivalent to url but with Revision set
    52  // to revision.
    53  func (url *URL) WithRevision(revision int) *URL {
    54  	urlCopy := *url
    55  	urlCopy.Revision = revision
    56  	return &urlCopy
    57  }
    58  
    59  // MustParseURL works like ParseURL, but panics in case of errors.
    60  func MustParseURL(url string) *URL {
    61  	u, err := ParseURL(url)
    62  	if err != nil {
    63  		panic(err)
    64  	}
    65  	return u
    66  }
    67  
    68  // ParseURL parses the provided charm URL string into its respective
    69  // structure.
    70  func ParseURL(url string) (*URL, error) {
    71  	u := &URL{}
    72  	i := strings.Index(url, ":")
    73  	if i > 0 {
    74  		u.Schema = url[:i]
    75  		i++
    76  	}
    77  	// cs: or local:
    78  	if u.Schema != "cs" && u.Schema != "local" {
    79  		return nil, fmt.Errorf("charm URL has invalid schema: %q", url)
    80  	}
    81  	parts := strings.Split(url[i:], "/")
    82  	if len(parts) < 1 || len(parts) > 3 {
    83  		return nil, fmt.Errorf("charm URL has invalid form: %q", url)
    84  	}
    85  
    86  	// ~<username>
    87  	if strings.HasPrefix(parts[0], "~") {
    88  		if u.Schema == "local" {
    89  			return nil, fmt.Errorf("local charm URL with user name: %q", url)
    90  		}
    91  		u.User = parts[0][1:]
    92  		if !IsValidUser(u.User) {
    93  			return nil, fmt.Errorf("charm URL has invalid user name: %q", url)
    94  		}
    95  		parts = parts[1:]
    96  	}
    97  
    98  	// <series>
    99  	if len(parts) < 2 {
   100  		return nil, fmt.Errorf("charm URL without series: %q", url)
   101  	}
   102  	if len(parts) == 2 {
   103  		u.Series = parts[0]
   104  		if !IsValidSeries(u.Series) {
   105  			return nil, fmt.Errorf("charm URL has invalid series: %q", url)
   106  		}
   107  		parts = parts[1:]
   108  	}
   109  
   110  	// <name>[-<revision>]
   111  	u.Name = parts[0]
   112  	u.Revision = -1
   113  	for i := len(u.Name) - 1; i > 0; i-- {
   114  		c := u.Name[i]
   115  		if c >= '0' && c <= '9' {
   116  			continue
   117  		}
   118  		if c == '-' && i != len(u.Name)-1 {
   119  			var err error
   120  			u.Revision, err = strconv.Atoi(u.Name[i+1:])
   121  			if err != nil {
   122  				panic(err) // We just checked it was right.
   123  			}
   124  			u.Name = u.Name[:i]
   125  		}
   126  		break
   127  	}
   128  	if !IsValidName(u.Name) {
   129  		return nil, fmt.Errorf("charm URL has invalid charm name: %q", url)
   130  	}
   131  	return u, nil
   132  }
   133  
   134  // InferURL returns a charm URL inferred from src. The provided
   135  // src may be a valid URL, in which case it is returned as-is,
   136  // or it may be an alias in one of the following formats:
   137  //
   138  //    name
   139  //    name-revision
   140  //    series/name
   141  //    series/name-revision
   142  //    schema:name
   143  //    schema:name-revision
   144  //    cs:~user/name
   145  //    cs:~user/name-revision
   146  //
   147  // The defaultSeries paramater is used to define the resulting URL
   148  // when src does not include that information; similarly, a missing
   149  // schema is assumed to be 'cs'.
   150  func InferURL(src, defaultSeries string) (*URL, error) {
   151  	if u, err := ParseURL(src); err == nil {
   152  		// src was a valid charm URL already
   153  		return u, nil
   154  	}
   155  	if strings.HasPrefix(src, "~") {
   156  		return nil, fmt.Errorf("cannot infer charm URL with user but no schema: %q", src)
   157  	}
   158  	orig := src
   159  	schema := "cs"
   160  	if i := strings.Index(src, ":"); i != -1 {
   161  		schema, src = src[:i], src[i+1:]
   162  	}
   163  	var full string
   164  	switch parts := strings.Split(src, "/"); len(parts) {
   165  	case 1:
   166  		if defaultSeries == "" {
   167  			return nil, fmt.Errorf("cannot infer charm URL for %q: no series provided", orig)
   168  		}
   169  		full = fmt.Sprintf("%s:%s/%s", schema, defaultSeries, src)
   170  	case 2:
   171  		if strings.HasPrefix(parts[0], "~") {
   172  			if defaultSeries == "" {
   173  				return nil, fmt.Errorf("cannot infer charm URL for %q: no series provided", orig)
   174  			}
   175  			full = fmt.Sprintf("%s:%s/%s/%s", schema, parts[0], defaultSeries, parts[1])
   176  		} else {
   177  			full = fmt.Sprintf("%s:%s", schema, src)
   178  		}
   179  	default:
   180  		full = fmt.Sprintf("%s:%s", schema, src)
   181  	}
   182  	u, err := ParseURL(full)
   183  	if err != nil && orig != full {
   184  		err = fmt.Errorf("%s (URL inferred from %q)", err, orig)
   185  	}
   186  	return u, err
   187  }
   188  
   189  func (u *URL) Path() string {
   190  	if u.User != "" {
   191  		if u.Revision >= 0 {
   192  			return fmt.Sprintf("~%s/%s/%s-%d", u.User, u.Series, u.Name, u.Revision)
   193  		}
   194  		return fmt.Sprintf("~%s/%s/%s", u.User, u.Series, u.Name)
   195  	}
   196  	if u.Revision >= 0 {
   197  		return fmt.Sprintf("%s/%s-%d", u.Series, u.Name, u.Revision)
   198  	}
   199  	return fmt.Sprintf("%s/%s", u.Series, u.Name)
   200  }
   201  
   202  func (u *URL) String() string {
   203  	return fmt.Sprintf("%s:%s", u.Schema, u.Path())
   204  }
   205  
   206  // GetBSON turns u into a bson.Getter so it can be saved directly
   207  // on a MongoDB database with mgo.
   208  func (u *URL) GetBSON() (interface{}, error) {
   209  	if u == nil {
   210  		return nil, nil
   211  	}
   212  	return u.String(), nil
   213  }
   214  
   215  // SetBSON turns u into a bson.Setter so it can be loaded directly
   216  // from a MongoDB database with mgo.
   217  func (u *URL) SetBSON(raw bson.Raw) error {
   218  	if raw.Kind == 10 {
   219  		return bson.SetZero
   220  	}
   221  	var s string
   222  	err := raw.Unmarshal(&s)
   223  	if err != nil {
   224  		return err
   225  	}
   226  	url, err := ParseURL(s)
   227  	if err != nil {
   228  		return err
   229  	}
   230  	*u = *url
   231  	return nil
   232  }
   233  
   234  var jsonNull = []byte("null")
   235  
   236  func (u *URL) MarshalJSON() ([]byte, error) {
   237  	if u == nil {
   238  		panic("cannot marshal nil *charm.URL")
   239  	}
   240  	return json.Marshal(u.String())
   241  }
   242  
   243  func (u *URL) UnmarshalJSON(b []byte) error {
   244  	var s string
   245  	if err := json.Unmarshal(b, &s); err != nil {
   246  		return err
   247  	}
   248  	url, err := ParseURL(s)
   249  	if err != nil {
   250  		return err
   251  	}
   252  	*u = *url
   253  	return nil
   254  }
   255  
   256  // Quote translates a charm url string into one which can be safely used
   257  // in a file path.  ASCII letters, ASCII digits, dot and dash stay the
   258  // same; other characters are translated to their hex representation
   259  // surrounded by underscores.
   260  func Quote(unsafe string) string {
   261  	safe := make([]byte, 0, len(unsafe)*4)
   262  	for i := 0; i < len(unsafe); i++ {
   263  		b := unsafe[i]
   264  		switch {
   265  		case b >= 'a' && b <= 'z',
   266  			b >= 'A' && b <= 'Z',
   267  			b >= '0' && b <= '9',
   268  			b == '.',
   269  			b == '-':
   270  			safe = append(safe, b)
   271  		default:
   272  			safe = append(safe, fmt.Sprintf("_%02x_", b)...)
   273  		}
   274  	}
   275  	return string(safe)
   276  }