Re: ensure-directories aka create-directories Lassi Kortela 02 Aug 2020 10:20 UTC

> Gauche took the layered approach, i.e. POSIX.1 calls are mostly
> supported as built-in (with sys-* name), and POSIX.2 features are built
> on top of it (e.g. file.util module).
>
> However, I browsed my file.util implementation again, and now I tend to
> agree with Lassi that 'mkdir -p' is tricky yet very often used (I now
> remember I had wrong implementation with a race condition and fixed it
> sometime ago.)
> copy-file is another one that requires more code than one naively
> thinks, but it is probably less frequently used.
>
> So yeah, I'm now fine with having 'mkdir -p'.

I tried writing it in C, and it's _very_ hard!

Now I'm no longer sure that it should be in a fundamental SRFI like 170
:D Maybe it doesn't matter which SRFI it goes in, as long as we have it.
I do agree that it's useful much more often than others like it.

The code below is wrong because it uses readlink() to normalize the
`path` argument, but that fails when it doesn't exist. And looping over
a non-normalized pathname would probably be prone to subtle bugs.

#include <sys/stat.h>

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static int ensure_directory(const char *path, mode_t mode) {
   struct stat st;
   char *buf;
   char *limit;
   char oldc;

   if (!(buf = realpath(path, NULL))) {
     return -1;
   }
   fprintf(stderr, "real = %s\n", buf);
   limit = buf;
   for (;;) {
     while (*limit == '/')
       limit++;
     while (*limit && (*limit != '/'))
       limit++;
     oldc = *limit;
     *limit = '\0';
     fprintf(stderr, "making %s\n", buf);
     if (mkdir(buf, mode) == 0)
       break;
     if ((errno != EEXIST) && (errno != EISDIR)) {
       free(buf);
       return -1;
     }
     *limit = oldc;
     if (!oldc)
       break;
     limit++;
   }
   if (lstat(buf, &st) == -1) {
     free(buf);
     return -1;
   }
   free(buf);
   if (!S_ISDIR(st.st_mode)) {
     errno = ENOTDIR;
     return -1;
   }
   return 0;
}

int main(int argc, char **argv) {
   const char *path;

   if (argc != 2) {
     fprintf(stderr, "usage: path\n");
     return 1;
   }
   path = argv[1];
   if (ensure_directory(path, 0600) == -1) {
     fprintf(stderr, "%s\n", strerror(errno));
     return 1;
   }
   printf("ok\n");
   return 0;
}

> OTOH, 'rm -r' doesn't
> seem too many pitfalls so probably we won't need it.

That's probably right. We only need to make sure we don't follow
symlinks at any point.

`rm -rf` also probably should stop at file system boundaries (where
struct stat.st_dev changes) like `rsync -x`. But trying to rmdir() a
mount point probably fails, which accomplishes the same thing.