| 1 | package server |
| 2 | |
| 3 | import "strings" |
| 4 | |
| 5 | // ModuleName extracts the top-level module name from a URL path. |
| 6 | // Strips any @version suffix (e.g., "/curator@v1.0.0/..." → "curator"). |
| 7 | // For multi-segment paths, returns the first segment only. |
| 8 | // Use ResolveModulePath for paths that may contain multi-segment module names. |
| 9 | func ModuleName(path string) string { |
| 10 | path = strings.TrimPrefix(path, "/") |
| 11 | if i := strings.Index(path, "/"); i != -1 { |
| 12 | path = path[:i] |
| 13 | } |
| 14 | if mod, _, ok := strings.Cut(path, "@"); ok { |
| 15 | return mod |
| 16 | } |
| 17 | return path |
| 18 | } |
| 19 | |
| 20 | // ResolveModulePath finds the longest path prefix that matches a known module. |
| 21 | // Returns the module name and the remaining subpath. |
| 22 | // The resolver function returns true if the name is a known module. |
| 23 | // Handles @version in the path (e.g., "/projects/revizor@v1.0.0/sub/pkg"). |
| 24 | func ResolveModulePath(urlPath string, resolver func(name string) bool) (modName, version, subpath string) { |
| 25 | path := strings.TrimPrefix(urlPath, "/") |
| 26 | if path == "" { |
| 27 | return "", "", "" |
| 28 | } |
| 29 | |
| 30 | segments := strings.Split(path, "/") |
| 31 | |
| 32 | // Find @version in any segment. |
| 33 | versionIdx := -1 |
| 34 | for i, seg := range segments { |
| 35 | if _, ver, ok := strings.Cut(seg, "@"); ok && ver != "" { |
| 36 | versionIdx = i |
| 37 | segments[i] = strings.SplitN(seg, "@", 2)[0] |
| 38 | version = ver |
| 39 | break |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | // Try progressively longer prefixes (longest match wins). |
| 44 | bestMatch := -1 |
| 45 | for i := len(segments); i >= 1; i-- { |
| 46 | // Don't try prefixes beyond the @version segment. |
| 47 | if versionIdx >= 0 && i > versionIdx+1 { |
| 48 | continue |
| 49 | } |
| 50 | candidate := strings.Join(segments[:i], "/") |
| 51 | if resolver(candidate) { |
| 52 | bestMatch = i |
| 53 | break |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | if bestMatch < 0 { |
| 58 | // No module found. Fall back to first segment (for go-import meta tags). |
| 59 | first := segments[0] |
| 60 | rest := "" |
| 61 | startIdx := 1 |
| 62 | if versionIdx == 0 { |
| 63 | startIdx = 1 |
| 64 | } |
| 65 | if startIdx < len(segments) { |
| 66 | rest = strings.Join(segments[startIdx:], "/") |
| 67 | } |
| 68 | return first, version, rest |
| 69 | } |
| 70 | |
| 71 | modName = strings.Join(segments[:bestMatch], "/") |
| 72 | if bestMatch < len(segments) { |
| 73 | subpath = strings.Join(segments[bestMatch:], "/") |
| 74 | } |
| 75 | return modName, version, subpath |
| 76 | } |
| 77 | |
| 78 | // ParseProxyPath parses "/<module>/@v/<file>" paths. |
| 79 | // Returns the full module path and file component (e.g. "list", "v1.0.0.info"). |
| 80 | func ParseProxyPath(urlPath string) (modPath, file string, ok bool) { |
| 81 | path := strings.TrimPrefix(urlPath, "/") |
| 82 | |
| 83 | idx := strings.Index(path, "/@v/") |
| 84 | if idx == -1 { |
| 85 | return "", "", false |
| 86 | } |
| 87 | |
| 88 | modPath = path[:idx] |
| 89 | file = path[idx+len("/@v/"):] |
| 90 | |
| 91 | if modPath == "" || file == "" { |
| 92 | return "", "", false |
| 93 | } |
| 94 | |
| 95 | return modPath, file, true |
| 96 | } |
| 97 | |
| 98 | // ParseDocPath parses documentation URLs: "/<module>@<version>/<subpath>". |
| 99 | // Returns the module name, version (empty if not specified), and sub-path. |
| 100 | // For multi-segment module paths, use ResolveModulePath instead. |
| 101 | func ParseDocPath(urlPath string) (modName, version, subpath string) { |
| 102 | path := strings.TrimPrefix(urlPath, "/") |
| 103 | |
| 104 | // Split on first "/" to get the first segment (which may contain @version). |
| 105 | first, rest, _ := strings.Cut(path, "/") |
| 106 | |
| 107 | // Check for @version in first segment. |
| 108 | if mod, ver, ok := strings.Cut(first, "@"); ok { |
| 109 | return mod, ver, rest |
| 110 | } |
| 111 | |
| 112 | return first, "", rest |
| 113 | } |
| 114 | |
| 115 | // ParseLatestPath parses "/<module>/@latest" paths. |
| 116 | // Returns the escaped module path if the URL matches, or empty string if not. |
| 117 | func ParseLatestPath(urlPath string) (escapedModPath string, ok bool) { |
| 118 | path := strings.TrimPrefix(urlPath, "/") |
| 119 | |
| 120 | if !strings.HasSuffix(path, "/@latest") { |
| 121 | return "", false |
| 122 | } |
| 123 | |
| 124 | modPath := strings.TrimSuffix(path, "/@latest") |
| 125 | if modPath == "" { |
| 126 | return "", false |
| 127 | } |
| 128 | |
| 129 | return modPath, true |
| 130 | } |
| 131 | |
| 132 | // StorageKey returns the S3 key for a proxy artifact. |
| 133 | func StorageKey(modName, file string) string { |
| 134 | return modName + "/@v/" + file |
| 135 | } |
| 136 | |