github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/kbfs/tlf/handle_extension.go (about)

     1  // Copyright 2016 Keybase Inc. All rights reserved.
     2  // Use of this source code is governed by a BSD
     3  // license that can be found in the LICENSE file.
     4  
     5  package tlf
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  	"regexp"
    11  	"strconv"
    12  	"strings"
    13  	"time"
    14  
    15  	kbname "github.com/keybase/client/go/kbun"
    16  	"github.com/keybase/go-codec/codec"
    17  )
    18  
    19  const (
    20  	// HandleExtensionSep is the string that separates the folder
    21  	// participants from an extension suffix in the TLF name.
    22  	HandleExtensionSep = " "
    23  	// HandleExtensionDateFormat is the date format for the HandleExtension string.
    24  	handleExtensionDateFormat = "2006-01-02"
    25  	// HandleExtensionDateRegex is the regular expression matching the HandleExtension
    26  	// date member in string form.
    27  	handleExtensionDateRegex = "2[0-9]{3}-[0-9]{2}-[0-9]{2}"
    28  	// HandleExtensionNumberRegex is the regular expression matching the HandleExtension
    29  	// number member in string form.
    30  	handleExtensionNumberRegex = "[0-9]+"
    31  	// HandleExtensionUsernameRegex is the regular expression matching the HandleExtension
    32  	// username member in string form.
    33  	handleExtensionUsernameRegex = "[a-z0-9_]+"
    34  	// HandleExtensionConflictString is the string identifying a conflict extension.
    35  	handleExtensionConflictString = "conflicted copy"
    36  	// HandleExtensionLocalConflictString is the string identifying a
    37  	// conflict extension for a local-only conflict branch of a TLF.
    38  	handleExtensionLocalConflictString = "local conflicted copy"
    39  	// HandleExtensionFinalizedString is the format string identifying a finalized extension.
    40  	handleExtensionFinalizedString = "files before %saccount reset"
    41  	// HandleExtensionFormat is the formate string for a HandleExtension.
    42  	handleExtensionFormat = "(%s %s%s)"
    43  	// HandleExtensionStaticTestDate is a static date used for tests (2016-03-14).
    44  	HandleExtensionStaticTestDate = 1457913600
    45  )
    46  
    47  // HandleExtensionType is the type of extension.
    48  type HandleExtensionType int
    49  
    50  const (
    51  	// HandleExtensionConflict means the handle conflicted as a result of a social
    52  	// assertion resolution.
    53  	HandleExtensionConflict HandleExtensionType = iota
    54  	// HandleExtensionFinalized means the folder ended up with no more valid writers as
    55  	// a result of an account reset.
    56  	HandleExtensionFinalized
    57  	// HandleExtensionLocalConflict means the handle conflicted as a
    58  	// result of a local conflict branch.
    59  	HandleExtensionLocalConflict
    60  	// HandleExtensionUnknown means the type is unknown.
    61  	HandleExtensionUnknown
    62  )
    63  
    64  // HandleExtensionFinalizedStringRegex is the regex identifying a finalized extension string.
    65  var handleExtensionFinalizedStringRegex = fmt.Sprintf(
    66  	handleExtensionFinalizedString, "(?:"+handleExtensionUsernameRegex+"[\\s]+)*",
    67  )
    68  
    69  // HandleExtensionTypeRegex is the regular expression matching the HandleExtension string.
    70  var handleExtensionTypeRegex = handleExtensionConflictString + "|" +
    71  	handleExtensionLocalConflictString + "|" +
    72  	handleExtensionFinalizedStringRegex
    73  
    74  // HandleExtensionFinalizedRegex is the compiled regular expression matching a finalized
    75  // handle extension.
    76  var handleExtensionFinalizedRegex = regexp.MustCompile(
    77  	fmt.Sprintf(handleExtensionFinalizedString, "(?:("+handleExtensionUsernameRegex+")[\\s]+)*"),
    78  )
    79  
    80  // String implements the fmt.Stringer interface for HandleExtensionType
    81  func (et HandleExtensionType) String(username kbname.NormalizedUsername) string {
    82  	switch et {
    83  	case HandleExtensionConflict:
    84  		return handleExtensionConflictString
    85  	case HandleExtensionLocalConflict:
    86  		return handleExtensionLocalConflictString
    87  	case HandleExtensionFinalized:
    88  		if len(username) != 0 {
    89  			username += " "
    90  		}
    91  		return fmt.Sprintf(handleExtensionFinalizedString, username)
    92  	}
    93  	return "<unknown extension type>"
    94  }
    95  
    96  // parseHandleExtensionString parses an extension type and optional username from a string.
    97  func parseHandleExtensionString(s string) (HandleExtensionType, kbname.NormalizedUsername) {
    98  	if handleExtensionConflictString == s {
    99  		return HandleExtensionConflict, ""
   100  	} else if handleExtensionLocalConflictString == s {
   101  		return HandleExtensionLocalConflict, ""
   102  	}
   103  	m := handleExtensionFinalizedRegex.FindStringSubmatch(s)
   104  	if len(m) < 2 {
   105  		return HandleExtensionUnknown, ""
   106  	}
   107  	return HandleExtensionFinalized, kbname.NewNormalizedUsername(m[1])
   108  }
   109  
   110  // ErrHandleExtensionInvalidString is returned when a given string is not parsable as a
   111  // valid extension suffix.
   112  var errHandleExtensionInvalidString = errors.New("Invalid TLF handle extension string")
   113  
   114  // ErrHandleExtensionInvalidNumber is returned when an invalid number is used in an
   115  // extension definition. Handle extension numbers present in the string must be >1. Numbers
   116  // passed to NewHandleExtension must be >0.
   117  var errHandleExtensionInvalidNumber = errors.New("Invalid TLF handle extension number")
   118  
   119  // HandleExtensionRegex is the compiled regular expression matching a valid combination
   120  // of TLF handle extensions in string form.
   121  var handleExtensionRegex = regexp.MustCompile(
   122  	fmt.Sprintf("\\"+handleExtensionFormat,
   123  		"("+handleExtensionTypeRegex+")",
   124  		"("+handleExtensionDateRegex+")",
   125  		"(?: #("+handleExtensionNumberRegex+"))?\\"),
   126  )
   127  
   128  // HandleExtension is information which identifies a particular extension.
   129  type HandleExtension struct {
   130  	Date     int64                     `codec:"date"`
   131  	Number   uint16                    `codec:"num"`
   132  	Type     HandleExtensionType       `codec:"type"`
   133  	Username kbname.NormalizedUsername `codec:"un,omitempty"`
   134  	codec.UnknownFieldSetHandler
   135  }
   136  
   137  // String implements the fmt.Stringer interface for HandleExtension.
   138  // Ex: "(conflicted copy 2016-05-09 #2)"
   139  func (e HandleExtension) string(isBackedByTeam bool) string {
   140  	date := time.Unix(e.Date, 0).UTC().Format(handleExtensionDateFormat)
   141  	var num string
   142  	minNumberSuffixToShow := uint16(2)
   143  	if isBackedByTeam {
   144  		// When a TLF is backed by an implicit team, it should always
   145  		// use the "#1" suffix, unlike for older TLFs.
   146  		minNumberSuffixToShow = 1
   147  	}
   148  	if e.Number >= minNumberSuffixToShow {
   149  		num = " #"
   150  		num += strconv.FormatUint(uint64(e.Number), 10)
   151  	}
   152  	return fmt.Sprintf(handleExtensionFormat, e.Type.String(e.Username), date, num)
   153  }
   154  
   155  // String implements the fmt.Stringer interface for HandleExtension.
   156  // Ex: "(conflicted copy 2016-05-09 #2)"
   157  func (e HandleExtension) String() string {
   158  	return e.string(false)
   159  }
   160  
   161  // NewHandleExtension returns a new HandleExtension struct
   162  // populated with the date from the given time and conflict number.
   163  func NewHandleExtension(extType HandleExtensionType, num uint16, un kbname.NormalizedUsername, now time.Time) (
   164  	*HandleExtension, error) {
   165  	return newHandleExtension(extType, num, un, now)
   166  }
   167  
   168  // NewTestHandleExtensionStaticTime returns a new HandleExtension struct populated with
   169  // a static date for testing.
   170  func NewTestHandleExtensionStaticTime(extType HandleExtensionType, num uint16, un kbname.NormalizedUsername) (
   171  	*HandleExtension, error) {
   172  	now := time.Unix(HandleExtensionStaticTestDate, 0)
   173  	return newHandleExtension(extType, num, un, now)
   174  }
   175  
   176  // Helper to instantiate a HandleExtension object.
   177  func newHandleExtension(extType HandleExtensionType, num uint16, un kbname.NormalizedUsername, now time.Time) (
   178  	*HandleExtension, error) {
   179  	if num == 0 {
   180  		return nil, errHandleExtensionInvalidNumber
   181  	}
   182  	// mask out everything but the date
   183  	date := now.UTC().Format(handleExtensionDateFormat)
   184  	now, err := time.Parse(handleExtensionDateFormat, date)
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  	return &HandleExtension{
   189  		Date:     now.UTC().Unix(),
   190  		Number:   num,
   191  		Type:     extType,
   192  		Username: un,
   193  	}, nil
   194  }
   195  
   196  // parseHandleExtension parses a HandleExtension array of string fields.
   197  func parseHandleExtension(fields []string) (*HandleExtension, error) {
   198  	if len(fields) != 4 {
   199  		return nil, errHandleExtensionInvalidString
   200  	}
   201  	extType, un := parseHandleExtensionString(fields[1])
   202  	if extType == HandleExtensionUnknown {
   203  		return nil, errHandleExtensionInvalidString
   204  	}
   205  	date, err := time.Parse(handleExtensionDateFormat, fields[2])
   206  	if err != nil {
   207  		return nil, err
   208  	}
   209  	var num uint64 = 1
   210  	if len(fields[3]) != 0 {
   211  		num, err = strconv.ParseUint(fields[3], 10, 16)
   212  		if err != nil {
   213  			return nil, err
   214  		}
   215  		if num < 1 {
   216  			return nil, errHandleExtensionInvalidNumber
   217  		}
   218  	}
   219  	return &HandleExtension{
   220  		Date:     date.UTC().Unix(),
   221  		Number:   uint16(num),
   222  		Type:     extType,
   223  		Username: un,
   224  	}, nil
   225  }
   226  
   227  // ParseHandleExtensionSuffix parses a TLF handle extension suffix string.
   228  func ParseHandleExtensionSuffix(s string) ([]HandleExtension, error) {
   229  	exts := handleExtensionRegex.FindAllStringSubmatch(s, 2)
   230  	if len(exts) < 1 || len(exts) > 2 {
   231  		return nil, errHandleExtensionInvalidString
   232  	}
   233  	extMap := make(map[HandleExtensionType]bool)
   234  	var extensions []HandleExtension
   235  	for _, e := range exts {
   236  		ext, err := parseHandleExtension(e)
   237  		if err != nil {
   238  			return nil, err
   239  		}
   240  		if extMap[ext.Type] {
   241  			// No duplicate extension types in the same suffix.
   242  			return nil, errHandleExtensionInvalidString
   243  		}
   244  		extMap[ext.Type] = true
   245  		extensions = append(extensions, *ext)
   246  	}
   247  	return extensions, nil
   248  }
   249  
   250  // newHandleExtensionSuffix creates a suffix string given a set of extensions.
   251  func newHandleExtensionSuffix(
   252  	extensions []HandleExtension, isBackedByTeam bool) string {
   253  	var suffix string
   254  	for _, extension := range extensions {
   255  		suffix += HandleExtensionSep
   256  		suffix += extension.string(isBackedByTeam)
   257  	}
   258  	return suffix
   259  }
   260  
   261  // HandleExtensionList allows us to sort extensions by type.
   262  type HandleExtensionList []HandleExtension
   263  
   264  func (l HandleExtensionList) Len() int {
   265  	return len(l)
   266  }
   267  
   268  func (l HandleExtensionList) Less(i, j int) bool {
   269  	return l[i].Type < l[j].Type
   270  }
   271  
   272  func (l HandleExtensionList) Swap(i, j int) {
   273  	l[i], l[j] = l[j], l[i]
   274  }
   275  
   276  // Splat will deconstruct the list for the caller into individual extension
   277  // pointers (or nil.)
   278  func (l HandleExtensionList) Splat() (ci, fi *HandleExtension) {
   279  	for _, extension := range l {
   280  		tmp := extension
   281  		switch extension.Type {
   282  		case HandleExtensionConflict, HandleExtensionLocalConflict:
   283  			if ci != nil {
   284  				panic("Conflict extension already exists")
   285  			}
   286  			ci = &tmp
   287  		case HandleExtensionFinalized:
   288  			fi = &tmp
   289  		}
   290  	}
   291  	return ci, fi
   292  }
   293  
   294  // Suffix outputs a suffix string for this extension list.
   295  func (l HandleExtensionList) Suffix() string {
   296  	return newHandleExtensionSuffix(l, false)
   297  }
   298  
   299  // SuffixForTeamHandle outputs a suffix string for this extension list
   300  // for a handle that's backed by a team (which must be an implicit
   301  // team, since there aren't any suffixes for regulat teams).
   302  func (l HandleExtensionList) SuffixForTeamHandle() string {
   303  	return newHandleExtensionSuffix(l, true)
   304  }
   305  
   306  // ContainsLocalConflictExtensionPrefix returns true if the string
   307  // contains the local conflict string.
   308  func ContainsLocalConflictExtensionPrefix(s string) bool {
   309  	return strings.Contains(s, "("+handleExtensionLocalConflictString)
   310  }