github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/utils/kbfs_path_utils.go (about) 1 package utils 2 3 import ( 4 "context" 5 "net/url" 6 "regexp" 7 "strconv" 8 "strings" 9 "unicode" 10 11 "github.com/keybase/client/go/kbun" 12 "github.com/keybase/client/go/libkb" 13 "github.com/keybase/client/go/protocol/chat1" 14 ) 15 16 const localUsernameRE = "(?:[a-zA-A0-0_]+-?)+" 17 18 var kbfsPathOuterRegExp = func() *regexp.Regexp { 19 const slashDivided = `(?:(?:/keybase|/Volumes/Keybase\\ \(` + kbun.UsernameRE + `\)|/Volumes/Keybase)((?:\\ |\S)*))` 20 const slashDividedQuoted = `"(?:(?:/keybase|/Volumes/Keybase \(` + localUsernameRE + `\)|/Volumes/Keybase)(.*))"` 21 const windows = `(?:(?:K:|k:)(\\\S*))` // don't support escape on windows 22 // TODO if in the future we want to support custom mount points we can 23 // probably tap into Env() to get it. 24 const windowsQuoted = `"(?:(?:K:|k:)(\\.*))"` 25 const deeplink = `(?:(?:keybase:/)((?:\S)*))` 26 return regexp.MustCompile(`(?:[^\w"]|^)(` + slashDivided + "|" + slashDividedQuoted + "|" + windows + "|" + windowsQuoted + "|" + deeplink + `)`) 27 }() 28 29 type outerMatch struct { 30 matchStartIndex int 31 wholeMatch string 32 afterKeybase string 33 } 34 35 func (m *outerMatch) isKBFSPath() bool { 36 return m.matchStartIndex >= 0 && libkb.IsKBFSAfterKeybasePath(m.afterKeybase) 37 } 38 39 func (m *outerMatch) standardPath() string { 40 return "/keybase" + m.afterKeybase 41 } 42 43 func unquotedTrailingTrimFuncWindows(r rune) bool { 44 return r != '\\' && unicode.IsPunct(r) 45 } 46 func unquotedTrailingTrimFuncUnix(r rune) bool { 47 return r != '/' && unicode.IsPunct(r) 48 } 49 50 func matchKBFSPathOuter(body string) (outerMatches []outerMatch) { 51 res := kbfsPathOuterRegExp.FindAllStringSubmatchIndex(body, -1) 52 for _, indices := range res { 53 // 2:3 match 54 // 4:5 slash-divided inside /keybase 55 // 6:7 quoted slash-divided inside /keybase 56 // 8:9 windows inside /keybase 57 // 10:11 quoted windows inside /keybase 58 // 12:13 deeplink after "keybase:/" 59 if len(indices) != 14 { 60 panic("bad regexp: len(indices): " + strconv.Itoa(len(indices))) 61 } 62 switch { 63 case indices[4] > 0: 64 outerMatches = append(outerMatches, outerMatch{ 65 matchStartIndex: indices[2], 66 wholeMatch: strings.TrimRightFunc(body[indices[2]:indices[3]], unquotedTrailingTrimFuncUnix), 67 afterKeybase: strings.TrimRight( 68 strings.ReplaceAll( 69 strings.ReplaceAll( 70 strings.TrimRightFunc(body[indices[4]:indices[5]], unquotedTrailingTrimFuncUnix), 71 `\\`, 72 `\`, 73 ), 74 `\ `, 75 ` `, 76 ), 77 "/", 78 ), 79 }) 80 case indices[6] > 0: 81 outerMatches = append(outerMatches, outerMatch{ 82 matchStartIndex: indices[2], 83 wholeMatch: body[indices[2]:indices[3]], 84 afterKeybase: strings.TrimRight( 85 body[indices[6]:indices[7]], 86 "/", 87 ), 88 }) 89 case indices[8] > 0: 90 outerMatches = append(outerMatches, outerMatch{ 91 matchStartIndex: indices[2], 92 wholeMatch: strings.TrimRightFunc(body[indices[2]:indices[3]], unquotedTrailingTrimFuncWindows), 93 afterKeybase: strings.TrimRight( 94 strings.ReplaceAll( 95 strings.TrimRightFunc(body[indices[8]:indices[9]], unquotedTrailingTrimFuncWindows), 96 `\`, 97 `/`, 98 ), 99 "/", 100 ), 101 }) 102 case indices[10] > 0: 103 outerMatches = append(outerMatches, outerMatch{ 104 matchStartIndex: indices[2], 105 wholeMatch: body[indices[2]:indices[3]], 106 afterKeybase: strings.TrimRight( 107 strings.ReplaceAll( 108 body[indices[10]:indices[11]], 109 `\`, 110 `/`, 111 ), 112 "/", 113 ), 114 }) 115 case indices[12] > 0: 116 unescaped, err := url.PathUnescape(strings.TrimRightFunc(body[indices[12]:indices[13]], unquotedTrailingTrimFuncUnix)) 117 if err != nil { 118 continue 119 } 120 outerMatches = append(outerMatches, outerMatch{ 121 matchStartIndex: indices[2], 122 wholeMatch: strings.TrimRightFunc(body[indices[2]:indices[3]], unquotedTrailingTrimFuncUnix), 123 afterKeybase: strings.TrimRight( 124 unescaped, 125 "/", 126 ), 127 }) 128 } 129 } 130 return outerMatches 131 } 132 133 func ParseKBFSPaths(ctx context.Context, body string) (paths []chat1.KBFSPath) { 134 outerMatches := matchKBFSPathOuter(ReplaceQuotedSubstrings(body, true)) 135 for _, match := range outerMatches { 136 if match.isKBFSPath() { 137 kbfsPathInfo, err := libkb.GetKBFSPathInfo(match.standardPath()) 138 if err != nil { 139 continue 140 } 141 paths = append(paths, 142 chat1.KBFSPath{ 143 StartIndex: match.matchStartIndex, 144 RawPath: match.wholeMatch, 145 StandardPath: match.standardPath(), 146 PathInfo: kbfsPathInfo, 147 }) 148 } 149 } 150 return paths 151 } 152 153 func DecorateWithKBFSPath( 154 ctx context.Context, body string, paths []chat1.KBFSPath) ( 155 res string) { 156 var offset, added int 157 for _, path := range paths { 158 body, added = DecorateBody(ctx, body, path.StartIndex+offset, len(path.RawPath), chat1.NewUITextDecorationWithKbfspath(path)) 159 offset += added 160 } 161 return body 162 }