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  }