blob: a1ec34b9a85b6e923143c58d0457ea4988f0c20b [file] [log] [blame]
// 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)
}