// 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 } } // ResolutionPaths returns the entered path and each path produced by a // longest-prefix rewrite. It supports metadata discovery along alias chains. // Installation validates termination before this method is used. func (resolver *Resolver) ResolutionPaths(path commandpath.Path) []commandpath.Path { paths := []commandpath.Path{path.Clone()} for { next, _, matched := resolver.ResolveOnce(paths[len(paths)-1]) if !matched { return paths } paths = append(paths, next) } } // 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 } }