]> Pileus Git - ~andy/gtk/blobdiff - gtk/fnmatch.c
System fnmatch wasn't going to be UTF-8 clean, neither was our version.
[~andy/gtk] / gtk / fnmatch.c
index 198363fc84963923ce6bbd9887fc7b6aeef7987c..e6ccb4073d2e4afff52afea8188594125ab4bff7 100644 (file)
  * GTK+ at ftp://ftp.gtk.org/pub/gtk/. 
  */
 
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
+/*
+ * Stripped down, converted to UTF-8 and test cases added
+ *
+ *                    Owen Taylor, 13 December 2002;
+ */
 
-#include <errno.h>
-#include <ctype.h> /* tolower */
+#include <string.h>
 
-/* Added for GTK. We need to make sure that all constants are defined
- * to properly compile this file */
+#include <glib.h>
+
+/* We need to make sure that all constants are defined
+ * to properly compile this file
+ */
 #ifndef _GNU_SOURCE
 #define _GNU_SOURCE
 #endif
-#include "fnmatch.h"
 
-/* We need glib.h for G_DIR_SEPARATOR and G_OS_WIN32 */
-#include <glib.h>
+static gunichar
+get_char (const char **str)
+{
+  gunichar c = g_utf8_get_char (*str);
+  *str = g_utf8_next_char (*str);
 
+#if defined(G_OS_WIN32) || defined(G_WITH_CYGWIN)
+  if (c <= 0xff)
+      c = g_ascii_tolower (c);
+#endif
 
-/* Comment out all this code if we are using the GNU C Library, and are not
-   actually compiling the library itself.  This code is part of the GNU C
-   Library, but also included in many other GNU distributions.  Compiling
-   and linking in this code is a waste when using the GNU C library
-   (especially if it is a shared library).  Rather than having every GNU
-   program understand `configure --with-gnu-libc' and omit the object files,
-   it is simpler to just do this in the source for each such file.  */
+  return c;
+}
 
-#if defined (_LIBC) || !defined (__GNU_LIBRARY__)
+#if defined(G_OS_WIN32) || defined(G_WITH_CYGWIN)
+#define DO_ESCAPE 0
+#else  
+#define DO_ESCAPE 1
+#endif  
 
-#if !defined(__GNU_LIBRARY__) && !defined(STDC_HEADERS)
-extern int errno;
-#endif
+static gunichar
+get_unescaped_char (const char **str,
+                   gboolean    *was_escaped)
+{
+  gunichar c = get_char (str);
+
+  *was_escaped = DO_ESCAPE && c == '\\';
+  if (*was_escaped)
+    c = get_char (str);
+  
+  return c;
+}
 
 /* Match STRING against the filename pattern PATTERN, returning zero if
    it matches, nonzero if not.  */
-int
-fnmatch (pattern, string, flags)
-     const char *pattern;
-     const char *string;
-     int flags;
-{
-  register const char *p = pattern, *n = string;
-  register char c;
-
-/* Note that this evalutes C many times.  */
-#if !defined(G_OS_WIN32) && !defined(G_WITH_CYGWIN)
-#define FOLD(c)        ((flags & FNM_CASEFOLD) && isupper ((unsigned char )(c)) ? tolower ((unsigned char)(c)) : (c))
-#else
-#define FOLD(c)        (tolower ((unsigned char)(c)))
-#endif
 
-  while ((c = *p++) != '\0')
+static gboolean
+gtk_fnmatch_intern (const char *pattern,
+                   const char *string,
+                   gboolean   component_start)
+{
+  const char *p = pattern, *n = string;
+  
+  while (*p)
     {
-      c = FOLD (c);
-
+      const char *last_n = n;
+      
+      gunichar c = get_char (&p);
+      gunichar nc = get_char (&n);
+      
       switch (c)
        {
-       case '?':
-         if (*n == '\0')
-           return FNM_NOMATCH;
-         else if ((flags & FNM_FILE_NAME) && *n == G_DIR_SEPARATOR)
-           return FNM_NOMATCH;
-         else if ((flags & FNM_PERIOD) && *n == '.' &&
-                  (n == string || ((flags & FNM_FILE_NAME) && n[-1] == G_DIR_SEPARATOR)))
-           return FNM_NOMATCH;
+       case '?':
+         if (nc == '\0')
+           return FALSE;
+         else if (nc == G_DIR_SEPARATOR)
+           return FALSE;
+         else if (nc == '.' && component_start)
+           return FALSE;
          break;
-#ifndef G_OS_WIN32
        case '\\':
-         if (!(flags & FNM_NOESCAPE))
-           {
-             c = *p++;
-             c = FOLD (c);
-           }
-         if (FOLD (*n) != c)
-           return FNM_NOMATCH;
+         if (DO_ESCAPE)
+           c = get_char (&p);
+         if (nc != c)
+           return FALSE;
          break;
-#endif
        case '*':
-         if ((flags & FNM_PERIOD) && *n == '.' &&
-             (n == string || ((flags & FNM_FILE_NAME) && n[-1] == G_DIR_SEPARATOR)))
-           return FNM_NOMATCH;
+         if (nc == '.' && component_start)
+           return FALSE;
+
+         {
+           const char *last_p = p;
+
+           for (last_p = p, c = get_char (&p);
+                c == '?' || c == '*';
+                last_p = p, c = get_char (&p))
+             {
+               if (c == '?')
+                 {
+                   if (nc == '\0')
+                     return FALSE;
+                   else if (nc == G_DIR_SEPARATOR)
+                     return FALSE;
+                   else
+                     {
+                       last_n = n; nc = get_char (&n);
+                     }
+                 }
+             }
 
-         for (c = *p++; c == '?' || c == '*'; c = *p++, ++n)
-           if (((flags & FNM_FILE_NAME) && *n == G_DIR_SEPARATOR) ||
-               (c == '?' && *n == '\0'))
-             return FNM_NOMATCH;
+           /* If the pattern ends with wildcards, we have a
+            * guaranteed match unless there is a dir separator
+            * in the remainder of the string.
+            */
+           if (c == '\0')
+             {
+               if (strchr (last_n, G_DIR_SEPARATOR) != NULL)
+                 return FALSE;
+               else
+                 return TRUE;
+             }
 
-         if (c == '\0')
-           return 0;
+           if (DO_ESCAPE && c == '\\')
+             c = get_char (&p);
 
-         {
-#ifndef G_OS_WIN32
-           char c1 = (!(flags & FNM_NOESCAPE) && c == '\\') ? *p : c;
-#else
-           char c1 = c;
-#endif
-           c1 = FOLD (c1);
-           for (--p; *n != '\0'; ++n)
-             if ((c == '[' || FOLD (*n) == c1) &&
-                 fnmatch (p, n, flags & ~FNM_PERIOD) == 0)
-               return 0;
-           return FNM_NOMATCH;
+           for (p = last_p; nc != '\0';)
+             {
+               if ((c == '[' || nc == c) &&
+                   gtk_fnmatch_intern (p, last_n, component_start))
+                 return TRUE;
+               
+               component_start = (nc == G_DIR_SEPARATOR);
+               last_n = n;
+               nc = get_char (&n);
+             }
+                 
+           return FALSE;
          }
 
        case '[':
          {
            /* Nonzero if the sense of the character class is inverted.  */
-           register int not;
+           gboolean not;
+           gboolean was_escaped;
 
-           if (*n == '\0')
-             return FNM_NOMATCH;
+           if (nc == '\0' || nc == G_DIR_SEPARATOR)
+             return FALSE;
 
-           if ((flags & FNM_PERIOD) && *n == '.' &&
-               (n == string || ((flags & FNM_FILE_NAME) && n[-1] == G_DIR_SEPARATOR)))
-             return FNM_NOMATCH;
+           if (nc == '.' && component_start)
+             return FALSE;
 
            not = (*p == '!' || *p == '^');
            if (not)
              ++p;
 
-           c = *p++;
+           c = get_unescaped_char (&p, &was_escaped);
            for (;;)
              {
-               register char cstart = c, cend = c;
-#ifndef G_OS_WIN32
-               if (!(flags & FNM_NOESCAPE) && c == '\\')
-                 cstart = cend = *p++;
-#endif
-               cstart = cend = FOLD (cstart);
-
+               register gunichar cstart = c, cend = c;
                if (c == '\0')
                  /* [ (unterminated) loses.  */
-                 return FNM_NOMATCH;
+                 return FALSE;
 
-               c = *p++;
-               c = FOLD (c);
-
-               if ((flags & FNM_FILE_NAME) && c == G_DIR_SEPARATOR)
-                 /* [/] can never match.  */
-                 return FNM_NOMATCH;
-
-               if (c == '-' && *p != ']')
+               c = get_unescaped_char (&p, &was_escaped);
+               
+               if (!was_escaped && c == '-' && *p != ']')
                  {
-                   cend = *p++;
-#ifndef G_OS_WIN32
-                   if (!(flags & FNM_NOESCAPE) && cend == '\\')
-                     cend = *p++;
-#endif
+                   cend = get_unescaped_char (&p, &was_escaped);
                    if (cend == '\0')
-                     return FNM_NOMATCH;
-                   cend = FOLD (cend);
+                     return FALSE;
 
-                   c = *p++;
+                   c = get_char (&p);
                  }
 
-               if (FOLD (*n) >= cstart && FOLD (*n) <= cend)
+               if (nc >= cstart && nc <= cend)
                  goto matched;
 
-               if (c == ']')
+               if (!was_escaped && c == ']')
                  break;
              }
            if (!not)
-             return FNM_NOMATCH;
+             return FALSE;
            break;
 
          matched:;
            /* Skip the rest of the [...] that already matched.  */
-           while (c != ']')
+           /* XXX 1003.2d11 is unclear if was_escaped is right.  */
+           while (was_escaped || c != ']')
              {
                if (c == '\0')
                  /* [... (unterminated) loses.  */
-                 return FNM_NOMATCH;
+                 return FALSE;
 
-               c = *p++;
-#ifndef G_OS_WIN32
-               if (!(flags & FNM_NOESCAPE) && c == '\\')
-                 /* XXX 1003.2d11 is unclear if this is right.  */
-                 ++p;
-#endif
+               c = get_unescaped_char (&p, &was_escaped);
              }
            if (not)
-             return FNM_NOMATCH;
+             return FALSE;
          }
          break;
 
        default:
-         if (c != FOLD (*n))
-           return FNM_NOMATCH;
+         if (c != nc)
+           return FALSE;
        }
 
-      ++n;
+      component_start = (nc == G_DIR_SEPARATOR);
     }
 
   if (*n == '\0')
-    return 0;
+    return TRUE;
+
+  return FALSE;
+}
+
+/* Match STRING against the filename pattern PATTERN, returning zero if
+ *  it matches, nonzero if not.
+ *
+ * GTK+ used to use a old version of GNU fnmatch() that was buggy
+ * in various ways and didn't handle UTF-8. The following is
+ * converted to UTF-8. To simplify the process of making it
+ * correct, this is special-cased to the combinations of flags
+ * that gtkfilesel.c uses.
+ *
+ *   FNM_FILE_NAME   - always set
+ *   FNM_LEADING_DIR - never set
+ *   FNM_PERIOD      - always set
+ *   FNM_NOESCAPE    - set only on windows
+ *   FNM_CASEFOLD    - set only on windows
+ */
+gboolean
+_gtk_fnmatch (const char *pattern,
+             const char *string)
+{
+  return gtk_fnmatch_intern (pattern, string, TRUE);
+}
+
+#define FNMATCH_TEST_CASES
+#ifdef FNMATCH_TEST_CASES
 
-  if ((flags & FNM_LEADING_DIR) && *n == G_DIR_SEPARATOR)
-    /* The FNM_LEADING_DIR flag says that "foo*" matches "foobar/frobozz".  */
-    return 0;
+#define TEST(pat, str, result) \
+  g_assert (_gtk_fnmatch ((pat), (str)) == result)
 
-  return FNM_NOMATCH;
+int main (int argc, char **argv)
+{
+  TEST ("[a-]", "-", TRUE);
+  
+  TEST ("a", "a", TRUE);
+  TEST ("a", "b", FALSE);
+
+  /* Test what ? matches */
+  TEST ("?", "a", TRUE);
+  TEST ("?", ".", FALSE);
+  TEST ("a?", "a.", TRUE);
+  TEST ("a/?", "a/b", TRUE);
+  TEST ("a/?", "a/.", FALSE);
+  TEST ("?", "/", FALSE);
+
+  /* Test what * matches */
+  TEST ("*", "a", TRUE);
+  TEST ("*", ".", FALSE);
+  TEST ("a*", "a.", TRUE);
+  TEST ("a/*", "a/b", TRUE);
+  TEST ("a/*", "a/.", FALSE);
+  TEST ("*", "/", FALSE);
+
+  /* Range tests */
+  TEST ("[ab]", "a", TRUE);
+  TEST ("[ab]", "c", FALSE);
+  TEST ("[^ab]", "a", FALSE);
+  TEST ("[!ab]", "a", FALSE);
+  TEST ("[^ab]", "c", TRUE);
+  TEST ("[!ab]", "c", TRUE);
+  TEST ("[a-c]", "b", TRUE);
+  TEST ("[a-c]", "d", FALSE);
+  TEST ("[a-]", "-", TRUE);
+  TEST ("[]]", "]", TRUE);
+  TEST ("[^]]", "a", TRUE);
+  TEST ("[!]]", "a", TRUE);
+
+  /* Various unclosed ranges */
+  TEST ("[ab", "a", FALSE);
+  TEST ("[a-", "a", FALSE);
+  TEST ("[ab", "c", FALSE);
+  TEST ("[a-", "c", FALSE);
+  TEST ("[^]", "a", FALSE);
+
+  /* Ranges and special no-wildcard matches */
+  TEST ("[.]", ".", FALSE);
+  TEST ("a[.]", "a.", TRUE);
+  TEST ("a/[.]", "a/.", FALSE);
+  TEST ("[/]", "/", FALSE);
+  TEST ("[^/]", "a", TRUE);
+  
+  /* Basic tests of * (and combinations of * and ?) */
+  TEST ("a*b", "ab", TRUE);
+  TEST ("a*b", "axb", TRUE);
+  TEST ("a*b", "axxb", TRUE);
+  TEST ("a**b", "ab", TRUE);
+  TEST ("a**b", "axb", TRUE);
+  TEST ("a**b", "axxb", TRUE);
+  TEST ("a*?*b", "ab", FALSE);
+  TEST ("a*?*b", "axb", TRUE);
+  TEST ("a*?*b", "axxb", TRUE);
+
+  /* Test of  *[range] */
+  TEST ("a*[cd]", "ac", TRUE);
+  TEST ("a*[cd]", "axc", TRUE);
+  TEST ("a*[cd]", "axx", FALSE);
+
+  TEST ("a/[.]", "a/.", FALSE);
+  TEST ("a*[.]", "a/.", FALSE);
+
+  /* Test of UTF-8 */
+
+  TEST ("ä", "ä", TRUE);      /* TEST ("ä", "ä", TRUE); */
+  TEST ("?", "ä", TRUE);       /* TEST ("?", "ä", TRUE); */
+  TEST ("*ö", "äö", TRUE);   /* TEST ("*ö", "äö", TRUE); */
+  TEST ("*ö", "ääö", TRUE); /* TEST ("*ö", "ääö", TRUE); */
+  TEST ("[ä]", "ä", TRUE);    /* TEST ("[ä]", "ä", TRUE); */
+  TEST ("[ä-ö]", "é", TRUE); /* TEST ("[ä-ö]", "é", TRUE); */
+  TEST ("[ä-ö]", "a", FALSE); /* TEST ("[ä-ö]", "a", FALSE); */
+
+#ifdef DO_ESCAPE
+  /* Tests of escaping */
+  TEST ("\\\\", "\\", TRUE);
+  TEST ("\\?", "?", TRUE);
+  TEST ("\\?", "a", FALSE);
+  TEST ("\\*", "*", TRUE);
+  TEST ("\\*", "a", FALSE);
+  TEST ("\\[a-b]", "[a-b]", TRUE);
+  TEST ("[\\\\]", "\\", TRUE);
+  TEST ("[\\^a]", "a", TRUE);
+  TEST ("[a\\-c]", "b", FALSE);
+  TEST ("[a\\-c]", "-", TRUE);
+  TEST ("[a\\]", "a", FALSE);
+#endif /* DO_ESCAPE */
+  
+  return 0;
 }
 
-#endif /* _LIBC or not __GNU_LIBRARY__.  */
+#endif /* FNMATCH_TEST_CASES */