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 }