mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
* i/prompting: implement path pattern expansion
Path patterns may include arbitrary nested groups, delimited by '{' and
'}', up to a limit on the total number of groups. In order to compare
the precedence of path patterns which match a given path, these path
patterns must be expanded until no groups remain, and thus the
particular group-free patterns which was resolved from the original
patterns when matching the path can be compared.
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting: add PathPattern type for pattern validation and expansion
Rather than separately validate and expand path patterns, storing the
result as a list of expanded patterns, parse a pattern into a
PathPattern type, which can dynamically render expanded path patterns as
needed with minimal overhead.
When path patterns are received from prompting clients, path patterns
can be unmarshalled and automatically validated, and any future use of
the pattern in-memory can use the pre-parsed PathPattern to iterate
through expanded path patterns without needing to explicitly expand and
store all path patterns.
Additionally, the new PathPattern type should be used in Constraints in
place of the old path pattern string.
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting: refactor path pattern parsing
Rather than keep separate stacks for the sequences and paths which the
parser is currently inside, instead keep a single stack, to which the
existing sequence and a new group is added whenever a '{' rune is
encountered.
Then there is no need to no need for a variable to hold the current
group, peeking the stack yields the most recent group, to which the
current sequence can be added whenever a ',' or '}' is encountered.
When a '}' is encountered, the most recent group is popped off the
stack, the current sequence is added to it (completing the group), and
then the previous sequence is popped off the stack and the completed
group is added to it. From there, that previous sequence is now
considered the current sequence until another '{' is encountered.
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting: use stack instead of non-temp current sequence variable
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting: improve error message prefixes
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting: moved patterns to dedicated subpackage of prompting
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: add scanner, parser, and renderer for path patterns
Co-authored-by: Oliver Calder <oliver.calder@canonical.com>
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: add minimal tests for scan and render
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: replace parser in path pattern struct
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: add recursion depth check for nested groups
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: adjusted error messages
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: preserve escape characters in expanded patterns
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: scanner detects invalid chars and returns error
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting: fix formatting
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: add helper for converting read runes into text token
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: consolidate render node types into render.go
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: only re-render differences from previous configuration
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: remove GoString functions from render config types
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting{,/patterns}: added dedicated Match method to PathPattern
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: unexport all internal types and interfaces
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: rename renderConfig to variantState
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: unexported internal renderAllVariants
Also improved naming and documentation.
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: renamed local variables to match new variantState naming
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: variantState has Render method and renderNode
Add a reference to the `renderNode` used to generate a given
`variantState` to that state itself. This allows methods on
`variantState` to be called without needing to pass as a parameter the
same `renderNode` which was used to generate the `variantState`.
Also, move the `Render` function to be a method on `variantState`
instead of `renderNode`. This makes sense semantically, since we render
particular variants, rather than nodes themselves, and makes sense
ergonomically since we now have a reference to the `renderNode` within
each `variantState`, so there is no need to pass parameters around for
nodes and variants which are required to be associated anyway.
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: consolidate optimize and fix nodeEqual
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: add tests for tokenType.String
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: use dedicated flag to tell when all seq variants are exhausted
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: use ..._internal_test.go for non-exported test files
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: fix growing of render buffer, unexport peek
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: merge literalVariant into literal
Add comment as such to the `literal` type definition, and have
`literal.NextVariant` return length 0 to make it consistent with other
`variantState` types.
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: preallocate render buffer for initial variant
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: improve error handling
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: moved simple bad pattern checks to scanner
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: return length along with initial variant
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
* i/prompting/patterns: simplify check if more variants remain when rendering
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
---------
Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
Co-authored-by: Zygmunt Krynicki <me@zygoon.pl>
370 lines
9.2 KiB
Go
370 lines
9.2 KiB
Go
// -*- Mode: Go; indent-tabs-mode: t -*-
|
|
|
|
/*
|
|
* Copyright (C) 2024 Canonical Ltd
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 3 as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
package patterns
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"strings"
|
|
)
|
|
|
|
// variantState is the current variant of a render node.
|
|
type variantState interface {
|
|
// Render renders the variant to the buffer if alreadyRendered equals 0 or
|
|
// the node is not a literal. The alreadyRendered parameter is the number
|
|
// of bytes of the variant which have already been rendered because they
|
|
// are unchanged from the previous variant.
|
|
//
|
|
// If alreadyRendered is greater than 0 and the associated renderNode is
|
|
// a literal, subtract from alreadyRendered the length in bytes of the
|
|
// literal and return the difference.
|
|
Render(buf *bytes.Buffer, alreadyRendered int) int
|
|
// NextVariant modifies the variant to the next state, if any remain.
|
|
// Returns the total length in bytes of the new variant, the number of
|
|
// bytes which remain unchanged since the previous variant, and true
|
|
// if more variants remain to be rendered.
|
|
NextVariant() (length, lengthUnchanged int, moreRemain bool)
|
|
// Length returns the total length of the rendered node in its current
|
|
// variant state, including all sub-nodes if the node is a seq or alt.
|
|
Length() int
|
|
}
|
|
|
|
// renderNode is a node which may be rendered in a particular variant state.
|
|
type renderNode interface {
|
|
// InitialVariant returns the initial variant state for this node, along
|
|
// with its length.
|
|
InitialVariant() (variantState, int)
|
|
// NumVariants returns the number of variants this node can be rendered as (recursively).
|
|
NumVariants() int
|
|
// nodeEqual returns true if two nodes are recursively equal.
|
|
nodeEqual(renderNode) bool
|
|
}
|
|
|
|
// renderAllVariants renders subsequent variants to a reusable buffer.
|
|
//
|
|
// Each variant is a fully expanded path pattern, with one alternative chosen
|
|
// for each group in the path pattern. The given observe closure is then called
|
|
// on each variant, along with its index, and it can perform some action with
|
|
// the variant, such as adding it to a data structure.
|
|
func renderAllVariants(n renderNode, observe func(index int, variant string)) {
|
|
var buf bytes.Buffer
|
|
|
|
v, length := n.InitialVariant()
|
|
lengthUnchanged := 0
|
|
moreRemain := true
|
|
|
|
for i := 0; moreRemain; i++ {
|
|
buf.Truncate(lengthUnchanged)
|
|
buf.Grow(length - lengthUnchanged)
|
|
v.Render(&buf, lengthUnchanged)
|
|
observe(i, buf.String())
|
|
length, lengthUnchanged, moreRemain = v.NextVariant()
|
|
}
|
|
}
|
|
|
|
// literal is a render node with a literal string.
|
|
//
|
|
// literal implements both renderNode and variantState, since literals can only
|
|
// be rendered one way, and thus only have one variant.
|
|
type literal string
|
|
|
|
func (n literal) NumVariants() int {
|
|
return 1
|
|
}
|
|
|
|
func (n literal) InitialVariant() (variantState, int) {
|
|
return n, len(n)
|
|
}
|
|
|
|
func (n literal) nodeEqual(other renderNode) bool {
|
|
if other, ok := other.(literal); ok {
|
|
return n == other
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (n literal) Render(buf *bytes.Buffer, alreadyRendered int) int {
|
|
if alreadyRendered > 0 {
|
|
return alreadyRendered - len(n)
|
|
}
|
|
buf.WriteString(string(n))
|
|
return 0
|
|
}
|
|
|
|
func (n literal) NextVariant() (length, lengthUnchanged int, moreRemain bool) {
|
|
return 0, 0, false
|
|
}
|
|
|
|
func (n literal) Length() int {
|
|
return len(n)
|
|
}
|
|
|
|
// seq is sequence of consecutive render nodes.
|
|
type seq []renderNode
|
|
|
|
func (n seq) NumVariants() int {
|
|
num := 1
|
|
|
|
for i := range n {
|
|
num *= n[i].NumVariants()
|
|
}
|
|
|
|
return num
|
|
}
|
|
|
|
func (n seq) InitialVariant() (variantState, int) {
|
|
v := &seqVariant{
|
|
seq: n,
|
|
elements: make([]variantState, len(n)),
|
|
currLength: 0,
|
|
}
|
|
|
|
for i := range n {
|
|
var length int
|
|
v.elements[i], length = n[i].InitialVariant()
|
|
v.currLength += length
|
|
}
|
|
|
|
return v, v.currLength
|
|
}
|
|
|
|
func (n seq) nodeEqual(other renderNode) bool {
|
|
if other, ok := other.(seq); ok {
|
|
if len(other) != len(n) {
|
|
return false
|
|
}
|
|
|
|
for i := range n {
|
|
if !n[i].nodeEqual(other[i]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// optimize for seq joins adjacent literals into a single node, reduces an
|
|
// empty seq to a literal "", and reduces a seq with one element to that
|
|
// element.
|
|
func (n seq) optimize() (renderNode, error) {
|
|
var b strings.Builder
|
|
|
|
var newSeq seq
|
|
|
|
for _, item := range n {
|
|
if l, ok := item.(literal); ok {
|
|
if l == "" {
|
|
continue
|
|
}
|
|
|
|
b.WriteString(string(l))
|
|
} else {
|
|
if b.Len() > 0 {
|
|
newSeq = append(newSeq, literal(b.String()))
|
|
b.Reset()
|
|
}
|
|
|
|
newSeq = append(newSeq, item)
|
|
}
|
|
}
|
|
|
|
if b.Len() > 0 {
|
|
newSeq = append(newSeq, literal(b.String()))
|
|
b.Reset()
|
|
}
|
|
|
|
// Reduce strength
|
|
switch len(newSeq) {
|
|
case 0:
|
|
return literal(""), nil
|
|
case 1:
|
|
return newSeq[0], nil
|
|
}
|
|
|
|
return newSeq, nil
|
|
}
|
|
|
|
// seqVariant is the variant state for a seqeunce of render nodes.
|
|
//
|
|
// Each render node in the seq has a corresponding variant at the same index.
|
|
// of the elements list.
|
|
type seqVariant struct {
|
|
seq seq
|
|
elements []variantState
|
|
currLength int
|
|
}
|
|
|
|
func (v *seqVariant) Render(buf *bytes.Buffer, alreadyRendered int) int {
|
|
for _, element := range v.elements {
|
|
alreadyRendered = element.Render(buf, alreadyRendered)
|
|
}
|
|
|
|
return alreadyRendered
|
|
}
|
|
|
|
func (v *seqVariant) NextVariant() (length, lengthUnchanged int, moreRemain bool) {
|
|
var i int
|
|
for i = len(v.elements) - 1; i >= 0; i-- {
|
|
componentLength, componentLengthUnchanged, componentMoreRemain := v.elements[i].NextVariant()
|
|
if componentMoreRemain {
|
|
length += componentLength
|
|
lengthUnchanged = componentLengthUnchanged
|
|
moreRemain = true
|
|
break
|
|
}
|
|
// Reset the variant state for the node whose variants we just exhausted
|
|
v.elements[i], componentLength = v.seq[i].InitialVariant()
|
|
// Include the render length of the reset node in the total length
|
|
length += componentLength
|
|
}
|
|
if !moreRemain {
|
|
// No expansions remain for any node in the sequence
|
|
return 0, 0, false
|
|
}
|
|
|
|
// We already counted v.elements[i], so count j : 0 <= j < i
|
|
for j := 0; j < i; j++ {
|
|
componentLength := v.elements[j].Length()
|
|
length += componentLength
|
|
lengthUnchanged += componentLength
|
|
}
|
|
|
|
v.currLength = length
|
|
|
|
return length, lengthUnchanged, true
|
|
}
|
|
|
|
func (v *seqVariant) Length() int {
|
|
return v.currLength
|
|
}
|
|
|
|
// alt is a sequence of alternative render nodes.
|
|
type alt []renderNode
|
|
|
|
func (n alt) NumVariants() int {
|
|
num := 0
|
|
|
|
for i := range n {
|
|
num += n[i].NumVariants()
|
|
}
|
|
|
|
return num
|
|
}
|
|
|
|
func (n alt) InitialVariant() (variantState, int) {
|
|
// alt can't have zero length, since even "{}" would be parsed as
|
|
// alt{ seq{ literal("") } }, which is optimized to alt{ literal("") },
|
|
// which is optimized to literal("").
|
|
// An error was thrown by optimize rather than return a 0-length alt.
|
|
currVariant, length := n[0].InitialVariant()
|
|
return &altVariant{
|
|
alt: n,
|
|
idx: 0,
|
|
variant: currVariant,
|
|
}, length
|
|
}
|
|
|
|
func (n alt) nodeEqual(other renderNode) bool {
|
|
if other, ok := other.(alt); ok {
|
|
if len(other) != len(n) {
|
|
return false
|
|
}
|
|
|
|
for i := range n {
|
|
if !n[i].nodeEqual(other[i]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// optimize for alt eliminates equivalent items and reduces alts with one item
|
|
// to that item.
|
|
func (n alt) optimize() (renderNode, error) {
|
|
var seen []renderNode
|
|
var newAlt alt
|
|
|
|
outer:
|
|
for _, item := range n {
|
|
for _, seenItem := range seen {
|
|
if seenItem.nodeEqual(item) {
|
|
continue outer
|
|
}
|
|
}
|
|
|
|
seen = append(seen, item)
|
|
newAlt = append(newAlt, item)
|
|
}
|
|
|
|
// Reduce strength
|
|
switch len(newAlt) {
|
|
case 0:
|
|
// Should not occur, since even "{}" would be parsed as
|
|
// alt{ seq{ literal("") } }, which is optimized to alt{ literal("") }.
|
|
return nil, errors.New("internal error: attempted to optimize 0-length alt")
|
|
case 1:
|
|
return newAlt[0], nil
|
|
}
|
|
|
|
return newAlt, nil
|
|
}
|
|
|
|
// altVariant is the variant state for an set of alternative render nodes.
|
|
type altVariant struct {
|
|
alt alt // Alt associated with this variant state.
|
|
idx int // Index of the alternative currently being explored.
|
|
variant variantState // Variant corresponding to the alternative currently being explored.
|
|
}
|
|
|
|
func (v *altVariant) Render(buf *bytes.Buffer, alreadyRendered int) int {
|
|
return v.variant.Render(buf, alreadyRendered)
|
|
}
|
|
|
|
func (v *altVariant) NextVariant() (length, lengthUnchanged int, moreRemain bool) {
|
|
// Keep exploring the current alternative until all possibilities are exhausted.
|
|
if length, lengthUnchanged, moreRemain = v.variant.NextVariant(); moreRemain {
|
|
return length, lengthUnchanged, true
|
|
}
|
|
|
|
// Advance to the next alternative if one exists and obtain the initial
|
|
// variant state for it.
|
|
|
|
v.idx++
|
|
if v.idx >= len(v.alt) {
|
|
return 0, 0, false
|
|
}
|
|
|
|
v.variant, length = v.alt[v.idx].InitialVariant()
|
|
|
|
return length, 0, true
|
|
}
|
|
|
|
func (v *altVariant) Length() int {
|
|
return v.variant.Length()
|
|
}
|