gopkg.in/juju/charm.v6-unstable@v6.0.0-20171026192109-50d0c219b496/url.go (about)

     1  // Copyright 2011, 2012, 2013 Canonical Ltd.
     2  // Licensed under the LGPLv3, see LICENCE file for details.
     3  
     4  package charm
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	gourl "net/url"
    10  	"regexp"
    11  	"strconv"
    12  	"strings"
    13  
    14  	"github.com/juju/errors"
    15  
    16  	"gopkg.in/juju/names.v2"
    17  	"gopkg.in/mgo.v2/bson"
    18  )
    19  
    20  // Location represents a charm location, which must declare a path component
    21  // and a string representaion.
    22  type Location interface {
    23  	Path() string
    24  	String() string
    25  }
    26  
    27  // URL represents a charm or bundle location:
    28  //
    29  //     cs:~joe/oneiric/wordpress
    30  //     cs:oneiric/wordpress-42
    31  //     local:oneiric/wordpress
    32  //     cs:~joe/wordpress
    33  //     cs:wordpress
    34  //     cs:precise/wordpress-20
    35  //     cs:development/precise/wordpress-20
    36  //     cs:~joe/development/wordpress
    37  //
    38  type URL struct {
    39  	Schema   string // "cs" or "local".
    40  	User     string // "joe".
    41  	Name     string // "wordpress".
    42  	Revision int    // -1 if unset, N otherwise.
    43  	Series   string // "precise" or "" if unset; "bundle" if it's a bundle.
    44  }
    45  
    46  var (
    47  	ErrUnresolvedUrl error = fmt.Errorf("charm or bundle url series is not resolved")
    48  	validSeries            = regexp.MustCompile("^[a-z]+([a-z0-9]+)?$")
    49  	validName              = regexp.MustCompile("^[a-z][a-z0-9]*(-[a-z0-9]*[a-z][a-z0-9]*)*$")
    50  )
    51  
    52  // ValidateSchema returns an error if the schema is invalid.
    53  func ValidateSchema(schema string) error {
    54  	if schema != "cs" && schema != "local" {
    55  		return errors.NotValidf("schema %q", schema)
    56  	}
    57  	return nil
    58  }
    59  
    60  // IsValidSeries reports whether series is a valid series in charm or bundle
    61  // URLs.
    62  func IsValidSeries(series string) bool {
    63  	return validSeries.MatchString(series)
    64  }
    65  
    66  // ValidateSeries returns an error if the given series is invalid.
    67  func ValidateSeries(series string) error {
    68  	if IsValidSeries(series) == false {
    69  		return errors.NotValidf("series name %q", series)
    70  	}
    71  	return nil
    72  }
    73  
    74  // IsValidName reports whether name is a valid charm or bundle name.
    75  func IsValidName(name string) bool {
    76  	return validName.MatchString(name)
    77  }
    78  
    79  // ValidateName returns an error if the given name is invalid.
    80  func ValidateName(name string) error {
    81  	if IsValidName(name) == false {
    82  		return errors.NotValidf("name %q", name)
    83  	}
    84  	return nil
    85  }
    86  
    87  // WithRevision returns a URL equivalent to url but with Revision set
    88  // to revision.
    89  func (url *URL) WithRevision(revision int) *URL {
    90  	urlCopy := *url
    91  	urlCopy.Revision = revision
    92  	return &urlCopy
    93  }
    94  
    95  // MustParseURL works like ParseURL, but panics in case of errors.
    96  func MustParseURL(url string) *URL {
    97  	u, err := ParseURL(url)
    98  	if err != nil {
    99  		panic(err)
   100  	}
   101  	return u
   102  }
   103  
   104  // ParseURL parses the provided charm URL string into its respective
   105  // structure.
   106  //
   107  // Additionally, fully-qualified charmstore URLs are supported; note that this
   108  // currently assumes that they will map to jujucharms.com (that is,
   109  // fully-qualified URLs currently map to the 'cs' schema):
   110  //
   111  //    https://jujucharms.com/name
   112  //    https://jujucharms.com/name/series
   113  //    https://jujucharms.com/name/revision
   114  //    https://jujucharms.com/name/series/revision
   115  //    https://jujucharms.com/u/user/name
   116  //    https://jujucharms.com/u/user/name/series
   117  //    https://jujucharms.com/u/user/name/revision
   118  //    https://jujucharms.com/u/user/name/series/revision
   119  //    https://jujucharms.com/channel/name
   120  //    https://jujucharms.com/channel/name/series
   121  //    https://jujucharms.com/channel/name/revision
   122  //    https://jujucharms.com/channel/name/series/revision
   123  //    https://jujucharms.com/u/user/channel/name
   124  //    https://jujucharms.com/u/user/channel/name/series
   125  //    https://jujucharms.com/u/user/channel/name/revision
   126  //    https://jujucharms.com/u/user/channel/name/series/revision
   127  //
   128  // A missing schema is assumed to be 'cs'.
   129  func ParseURL(url string) (*URL, error) {
   130  	// Check if we're dealing with a v1 or v2 URL.
   131  	u, err := gourl.Parse(url)
   132  	if err != nil {
   133  		return nil, errors.Errorf("cannot parse charm or bundle URL: %q", url)
   134  	}
   135  	if u.RawQuery != "" || u.Fragment != "" || u.User != nil {
   136  		return nil, errors.Errorf("charm or bundle URL %q has unrecognized parts", url)
   137  	}
   138  	var curl *URL
   139  	switch {
   140  	case u.Opaque != "":
   141  		// Shortcut old-style URLs.
   142  		u.Path = u.Opaque
   143  		curl, err = parseV1URL(u, url)
   144  	case u.Scheme == "http" || u.Scheme == "https":
   145  		// Shortcut new-style URLs.
   146  		curl, err = parseV2URL(u)
   147  	default:
   148  		// TODO: for now, fall through to parsing v1 references; this will be
   149  		// expanded to be more robust in the future.
   150  		curl, err = parseV1URL(u, url)
   151  	}
   152  	if err != nil {
   153  		return nil, errors.Trace(err)
   154  	}
   155  	if curl.Schema == "" {
   156  		curl.Schema = "cs"
   157  	}
   158  	return curl, nil
   159  }
   160  
   161  func parseV1URL(url *gourl.URL, originalURL string) (*URL, error) {
   162  	var r URL
   163  	if url.Scheme != "" {
   164  		r.Schema = url.Scheme
   165  		if err := ValidateSchema(r.Schema); err != nil {
   166  			return nil, errors.Annotatef(err, "cannot parse URL %q", url)
   167  		}
   168  	}
   169  	i := 0
   170  	parts := strings.Split(url.Path[i:], "/")
   171  	if len(parts) < 1 || len(parts) > 4 {
   172  		return nil, errors.Errorf("charm or bundle URL has invalid form: %q", originalURL)
   173  	}
   174  
   175  	// ~<username>
   176  	if strings.HasPrefix(parts[0], "~") {
   177  		if r.Schema == "local" {
   178  			return nil, errors.Errorf("local charm or bundle URL with user name: %q", originalURL)
   179  		}
   180  		r.User, parts = parts[0][1:], parts[1:]
   181  	}
   182  
   183  	if len(parts) > 2 {
   184  		return nil, errors.Errorf("charm or bundle URL has invalid form: %q", originalURL)
   185  	}
   186  
   187  	// <series>
   188  	if len(parts) == 2 {
   189  		r.Series, parts = parts[0], parts[1:]
   190  		if err := ValidateSeries(r.Series); err != nil {
   191  			return nil, errors.Annotatef(err, "cannot parse URL %q", originalURL)
   192  		}
   193  	}
   194  	if len(parts) < 1 {
   195  		return nil, errors.Errorf("URL without charm or bundle name: %q", originalURL)
   196  	}
   197  
   198  	// <name>[-<revision>]
   199  	r.Name = parts[0]
   200  	r.Revision = -1
   201  	for i := len(r.Name) - 1; i > 0; i-- {
   202  		c := r.Name[i]
   203  		if c >= '0' && c <= '9' {
   204  			continue
   205  		}
   206  		if c == '-' && i != len(r.Name)-1 {
   207  			var err error
   208  			r.Revision, err = strconv.Atoi(r.Name[i+1:])
   209  			if err != nil {
   210  				panic(err) // We just checked it was right.
   211  			}
   212  			r.Name = r.Name[:i]
   213  		}
   214  		break
   215  	}
   216  	if r.User != "" {
   217  		if !names.IsValidUser(r.User) {
   218  			return nil, errors.Errorf("charm or bundle URL has invalid user name: %q", originalURL)
   219  		}
   220  	}
   221  	if err := ValidateName(r.Name); err != nil {
   222  		return nil, errors.Annotatef(err, "cannot parse URL %q", url)
   223  	}
   224  	return &r, nil
   225  }
   226  
   227  func parseV2URL(url *gourl.URL) (*URL, error) {
   228  	var r URL
   229  	r.Schema = "cs"
   230  	parts := strings.Split(strings.Trim(url.Path, "/"), "/")
   231  	if parts[0] == "u" {
   232  		if len(parts) < 3 {
   233  			return nil, errors.Errorf(`charm or bundle URL %q malformed, expected "/u/<user>/<name>"`, url)
   234  		}
   235  		r.User, parts = parts[1], parts[2:]
   236  	}
   237  	r.Name, parts = parts[0], parts[1:]
   238  	r.Revision = -1
   239  	if len(parts) > 0 {
   240  		revision, err := strconv.Atoi(parts[0])
   241  		if err == nil {
   242  			r.Revision = revision
   243  		} else {
   244  			r.Series = parts[0]
   245  			if err := ValidateSeries(r.Series); err != nil {
   246  				return nil, errors.Annotatef(err, "cannot parse URL %q", url)
   247  			}
   248  			parts = parts[1:]
   249  			if len(parts) == 1 {
   250  				r.Revision, err = strconv.Atoi(parts[0])
   251  				if err != nil {
   252  					return nil, errors.Errorf("charm or bundle URL has malformed revision: %q in %q", parts[0], url)
   253  				}
   254  			} else {
   255  				if len(parts) != 0 {
   256  					return nil, errors.Errorf("charm or bundle URL has invalid form: %q", url)
   257  				}
   258  			}
   259  		}
   260  	}
   261  	if r.User != "" {
   262  		if !names.IsValidUser(r.User) {
   263  			return nil, errors.Errorf("charm or bundle URL has invalid user name: %q", url)
   264  		}
   265  	}
   266  	if err := ValidateName(r.Name); err != nil {
   267  		return nil, errors.Annotatef(err, "cannot parse URL %q", url)
   268  	}
   269  	return &r, nil
   270  }
   271  
   272  func (r *URL) path() string {
   273  	var parts []string
   274  	if r.User != "" {
   275  		parts = append(parts, fmt.Sprintf("~%s", r.User))
   276  	}
   277  	if r.Series != "" {
   278  		parts = append(parts, r.Series)
   279  	}
   280  	if r.Revision >= 0 {
   281  		parts = append(parts, fmt.Sprintf("%s-%d", r.Name, r.Revision))
   282  	} else {
   283  		parts = append(parts, r.Name)
   284  	}
   285  	return strings.Join(parts, "/")
   286  }
   287  
   288  func (r URL) Path() string {
   289  	return r.path()
   290  }
   291  
   292  // InferURL parses src as a reference, fills out the series in the
   293  // returned URL using defaultSeries if necessary.
   294  //
   295  // This function is deprecated. New code should use ParseURL instead.
   296  func InferURL(src, defaultSeries string) (*URL, error) {
   297  	u, err := ParseURL(src)
   298  	if err != nil {
   299  		return nil, err
   300  	}
   301  	if u.Series == "" {
   302  		if defaultSeries == "" {
   303  			return nil, errors.Errorf("cannot infer charm or bundle URL for %q: charm or bundle url series is not resolved", src)
   304  		}
   305  		u.Series = defaultSeries
   306  	}
   307  	return u, nil
   308  }
   309  
   310  func (u URL) String() string {
   311  	return fmt.Sprintf("%s:%s", u.Schema, u.Path())
   312  }
   313  
   314  // GetBSON turns u into a bson.Getter so it can be saved directly
   315  // on a MongoDB database with mgo.
   316  func (u *URL) GetBSON() (interface{}, error) {
   317  	if u == nil {
   318  		return nil, nil
   319  	}
   320  	return u.String(), nil
   321  }
   322  
   323  // SetBSON turns u into a bson.Setter so it can be loaded directly
   324  // from a MongoDB database with mgo.
   325  func (u *URL) SetBSON(raw bson.Raw) error {
   326  	if raw.Kind == 10 {
   327  		return bson.SetZero
   328  	}
   329  	var s string
   330  	err := raw.Unmarshal(&s)
   331  	if err != nil {
   332  		return err
   333  	}
   334  	url, err := ParseURL(s)
   335  	if err != nil {
   336  		return err
   337  	}
   338  	*u = *url
   339  	return nil
   340  }
   341  
   342  func (u *URL) MarshalJSON() ([]byte, error) {
   343  	if u == nil {
   344  		panic("cannot marshal nil *charm.URL")
   345  	}
   346  	return json.Marshal(u.String())
   347  }
   348  
   349  func (u *URL) UnmarshalJSON(b []byte) error {
   350  	var s string
   351  	if err := json.Unmarshal(b, &s); err != nil {
   352  		return err
   353  	}
   354  	url, err := ParseURL(s)
   355  	if err != nil {
   356  		return err
   357  	}
   358  	*u = *url
   359  	return nil
   360  }
   361  
   362  // MarshalText implements encoding.TextMarshaler by
   363  // returning u.String()
   364  func (u *URL) MarshalText() ([]byte, error) {
   365  	if u == nil {
   366  		return nil, nil
   367  	}
   368  	return []byte(u.String()), nil
   369  }
   370  
   371  // UnmarshalText implements encoding.TestUnmarshaler by
   372  // parsing the data with ParseURL.
   373  func (u *URL) UnmarshalText(data []byte) error {
   374  	url, err := ParseURL(string(data))
   375  	if err != nil {
   376  		return err
   377  	}
   378  	*u = *url
   379  	return nil
   380  }
   381  
   382  // Quote translates a charm url string into one which can be safely used
   383  // in a file path.  ASCII letters, ASCII digits, dot and dash stay the
   384  // same; other characters are translated to their hex representation
   385  // surrounded by underscores.
   386  func Quote(unsafe string) string {
   387  	safe := make([]byte, 0, len(unsafe)*4)
   388  	for i := 0; i < len(unsafe); i++ {
   389  		b := unsafe[i]
   390  		switch {
   391  		case b >= 'a' && b <= 'z',
   392  			b >= 'A' && b <= 'Z',
   393  			b >= '0' && b <= '9',
   394  			b == '.',
   395  			b == '-':
   396  			safe = append(safe, b)
   397  		default:
   398  			safe = append(safe, fmt.Sprintf("_%02x_", b)...)
   399  		}
   400  	}
   401  	return string(safe)
   402  }