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 }