]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
add wrapper API for iterating files, directories, or globs
authorAlan T. DeKok <aland@freeradius.org>
Tue, 23 Sep 2025 15:39:29 +0000 (11:39 -0400)
committerAlan T. DeKok <aland@freeradius.org>
Wed, 24 Sep 2025 15:07:53 +0000 (11:07 -0400)
src/lib/util/file.c
src/lib/util/file.h

index 2ac616577d4367233b5c8a828bad0f6d5731e743..710cc52ec6941ff893be08ee716e1779e0ec8153 100644 (file)
@@ -428,3 +428,416 @@ int fr_dirfd(int *dirfd, char const **filename, char const *pathname)
                return (*dirfd < 0) ? -1 : 0;
        }
 }
+
+#ifdef HAVE_DIRENT_H
+/*
+ *     Filter filenames when reading a directory.
+ */
+static int fr_globdir_dir_next(char const **filename, fr_globdir_iter_t *iter)
+{
+       struct dirent *dp;
+       char const *p;
+       size_t len;
+
+       fr_assert(iter->dir != NULL);
+
+       /*
+        *      Reading all of the directory entries will result in
+        *      reading ones we don't want, so we filter them.
+        */
+       while (true) {
+               errno = 0;
+               dp = readdir(iter->dir);
+               if (!dp) {
+                       if (errno != 0) return -1;
+
+                       *filename = NULL;
+                       return 0;
+               }
+
+               /*
+                *      Filter the filenames.
+                */
+               if (dp->d_name[0] == '.') continue;
+
+               /*
+                *      Check for valid characters
+                */
+               for (p = dp->d_name; *p != '\0'; p++) {
+                       if (isalpha((uint8_t)*p) ||
+                           isdigit((uint8_t)*p) ||
+                           (*p == '-') ||
+                           (*p == '_') ||
+                           (*p == '.')) continue;
+                       break;
+               }
+
+               /*
+                *      Invalid character in filename.  Skip it.
+                */
+               if (*p != '\0') continue;
+
+               /*
+                *      Ignore config files generated by deb / rpm packaging updates.
+                */
+               len = strlen(dp->d_name);
+               if ((len > 10) && (strncmp(&dp->d_name[len - 10], ".dpkg-dist", 10) == 0)) continue;
+               if ((len > 9) && (strncmp(&dp->d_name[len - 9], ".dpkg-old", 9) == 0)) continue;
+               if ((len > 7) && (strncmp(&dp->d_name[len - 7], ".rpmnew", 9) == 0)) continue;
+               if ((len > 8) && (strncmp(&dp->d_name[len - 8], ".rpmsave", 10) == 0)) continue;
+       }
+
+       *filename = dp->d_name;
+       return 0;
+}
+#endif
+
+/** Create a full path from dir + pattern.
+ *
+ */
+static int fr_globdir_get_path(char const *dir, char const *pattern, fr_globdir_iter_t *iter)
+{
+       char const *p;
+       char *q;
+       bool slash;
+       size_t size = strlen(dir) + 1 + strlen(pattern) + 1;
+
+       /*
+        *      Bootstrap the full path.
+        */
+       iter->path = malloc(size);
+       if (!iter->path) {
+               errno = ENOMEM;
+               return -1;
+       }
+
+       /*
+        *      Sanity check the directory name.
+        *
+        *      We assume that the directory name is otherwise safe.
+        */
+       p = dir;
+       q = iter->path;
+       while (*p) {
+               /*
+                *      Suppress duplicate '/', and trailing '/'.
+                */
+               if (*p == '/') {
+                       *(q++) = *(p++);
+
+                       while (*p == '/') p++;
+
+                       if (!p[1]) break;
+                       continue;
+               }
+
+               if ((size_t) (q - iter->path) >= size) {
+                       free(iter->path);
+                       errno = ENOMEM;
+                       return -1;
+               }
+
+               *(q++) = *(p++);
+       }
+
+       /*
+        *      Add in a trailing '/' to the directory.
+        */
+       *(q++) = '/';
+       slash = true;
+       iter->filename = q;
+
+       /*
+        *      Copy over the pattern name, but sanity check it.
+        */
+       p = pattern;
+       while (*p) {
+               if ((size_t) (q - iter->path) >= size) {
+                       free(iter->path);
+                       errno = ENOMEM;
+                       return -1;
+               }
+
+               /*
+                *      @todo - if it's a pattern, then handle [./] as a special case.
+                */
+
+               /*
+                *      Handle the case of bad things after the directory name, too.
+                *
+                *      But otherwise copy over any '/' which we see in the pattern.
+                */
+               if (!slash) {
+                       slash = (*p == '/');
+                       *(q++) = *(p++);
+
+                       if (!slash) continue;
+               }
+
+       more_slash:
+               /*
+                *      ///// --> /
+                */
+               while (*p == '/') p++;
+
+               /*
+                *      foo/. may be special
+                */
+               if (*p != '.') {
+                       slash = false;
+                       continue;
+               }
+
+               /*
+                *      foo/./ --> foo/
+                */
+               if (p[1] == '/') {
+                       p += 2;
+                       goto more_slash;
+               }
+
+               /*
+                *      foo/../ --> error
+                */
+               if ((p[1] == '.') && (p[2] == '/')) {
+                       free(iter->path);
+                       errno = ENOENT;
+                       return -1;
+               }
+
+               /*
+                *      foo/.bar is OK
+                */
+               slash = false;
+       }
+       *q = '\0';
+
+       return 0;
+}
+
+/** Initialize an iterator over filenames.
+ *
+ * @param[out] filename        the _relative_ filename which should be opened
+ * @param[in] dir      the directory to read
+ * @param[in] pattern  the filename or pattern to read.  Cannot be an absolute path.
+ * @param[in,out] iter the iteration structure.
+ * @return
+ *     - <0 on error
+ *     - 0 on success, but no filename
+ *     - 1 for "have file".
+ */
+int fr_globdir_iter_init(char const **filename, char const *dir, char const *pattern, fr_globdir_iter_t *iter)
+{
+       char const *p;
+       char const *to_open;
+
+       /*
+        *      Default to files, which is the most common case.
+        */
+       *iter = (fr_globdir_iter_t) {
+               .type= FR_GLOBDIR_FILE,
+       };
+
+       /*
+        *      Figure out what kind of thing we're opening.
+        */
+       for (p = pattern; *p != '\0'; p++) {
+               /*
+                *      foo/ - read the entire directory.
+                */
+               if (*p == '/') {
+                       if (p[1]) continue;
+
+#ifdef HAVE_DIRENT_H
+                       iter->type = FR_GLOBDIR_DIR;
+                       break;
+#else
+                       errno = ENOENT;
+                       return -1;
+#endif
+               }
+
+               /*
+                *      foo*.txt
+                *      foo?.txt
+                *      foo.[ch]
+                *
+                *      - file globbing.
+                *
+                *      File globbing is either full path, or a path relative to CWD.  It is most notably NOT
+                *      relative to the input "dir".  So if there are globs, we need a full path.
+                */
+               if ((*p == '*') || (*p == '?') || (*p == '[')) {
+#ifdef HAVE_GLOB_H
+                       if ((pattern[0] != '/') && (dir[0] != '/')) {
+                               errno = ENOENT;
+                               return -1;
+                       }
+
+                       iter->type = FR_GLOBDIR_GLOB;
+                       break;
+#else
+                       errno = ENOENT;
+                       return -1;
+#endif
+               }
+       }
+
+       /*
+        *      The pattern is an absolute path, we just use that as-is.
+        */
+       if (pattern[0] == '/') {
+               to_open = pattern;
+
+       } else if (iter->type == FR_GLOBDIR_FILE) {
+               /*
+                *      Short-circuit the common case for files.  We're just opening a file, and the file is
+                *      relative to the directory which was passed in.
+                */
+               *filename = pattern;
+               return 1;
+
+       } else {
+               /*
+                *      Either dir is absolute and pattern is relative, or they're both relative.  Merge dir +
+                *      pattern into a path.
+                *
+                *      Note that globs are relative to CWD, so relative globs must be passed in correctly
+                *      (that's an @todo), otherwise they won't work.
+                */
+               if (fr_globdir_get_path(dir, pattern, iter) < 0) {
+                       return -1;
+               }
+
+               to_open = iter->path;
+       }
+
+       /*
+        *      Now that we know what type of thing it is, go do the
+        *      right thing.
+        */
+       switch (iter->type) {
+       case FR_GLOBDIR_INVALID:
+               (void) fr_globdir_iter_free(iter);
+               errno = ENOENT;
+               return -1;
+
+       case FR_GLOBDIR_FILE:
+               *filename = iter->path;
+               break;
+
+#ifdef HAVE_DIRENT_H
+       case FR_GLOBDIR_DIR:
+               /*
+                *      No directory means no file.  The caller then decides if the file is required.
+                */
+               iter->dir = opendir(to_open);
+               if (!iter->dir) {
+                       (void) fr_globdir_iter_free(iter);
+                       return 0;
+               }
+
+               if (fr_globdir_dir_next(filename, iter) < 0) {
+                       (void) fr_globdir_iter_free(iter);
+                       return -1;
+               }
+               break;
+#endif
+
+#ifdef HAVE_GLOB_H
+       case FR_GLOBDIR_GLOB:
+               if (glob(to_open, GLOB_NOESCAPE | GLOB_ERR, NULL, &iter->glob) < 0) {
+                       (void) fr_globdir_iter_free(iter);
+                       return -1;
+               }
+
+               if (iter->glob.gl_pathc == 0) {
+                       *filename = NULL;
+               } else {
+                       iter->gl_current = 0;
+                       *filename = iter->glob.gl_pathv[iter->gl_current];
+               }
+               break;
+#endif
+       }
+
+       return 0 + (*filename != NULL);
+}
+
+/** Get the next filename.
+ *
+ *  fr_globdir_iter_init()
+ *  do {
+ *     ... use filename
+ *  } while (fr_globdir_iter_next() == 1);
+ *  fr_globdir_iter_free()
+ *
+ * @return
+ *     - <0 for error
+ *     - 0 for done
+ *     - 1 for "have filename"
+ */
+int fr_globdir_iter_next(char const **filename, fr_globdir_iter_t *iter)
+{
+       switch (iter->type) {
+       case FR_GLOBDIR_INVALID:
+               break;
+
+       case FR_GLOBDIR_FILE:
+               *filename = NULL;
+               return 0;
+
+#ifdef HAVE_DIRENT_H
+       case FR_GLOBDIR_DIR:
+               if (fr_globdir_dir_next(filename, iter) < 0) return -1;
+
+               return 0 + (*filename != NULL);
+#endif
+
+#ifdef HAVE_GLOB_H
+       case FR_GLOBDIR_GLOB:
+               iter->gl_current++;
+               if (iter->gl_current >= iter->glob.gl_pathc) {
+                       return 0;
+               }
+
+               *filename = iter->glob.gl_pathv[iter->gl_current];
+               fr_assert(*filename != NULL);
+
+               return 1;
+#endif
+       }
+
+       return -1;
+}
+
+int fr_globdir_iter_free(fr_globdir_iter_t *iter)
+{
+       if (iter->path) {
+               free(iter->path);
+               iter->path = NULL;
+       }
+
+       switch (iter->type) {
+       case FR_GLOBDIR_INVALID:
+               return -1;
+
+       case FR_GLOBDIR_FILE:
+               break;
+
+#ifdef HAVE_DIRENT_H
+       case FR_GLOBDIR_DIR:
+               if (!iter->dir) return 0;
+
+               return closedir(iter->dir);
+#endif
+
+#ifdef HAVE_GLOB_H
+       case FR_GLOBDIR_GLOB:
+               globfree(&iter->glob);
+               break;
+#endif
+       }
+
+       return 0;
+}
index a95d4ecbddee28a251d2f0c415fa7c7557fe8951..55f19fb1158e6ed87fd9831fa138ba3d12cffc27 100644 (file)
@@ -31,6 +31,14 @@ extern "C" {
 #include <freeradius-devel/util/talloc.h>
 #include <stdbool.h>
 
+#ifdef HAVE_DIRENT_H
+#  include <dirent.h>
+#endif
+
+#ifdef HAVE_GLOB_H
+#include <glob.h>
+#endif
+
 /** Callback for allowing additional operations on newly created directories
  *
  * @param[in] fd       Of newly created directory.
@@ -64,6 +72,40 @@ char const   *fr_cwd_strip(char const *filename);
 
 int            fr_dirfd(int *dirfd, char const **filename, char const *pathname) CC_HINT(nonnull);
 
+typedef enum {
+       FR_GLOBDIR_INVALID = 0,
+       FR_GLOBDIR_FILE,
+#ifdef HAVE_DIRENT_H
+       FR_GLOBDIR_DIR,
+#endif
+#ifdef HAVE_GLOB_H
+       FR_GLOBDIR_GLOB,
+#endif
+} fr_globdir_type_t;
+
+typedef struct {
+       fr_globdir_type_t       type;
+
+       bool                    opened;
+
+       char                    *path;
+       char                    *filename;
+
+       union {
+               DIR             *dir;
+#ifdef HAVE_GLOB_H
+               struct {
+                       size_t  gl_current;
+                       glob_t  glob;
+               };
+#endif
+       };
+} fr_globdir_iter_t;
+
+int            fr_globdir_iter_init(char const **filename, char const *dir, char const *pattern, fr_globdir_iter_t *iter);
+int            fr_globdir_iter_next(char const **filename, fr_globdir_iter_t *iter);
+int            fr_globdir_iter_free(fr_globdir_iter_t *iter);
+
 #ifdef __cplusplus
 }
 #endif