2026-10-19 15:11:43 +00:00
|
|
|
// Package alias models application command-path rewriting independently of
|
|
|
|
|
// command execution and the parsing backend.
|
|
|
|
|
package alias
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
|
|
|
|
|
"gitea.maximumdirect.net/eric/CommandKit/internal/commandpath"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Mapping replaces Source with Target when Source is the longest matching
|
|
|
|
|
// prefix of an entered command path.
|
|
|
|
|
type Mapping struct {
|
|
|
|
|
Source commandpath.Path
|
|
|
|
|
Target commandpath.Path
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type trieNode struct {
|
|
|
|
|
children map[string]int
|
|
|
|
|
mapping int
|
|
|
|
|
best int
|
|
|
|
|
parent int
|
|
|
|
|
path commandpath.Path
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Resolver selects longest matching command-path mappings.
|
|
|
|
|
type Resolver struct {
|
|
|
|
|
mappings []Mapping
|
|
|
|
|
trie []trieNode
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewResolver builds a resolver from normalized command-path mappings.
|
|
|
|
|
func NewResolver(mappings []Mapping) (*Resolver, error) {
|
|
|
|
|
resolver := &Resolver{trie: []trieNode{{mapping: -1, best: -1, parent: -1}}}
|
|
|
|
|
for _, mapping := range mappings {
|
|
|
|
|
if len(mapping.Source) == 0 {
|
|
|
|
|
return nil, fmt.Errorf("command alias source path must not be empty")
|
|
|
|
|
}
|
|
|
|
|
if len(mapping.Target) == 0 {
|
|
|
|
|
return nil, fmt.Errorf("command alias target path must not be empty")
|
|
|
|
|
}
|
|
|
|
|
for _, component := range append(mapping.Source.Clone(), mapping.Target...) {
|
|
|
|
|
if err := commandpath.ValidateComponent(component); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
normalized := Mapping{Source: mapping.Source.Clone(), Target: mapping.Target.Clone()}
|
|
|
|
|
index := len(resolver.mappings)
|
|
|
|
|
resolver.mappings = append(resolver.mappings, normalized)
|
|
|
|
|
nodeIndex := 0
|
|
|
|
|
for _, component := range normalized.Source {
|
|
|
|
|
node := &resolver.trie[nodeIndex]
|
|
|
|
|
if node.children == nil {
|
|
|
|
|
node.children = make(map[string]int)
|
|
|
|
|
}
|
|
|
|
|
childIndex, exists := node.children[component]
|
|
|
|
|
if !exists {
|
|
|
|
|
childIndex = len(resolver.trie)
|
|
|
|
|
node.children[component] = childIndex
|
|
|
|
|
childPath := append(resolver.trie[nodeIndex].path.Clone(), component)
|
|
|
|
|
resolver.trie = append(resolver.trie, trieNode{mapping: -1, best: -1, parent: nodeIndex, path: childPath})
|
|
|
|
|
}
|
|
|
|
|
nodeIndex = childIndex
|
|
|
|
|
}
|
|
|
|
|
node := &resolver.trie[nodeIndex]
|
|
|
|
|
if node.mapping >= 0 {
|
|
|
|
|
previous := resolver.mappings[node.mapping]
|
|
|
|
|
if previous.Target.Equal(normalized.Target) {
|
|
|
|
|
resolver.mappings = resolver.mappings[:index]
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
return nil, fmt.Errorf("conflicting command aliases for %q", normalized.Source.String())
|
|
|
|
|
}
|
|
|
|
|
node.mapping = index
|
|
|
|
|
node.path = normalized.Source.Clone()
|
|
|
|
|
}
|
|
|
|
|
resolver.setFallbacks()
|
|
|
|
|
return resolver, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (resolver *Resolver) setFallbacks() {
|
|
|
|
|
for index := 0; index < len(resolver.trie); index++ {
|
|
|
|
|
node := &resolver.trie[index]
|
|
|
|
|
if index > 0 {
|
|
|
|
|
node.best = resolver.trie[node.parent].best
|
|
|
|
|
}
|
|
|
|
|
if node.mapping >= 0 {
|
|
|
|
|
node.best = node.mapping
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ResolveOnce applies at most one longest-prefix mapping. It returns the
|
|
|
|
|
// original input unchanged when no mapping matches.
|
|
|
|
|
func (resolver *Resolver) ResolveOnce(path commandpath.Path) (commandpath.Path, Mapping, bool) {
|
|
|
|
|
if resolver == nil {
|
|
|
|
|
return path.Clone(), Mapping{}, false
|
|
|
|
|
}
|
|
|
|
|
nodeIndex := 0
|
|
|
|
|
matched := -1
|
|
|
|
|
matchedLength := 0
|
|
|
|
|
for index, component := range path {
|
|
|
|
|
childIndex, exists := resolver.trie[nodeIndex].children[component]
|
|
|
|
|
if !exists {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
nodeIndex = childIndex
|
|
|
|
|
if mapping := resolver.trie[nodeIndex].mapping; mapping >= 0 {
|
|
|
|
|
matched = mapping
|
|
|
|
|
matchedLength = index + 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if matched < 0 {
|
|
|
|
|
return path.Clone(), Mapping{}, false
|
|
|
|
|
}
|
|
|
|
|
mapping := resolver.mappings[matched]
|
|
|
|
|
resolved := make(commandpath.Path, 0, len(mapping.Target)+len(path)-matchedLength)
|
|
|
|
|
resolved = append(resolved, mapping.Target...)
|
|
|
|
|
resolved = append(resolved, path[matchedLength:]...)
|
|
|
|
|
return resolved, Mapping{Source: mapping.Source.Clone(), Target: mapping.Target.Clone()}, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Resolve applies mappings until no source matches. Installation validates
|
|
|
|
|
// termination before a resolver is used for application input.
|
|
|
|
|
func (resolver *Resolver) Resolve(path commandpath.Path) commandpath.Path {
|
|
|
|
|
current := path.Clone()
|
|
|
|
|
for {
|
|
|
|
|
next, _, matched := resolver.ResolveOnce(current)
|
|
|
|
|
if !matched {
|
|
|
|
|
return current
|
|
|
|
|
}
|
|
|
|
|
current = next
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-19 17:37:59 +00:00
|
|
|
|
|
|
|
|
// ResolveWithProvenance resolves a command path and carries the input token
|
|
|
|
|
// indexes that contributed to each output component. Rewritten target
|
|
|
|
|
// components inherit the origins of the matched source prefix.
|
|
|
|
|
func (resolver *Resolver) ResolveWithProvenance(path commandpath.Path) (commandpath.Path, [][]int) {
|
|
|
|
|
current := path.Clone()
|
|
|
|
|
origins := make([][]int, len(path))
|
|
|
|
|
for index := range path {
|
|
|
|
|
origins[index] = []int{index}
|
|
|
|
|
}
|
|
|
|
|
for {
|
|
|
|
|
next, mapping, matched := resolver.ResolveOnce(current)
|
|
|
|
|
if !matched {
|
|
|
|
|
return current, origins
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
matchedOrigins := make([]int, 0)
|
|
|
|
|
seen := make(map[int]struct{})
|
|
|
|
|
for _, componentOrigins := range origins[:len(mapping.Source)] {
|
|
|
|
|
for _, origin := range componentOrigins {
|
|
|
|
|
if _, exists := seen[origin]; exists {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
seen[origin] = struct{}{}
|
|
|
|
|
matchedOrigins = append(matchedOrigins, origin)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
nextOrigins := make([][]int, 0, len(mapping.Target)+len(origins)-len(mapping.Source))
|
|
|
|
|
for range mapping.Target {
|
|
|
|
|
nextOrigins = append(nextOrigins, append([]int(nil), matchedOrigins...))
|
|
|
|
|
}
|
|
|
|
|
nextOrigins = append(nextOrigins, origins[len(mapping.Source):]...)
|
|
|
|
|
current = next
|
|
|
|
|
origins = nextOrigins
|
|
|
|
|
}
|
|
|
|
|
}
|