~turminal/go-fnmatch

1a55764af6de09d8612bf0bf309efca566581506 — Bor Grošelj Simić 2 years ago 2d6f71d master
limit recursion to max 25 asterisks

The algorithm used is exponential in number of '*' characters. By
limiting the recursion to 25 we can guarantee it will finish reasonably
fast anyway. A testcase for this behavior is provided.

This is a port of the fix for the same bug in NetBSD libc.
2 files changed, 66 insertions(+), 18 deletions(-)

M fnmatch.go
M fnmatch_test.go
M fnmatch.go => fnmatch.go +35 -18
@@ 11,6 11,7 @@ package fnmatch
//   a period must be matched explicitly, but a range will match it too

import (
	"errors"
	"unicode"
	"unicode/utf8"
)


@@ 33,10 34,23 @@ func unpackRune(str *string) rune {
	return rune
}

func Match(pattern, s string, flags int) bool {
	found, err := match(pattern, s, flags, 25)
	if err != nil || !found {
		return false
	} else {
		return true
	}
}

// Matches the pattern against the string, with the given flags,
// and returns true if the match is successful.
// This function should match fnmatch.3 as closely as possible.
func Match(pattern, s string, flags int) bool {
func match(pattern, s string, flags int, limit uint) (bool, error) {
	if limit == 0 {
		return false, errors.New("recursion limit exceeded")
	}

	// The implementation for this function was patterned after the BSD fnmatch.c
	// source found at http://src.gnu-darwin.org/src/contrib/csup/fnmatch.c.html
	noescape := (flags&FNM_NOESCAPE != 0)


@@ 63,14 77,14 @@ func Match(pattern, s string, flags int) bool {
		switch c {
		case '?':
			if len(s) == 0 {
				return false
				return false, nil
			}
			sc := unpackS()
			if pathname && sc == '/' {
				return false
				return false, nil
			}
			if period && sc == '.' && (sLastAtStart || (pathname && sLastSlash)) {
				return false
				return false, nil
			}
		case '*':
			// collapse multiple *'s


@@ 79,20 93,19 @@ func Match(pattern, s string, flags int) bool {
				pattern = pattern[1:]
			}
			if period && s[0] == '.' && (sAtStart || (pathname && sLastUnpacked == '/')) {
				return false
				return false, nil
			}
			// optimize for patterns with * at end or before /
			if len(pattern) == 0 {
				if pathname {
					return leadingdir || (strchr(s, '/') == -1)
					return leadingdir || (strchr(s, '/') == -1), nil
				} else {
					return true
					return true, nil
				}
				return !(pathname && strchr(s, '/') >= 0)
			} else if pathname && pattern[0] == '/' {
				offset := strchr(s, '/')
				if offset == -1 {
					return false
					return false, nil
				} else {
					// we already know our pattern and string have a /, skip past it
					s = s[offset:] // use unpackS here to maintain our bookkeeping state


@@ 105,23 118,27 @@ func Match(pattern, s string, flags int) bool {
			for test := s; len(test) > 0; unpackRune(&test) {
				// I believe the (flags &^ FNM_PERIOD) is a bug when FNM_PATHNAME is specified
				// but this follows exactly from how fnmatch.c implements it
				if Match(pattern, test, (flags &^ FNM_PERIOD)) {
					return true
				found, err :=  match(pattern, test, (flags &^ FNM_PERIOD), limit-1)
				if err != nil {
					return false, err
				}
				if found {
					return true, nil
				} else if pathname && test[0] == '/' {
					break
				}
			}
			return false
			return false, nil
		case '[':
			if len(s) == 0 {
				return false
				return false, nil
			}
			if pathname && s[0] == '/' {
				return false
				return false, nil
			}
			sc := unpackS()
			if !rangematch(&pattern, sc, flags) {
				return false
				return false, nil
			}
		case '\\':
			if !noescape {


@@ 132,18 149,18 @@ func Match(pattern, s string, flags int) bool {
			fallthrough
		default:
			if len(s) == 0 {
				return false
				return false, nil
			}
			sc := unpackS()
			switch {
			case sc == c:
			case casefold && unicode.ToLower(sc) == unicode.ToLower(c):
			default:
				return false
				return false, nil
			}
		}
	}
	return len(s) == 0 || (leadingdir && s[0] == '/')
	return len(s) == 0 || (leadingdir && s[0] == '/'), nil
}

func rangematch(pattern *string, test rune, flags int) bool {

M fnmatch_test.go => fnmatch_test.go +31 -0
@@ 2,6 2,7 @@ package fnmatch

import (
	"testing"
	"time"
)

// This is a set of tests ported from a set of tests for C fnmatch


@@ 347,3 348,33 @@ func TestFNMLeadingDir(t *testing.T) {
		}
	}
}

func TestRecursionLimit(t *testing.T) {
	timeout := time.After(10 * time.Second)
	success := make(chan bool)
	go func() {
		success <- Match("x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*", "xxxxxxxxxxxxxxxxxxxxxxxx", 0)
	}()

	select {
	case <-timeout:
		t.Errorf("Timed out\n");
	case found := <-success:
		if found {
			t.Errorf("Failed\n");
		}
	}

	go func() {
		success <- Match("x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 0)
	}()

	select {
	case <-timeout:
		t.Errorf("Timed out\n");
	case found := <-success:
		if found {
			t.Errorf("Failed\n");
		}
	}
}