diff --git a/html/filters.html b/html/filters.html index 18d6ddb8..1ec7b520 100644 --- a/html/filters.html +++ b/html/filters.html @@ -247,7 +247,7 @@

1.a. Scan rules based on URL or extension

the \ character - *[\[\]] + *[\[,\]] the [ or ] character diff --git a/src/htsfilters.c b/src/htsfilters.c index 53d7f327..e07a5d82 100644 --- a/src/htsfilters.c +++ b/src/htsfilters.c @@ -193,7 +193,12 @@ HTS_INLINE const char *strjoker(const char *chaine, const char *joker, LLint * s int len = (int) strlen(joker); while((joker[i] != RIGHT) && (joker[i]) && (i < len)) { - if ((joker[i] == '<') || (joker[i] == '>')) { // *[<10] + // '\' escapes the next char as a literal member, e.g. *[\[\]] + if (joker[i] == '\\' && joker[i + 1] != '\0') { + i++; + pass[(int) (unsigned char) joker[i]] = 1; + i++; + } else if ((joker[i] == '<') || (joker[i] == '>')) { // *[<10] int lsize = 0; int lverdict; @@ -221,7 +226,9 @@ HTS_INLINE const char *strjoker(const char *chaine, const char *joker, LLint * s while(isdigit((unsigned char) joker[i])) i++; } - } else if (joker[i + 1] == '-') { // 2 car, ex: *[A-Z] + } else if (joker[i + 1] == '-' && joker[i + 2] != '\0') { + // range *[A-Z]; the '\0' guard rejects a truncated *[a- (else + // i+=3 overshoots the NUL) if ((int) (unsigned char) joker[i + 2] > (int) (unsigned char) joker[i]) { int j; @@ -233,10 +240,7 @@ HTS_INLINE const char *strjoker(const char *chaine, const char *joker, LLint * s } // else err=1; i += 3; - } else { // 1 car, ex: *[ ] - if (joker[i + 2] == '\\' && joker[i + 3] != 0) { // escaped char, such as *[\[] or *[\]] - i++; - } + } else { // 1 car, ex: *[ ] pass[(int) (unsigned char) joker[i]] = 1; i++; } diff --git a/src/htsselftest.c b/src/htsselftest.c index aca1c96b..1ab68a8b 100644 --- a/src/htsselftest.c +++ b/src/htsselftest.c @@ -512,15 +512,21 @@ static int string_safety_selftests(void) { /* ------------------------------------------------------------ */ static int st_filter(httrackp *opt, int argc, char **argv) { + char *str, *pat; + int matched; + (void) opt; if (argc < 2) { fprintf(stderr, "filter: needs a filter pattern and a string\n"); return 1; } - if (strjoker(argv[1], argv[0], NULL, NULL)) - printf("%s does match %s\n", argv[1], argv[0]); - else - printf("%s does NOT match %s\n", argv[1], argv[0]); + /* exact-size heap copies so a sanitizer traps any over-read of the pattern */ + str = strdupt(argv[1]); + pat = strdupt(argv[0]); + matched = strjoker(str, pat, NULL, NULL) != NULL; + printf("%s does %s %s\n", argv[1], matched ? "match" : "NOT match", argv[0]); + freet(str); + freet(pat); return 0; } diff --git a/tests/01_engine-filter.test b/tests/01_engine-filter.test index 528592bb..ddddf5dc 100755 --- a/tests/01_engine-filter.test +++ b/tests/01_engine-filter.test @@ -50,27 +50,54 @@ match '*foo*bar' 'foozbar' # '?' is the query-string marker, not a single-char wildcard nomatch 'a?c' 'abc' -# backslash escapes a metacharacter inside a class so it is matched literally. -# Quirk: the decoder also adds the backslash itself to the set, so '\X' matches -# both X and '\'. These assertions pin that behavior. +# Inside a class, backslash escapes the next char as a literal member (#148): +# '\X' matches X only (not '\'), and an escaped ']' is a member, not the terminator. match '*[\*]' '*' -match '*[\*]' "\\" -nomatch '*[\*]' 'a' +nomatch '*[\*]' "\\" match '*[\\]' "\\" -nomatch '*[\\]' 'a' +nomatch '*[\\]' '*' match '*[\[]' '[' -match '*[\[]' "\\" -nomatch '*[\[]' 'a' - -# A literal ']' cannot be a class member: the class parser stops at the first -# ']', escaped or not. So '*[\[\]]' does NOT mean "the [ or ] character" as the -# filter guide claims (GitHub #148); it parses as the class {'[','\'} followed -# by a trailing literal ']'. These assertions document the current (buggy) -# behavior so any future matcher fix is a deliberate, visible change. -nomatch '*[\[\]]' '[' # not matched, despite the docs -match '*[\[\]]' ']' # only via the empty class-match + trailing ']' -match '*[\[\]]' '[]' # one of {'[','\'} then the trailing ']' -nomatch '*[\[\]]' '[]x' +nomatch '*[\[]' "\\" +match '*[\]]' ']' +nomatch '*[\]]' "\\" + +# '*[\[\]]' is "the [ or ] character", as the filter guide documents. +match '*[\[\]]' '[' +match '*[\[\]]' ']' +nomatch '*[\[\]]' 'a' +match '*[\[,\]]' '[' # comma between members is optional +match '*[\[,\]]' ']' +match '*[a,\[]' 'a' # an escaped member no longer eats the preceding one +match '*[a,\[]' '[' + +# Escape is decoded before the range/separator/size checks, so '\-' '\,' '\<' +# are literal members, not operators. +match '*[a\-z]' 'a' +match '*[a\-z]' 'z' +nomatch '*[a\-z]' 'b' # not the a..z range +match '*[\,]' ',' +nomatch '*[\,]' "\\" # the escape must not leak '\' into the class +match '*[\<]' '<' +nomatch '*[\<]' "\\" +match '*[\[,\],a]' '[' +match '*[\[,\],a]' ']' +match '*[\[,\],a]' 'a' + +# A truncated range '*[a-' is the literal members {a,-}; the parser must not +# read past the end decoding it (was a 1-byte heap over-read in the range arm). +match '*[a-' 'a' +nomatch '*[a-' 'b' + +# *(...) matches exactly one char from the class; *[...] matches a run. +match '*(a,b)' 'a' +nomatch '*(a,b)' 'aa' +nomatch '*(a,b)' 'c' + +# documented composite filters (filters.html) +match 'www.*[path].com/*[path].zip' 'www.foo.com/a/b.zip' +nomatch 'www.*[path].com/*[path].zip' 'www.foo.com/a/b.tar' +match '*.html*[]' 'page.html' +nomatch '*.html*[]' 'page.html?x=1' # *[] forbids the trailing query # Size-based rules (-#test=filtersize ): a negative size # means the size is still unknown (scan time). A size exclusion must stay neutral