Skip to content

Commit

Permalink
feat: improve url
Browse files Browse the repository at this point in the history
- Removing the concept of "kind" within the structure while still providing helpers to check if the method is pure or a realm:
- Adding a new File field, trimming any file from the path when parsing and adding it to the structure.
- Refining the regex to define what a path can be, based on what we have in `gnovm/pkg/gnolang/helpers.go`
"var rePkgOrRealmPath = regexp.MustCompile(`^/[a-z][a-z0-9_/]*$`)"

Signed-off-by: gfanton <[email protected]>
  • Loading branch information
gfanton committed Dec 23, 2024
1 parent 6ffc310 commit d0eb4b0
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 72 deletions.
42 changes: 14 additions & 28 deletions gno.land/pkg/gnoweb/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,11 @@ func (h *WebHandler) Get(w http.ResponseWriter, r *http.Request) {
indexData.HeaderData.WebQuery = gnourl.WebQuery

// Render
switch gnourl.Kind() {
case KindRealm, KindPure:
switch {
case gnourl.IsRealm(), gnourl.IsPure():
status, err = h.renderPackage(&body, gnourl)
default:
h.logger.Debug("invalid page kind", "kind", gnourl.Kind)
h.logger.Debug("invalid path: path is neither a pure package or a realm")
status, err = http.StatusNotFound, components.RenderStatusComponent(&body, "page not found")
}
}
Expand All @@ -129,38 +129,20 @@ func (h *WebHandler) Get(w http.ResponseWriter, r *http.Request) {
func (h *WebHandler) renderPackage(w io.Writer, gnourl *GnoURL) (status int, err error) {
h.logger.Info("component render", "path", gnourl.Path, "args", gnourl.Args)

kind := gnourl.Kind()

// Display realm help page?
if kind == KindRealm && gnourl.WebQuery.Has("help") {
if gnourl.WebQuery.Has("help") {
return h.renderRealmHelp(w, gnourl)
}

// Display package source page?
switch {
case gnourl.WebQuery.Has("source"):
return h.renderRealmSource(w, gnourl)
case kind == KindPure, gnourl.IsFile(), gnourl.IsDir():
i := strings.LastIndexByte(gnourl.Path, '/')
if i < 0 {
return http.StatusInternalServerError, fmt.Errorf("unable to get ending slash for %q", gnourl.Path)
}

case gnourl.IsFile():
// Fill webquery with file infos
gnourl.WebQuery.Set("source", "") // set source

file := gnourl.Path[i+1:]
// If there nothing after the last slash that mean its a
// directory ...
if file == "" {
return h.renderRealmDirectory(w, gnourl)
}

// ... else, remaining part is a file
gnourl.WebQuery.Set("file", file)
gnourl.Path = gnourl.Path[:i]

return h.renderRealmSource(w, gnourl)
case gnourl.IsDir(), gnourl.IsPure():
return h.renderRealmDirectory(w, gnourl)
}

// Render content into the content buffer
Expand Down Expand Up @@ -251,12 +233,16 @@ func (h *WebHandler) renderRealmSource(w io.Writer, gnourl *GnoURL) (status int,
return http.StatusOK, components.RenderStatusComponent(w, "no files available")
}

file := gnourl.WebQuery.Get("file") // webquery override file
if file == "" {
file = gnourl.File
}

var fileName string
file := gnourl.WebQuery.Get("file")
if file == "" {
fileName = files[0]
fileName = files[0] // Default to the first file if none specified
} else if slices.Contains(files, file) {
fileName = file
fileName = file // Use specified file if it exists
} else {
h.logger.Error("unable to render source", "file", file, "err", "file does not exist")
return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error")
Expand Down
93 changes: 53 additions & 40 deletions gno.land/pkg/gnoweb/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,33 @@ import (
"strings"
)

type PathKind byte
var ErrURLInvalidPath = errors.New("invalid path")

const (
KindUnknown PathKind = 0
KindRealm PathKind = 'r'
KindPure PathKind = 'p'
)

// rePkgOrRealmPath matches and validates a realm or package path.
var rePkgOrRealmPath = regexp.MustCompile(`^/[a-z][a-zA-Z0-9_/.]*$`)
// rePkgOrRealmPath matches and validates a flexible path.
var rePkgOrRealmPath = regexp.MustCompile(`^/[a-z][a-z0-9_/]*$`)

// GnoURL decomposes the parts of an URL to query a realm.
type GnoURL struct {
// Example full path:
// gno.land/r/demo/users:jae$help&a=b?c=d
// gno.land/r/demo/users/render.gno:jae$help&a=b?c=d

Domain string // gno.land
Path string // /r/demo/users
Args string // jae
WebQuery url.Values // help&a=b
Query url.Values // c=d
File string // render.gno
}

// EncodeFlag is used to compose and encode URL components.
// EncodeFlag is used to specify which URL components to encode.
type EncodeFlag int

const (
EncodePath EncodeFlag = 1 << iota
EncodeArgs
EncodeWebQuery
EncodeQuery
EncodeNoEscape // Disable escaping on arg
EncodePath EncodeFlag = 1 << iota // Encode the path component
EncodeArgs // Encode the arguments component
EncodeWebQuery // Encode the web query component
EncodeQuery // Encode the query component
EncodeNoEscape // Disable escaping of arguments
)

// Has checks if the EncodeFlag contains all the specified flags.
Expand All @@ -49,14 +44,23 @@ func (f EncodeFlag) Has(flags EncodeFlag) bool {
}

// Encode encodes the URL components based on the provided flags.
// Encode assums the URL is valid.
func (gnoURL GnoURL) Encode(encodeFlags EncodeFlag) string {
var urlstr strings.Builder

if encodeFlags.Has(EncodePath) {
path := gnoURL.Path
if !encodeFlags.Has(EncodeNoEscape) {
path = url.PathEscape(path)
}

urlstr.WriteString(gnoURL.Path)
}

if len(gnoURL.File) > 0 {
urlstr.WriteRune('/')
urlstr.WriteString(gnoURL.File)
}

if encodeFlags.Has(EncodeArgs) && gnoURL.Args != "" {
if encodeFlags.Has(EncodePath) {
urlstr.WriteRune(':')
Expand All @@ -80,11 +84,16 @@ func (gnoURL GnoURL) Encode(encodeFlags EncodeFlag) string {
if encodeFlags.Has(EncodeQuery) && len(gnoURL.Query) > 0 {
urlstr.WriteRune('?')
urlstr.WriteString(gnoURL.Query.Encode())

}

return urlstr.String()
}

func escapeDollarSign(s string) string {
return strings.ReplaceAll(s, "$", "%24")
}

// EncodeArgs encodes the arguments and query parameters into a string.
// This function is intended to be passed as a realm `Render` argument.
func (gnoURL GnoURL) EncodeArgs() string {
Expand All @@ -103,33 +112,31 @@ func (gnoURL GnoURL) EncodeWebURL() string {
return gnoURL.Encode(EncodePath | EncodeArgs | EncodeWebQuery | EncodeQuery)
}

// Kind determines the kind of path (invalid, realm, or pure) based on the path structure.
func (gnoURL GnoURL) Kind() PathKind {
if len(gnoURL.Path) > 2 && gnoURL.Path[0] == '/' && gnoURL.Path[2] == '/' {
switch k := PathKind(gnoURL.Path[1]); k {
case KindPure, KindRealm:
return k
}
}
return KindUnknown
// IsPure checks if the URL path represents a pure path.
func (gnoURL GnoURL) IsPure() bool {
return strings.HasPrefix(gnoURL.Path, "/p/")
}

func (gnoURL GnoURL) IsValid() bool {
return rePkgOrRealmPath.MatchString(gnoURL.Path)
// IsRealm checks if the URL path represents a realm path.
func (gnoURL GnoURL) IsRealm() bool {
return strings.HasPrefix(gnoURL.Path, "/r/")
}

// IsFile checks if the URL path represents a file.
func (gnoURL GnoURL) IsFile() bool {
return gnoURL.File != ""
}

// IsDir checks if the URL path represents a directory.
func (gnoURL GnoURL) IsDir() bool {
return len(gnoURL.Path) > 0 && gnoURL.Path[len(gnoURL.Path)-1] == '/'
return !gnoURL.IsFile() &&
len(gnoURL.Path) > 0 && gnoURL.Path[len(gnoURL.Path)-1] == '/'
}

// IsFile checks if the URL path represents a file.
func (gnoURL GnoURL) IsFile() bool {
return filepath.Ext(gnoURL.Path) != ""
func (gnoURL GnoURL) IsValid() bool {
return rePkgOrRealmPath.MatchString(gnoURL.Path)
}

var ErrURLInvalidPath = errors.New("invalid or malformed path")

// ParseGnoURL parses a URL into a GnoURL structure, extracting and validating its components.
func ParseGnoURL(u *url.URL) (*GnoURL, error) {
var webargs string
Expand All @@ -140,12 +147,22 @@ func ParseGnoURL(u *url.URL) (*GnoURL, error) {
path, webargs, _ = strings.Cut(path, "$")
}

// NOTE: `PathUnescape` should already unescape dollar signs.
upath, err := url.PathUnescape(path)
if err != nil {
return nil, fmt.Errorf("unable to unescape path %q: %w", path, err)
}

var file string
if ext := filepath.Ext(upath); ext != "" {
file = filepath.Base(upath)
upath = strings.TrimSuffix(upath, file)

// Trim last slash
if i := strings.LastIndexByte(upath, '/'); i > 0 {
upath = upath[:i]
}
}

if !rePkgOrRealmPath.MatchString(upath) {
return nil, fmt.Errorf("%w: %q", ErrURLInvalidPath, upath)
}
Expand All @@ -169,10 +186,6 @@ func ParseGnoURL(u *url.URL) (*GnoURL, error) {
WebQuery: webquery,
Query: u.Query(),
Domain: u.Hostname(),
File: file,
}, nil
}

// escapeDollarSign replaces dollar signs with their URL-encoded equivalent.
func escapeDollarSign(s string) string {
return strings.ReplaceAll(s, "$", "%24")
}
68 changes: 64 additions & 4 deletions gno.land/pkg/gnoweb/url_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,30 @@ func TestParseGnoURL(t *testing.T) {
},
},

{
Name: "file",
Input: "https://gno.land/r/simple/test/encode.gno",
Expected: &GnoURL{
Domain: "gno.land",
Path: "/r/simple/test",
WebQuery: url.Values{},
Query: url.Values{},
File: "encode.gno",
},
},

{
Name: "complex file path",
Input: "https://gno.land/r/simple/test///...gno",
Expected: &GnoURL{
Domain: "gno.land",
Path: "/r/simple/test//",
WebQuery: url.Values{},
Query: url.Values{},
File: "...gno",
},
},

{
Name: "webquery + query",
Input: "https://gno.land/r/demo/foo$help&func=Bar&name=Baz",
Expand Down Expand Up @@ -166,16 +190,16 @@ func TestParseGnoURL(t *testing.T) {

{
Name: "webquery-args-webquery",
Input: "https://gno.land/r/demo/AAA$BBB:CCC&DDD$EEE",
Err: ErrURLInvalidPath, // `/r/demo/AAA$BBB` is an invalid path
Input: "https://gno.land/r/demo/aaa$bbb:CCC&DDD$EEE",
Err: ErrURLInvalidPath, // `/r/demo/aaa$bbb` is an invalid path
},

{
Name: "args-webquery-args",
Input: "https://gno.land/r/demo/AAA:BBB$CCC&DDD:EEE",
Input: "https://gno.land/r/demo/aaa:BBB$CCC&DDD:EEE",
Expected: &GnoURL{
Domain: "gno.land",
Path: "/r/demo/AAA",
Path: "/r/demo/aaa",
Args: "BBB",
WebQuery: url.Values{
"CCC": []string{""},
Expand All @@ -198,6 +222,21 @@ func TestParseGnoURL(t *testing.T) {
Domain: "gno.land",
},
},

{
Name: "file in path + args + query",
Input: "https://gno.land/r/demo/foo/render.gno:example$tz=Europe/Paris",
Expected: &GnoURL{
Path: "/r/demo/foo",
File: "render.gno",
Args: "example",
WebQuery: url.Values{
"tz": []string{"Europe/Paris"},
},
Query: url.Values{},
Domain: "gno.land",
},
},
}

for _, tc := range testCases {
Expand Down Expand Up @@ -237,6 +276,27 @@ func TestEncode(t *testing.T) {
Expected: "/r/demo/foo",
},

{
Name: "Encode Path and File",
GnoURL: GnoURL{
Path: "/r/demo/foo",
File: "render.gno",
},
EncodeFlags: EncodePath,
Expected: "/r/demo/foo/render.gno",
},

{
Name: "Encode Path, File, and Args",
GnoURL: GnoURL{
Path: "/r/demo/foo",
File: "render.gno",
Args: "example",
},
EncodeFlags: EncodePath | EncodeArgs,
Expected: "/r/demo/foo/render.gno:example",
},

{
Name: "Encode Path and Args",
GnoURL: GnoURL{
Expand Down

0 comments on commit d0eb4b0

Please sign in to comment.