github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/snap/channel/channel.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2018-2019 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package channel 21 22 import ( 23 "errors" 24 "fmt" 25 "strings" 26 27 "github.com/snapcore/snapd/arch" 28 "github.com/snapcore/snapd/strutil" 29 ) 30 31 var channelRisks = []string{"stable", "candidate", "beta", "edge"} 32 33 // Channel identifies and describes completely a store channel. 34 type Channel struct { 35 Architecture string `json:"architecture"` 36 Name string `json:"name"` 37 Track string `json:"track"` 38 Risk string `json:"risk"` 39 Branch string `json:"branch,omitempty"` 40 } 41 42 func isSlash(r rune) bool { return r == '/' } 43 44 // TODO: currently there's some overlap between the toplevel Full, and 45 // methods Clean, String, and Full. Needs further refactoring. 46 47 func Full(s string) (string, error) { 48 if s == "" { 49 return "", nil 50 } 51 components := strings.FieldsFunc(s, isSlash) 52 switch len(components) { 53 case 0: 54 return "", nil 55 case 1: 56 if strutil.ListContains(channelRisks, components[0]) { 57 return "latest/" + components[0], nil 58 } 59 return components[0] + "/stable", nil 60 case 2: 61 if strutil.ListContains(channelRisks, components[0]) { 62 return "latest/" + strings.Join(components, "/"), nil 63 } 64 fallthrough 65 case 3: 66 return strings.Join(components, "/"), nil 67 default: 68 return "", errors.New("invalid channel") 69 } 70 } 71 72 // ParseVerbatim parses a string representing a store channel and 73 // includes the given architecture, if architecture is "" the system 74 // architecture is included. The channel representation is not normalized. 75 // Parse() should be used in most cases. 76 func ParseVerbatim(s string, architecture string) (Channel, error) { 77 if s == "" { 78 return Channel{}, fmt.Errorf("channel name cannot be empty") 79 } 80 p := strings.Split(s, "/") 81 var risk, track, branch *string 82 switch len(p) { 83 default: 84 return Channel{}, fmt.Errorf("channel name has too many components: %s", s) 85 case 3: 86 track, risk, branch = &p[0], &p[1], &p[2] 87 case 2: 88 if strutil.ListContains(channelRisks, p[0]) { 89 risk, branch = &p[0], &p[1] 90 } else { 91 track, risk = &p[0], &p[1] 92 } 93 case 1: 94 if strutil.ListContains(channelRisks, p[0]) { 95 risk = &p[0] 96 } else { 97 track = &p[0] 98 } 99 } 100 101 if architecture == "" { 102 architecture = arch.DpkgArchitecture() 103 } 104 105 ch := Channel{ 106 Architecture: architecture, 107 } 108 109 if risk != nil { 110 if !strutil.ListContains(channelRisks, *risk) { 111 return Channel{}, fmt.Errorf("invalid risk in channel name: %s", s) 112 } 113 ch.Risk = *risk 114 } 115 if track != nil { 116 if *track == "" { 117 return Channel{}, fmt.Errorf("invalid track in channel name: %s", s) 118 } 119 ch.Track = *track 120 } 121 if branch != nil { 122 if *branch == "" { 123 return Channel{}, fmt.Errorf("invalid branch in channel name: %s", s) 124 } 125 ch.Branch = *branch 126 } 127 128 return ch, nil 129 } 130 131 // Parse parses a string representing a store channel and includes given 132 // architecture, , if architecture is "" the system architecture is included. 133 // The returned channel's track, risk and name are normalized. 134 func Parse(s string, architecture string) (Channel, error) { 135 channel, err := ParseVerbatim(s, architecture) 136 if err != nil { 137 return Channel{}, err 138 } 139 return channel.Clean(), nil 140 } 141 142 // Clean returns a Channel with a normalized track, risk and name. 143 func (c Channel) Clean() Channel { 144 track := c.Track 145 risk := c.Risk 146 147 if track == "latest" { 148 track = "" 149 } 150 if risk == "" { 151 risk = "stable" 152 } 153 154 // normalized name 155 name := risk 156 if track != "" { 157 name = track + "/" + name 158 } 159 if c.Branch != "" { 160 name = name + "/" + c.Branch 161 } 162 163 return Channel{ 164 Architecture: c.Architecture, 165 Name: name, 166 Track: track, 167 Risk: risk, 168 Branch: c.Branch, 169 } 170 } 171 172 func (c Channel) String() string { 173 return c.Name 174 } 175 176 // Full returns the full name of the channel, inclusive the default track "latest". 177 func (c *Channel) Full() string { 178 ch := c.String() 179 full, err := Full(ch) 180 if err != nil { 181 // unpossible 182 panic("channel.String() returned a malformed channel: " + ch) 183 } 184 return full 185 } 186 187 // VerbatimTrackOnly returns whether the channel represents a track only. 188 func (c *Channel) VerbatimTrackOnly() bool { 189 return c.Track != "" && c.Risk == "" && c.Branch == "" 190 } 191 192 // VerbatimRiskOnly returns whether the channel represents a risk only. 193 func (c *Channel) VerbatimRiskOnly() bool { 194 return c.Track == "" && c.Risk != "" && c.Branch == "" 195 } 196 197 func riskLevel(risk string) int { 198 for i, r := range channelRisks { 199 if r == risk { 200 return i 201 } 202 } 203 return -1 204 } 205 206 // ChannelMatch represents on which fields two channels are matching. 207 type ChannelMatch struct { 208 Architecture bool 209 Track bool 210 Risk bool 211 } 212 213 // String returns the string represantion of the match, results can be: 214 // "architecture:track:risk" 215 // "architecture:track" 216 // "architecture:risk" 217 // "track:risk" 218 // "architecture" 219 // "track" 220 // "risk" 221 // "" 222 func (cm ChannelMatch) String() string { 223 matching := []string{} 224 if cm.Architecture { 225 matching = append(matching, "architecture") 226 } 227 if cm.Track { 228 matching = append(matching, "track") 229 } 230 if cm.Risk { 231 matching = append(matching, "risk") 232 } 233 return strings.Join(matching, ":") 234 235 } 236 237 // Match returns a ChannelMatch of which fields among architecture,track,risk match between c and c1 store channels, risk is matched taking channel inheritance into account and considering c the requested channel. 238 func (c *Channel) Match(c1 *Channel) ChannelMatch { 239 requestedRiskLevel := riskLevel(c.Risk) 240 rl1 := riskLevel(c1.Risk) 241 return ChannelMatch{ 242 Architecture: c.Architecture == c1.Architecture, 243 Track: c.Track == c1.Track, 244 Risk: requestedRiskLevel >= rl1, 245 } 246 } 247 248 // Resolve resolves newChannel wrt channel, this means if newChannel 249 // is risk/branch only it will preserve the track of channel. It 250 // assumes that if both are not empty, channel is parseable. 251 func Resolve(channel, newChannel string) (string, error) { 252 if newChannel == "" { 253 return channel, nil 254 } 255 if channel == "" { 256 return newChannel, nil 257 } 258 ch, err := ParseVerbatim(channel, "-") 259 if err != nil { 260 return "", err 261 } 262 p := strings.Split(newChannel, "/") 263 if strutil.ListContains(channelRisks, p[0]) && ch.Track != "" { 264 // risk/branch inherits the track if any 265 return ch.Track + "/" + newChannel, nil 266 } 267 return newChannel, nil 268 } 269 270 var ErrPinnedTrackSwitch = errors.New("cannot switch pinned track") 271 272 // ResolvePinned resolves newChannel wrt a pinned track, newChannel 273 // can only be risk/branch-only or have the same track, otherwise 274 // ErrPinnedTrackSwitch is returned. 275 func ResolvePinned(track, newChannel string) (string, error) { 276 if track == "" { 277 return newChannel, nil 278 } 279 ch, err := ParseVerbatim(track, "-") 280 if err != nil || !ch.VerbatimTrackOnly() { 281 return "", fmt.Errorf("invalid pinned track: %s", track) 282 } 283 if newChannel == "" { 284 return track, nil 285 } 286 trackPrefix := ch.Track + "/" 287 p := strings.Split(newChannel, "/") 288 if strutil.ListContains(channelRisks, p[0]) && ch.Track != "" { 289 // risk/branch inherits the track if any 290 return trackPrefix + newChannel, nil 291 } 292 if newChannel != track && !strings.HasPrefix(newChannel, trackPrefix) { 293 // the track is pinned 294 return "", ErrPinnedTrackSwitch 295 } 296 return newChannel, nil 297 }