From ab089f91bf2d52914d883b3d8ee2c36bfc86650c Mon Sep 17 00:00:00 2001 From: lczyk Date: Mon, 8 Dec 2025 19:43:22 +0000 Subject: [PATCH 1/3] feat!: impl --- internal/deb/extract.go | 16 ++-- internal/setup/setup.go | 28 ++++-- internal/setup/setup_test.go | 123 ++++++++++++++++++++++++++ internal/setup/yaml.go | 80 +++++++++++++---- internal/slicer/slicer.go | 5 +- internal/strdist/strdist.go | 144 +++++++++++++++++++++++++++++-- internal/strdist/strdist_test.go | 30 +++++++ 7 files changed, 387 insertions(+), 39 deletions(-) diff --git a/internal/deb/extract.go b/internal/deb/extract.go index 5f1f09d6..b95cc74e 100644 --- a/internal/deb/extract.go +++ b/internal/deb/extract.go @@ -32,10 +32,11 @@ type ExtractOptions struct { } type ExtractInfo struct { - Path string - Mode uint - Optional bool - Context any + Path string + Mode uint + Optional bool + Context any + SpecialGlobs map[rune]string } func getValidOptions(options *ExtractOptions) (*ExtractOptions, error) { @@ -152,7 +153,12 @@ func extractData(pkgReader io.ReadSeeker, options *ExtractOptions) error { continue } if strings.ContainsAny(extractPath, "*?") { - if strdist.GlobPath(extractPath, sourcePath) { + // Get special globs from the first extractInfo if available + var extractSpecial map[rune]string + if len(extractInfos) > 0 { + extractSpecial = extractInfos[0].SpecialGlobs + } + if strdist.GlobPathWithSpecial(extractPath, sourcePath, extractSpecial, nil) { targetPaths[sourcePath] = append(targetPaths[sourcePath], extractInfos...) delete(pendingPaths, extractPath) } diff --git a/internal/setup/setup.go b/internal/setup/setup.go index db3d9518..f4e24c57 100644 --- a/internal/setup/setup.go +++ b/internal/setup/setup.go @@ -105,11 +105,12 @@ type PathInfo struct { Info string Mode uint - Mutable bool - Until PathUntil - Arch []string - Generate GenerateKind - Prefer string + Mutable bool + Until PathUntil + Arch []string + Generate GenerateKind + Prefer string + SpecialGlobs map[rune]string } // SameContent returns whether the path has the same content properties as some @@ -206,6 +207,21 @@ func (r *Release) validate() error { keys := []SliceKey(nil) + // Validate special-globs regex patterns can compile + for _, pkg := range r.Packages { + for _, slice := range pkg.Slices { + for path, info := range slice.Contents { + if len(info.SpecialGlobs) > 0 { + for char, pattern := range info.SpecialGlobs { + if err := strdist.ValidateSpecialGlob(char, pattern); err != nil { + return fmt.Errorf("slice %s path %s has invalid special-glob: %w", slice, path, err) + } + } + } + } + } + } + // Check for info conflicts and prepare for following checks. A conflict // means that two slices attempt to extract different files or directories // to the same location. @@ -288,7 +304,7 @@ func (r *Release) validate() error { continue } } - if strdist.GlobPath(newPath, oldPath) { + if strdist.GlobPathWithSpecial(newPath, oldPath, newInfo.SpecialGlobs, oldInfo.SpecialGlobs) { if (old.Package > new.Package) || (old.Package == new.Package && old.Name > new.Name) || (old.Package == new.Package && old.Name == new.Name && oldPath > newPath) { old, new = new, old diff --git a/internal/setup/setup_test.go b/internal/setup/setup_test.go index ac956ce1..1ddbecf0 100644 --- a/internal/setup/setup_test.go +++ b/internal/setup/setup_test.go @@ -507,6 +507,73 @@ var setupTests = []setupTest{{ `, }, relerror: `slices mypkg1_myslice and mypkg2_myslice conflict on /file/f\*obar and /file/foob\*r`, +}, { + summary: "Special globs", + input: map[string]string{ + "slices/mydir/mypkg1.yaml": ` + package: mypkg1 + slices: + myslice: + contents: + /file/f๐ŸŽ„obar: + special-globs: + ๐ŸŽ„: '[^o]' + `, + "slices/mydir/mypkg2.yaml": ` + package: mypkg2 + slices: + myslice: + contents: + /file/foob*r: + `, + }, + release: &setup.Release{ + Archives: map[string]*setup.Archive{ + "ubuntu": { + Name: "ubuntu", + Version: "22.04", + Suites: []string{"jammy"}, + Components: []string{"main", "universe"}, + PubKeys: []*packet.PublicKey{testKey.PubKey}, + Maintained: true, + }, + }, + Packages: map[string]*setup.Package{ + "mypkg1": { + Name: "mypkg1", + Path: "slices/mydir/mypkg1.yaml", + Slices: map[string]*setup.Slice{ + "myslice": { + Package: "mypkg1", + Name: "myslice", + Contents: map[string]setup.PathInfo{ + "/file/f๐ŸŽ„obar": { + Kind: "glob", + SpecialGlobs: map[rune]string{'๐ŸŽ„': "[^o]"}, + }, + }, + }, + }, + }, + "mypkg2": { + Name: "mypkg2", + Path: "slices/mydir/mypkg2.yaml", + Slices: map[string]*setup.Slice{ + "myslice": { + Package: "mypkg2", + Name: "myslice", + Contents: map[string]setup.PathInfo{ + "/file/foob*r": {Kind: "glob"}, + }, + }, + }, + }, + }, + Maintenance: &setup.Maintenance{ + Standard: time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC), + EndOfLife: time.Date(2100, time.January, 1, 0, 0, 0, 0, time.UTC), + }, + }, }, { summary: "Conflicting globs and plain copies", input: map[string]string{ @@ -584,6 +651,62 @@ var setupTests = []setupTest{{ /file/foob*r: {until: mutate} `, }, +}, { + summary: "Special globs with multi-character key is invalid", + input: map[string]string{ + "slices/mydir/mypkg.yaml": ` + package: mypkg + slices: + myslice: + contents: + /file/fXYobar: + special-globs: + XY: '[a-z]' + `, + }, + relerror: `slice mypkg_myslice path /file/fXYobar special-globs key must be a single character: "XY"`, +}, { + summary: "Special globs cannot use standard glob characters", + input: map[string]string{ + "slices/mydir/mypkg.yaml": ` + package: mypkg + slices: + myslice: + contents: + /file/f*obar: + special-globs: + '*': '[a-z]' + `, + }, + relerror: `slice mypkg_myslice path /file/f\*obar special-globs key cannot be a standard glob character: "\*"`, +}, { + summary: "Special globs cannot use slash", + input: map[string]string{ + "slices/mydir/mypkg.yaml": ` + package: mypkg + slices: + myslice: + contents: + /file/f/obar: + special-globs: + '/': '[a-z]' + `, + }, + relerror: `slice mypkg_myslice path /file/f/obar special-globs key cannot be a standard glob character: "/"`, +}, { + summary: "Special globs with invalid regex pattern", + input: map[string]string{ + "slices/mydir/mypkg.yaml": ` + package: mypkg + slices: + myslice: + contents: + /file/fXobar: + special-globs: + X: '[invalid' + `, + }, + relerror: `slice mypkg_myslice path /file/fXobar has invalid special-glob: special-glob "X" has invalid regex pattern "\[invalid": error parsing regexp: missing closing \]: ` + "`" + `\[invalid\$` + "`", }, { summary: "Mutable does not work for directories extractions", input: map[string]string{ diff --git a/internal/setup/yaml.go b/internal/setup/yaml.go index b7fc65b4..5f1d3243 100644 --- a/internal/setup/yaml.go +++ b/internal/setup/yaml.go @@ -63,16 +63,17 @@ type yamlPackage struct { } type yamlPath struct { - Dir bool `yaml:"make,omitempty"` - Mode yamlMode `yaml:"mode,omitempty"` - Copy string `yaml:"copy,omitempty"` - Text *string `yaml:"text,omitempty"` - Symlink string `yaml:"symlink,omitempty"` - Mutable bool `yaml:"mutable,omitempty"` - Until PathUntil `yaml:"until,omitempty"` - Arch yamlArch `yaml:"arch,omitempty"` - Generate GenerateKind `yaml:"generate,omitempty"` - Prefer string `yaml:"prefer,omitempty"` + Dir bool `yaml:"make,omitempty"` + Mode yamlMode `yaml:"mode,omitempty"` + Copy string `yaml:"copy,omitempty"` + Text *string `yaml:"text,omitempty"` + Symlink string `yaml:"symlink,omitempty"` + Mutable bool `yaml:"mutable,omitempty"` + Until PathUntil `yaml:"until,omitempty"` + Arch yamlArch `yaml:"arch,omitempty"` + Generate GenerateKind `yaml:"generate,omitempty"` + Prefer string `yaml:"prefer,omitempty"` + SpecialGlobs map[string]string `yaml:"special-globs,omitempty"` } func (yp *yamlPath) MarshalYAML() (any, error) { @@ -490,6 +491,26 @@ func parsePackage(baseDir, pkgName, pkgPath string, data []byte) (*Package, erro var arch []string var generate GenerateKind var prefer string + var specialGlobs map[rune]string + + // Parse special-globs first if present + if yamlPath != nil && len(yamlPath.SpecialGlobs) > 0 { + specialGlobs = make(map[rune]string) + for key, pattern := range yamlPath.SpecialGlobs { + // Ensure key is a single rune + runes := []rune(key) + if len(runes) != 1 { + return nil, fmt.Errorf("slice %s_%s path %s special-globs key must be a single character: %q", pkgName, sliceName, contPath, key) + } + r := runes[0] + // Disallow conflicts with basic glob tokens + if r == '*' || r == '?' || r == '/' { + return nil, fmt.Errorf("slice %s_%s path %s special-globs key cannot be a standard glob character: %q", pkgName, sliceName, contPath, key) + } + specialGlobs[r] = pattern + } + } + if yamlPath != nil && yamlPath.Generate != "" { zeroPathGenerate := zeroPath zeroPathGenerate.Generate = yamlPath.Generate @@ -501,7 +522,7 @@ func parsePackage(baseDir, pkgName, pkgPath string, data []byte) (*Package, erro return nil, fmt.Errorf("slice %s_%s has invalid generate path: %s", pkgName, sliceName, err) } kinds = append(kinds, GeneratePath) - } else if strings.ContainsAny(contPath, "*?") { + } else if strings.ContainsAny(contPath, "*?") || hasSpecialGlobChar(contPath, specialGlobs) { if yamlPath != nil { if !yamlPath.SameContent(&zeroPath) || yamlPath.Prefer != "" { return nil, fmt.Errorf("slice %s_%s path %s has invalid wildcard options", @@ -567,14 +588,15 @@ func parsePackage(baseDir, pkgName, pkgPath string, data []byte) (*Package, erro return nil, fmt.Errorf("slice %s_%s mutable is not a regular file: %s", pkgName, sliceName, contPath) } slice.Contents[contPath] = PathInfo{ - Kind: kinds[0], - Info: info, - Mode: mode, - Mutable: mutable, - Until: until, - Arch: arch, - Generate: generate, - Prefer: prefer, + Kind: kinds[0], + Info: info, + Mode: mode, + Mutable: mutable, + Until: until, + Arch: arch, + Generate: generate, + Prefer: prefer, + SpecialGlobs: specialGlobs, } } @@ -600,6 +622,19 @@ func validateGeneratePath(path string) (string, error) { return dirPath, nil } +// hasSpecialGlobChar checks if path contains any runes defined in specialGlobs +func hasSpecialGlobChar(path string, specialGlobs map[rune]string) bool { + if len(specialGlobs) == 0 { + return false + } + for _, r := range path { + if _, ok := specialGlobs[r]; ok { + return true + } + } + return false +} + // pathInfoToYAML converts a PathInfo object to a yamlPath object. // The returned object takes pointers to the given PathInfo object. func pathInfoToYAML(pi *PathInfo) (*yamlPath, error) { @@ -611,6 +646,13 @@ func pathInfoToYAML(pi *PathInfo) (*yamlPath, error) { Generate: pi.Generate, Prefer: pi.Prefer, } + // Convert special globs back to yaml format + if len(pi.SpecialGlobs) > 0 { + path.SpecialGlobs = make(map[string]string) + for r, pattern := range pi.SpecialGlobs { + path.SpecialGlobs[string(r)] = pattern + } + } switch pi.Kind { case DirPath: path.Dir = true diff --git a/internal/slicer/slicer.go b/internal/slicer/slicer.go index 9d3447fb..e65010f4 100644 --- a/internal/slicer/slicer.go +++ b/internal/slicer/slicer.go @@ -126,8 +126,9 @@ func Run(options *RunOptions) error { sourcePath = targetPath } extractPackage[sourcePath] = append(extractPackage[sourcePath], deb.ExtractInfo{ - Path: targetPath, - Context: slice, + Path: targetPath, + Context: slice, + SpecialGlobs: pathInfo.SpecialGlobs, }) } else { // When the content is not extracted from the package (i.e. path is diff --git a/internal/strdist/strdist.go b/internal/strdist/strdist.go index d5b640ef..e6e5363a 100644 --- a/internal/strdist/strdist.go +++ b/internal/strdist/strdist.go @@ -2,9 +2,24 @@ package strdist import ( "fmt" + "regexp" "strings" ) +// ValidateSpecialGlob validates that a special glob character and its pattern are valid +func ValidateSpecialGlob(char rune, pattern string) error { + // Validate that char is not a standard glob character + if char == '*' || char == '?' || char == '/' { + return fmt.Errorf("special-glob %q cannot be a standard glob character", string(char)) + } + // Validate regex compiles + _, err := regexp.Compile("^" + pattern + "$") + if err != nil { + return fmt.Errorf("special-glob %q has invalid regex pattern %q: %w", string(char), pattern, err) + } + return nil +} + type CostInt int64 func (cv CostInt) String() string { @@ -105,18 +120,92 @@ func Distance(a, b string, f CostFunc, cut int64) int64 { // * - Any zero or more characters, except for / // ** - Any zero or more characters, including / func GlobPath(a, b string) bool { - if !wildcardPrefixMatch(a, b) { + return GlobPathWithSpecial(a, b, nil, nil) +} + +// GlobPathWithSpecial returns true if a and b match using supported wildcards +// and optional special glob characters defined in aSpecial and bSpecial. +// Special globs are custom single-character wildcards with regex patterns. +func GlobPathWithSpecial(a, b string, aSpecial, bSpecial map[rune]string) bool { + if !wildcardPrefixMatchWithSpecial(a, b, aSpecial, bSpecial) { // Fast path. return false } - if !wildcardSuffixMatch(a, b) { + if !wildcardSuffixMatchWithSpecial(a, b, aSpecial, bSpecial) { // Fast path. return false } a = strings.ReplaceAll(a, "**", "โ‘") b = strings.ReplaceAll(b, "**", "โ‘") - return Distance(a, b, globCost, 1) == 0 + + costFunc := makeGlobCostFunc(aSpecial, bSpecial) + return Distance(a, b, costFunc, 1) == 0 +} + +func makeGlobCostFunc(aSpecial, bSpecial map[rune]string) CostFunc { + // Compile regex patterns once + aRegex := make(map[rune]*regexp.Regexp) + bRegex := make(map[rune]*regexp.Regexp) + + for r, pattern := range aSpecial { + re, err := regexp.Compile("^" + pattern + "$") + if err == nil { + aRegex[r] = re + } + } + for r, pattern := range bSpecial { + re, err := regexp.Compile("^" + pattern + "$") + if err == nil { + bRegex[r] = re + } + } + + return func(ar, br rune) Cost { + // Handle standard wildcards first + if ar == 'โ‘' || br == 'โ‘' { + return Cost{SwapAB: 0, DeleteA: 0, InsertB: 0} + } + if ar == '/' || br == '/' { + return Cost{SwapAB: Inhibit, DeleteA: Inhibit, InsertB: Inhibit} + } + if ar == '*' || br == '*' { + return Cost{SwapAB: 0, DeleteA: 0, InsertB: 0} + } + if ar == '?' || br == '?' { + return Cost{SwapAB: 0, DeleteA: 1, InsertB: 1} + } + + // Handle special globs + if re, ok := aRegex[ar]; ok { + // ar is a special glob from a + if br == '/' { + // Special globs cannot match / unless explicitly allowed + if !re.MatchString("/") { + return Cost{SwapAB: Inhibit, DeleteA: 1, InsertB: Inhibit} + } + } + // Special glob matches any single character matching its pattern + if re.MatchString(string(br)) { + return Cost{SwapAB: 0, DeleteA: 1, InsertB: Inhibit} + } + return Cost{SwapAB: Inhibit, DeleteA: 1, InsertB: Inhibit} + } + if re, ok := bRegex[br]; ok { + // br is a special glob from b + if ar == '/' { + if !re.MatchString("/") { + return Cost{SwapAB: Inhibit, DeleteA: Inhibit, InsertB: 1} + } + } + if re.MatchString(string(ar)) { + return Cost{SwapAB: 0, DeleteA: Inhibit, InsertB: 1} + } + return Cost{SwapAB: Inhibit, DeleteA: Inhibit, InsertB: 1} + } + + return Cost{SwapAB: 1, DeleteA: 1, InsertB: 1} + } } func globCost(ar, br rune) Cost { @@ -139,8 +228,12 @@ func globCost(ar, br rune) Cost { // to the shortest one. The prefix is defined as the longest substring that // starts at index 0 and does not contain a wildcard. func wildcardPrefixMatch(a, b string) bool { - ai := strings.IndexAny(a, "*?") - bi := strings.IndexAny(b, "*?") + return wildcardPrefixMatchWithSpecial(a, b, nil, nil) +} + +func wildcardPrefixMatchWithSpecial(a, b string, aSpecial, bSpecial map[rune]string) bool { + ai := indexAnyWildcard(a, aSpecial) + bi := indexAnyWildcard(b, bSpecial) if ai == -1 { ai = len(a) } @@ -155,16 +248,53 @@ func wildcardPrefixMatch(a, b string) bool { // to the shortest one. The suffix is defined as the longest substring that ends // at the string length and does not contain a wildcard. func wildcardSuffixMatch(a, b string) bool { - ai := strings.LastIndexAny(a, "*?") + return wildcardSuffixMatchWithSpecial(a, b, nil, nil) +} + +func wildcardSuffixMatchWithSpecial(a, b string, aSpecial, bSpecial map[rune]string) bool { + ai := lastIndexAnyWildcard(a, aSpecial) la := 0 if ai != -1 { la = len(a) - ai - 1 } lb := 0 - bi := strings.LastIndexAny(b, "*?") + bi := lastIndexAnyWildcard(b, bSpecial) if bi != -1 { lb = len(b) - bi - 1 } minl := min(la, lb) return a[len(a)-minl:] == b[len(b)-minl:] } + +// indexAnyWildcard returns the index of the first wildcard in s +func indexAnyWildcard(s string, special map[rune]string) int { + for i, r := range s { + if r == '*' || r == '?' { + return i + } + if special != nil { + if _, ok := special[r]; ok { + return i + } + } + } + return -1 +} + +// lastIndexAnyWildcard returns the index of the last wildcard in s +func lastIndexAnyWildcard(s string, special map[rune]string) int { + runes := []rune(s) + for i := len(runes) - 1; i >= 0; i-- { + r := runes[i] + if r == '*' || r == '?' { + // Calculate byte position + return len(string(runes[:i])) + } + if special != nil { + if _, ok := special[r]; ok { + return len(string(runes[:i])) + } + } + } + return -1 +} diff --git a/internal/strdist/strdist_test.go b/internal/strdist/strdist_test.go index 7f8975a0..a12e8364 100644 --- a/internal/strdist/strdist_test.go +++ b/internal/strdist/strdist_test.go @@ -92,3 +92,33 @@ func BenchmarkDistanceCut(b *testing.B) { strdist.Distance(one, two, strdist.StandardCost, 1) } } + +func (s *S) TestGlobPathWithSpecial(c *C) { + // Test special glob matching + specialA := map[rune]string{ + '๐ŸŽ„': "[^o]", + } + + // Should match: ๐ŸŽ„ matches any character except 'o' + c.Assert(strdist.GlobPathWithSpecial("/file/f๐ŸŽ„obar", "/file/faobar", specialA, nil), Equals, true) + c.Assert(strdist.GlobPathWithSpecial("/file/f๐ŸŽ„obar", "/file/fbobar", specialA, nil), Equals, true) + c.Assert(strdist.GlobPathWithSpecial("/file/f๐ŸŽ„obar", "/file/fxobar", specialA, nil), Equals, true) + + // Should not match: ๐ŸŽ„ doesn't match 'o' + c.Assert(strdist.GlobPathWithSpecial("/file/f๐ŸŽ„obar", "/file/foobar", specialA, nil), Equals, false) + + // Should not match standard glob pattern /file/foob*r with special glob /file/f๐ŸŽ„obar + // because 'f๐ŸŽ„obar' would need ๐ŸŽ„ to match 'oob', but it only matches single char + c.Assert(strdist.GlobPathWithSpecial("/file/foob*r", "/file/f๐ŸŽ„obar", nil, specialA), Equals, false) + + // Test that special globs don't match / + specialSlash := map[rune]string{ + 'X': "[^/]", + } + c.Assert(strdist.GlobPathWithSpecial("/fileXbar", "/file/bar", specialSlash, nil), Equals, false) + c.Assert(strdist.GlobPathWithSpecial("/fileXbar", "/filexbar", specialSlash, nil), Equals, true) + + // Test nil special globs (should work like normal GlobPath) + c.Assert(strdist.GlobPathWithSpecial("/file/*", "/file/test", nil, nil), Equals, true) + c.Assert(strdist.GlobPathWithSpecial("/file/?", "/file/a", nil, nil), Equals, true) +} From 3c9ab7a164177bb2d64b3b444f258d55fae24fc3 Mon Sep 17 00:00:00 2001 From: lczyk Date: Mon, 8 Dec 2025 19:43:50 +0000 Subject: [PATCH 2/3] chore: go fmt --- internal/setup/yaml.go | 4 ++-- internal/strdist/strdist.go | 10 +++++----- internal/strdist/strdist_test.go | 10 +++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/internal/setup/yaml.go b/internal/setup/yaml.go index 5f1d3243..f4575b0c 100644 --- a/internal/setup/yaml.go +++ b/internal/setup/yaml.go @@ -492,7 +492,7 @@ func parsePackage(baseDir, pkgName, pkgPath string, data []byte) (*Package, erro var generate GenerateKind var prefer string var specialGlobs map[rune]string - + // Parse special-globs first if present if yamlPath != nil && len(yamlPath.SpecialGlobs) > 0 { specialGlobs = make(map[rune]string) @@ -510,7 +510,7 @@ func parsePackage(baseDir, pkgName, pkgPath string, data []byte) (*Package, erro specialGlobs[r] = pattern } } - + if yamlPath != nil && yamlPath.Generate != "" { zeroPathGenerate := zeroPath zeroPathGenerate.Generate = yamlPath.Generate diff --git a/internal/strdist/strdist.go b/internal/strdist/strdist.go index e6e5363a..951800bb 100644 --- a/internal/strdist/strdist.go +++ b/internal/strdist/strdist.go @@ -138,7 +138,7 @@ func GlobPathWithSpecial(a, b string, aSpecial, bSpecial map[rune]string) bool { a = strings.ReplaceAll(a, "**", "โ‘") b = strings.ReplaceAll(b, "**", "โ‘") - + costFunc := makeGlobCostFunc(aSpecial, bSpecial) return Distance(a, b, costFunc, 1) == 0 } @@ -147,7 +147,7 @@ func makeGlobCostFunc(aSpecial, bSpecial map[rune]string) CostFunc { // Compile regex patterns once aRegex := make(map[rune]*regexp.Regexp) bRegex := make(map[rune]*regexp.Regexp) - + for r, pattern := range aSpecial { re, err := regexp.Compile("^" + pattern + "$") if err == nil { @@ -160,7 +160,7 @@ func makeGlobCostFunc(aSpecial, bSpecial map[rune]string) CostFunc { bRegex[r] = re } } - + return func(ar, br rune) Cost { // Handle standard wildcards first if ar == 'โ‘' || br == 'โ‘' { @@ -175,7 +175,7 @@ func makeGlobCostFunc(aSpecial, bSpecial map[rune]string) CostFunc { if ar == '?' || br == '?' { return Cost{SwapAB: 0, DeleteA: 1, InsertB: 1} } - + // Handle special globs if re, ok := aRegex[ar]; ok { // ar is a special glob from a @@ -203,7 +203,7 @@ func makeGlobCostFunc(aSpecial, bSpecial map[rune]string) CostFunc { } return Cost{SwapAB: Inhibit, DeleteA: Inhibit, InsertB: 1} } - + return Cost{SwapAB: 1, DeleteA: 1, InsertB: 1} } } diff --git a/internal/strdist/strdist_test.go b/internal/strdist/strdist_test.go index a12e8364..59bb105f 100644 --- a/internal/strdist/strdist_test.go +++ b/internal/strdist/strdist_test.go @@ -98,26 +98,26 @@ func (s *S) TestGlobPathWithSpecial(c *C) { specialA := map[rune]string{ '๐ŸŽ„': "[^o]", } - + // Should match: ๐ŸŽ„ matches any character except 'o' c.Assert(strdist.GlobPathWithSpecial("/file/f๐ŸŽ„obar", "/file/faobar", specialA, nil), Equals, true) c.Assert(strdist.GlobPathWithSpecial("/file/f๐ŸŽ„obar", "/file/fbobar", specialA, nil), Equals, true) c.Assert(strdist.GlobPathWithSpecial("/file/f๐ŸŽ„obar", "/file/fxobar", specialA, nil), Equals, true) - + // Should not match: ๐ŸŽ„ doesn't match 'o' c.Assert(strdist.GlobPathWithSpecial("/file/f๐ŸŽ„obar", "/file/foobar", specialA, nil), Equals, false) - + // Should not match standard glob pattern /file/foob*r with special glob /file/f๐ŸŽ„obar // because 'f๐ŸŽ„obar' would need ๐ŸŽ„ to match 'oob', but it only matches single char c.Assert(strdist.GlobPathWithSpecial("/file/foob*r", "/file/f๐ŸŽ„obar", nil, specialA), Equals, false) - + // Test that special globs don't match / specialSlash := map[rune]string{ 'X': "[^/]", } c.Assert(strdist.GlobPathWithSpecial("/fileXbar", "/file/bar", specialSlash, nil), Equals, false) c.Assert(strdist.GlobPathWithSpecial("/fileXbar", "/filexbar", specialSlash, nil), Equals, true) - + // Test nil special globs (should work like normal GlobPath) c.Assert(strdist.GlobPathWithSpecial("/file/*", "/file/test", nil, nil), Equals, true) c.Assert(strdist.GlobPathWithSpecial("/file/?", "/file/a", nil, nil), Equals, true) From 6308bff29b1b3a1e285e1166cba6fb3bb455804f Mon Sep 17 00:00:00 2001 From: lczyk Date: Mon, 8 Dec 2025 19:49:17 +0000 Subject: [PATCH 3/3] chore: nolint unused --- internal/strdist/strdist.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/strdist/strdist.go b/internal/strdist/strdist.go index 951800bb..f3d4c8c2 100644 --- a/internal/strdist/strdist.go +++ b/internal/strdist/strdist.go @@ -227,7 +227,7 @@ func globCost(ar, br rune) Cost { // wildcardPrefixMatch compares whether the prefixes of a and b are equal up // to the shortest one. The prefix is defined as the longest substring that // starts at index 0 and does not contain a wildcard. -func wildcardPrefixMatch(a, b string) bool { +func wildcardPrefixMatch(a, b string) bool { //nolint:all return wildcardPrefixMatchWithSpecial(a, b, nil, nil) } @@ -247,7 +247,7 @@ func wildcardPrefixMatchWithSpecial(a, b string, aSpecial, bSpecial map[rune]str // wildcardSuffixMatch compares whether the suffixes of a and b are equal up // to the shortest one. The suffix is defined as the longest substring that ends // at the string length and does not contain a wildcard. -func wildcardSuffixMatch(a, b string) bool { +func wildcardSuffixMatch(a, b string) bool { //nolint:all return wildcardSuffixMatchWithSpecial(a, b, nil, nil) }