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 }