| // Copyright 2019 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package urlinfo provides functions for extracting information out |
| // of url paths. |
| package urlinfo |
| |
| import ( |
| "fmt" |
| "strings" |
| |
| "golang.org/x/mod/module" |
| "golang.org/x/mod/semver" |
| "golang.org/x/pkgsite/internal" |
| "golang.org/x/pkgsite/internal/derrors" |
| "golang.org/x/pkgsite/internal/fetch" |
| "golang.org/x/pkgsite/internal/stdlib" |
| "golang.org/x/pkgsite/internal/version" |
| ) |
| |
| // URLPathInfo contains the information about what unit is requested in a URL path. |
| type URLPathInfo struct { |
| // FullPath is the full import path corresponding to the requested |
| // package/module/directory page. |
| FullPath string |
| // ModulePath is the path of the module corresponding to the FullPath and |
| // resolvedVersion. If unknown, it is set to internal.UnknownModulePath. |
| ModulePath string |
| // requestedVersion is the version requested by the user, which will be one |
| // of the following: "latest", "master", a Go version tag, or a semantic |
| // version. |
| RequestedVersion string |
| } |
| |
| type UserError struct { |
| UserMessage string |
| err error |
| } |
| |
| func (e *UserError) Error() string { |
| return e.err.Error() |
| } |
| |
| func (e *UserError) Unwrap() error { |
| return e.err |
| } |
| |
| // ExtractURLPathInfo extracts information from a request to pkg.go.dev. |
| // If an error is returned, the user will be served an http.StatusBadRequest. |
| func ExtractURLPathInfo(urlPath string) (_ *URLPathInfo, err error) { |
| defer derrors.Wrap(&err, "ExtractURLPathInfo(%q)", urlPath) |
| |
| if m, _, _ := strings.Cut(strings.TrimPrefix(urlPath, "/"), "@"); stdlib.Contains(m) { |
| return parseStdlibURLPath(urlPath) |
| } |
| return ParseDetailsURLPath(urlPath) |
| } |
| |
| // ParseDetailsURLPath parses a URL path that refers (or may refer) to something |
| // in the Go ecosystem. |
| // |
| // After trimming leading and trailing slashes, the path is expected to have one |
| // of three forms, and we divide it into three parts: a full path, a module |
| // path, and a version. |
| // |
| // 1. The path has no '@', like github.com/hashicorp/vault/api. |
| // This is the full path. The module path is unknown. So is the version, so we |
| // treat it as the latest version for whatever the path denotes. |
| // |
| // 2. The path has "@version" at the end, like github.com/hashicorp/vault/api@v1.2.3. |
| // We split this at the '@' into a full path (github.com/hashicorp/vault/api) |
| // and version (v1.2.3); the module path is still unknown. |
| // |
| // 3. The path has "@version" in the middle, like github.com/hashicorp/vault@v1.2.3/api. |
| // (We call this the "canonical" form of a path.) |
| // We remove the version to get the full path, which is again |
| // github.com/hashicorp/vault/api. The version is v1.2.3, and the module path is |
| // the part before the '@', github.com/hashicorp/vault. |
| // |
| // In one case, we do a little more than parse the urlPath into parts: if the full path |
| // could be a part of the standard library (because it has no '.'), we assume it |
| // is and set the ModulePath to indicate the standard library. |
| func ParseDetailsURLPath(urlPath string) (_ *URLPathInfo, err error) { |
| defer derrors.Wrap(&err, "ParseDetailsURLPath(%q)", urlPath) |
| |
| // This splits urlPath into either: |
| // /<module-path>[/<suffix>] |
| // or |
| // /<module-path>, @<version>/<suffix> |
| // or |
| // /<module-path>/<suffix>, @<version> |
| modulePath, rest, found := strings.Cut(urlPath, "@") |
| info := &URLPathInfo{ |
| FullPath: strings.TrimSuffix(strings.TrimPrefix(modulePath, "/"), "/"), |
| ModulePath: internal.UnknownModulePath, |
| RequestedVersion: version.Latest, |
| } |
| if found { |
| // The urlPath contains a "@". Parse the version and suffix from |
| // parts[1], the string after the '@'. |
| endParts := strings.Split(rest, "/") |
| |
| // Parse the requestedVersion from the urlPath. |
| // The first path component after the '@' is the version. |
| // You cannot explicitly write "latest" for the version. |
| if endParts[0] == version.Latest { |
| return nil, &UserError{ |
| err: fmt.Errorf("invalid version: %q", info.RequestedVersion), |
| UserMessage: fmt.Sprintf("%q is not a valid version", endParts[0]), |
| } |
| } |
| info.RequestedVersion = endParts[0] |
| |
| // Parse the suffix following the "@version" from the urlPath. |
| suffix := strings.Join(endParts[1:], "/") |
| if suffix != "" { |
| // If "@version" occurred in the middle of the path, the part before it |
| // is the module path. |
| info.ModulePath = info.FullPath |
| info.FullPath = info.FullPath + "/" + suffix |
| } |
| } |
| if !IsValidPath(info.FullPath) { |
| return nil, &UserError{ |
| err: fmt.Errorf("IsValidPath(%q) is false", info.FullPath), |
| UserMessage: fmt.Sprintf("%q is not a valid import path", info.FullPath), |
| } |
| } |
| return info, nil |
| } |
| |
| func parseStdlibURLPath(urlPath string) (_ *URLPathInfo, err error) { |
| defer derrors.Wrap(&err, "parseStdlibURLPath(%q)", urlPath) |
| |
| // This splits urlPath into either: |
| // /<path>@<tag> or /<path> |
| fullPath, tag, found := strings.Cut(urlPath, "@") |
| fullPath = strings.TrimSuffix(strings.TrimPrefix(fullPath, "/"), "/") |
| if !IsValidPath(fullPath) { |
| return nil, &UserError{ |
| err: fmt.Errorf("IsValidPath(%q) is false", fullPath), |
| UserMessage: fmt.Sprintf("%q is not a valid import path", fullPath), |
| } |
| } |
| |
| info := &URLPathInfo{ |
| FullPath: fullPath, |
| ModulePath: stdlib.ModulePath, |
| } |
| if !found { |
| info.RequestedVersion = version.Latest |
| return info, nil |
| } |
| tag = strings.TrimSuffix(tag, "/") |
| info.RequestedVersion = stdlib.VersionForTag(tag) |
| if info.RequestedVersion == "" { |
| if tag == fetch.LocalVersion { |
| // Special case: 0.0.0 is the version for a local stdlib |
| info.RequestedVersion = fetch.LocalVersion |
| return info, nil |
| } |
| return nil, &UserError{ |
| err: fmt.Errorf("invalid Go tag for url: %q", urlPath), |
| UserMessage: fmt.Sprintf("%q is not a valid tag for the standard library", tag), |
| } |
| } |
| return info, nil |
| } |
| |
| // IsValidPath reports whether a requested path could be a valid unit. |
| func IsValidPath(fullPath string) bool { |
| if err := module.CheckImportPath(fullPath); err != nil { |
| return false |
| } |
| parts := strings.Split(fullPath, "/") |
| if parts[0] == "golang.org" { |
| if len(parts) < 2 { |
| return false |
| } |
| switch parts[1] { |
| case "dl": |
| return true |
| case "x": |
| return len(parts) >= 3 |
| default: |
| return false |
| } |
| } |
| if internal.VCSHostWithThreeElementRepoName(parts[0]) && len(parts) < 3 { |
| return false |
| } |
| return true |
| } |
| |
| // IsSupportedVersion reports whether the version is supported by the frontend. |
| func IsSupportedVersion(fullPath, requestedVersion string) bool { |
| if stdlib.Contains(fullPath) && stdlib.SupportedBranches[requestedVersion] { |
| return true |
| } |
| if _, ok := internal.DefaultBranches[requestedVersion]; ok { |
| return !stdlib.Contains(fullPath) || requestedVersion == "master" |
| } |
| return requestedVersion == version.Latest || semver.IsValid(requestedVersion) |
| } |