mirror of
https://github.com/gomods/athens
synced 2026-02-03 13:20:30 +00:00
* Working tests * More tests, a bugfix (yay tests) and docs * Changed filter description adding v prefix to versions * Updated docs to be more precise * More robust, changed docs * Fixed copy paste bug
310 lines
6.3 KiB
Go
310 lines
6.3 KiB
Go
package module
|
|
|
|
import (
|
|
"bufio"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gomods/athens/pkg/errors"
|
|
)
|
|
|
|
var (
|
|
pathSeparator = "/"
|
|
versionSeparator = "."
|
|
)
|
|
|
|
// Filter is a filter of modules
|
|
type Filter struct {
|
|
root ruleNode
|
|
filePath string
|
|
}
|
|
|
|
// NewFilter creates new filter based on rules defined in a configuration file
|
|
// WARNING: this is not concurrently safe
|
|
// Configuration consists of two operations: + for include and - for exclude
|
|
// e.g.
|
|
// - github.com/a
|
|
// + github.com/a/b
|
|
// will communicate all modules except github.com/a and its children, but github.com/a/b will be communicated
|
|
// example 2:
|
|
// -
|
|
// + github.com/a
|
|
// will exclude all items from communication except github.com/a
|
|
func NewFilter(filterFilePath string) (*Filter, error) {
|
|
// Do not return an error if the file path is empty
|
|
// Do not attempt to parse it as well.
|
|
if filterFilePath == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
return initFromConfig(filterFilePath)
|
|
|
|
}
|
|
|
|
// AddRule adds rule for specified path
|
|
func (f *Filter) AddRule(path string, qualifiers []string, rule FilterRule) {
|
|
f.ensurePath(path)
|
|
|
|
segments := getPathSegments(path)
|
|
|
|
if len(segments) == 0 {
|
|
f.root.rule = rule
|
|
return
|
|
}
|
|
|
|
// look for latest node in a path
|
|
latest := f.root
|
|
for _, p := range segments[:len(segments)-1] {
|
|
latest = latest.next[p]
|
|
}
|
|
|
|
// replace with updated node
|
|
last := segments[len(segments)-1]
|
|
rn := latest.next[last]
|
|
rn.rule = rule
|
|
rn.qualifiers = qualifiers
|
|
latest.next[last] = rn
|
|
}
|
|
|
|
// Rule returns the filter rule to be applied to the given path
|
|
func (f *Filter) Rule(path, version string) FilterRule {
|
|
segs := getPathSegments(path)
|
|
rule := f.getAssociatedRule(version, segs...)
|
|
if rule == Default {
|
|
rule = Include
|
|
}
|
|
|
|
return rule
|
|
}
|
|
|
|
func (f *Filter) ensurePath(path string) {
|
|
latest := f.root.next
|
|
pathSegments := getPathSegments(path)
|
|
|
|
for _, p := range pathSegments {
|
|
if _, ok := latest[p]; !ok {
|
|
latest[p] = newRule(Default)
|
|
}
|
|
latest = latest[p].next
|
|
}
|
|
}
|
|
|
|
func (f *Filter) getAssociatedRule(version string, path ...string) FilterRule {
|
|
if len(path) == 0 {
|
|
return f.root.rule
|
|
}
|
|
|
|
rules := make([]FilterRule, 0, len(path))
|
|
rn := f.root
|
|
for _, p := range path {
|
|
if _, ok := rn.next[p]; !ok {
|
|
break
|
|
}
|
|
rn = rn.next[p]
|
|
// default to true if no version filter, false otherwise
|
|
match := len(rn.qualifiers) == 0
|
|
for _, q := range rn.qualifiers {
|
|
if matches(version, q) {
|
|
match = true
|
|
break
|
|
}
|
|
}
|
|
if match || version == "" {
|
|
rules = append(rules, rn.rule)
|
|
}
|
|
}
|
|
|
|
if len(rules) == 0 {
|
|
return f.root.rule
|
|
}
|
|
|
|
for i := len(rules) - 1; i >= 0; i-- {
|
|
if rules[i] != Default {
|
|
return rules[i]
|
|
}
|
|
}
|
|
|
|
return f.root.rule
|
|
}
|
|
|
|
func initFromConfig(filePath string) (*Filter, error) {
|
|
const op errors.Op = "module.initFromConfig"
|
|
lines, err := getConfigLines(filePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rn := newRule(Default)
|
|
f := &Filter{
|
|
filePath: filePath,
|
|
}
|
|
f.root = rn
|
|
|
|
for idx, line := range lines {
|
|
|
|
// Ignore newline
|
|
if len(line) == 0 {
|
|
continue
|
|
}
|
|
if len(line) > 0 && line[0] == '#' {
|
|
continue
|
|
}
|
|
|
|
split := strings.Split(line, " ")
|
|
if len(split) > 3 {
|
|
return nil, errors.E(op, "Invalid configuration found in filter file at the line "+strconv.Itoa(idx+1))
|
|
}
|
|
|
|
ruleSign := strings.TrimSpace(split[0])
|
|
rule := Default
|
|
switch ruleSign {
|
|
case "+":
|
|
rule = Include
|
|
case "-":
|
|
rule = Exclude
|
|
case "D":
|
|
rule = Direct
|
|
default:
|
|
return nil, errors.E(op, "Invalid configuration found in filter file at the line "+strconv.Itoa(idx+1))
|
|
}
|
|
// is root config
|
|
if len(split) == 1 {
|
|
f.AddRule("", nil, rule)
|
|
continue
|
|
}
|
|
var qual []string
|
|
if len(split) == 3 {
|
|
qual = strings.Split(split[2], ",")
|
|
for i := range qual {
|
|
qual[i] = strings.TrimRight(qual[i], "*")
|
|
if qual[i][len(qual[i])-1] != '.' && strings.Count(qual[i], ".") < 2 {
|
|
qual[i] += "."
|
|
}
|
|
}
|
|
}
|
|
|
|
path := strings.TrimSpace(split[1])
|
|
f.AddRule(path, qual, rule)
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
// matches checks if the given version matches the given qualifier.
|
|
// Qualifiers can be:
|
|
// - plain versions
|
|
// - v1.2.3 enables v1.2.3
|
|
// - ~1.2.3: enables 1.2.x which are at least 1.2.3
|
|
// - ^1.2.3: enables 1.x.x which are at least 1.2.3
|
|
// - <1.2.3: enables everything lower than 1.2.3 includes 1.2.2 and 0.58.9 as well
|
|
func matches(version, qualifier string) bool {
|
|
if len(qualifier) < 2 || len(version) < 1 {
|
|
return false
|
|
}
|
|
|
|
prefix := qualifier[0]
|
|
first := qualifier[1]
|
|
|
|
// v1.2.3 means we accept every version starting with v1.2.3
|
|
// handle this special case first, then go for ~v1.2.3 and similar
|
|
if prefix == 'v' && first >= '0' && first <= '9' { // a number
|
|
return strings.HasPrefix(version, qualifier)
|
|
}
|
|
|
|
v, err := getVersionSegments(version[1:])
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
q, err := getVersionSegments(qualifier[2:])
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
if len(v) != len(q) {
|
|
return false
|
|
}
|
|
// no semver
|
|
if len(v) != 3 || len(q) != 3 {
|
|
return false
|
|
}
|
|
|
|
switch prefix {
|
|
case '~':
|
|
if v[0] == q[0] && v[1] == q[1] && v[2] >= q[2] {
|
|
return true
|
|
}
|
|
return false
|
|
case '^':
|
|
if v[0] == q[0] && v[1] > q[1] {
|
|
return true
|
|
}
|
|
if v[0] == q[0] && v[1] == q[1] && v[2] >= q[2] {
|
|
return true
|
|
}
|
|
return false
|
|
case '<':
|
|
if v[0] < q[0] {
|
|
return true
|
|
}
|
|
if v[0] == q[0] && v[1] < q[1] {
|
|
return true
|
|
}
|
|
if v[0] == q[0] && v[1] == q[1] && v[2] <= q[2] {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
|
|
func getPathSegments(path string) []string {
|
|
return getSegments(path, pathSeparator)
|
|
}
|
|
|
|
func getVersionSegments(path string) ([]int, error) {
|
|
vv := getSegments(path, versionSeparator)
|
|
res := make([]int, len(vv))
|
|
for i, v := range vv {
|
|
n, err := strconv.Atoi(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res[i] = n
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func getSegments(path, separator string) []string {
|
|
path = strings.TrimSpace(path)
|
|
path = strings.Trim(path, separator)
|
|
if path == "" {
|
|
return []string{}
|
|
}
|
|
return strings.Split(path, separator)
|
|
}
|
|
|
|
func newRule(r FilterRule) ruleNode {
|
|
rn := ruleNode{}
|
|
rn.next = make(map[string]ruleNode)
|
|
rn.rule = r
|
|
return rn
|
|
}
|
|
|
|
func getConfigLines(filterFile string) ([]string, error) {
|
|
const op errors.Op = "module.getConfigLines"
|
|
|
|
f, err := os.Open(filterFile)
|
|
if err != nil {
|
|
return nil, errors.E(op, err)
|
|
}
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
var lines []string
|
|
for scanner.Scan() {
|
|
lines = append(lines, strings.TrimSpace(scanner.Text()))
|
|
}
|
|
|
|
return lines, f.Close()
|
|
}
|