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  }