forked from keybase/client
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
move kbfs path parsing to Go and use service decoration (keybase#19148)
* wip * rewrite regexp * add deeplink and platform paths * use service decoration for kbfs paths * story * just parse when decorating * review feedback from strib * gofmt
- Loading branch information
Showing
16 changed files
with
482 additions
and
831 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
package utils | ||
|
||
import ( | ||
"context" | ||
"net/url" | ||
"regexp" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/keybase/client/go/kbun" | ||
"github.com/keybase/client/go/libkb" | ||
"github.com/keybase/client/go/protocol/chat1" | ||
"github.com/keybase/client/go/protocol/keybase1" | ||
) | ||
|
||
const localUsernameRE = "(?:[a-zA-A0-0_]+-?)+" | ||
|
||
var kbfsPathOuterRegExp = func() *regexp.Regexp { | ||
const slashDivided = `(?:(?:/keybase|/Volumes/Keybase\\ \(` + kbun.UsernameRE + `\)|/Volumes/Keybase)((?:\\ |\S)*))` | ||
const slashDividedQuoted = `"(?:(?:/keybase|/Volumes/Keybase \(` + localUsernameRE + `\)|/Volumes/Keybase)(.*))"` | ||
const windows = `(?:(?:K:|k:)(\\\S*))` // don't support escape on windows | ||
// TODO if in the future we want to support custom mount points we can | ||
// probably tap into Env() to get it. | ||
const windowsQuoted = `"(?:(?:K:|k:)(\\.*))"` | ||
const deeplink = `(?:(?:keybase:/)((?:\S)*))` | ||
return regexp.MustCompile(`(?:[^\w"]|^)(` + slashDivided + "|" + slashDividedQuoted + "|" + windows + "|" + windowsQuoted + "|" + deeplink + `)`) | ||
}() | ||
|
||
var kbfsPathInnerRegExp = func() *regexp.Regexp { | ||
const socialAssertion = `[-_a-zA-Z0-9.]+@[a-zA-Z.]+` | ||
const user = `(?:(?:` + kbun.UsernameRE + `)|(?:` + socialAssertion + `))` | ||
const usernames = user + `(?:,` + user + `)*` | ||
const teamName = kbun.UsernameRE + `(?:\.` + kbun.UsernameRE + `)*` | ||
const tlfType = "/(?:private|public|team)$" | ||
// TODO support name suffix e.g. conflict | ||
const tlf = "/(?:(?:private|public)/" + usernames + "(?:#" + usernames + ")?|team/" + teamName + `)(?:/|$)` | ||
const specialFiles = "/(?:.kbfs_.+)" | ||
return regexp.MustCompile(`^(?:(?:` + tlf + `)|(?:` + tlfType + `)|(?:` + specialFiles + `))`) | ||
}() | ||
|
||
type outerMatch struct { | ||
matchStartIndex int | ||
wholeMatch string | ||
afterKeybase string | ||
} | ||
|
||
func (m *outerMatch) isKBFSPath() bool { | ||
return m.matchStartIndex >= 0 && (len(m.afterKeybase) == 0 || kbfsPathInnerRegExp.MatchString(m.afterKeybase)) | ||
} | ||
|
||
func (m *outerMatch) standardPath() string { | ||
return "/keybase" + m.afterKeybase | ||
} | ||
|
||
func (m *outerMatch) deeplinkPath() string { | ||
if len(m.afterKeybase) == 0 { | ||
return "" | ||
} | ||
var segments []string | ||
for _, segment := range strings.Split(m.afterKeybase, "/") { | ||
segments = append(segments, url.PathEscape(segment)) | ||
} | ||
return "keybase:/" + strings.Join(segments, "/") | ||
} | ||
|
||
func (m *outerMatch) afterMountPath(backslash bool) string { | ||
afterMount := m.afterKeybase | ||
if len(afterMount) == 0 { | ||
afterMount = "/" | ||
} | ||
if backslash { | ||
return strings.ReplaceAll(afterMount, "/", `\`) | ||
} | ||
return afterMount | ||
} | ||
|
||
func matchKBFSPathOuter(body string) (outerMatches []outerMatch) { | ||
res := kbfsPathOuterRegExp.FindAllStringSubmatchIndex(body, -1) | ||
for _, indices := range res { | ||
// 2:3 match | ||
// 4:5 slash-divided inside /keybase | ||
// 6:7 quoted slash-divided inside /keybase | ||
// 8:9 windows inside /keybase | ||
// 10:11 quoted windows inside /keybase | ||
// 12:13 deeplink after "keybase:/" | ||
if len(indices) != 14 { | ||
panic("bad regexp: len(indices): " + strconv.Itoa(len(indices))) | ||
} | ||
switch { | ||
case indices[4] > 0: | ||
outerMatches = append(outerMatches, outerMatch{ | ||
matchStartIndex: indices[2], | ||
wholeMatch: body[indices[2]:indices[3]], | ||
afterKeybase: strings.TrimRight( | ||
strings.ReplaceAll( | ||
strings.ReplaceAll( | ||
body[indices[4]:indices[5]], | ||
`\\`, | ||
`\`, | ||
), | ||
`\ `, | ||
` `, | ||
), | ||
"/", | ||
), | ||
}) | ||
case indices[6] > 0: | ||
outerMatches = append(outerMatches, outerMatch{ | ||
matchStartIndex: indices[2], | ||
wholeMatch: body[indices[2]:indices[3]], | ||
afterKeybase: strings.TrimRight( | ||
body[indices[6]:indices[7]], | ||
"/", | ||
), | ||
}) | ||
case indices[8] > 0: | ||
outerMatches = append(outerMatches, outerMatch{ | ||
matchStartIndex: indices[2], | ||
wholeMatch: body[indices[2]:indices[3]], | ||
afterKeybase: strings.TrimRight( | ||
strings.ReplaceAll( | ||
body[indices[8]:indices[9]], | ||
`\`, | ||
`/`, | ||
), | ||
"/", | ||
), | ||
}) | ||
case indices[10] > 0: | ||
outerMatches = append(outerMatches, outerMatch{ | ||
matchStartIndex: indices[2], | ||
wholeMatch: body[indices[2]:indices[3]], | ||
afterKeybase: strings.TrimRight( | ||
strings.ReplaceAll( | ||
body[indices[10]:indices[11]], | ||
`\`, | ||
`/`, | ||
), | ||
"/", | ||
), | ||
}) | ||
case indices[12] > 0: | ||
unescaped, err := url.PathUnescape(body[indices[12]:indices[13]]) | ||
if err != nil { | ||
continue | ||
} | ||
outerMatches = append(outerMatches, outerMatch{ | ||
matchStartIndex: indices[2], | ||
wholeMatch: body[indices[2]:indices[3]], | ||
afterKeybase: strings.TrimRight( | ||
unescaped, | ||
"/", | ||
), | ||
}) | ||
} | ||
} | ||
return outerMatches | ||
} | ||
|
||
func ParseKBFSPaths(ctx context.Context, body string) (paths []chat1.KBFSPath) { | ||
outerMatches := matchKBFSPathOuter(body) | ||
for _, match := range outerMatches { | ||
if match.isKBFSPath() { | ||
var platformAfterMountPath string | ||
if libkb.RuntimeGroup() == keybase1.RuntimeGroup_WINDOWSLIKE { | ||
platformAfterMountPath = match.afterMountPath(true) | ||
} else { | ||
platformAfterMountPath = match.afterMountPath(false) | ||
} | ||
paths = append(paths, | ||
chat1.KBFSPath{ | ||
StartIndex: match.matchStartIndex, | ||
RawPath: match.wholeMatch, | ||
StandardPath: match.standardPath(), | ||
DeeplinkPath: match.deeplinkPath(), | ||
PlatformAfterMountPath: platformAfterMountPath, | ||
}) | ||
} | ||
} | ||
return paths | ||
} | ||
|
||
func DecorateWithKBFSPath( | ||
ctx context.Context, body string, paths []chat1.KBFSPath) ( | ||
res string) { | ||
var offset, added int | ||
for _, path := range paths { | ||
body, added = DecorateBody(ctx, body, path.StartIndex+offset, len(path.RawPath), chat1.NewUITextDecorationWithKbfspath(path)) | ||
offset += added | ||
} | ||
return body | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
package utils | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/keybase/client/go/libkb" | ||
"github.com/keybase/client/go/protocol/chat1" | ||
"github.com/keybase/client/go/protocol/keybase1" | ||
"github.com/stretchr/testify/require" | ||
"golang.org/x/net/context" | ||
) | ||
|
||
func strPointer(str string) *string { return &str } | ||
func makeKBFSPathForTest(rawPath string, standardPath *string) chat1.KBFSPath { | ||
if standardPath == nil { | ||
return chat1.KBFSPath{RawPath: rawPath, StandardPath: rawPath} | ||
} | ||
return chat1.KBFSPath{RawPath: rawPath, StandardPath: *standardPath} | ||
} | ||
|
||
var kbfsPathTests = map[string]chat1.KBFSPath{ | ||
"/keybase ha": makeKBFSPathForTest("/keybase", nil), | ||
"/keybase/哟": {}, | ||
"before/keybase": {}, | ||
"之前/keybase": makeKBFSPathForTest("/keybase", nil), | ||
"/keybase/public": makeKBFSPathForTest("/keybase/public", nil), | ||
"/keybase/team": makeKBFSPathForTest("/keybase/team", nil), | ||
"/keybase/private/": makeKBFSPathForTest("/keybase/private/", strPointer("/keybase/private")), | ||
"/keybase/team/keybase": makeKBFSPathForTest("/keybase/team/keybase", nil), | ||
"/keybase/team/keybase/blahblah": makeKBFSPathForTest("/keybase/team/keybase/blahblah", nil), | ||
`/keybase/team/keybase/blah\ blah\ blah`: makeKBFSPathForTest(`/keybase/team/keybase/blah\ blah\ blah`, strPointer("/keybase/team/keybase/blah blah blah")), | ||
`/keybase/team/keybase/blah\\blah\\blah`: makeKBFSPathForTest(`/keybase/team/keybase/blah\\blah\\blah`, strPointer(`/keybase/team/keybase/blah\blah\blah`)), | ||
"/keybase/team/keybase/blahblah/": makeKBFSPathForTest("/keybase/team/keybase/blahblah/", strPointer("/keybase/team/keybase/blahblah")), | ||
"/keybase/private/songgao/🍻": makeKBFSPathForTest("/keybase/private/songgao/🍻", nil), | ||
"/keybase/private/songgao/🍻/🍹.png/": makeKBFSPathForTest("/keybase/private/songgao/🍻/🍹.png/", strPointer("/keybase/private/songgao/🍻/🍹.png")), | ||
"/keybase/private/songgao/囧/yo": makeKBFSPathForTest("/keybase/private/songgao/囧/yo", nil), | ||
"/keybase/team/keybase,blah": {}, | ||
"/keybase/team/keybase.blah": makeKBFSPathForTest("/keybase/team/keybase.blah", nil), | ||
"/keybaseprivate": {}, | ||
"/keybaseprivate/team": {}, | ||
"/keybase/teamaa/keybase": {}, | ||
"/keybase/.kbfs_status": makeKBFSPathForTest("/keybase/.kbfs_status", nil), | ||
"/foo": {}, | ||
"/keybase.": {}, | ||
|
||
"/keybase/private/songgao,strib#jzila/file": makeKBFSPathForTest("/keybase/private/songgao,strib#jzila/file", nil), | ||
"/keybase/private/song-gao,strib#jzila/file": {}, | ||
"/keybase/private/songgao,strib#jzila,jakob223/file": makeKBFSPathForTest("/keybase/private/songgao,strib#jzila,jakob223/file", nil), | ||
"/keybase/private/__songgao__@twitter,strib@github,jzila@reddit,jakob.weisbl.at@dns/file": makeKBFSPathForTest("/keybase/private/__songgao__@twitter,strib@github,jzila@reddit,jakob.weisbl.at@dns/file", nil), | ||
|
||
"keybase://team/keybase/blahblah": makeKBFSPathForTest("keybase://team/keybase/blahblah", strPointer("/keybase/team/keybase/blahblah")), | ||
"keybase://private/foo/blahblah": makeKBFSPathForTest("keybase://private/foo/blahblah", strPointer("/keybase/private/foo/blahblah")), | ||
"keybase://public/foo/blahblah": makeKBFSPathForTest("keybase://public/foo/blahblah", strPointer("/keybase/public/foo/blahblah")), | ||
"keybase://public/foo/blah%20blah": makeKBFSPathForTest("keybase://public/foo/blah%20blah", strPointer("/keybase/public/foo/blah blah")), | ||
"keybase://chat/blah": {}, | ||
|
||
"/Volumes/Keybase/team/keybase/blahblah": makeKBFSPathForTest("/Volumes/Keybase/team/keybase/blahblah", strPointer("/keybase/team/keybase/blahblah")), | ||
"/Volumes/Keybase/private/foo/blahblah": makeKBFSPathForTest("/Volumes/Keybase/private/foo/blahblah", strPointer("/keybase/private/foo/blahblah")), | ||
"/Volumes/Keybase/public/foo/blahblah": makeKBFSPathForTest("/Volumes/Keybase/public/foo/blahblah", strPointer("/keybase/public/foo/blahblah")), | ||
`/Volumes/Keybase\ (meatball)/team/keybase/blahblah`: makeKBFSPathForTest(`/Volumes/Keybase\ (meatball)/team/keybase/blahblah`, strPointer("/keybase/team/keybase/blahblah")), | ||
`/Volumes/Keybase\ (meatball)/private/foo/blahblah`: makeKBFSPathForTest(`/Volumes/Keybase\ (meatball)/private/foo/blahblah`, strPointer("/keybase/private/foo/blahblah")), | ||
`/Volumes/Keybase\ (meatball)/public/foo/blahblah`: makeKBFSPathForTest(`/Volumes/Keybase\ (meatball)/public/foo/blahblah`, strPointer("/keybase/public/foo/blahblah")), | ||
`"/Volumes/Keybase (meatball)/public/foo/blahblah"`: makeKBFSPathForTest(`"/Volumes/Keybase (meatball)/public/foo/blahblah"`, strPointer("/keybase/public/foo/blahblah")), | ||
|
||
`K:\team\keybase\blahblah`: makeKBFSPathForTest(`K:\team\keybase\blahblah`, strPointer("/keybase/team/keybase/blahblah")), | ||
`K:\private\foo\blahblah`: makeKBFSPathForTest(`K:\private\foo\blahblah`, strPointer("/keybase/private/foo/blahblah")), | ||
`k:\public\foo\blahblah`: makeKBFSPathForTest(`k:\public\foo\blahblah`, strPointer("/keybase/public/foo/blahblah")), | ||
`K:\public\foo\blahblah lalala`: makeKBFSPathForTest(`K:\public\foo\blahblah`, strPointer("/keybase/public/foo/blahblah")), | ||
`"K:\public\foo\blahblah lalala"`: makeKBFSPathForTest(`"K:\public\foo\blahblah lalala"`, strPointer("/keybase/public/foo/blahblah lalala")), | ||
} | ||
|
||
func TestParseKBFSPathMatches(t *testing.T) { | ||
for input, expected := range kbfsPathTests { | ||
paths := ParseKBFSPaths(context.Background(), input) | ||
if len(expected.RawPath) > 0 { | ||
require.Len(t, paths, 1, "error matching: %s", input) | ||
require.Equal(t, expected.RawPath, paths[0].RawPath, "wrong RawPath %q", input) | ||
require.Equal(t, expected.StandardPath, paths[0].StandardPath, "wrong RebasePath %q", input) | ||
} else { | ||
require.Len(t, paths, 0, "unexpected match: %s", input) | ||
} | ||
} | ||
} | ||
|
||
func TestParseKBFSPathDetailed(t *testing.T) { | ||
for _, input := range []string{ | ||
`this is a kbfs path /keybase/team/keybase/blah\ blah\ blah`, | ||
`this is a kbfs path "K:\team\keybase\blah blah blah"`, | ||
} { | ||
paths := ParseKBFSPaths(context.Background(), input) | ||
require.Len(t, paths, 1, "input: %s", input) | ||
require.Equal(t, 20, paths[0].StartIndex, "input: %s", input) | ||
require.Equal(t, "/keybase/team/keybase/blah blah blah", paths[0].StandardPath, "input: %s", input) | ||
require.Equal(t, "keybase://team/keybase/blah%20blah%20blah", paths[0].DeeplinkPath, "input: %s", input) | ||
if libkb.RuntimeGroup() == keybase1.RuntimeGroup_WINDOWSLIKE { | ||
require.Equal(t, `\team\keybase\blah blah blah`, paths[0].PlatformAfterMountPath, "input: %s", input) | ||
} else { | ||
require.Equal(t, "/team/keybase/blah blah blah", paths[0].PlatformAfterMountPath, "input: %s", input) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.