pure-data/src/x_file.c

/* Copyright (c) 2021 IOhannes m zmölnig.
 * For information on usage and redistribution, and for a DISCLAIMER OF ALL
 * WARRANTIES, see the file, "LICENSE.txt," in this distribution.  */
/* The "file" object. */
#define _XOPEN_SOURCE
#define _DEFAULT_SOURCE

#include "m_pd.h"
#include "g_canvas.h"
#include "s_stuff.h"
#include "s_utf8.h"

#include "m_private_utils.h"

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

#include <errno.h>
#ifdef HAVE_UNISTD_H
# include <unistd.h>
#endif

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>

#ifdef _WIN32
# include <windows.h>
# include <direct.h>
# include <io.h>
#else
# include <glob.h>
# include <ftw.h>
#endif

#ifdef _MSC_VER
# include <BaseTsd.h>
typedef unsigned int mode_t;
typedef SSIZE_T ssize_t;
#define wstat
#endif

#ifndef S_ISREG
  #define S_ISREG
#endif
#ifndef S_ISDIR
#define S_ISDIR
#endif

#ifndef X_FILE_DEBUG
#define X_FILE_DEBUG
#endif

#ifdef _WIN32
static int do_delete_ucs2(wchar_t*pathname) {
    struct stat sb;
    if (!wstat(pathname, &sb) && (S_ISDIR(sb.st_mode))) {
            /* a directory */
        return !(RemoveDirectoryW(pathname));
    } else {
            /* probably a file */
        return !(DeleteFileW(pathname));
    }
}

static int sys_stat(const char *pathname, struct stat *statbuf) {
    uint16_t ucs2buf[MAX_PATH];
    u8_utf8toucs2(ucs2buf, MAX_PATH, pathname, strlen(pathname));
    return wstat(ucs2buf, statbuf);
}

static int sys_rename(const char *oldpath, const char *newpath) {
    uint16_t src[MAX_PATH], dst[MAX_PATH];
    u8_utf8toucs2(src, MAX_PATH, oldpath, MAX_PATH);
    u8_utf8toucs2(dst, MAX_PATH, newpath, MAX_PATH);
    return _wrename(src, dst);
}
static int sys_mkdir(const char *pathname, mode_t mode) {
    uint16_t ucs2name[MAX_PATH];
    (void)mode;
    u8_utf8toucs2(ucs2name, MAX_PATH, pathname, MAX_PATH);
    return !(CreateDirectoryW(ucs2name, 0));
}
static int sys_remove(const char *pathname) {
    uint16_t ucs2buf[MAXPDSTRING];
    u8_utf8toucs2(ucs2buf, MAXPDSTRING, pathname, MAXPDSTRING);
    return do_delete_ucs2(ucs2buf);
}
static char* sys_getcwd(char *buf) {
    uint16_t ucs2buf[MAXPDSTRING];
    memset(ucs2buf, 0, sizeof(ucs2buf));
    if (!_wgetcwd(ucs2buf, MAXPDSTRING))
        return 0;

    u8_ucs2toutf8(buf, MAXPDSTRING-1, ucs2buf, -1);
    buf[MAXPDSTRING-1] = 0;
    sys_unbashfilename(buf, buf);
    return buf;
}
static int sys_chdir(const char *path) {
    uint16_t ucs2buf[MAXPDSTRING];
    u8_utf8toucs2(ucs2buf, MAXPDSTRING, path, MAXPDSTRING);
    return _wchdir(ucs2buf);
}
#else
static int sys_stat(const char *pathname, struct stat *statbuf) {}

static int sys_rename(const char *oldpath, const char *newpath) {}
static int sys_mkdir(const char *pathname, mode_t mode) {}
static int sys_remove(const char *pathname) {}
static char* sys_getcwd(char *buf) {}
static int sys_chdir(const char *path) {}
#endif

    /* expand env vars and ~ at the beginning of a path and make a copy to return */
static char*do_expandpath(const char *from, char *to, int bufsize)
{}

    /* unbash '\' to '/', and drop duplicate '/' */
static char*do_pathnormalize(const char *from, char *to) {}

static char*do_expandunbash(const char *from, char *to, int bufsize) {}

static int str_endswith(char* str, char* end){}

static t_symbol*do_splitpath(const char*path, int*argc, t_atom**argv) {}

/* joins up all the path-components (using '/' as the path delimiter)
 * the (optional) prefix is prepended to the string
   (to be used for Windows volumes)
 * the (optional) suffix is present in the result (and appended if required to the string)
   (to be used for trailing slash)
 * atoms that are A_NULL are skipped
 */
static t_symbol*do_joinpath(t_symbol*prefix, int argc, t_atom*argv, t_symbol*suffix) {}


#ifdef _WIN32
static const char*do_errmsg(char*buffer, size_t bufsize) {
    char errcode[10];
    char*s;
    wchar_t wbuf[MAXPDSTRING];
    DWORD err = GetLastError();
    DWORD count = FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        0, err, MAKELANGID (LANG_NEUTRAL, SUBLANG_DEFAULT), wbuf, MAXPDSTRING, NULL);
    if (!count || !WideCharToMultiByte(CP_UTF8, 0, wbuf, count+1, buffer, bufsize, 0, 0))
        *buffer = '\0';
    s=buffer + strlen(buffer)-1;
    while(('\r' == *s || '\n' == *s) && s>buffer)
        *s--=0;
    pd_snprintf(errcode, sizeof(errcode), " [%ld]", err);
    errcode[sizeof(errcode)-1] = 0;
    strcat(buffer, errcode);
    return buffer;
}
#else /* !_WIN32 */
static const char*do_errmsg(char*buffer, size_t bufsize) {}
#endif /* !_WIN32 */

t_file_handler;
#define x_fd
#define x_mode
t_file_handle;


static int do_checkpathname(t_file_handle*x, const char*path) {}

t_canvas*do_getparentcanvas(t_file_handle*x, int parentlevel, int*effectivelevel) {}


static int do_parse_creationmode(t_atom*ap) {}

static void do_parse_args(t_file_handle*x, int argc, t_atom*argv) {}

static t_file_handle* do_file_handle_new(t_class*cls, t_symbol*s, int argc, t_atom*argv, int verbose, mode_t creationmode) {}

static int do_file_open(t_file_handle*x, const char* filename, int mode) {}


static void file_set_verbosity(t_file_handle*x, t_float f) {}
static void file_set_creationmode(t_file_handle*x, t_symbol*s, int argc, t_atom*argv) {}

    /* ================ [file handle] ====================== */
t_class *file_define_class;
static int file_handle_getdefine(t_file_handle*x) {}

static void file_handle_close(t_file_handle*x) {}
static int file_handle_checkopen(t_file_handle*x, const char*cmd) {}
static void file_handle_do_read(t_file_handle*x, t_float f) {}
static void file_handle_do_write(t_file_handle*x, int argc, t_atom*argv) {}
static void file_handle_list(t_file_handle*x, t_symbol*s, int argc, t_atom*argv) {}
static void file_handle_set(t_file_handle*x, t_symbol*s) {}
static void file_handle_seek(t_file_handle*x, t_symbol*s, int argc, t_atom*argv) {}
static void file_handle_open(t_file_handle*x, t_symbol*file, t_symbol*smode) {}

static void file_handle_free(t_file_handle*x) {}

    /* ================ [file stat] ====================== */
static int do_file_stat(t_file_handle*x, const char*filename, struct stat*sb, int*is_symlink) {}
static void do_dataout_symbol(t_file_handle*x, const char*selector, t_symbol*s) {}
static void do_dataout_float(t_file_handle*x, const char*selector, t_float f) {}
static void do_dataout_time(t_file_handle*x, const char*selector, time_t t) {}
static void file_stat_symbol(t_file_handle*x, t_symbol*filename) {}

static void file_size_symbol(t_file_handle*x, t_symbol*filename) {}
static void file_isfile_symbol(t_file_handle*x, t_symbol*filename) {}
static void file_isdirectory_symbol(t_file_handle*x, t_symbol*filename) {}

    /* ================ [file glob] ====================== */
#ifdef _WIN32
/* idiosyncrasies:
 * - cases are ignored ('a*' matches 'A.txt' and 'a.txt'), even with wine on ext4
 * - only the filename component is returned (must prefix path separately)
 * - non-ASCII needs special handling
 * - '*?' seems to be illegal (e.g. 'f*?.txt'); '?*' seems to be fine though
 * - "*" matches files starting with '.' (including '.', '..', but also .gitignore)
 * - if the pattern includes '*.', it matches a trailing '~'
 * - wildcards do not apply to directory-components (e.g. 'foo/ * /' (without the spaces, they are just due to C-comments constraints))
 *
 * plan:
 * - concat the path and the filename
 * - convert to utf16 (and back again)
 * - replace '*?' with '*' in the pattern
 * - manually filter out:
 *   - matches starting with '.' if the pattern does not start with '.'
 *   - matches ending in '~' if the pattern does not end with '[*?~]'
 * - only (officially) support wildcards in the filename component (not in the paths)
 * - if the pattern ends with '/', strip it, but return only directories
 */
static void file_glob_symbol(t_file_handle*x, t_symbol*spattern) {
    WIN32_FIND_DATAW FindFileData;
    HANDLE hFind;
    uint16_t ucs2pattern[MAXPDSTRING];
    char pattern[MAXPDSTRING];
    int nostartdot=0, noendtilde=0, onlydirs=0;
    char *filepattern, *strin, *strout;
    int pathpatternlength=0;
    int matchdot=0;

    do_expandunbash(spattern->s_name, pattern, MAXPDSTRING);

        /* '.' and '..' should only match if the pattern exquisitely asked for them */
    if(!strcmp(".", pattern) || !strcmp("./", pattern)
        || str_endswith(pattern, "/.") || str_endswith(pattern, "/./"))
        matchdot=1;
    else if(!strcmp("..", pattern) || !strcmp("../", pattern)
        || str_endswith(pattern, "/..") || str_endswith(pattern, "/../"))
        matchdot=2;

    if (matchdot) {
            /* windows FindFile would return the actual path rather than '.'
             * (which would confuse our full-path construction)
             * so we just return the result directly
             */
        struct stat sb;
        if (!do_file_stat(0, pattern, &sb, 0)) {
            t_atom outv[2];
            size_t end = strlen(pattern);
                /* get rid of trailing slash */
            if('/' == pattern[end-1])
                pattern[end-1]=0;
            SETSYMBOL(outv+0, gensym(pattern));
            SETFLOAT(outv+1, S_ISDIR(sb.st_mode));
            outlet_list(x->x_dataout, gensym("list"), 2, outv);
        } else {
                // this gets triggered if there is no match...
            outlet_bang(x->x_infoout);
        }
        return;
    }


    filepattern=strrchr(pattern, '/');
    if(filepattern && !filepattern[1]) {
            /* patterns ends with slashes: filter for dirs, and bash the trailing slashes */
        onlydirs=1;
        while('/' == *filepattern && filepattern>pattern) {
            *filepattern--=0;
        }
        filepattern=strrchr(pattern, '/');
    }
    if(!filepattern)
        filepattern=pattern;
    else {
        filepattern++;
        pathpatternlength=filepattern-pattern;
    }
    nostartdot=('.' != *filepattern);
    strin=filepattern;
    strout=filepattern;
    while(*strin) {
        char c = *strin++;
        *strout++ = c;
        if('*' == c) {
            while('?' == *strin || '*' == *strin)
                strin++;
        }
    }
    *strout=0;
    if (strout>pattern) {
        switch(strout[-1]) {
        case '~':
        case '*':
        case '?':
            noendtilde=0;
            break;
        default:
            noendtilde=1;
        }
    }
    u8_utf8toucs2(ucs2pattern, MAXPDSTRING, pattern, MAXPDSTRING);

    hFind = FindFirstFileW(ucs2pattern, &FindFileData);
    if (hFind == INVALID_HANDLE_VALUE) {
            // this gets triggered if there is no match...
        outlet_bang(x->x_infoout);
        return;
    }
    do {
        t_symbol*s;
        t_atom outv[2];
        int len = 0;
        int isdir = !!(FILE_ATTRIBUTE_DIRECTORY & FindFileData.dwFileAttributes);
        if (matchdot!=1 && !wcscmp(L"." , FindFileData.cFileName))
            continue;
        if (matchdot!=2 && !wcscmp(L".." , FindFileData.cFileName))
            continue;
        u8_ucs2toutf8(filepattern, MAXPDSTRING-pathpatternlength, FindFileData.cFileName, MAX_PATH);
        len = strlen(filepattern);

        if(onlydirs && !isdir)
            continue;
        if(nostartdot && '.' == filepattern[0])
            continue;
        if(noendtilde && '~' == filepattern[len-1])
            continue;

        s = gensym(pattern);
        SETSYMBOL(outv+0, s);
        SETFLOAT(outv+1, isdir);
        outlet_list(x->x_dataout, gensym("list"), 2, outv);
    } while (FindNextFileW(hFind, &FindFileData) != 0);
    FindClose(hFind);
}
#else /* !_WIN32 */
static void file_glob_symbol(t_file_handle*x, t_symbol*spattern) {}
#endif /* _WIN32 */


    /* ================ [file which] ====================== */

static void file_which_doit(t_file_handle*x, t_symbol*s, int depth) {}

static void file_which_list(t_file_handle*x, t_symbol*s, int argc, t_atom*argv) {}


    /* ================ [file patchpath] ====================== */

static void file_patchpath_list(t_file_handle*x, t_symbol*s, int argc, t_atom*argv) {}

    /* ================ [file mkdir] ====================== */
static void file_mkdir_symbol(t_file_handle*x, t_symbol*dir) {}

    /* ================ [file delete] ====================== */
#ifdef _WIN32
static int file_do_delete_recursive_ucs2(uint16_t*path) {
    WIN32_FIND_DATAW FindFileData;
    HANDLE hFind;
    uint16_t pattern[MAX_PATH];
    swprintf(pattern, MAX_PATH, L"%ls/*", path);
    hFind = FindFirstFileW(pattern, &FindFileData);
    if (hFind == INVALID_HANDLE_VALUE) {
        return 1;
    }
    do {
        int isdir = !!(FILE_ATTRIBUTE_DIRECTORY & FindFileData.dwFileAttributes);
        swprintf(pattern, MAX_PATH, L"%ls/%ls", path, FindFileData.cFileName);
        if(!isdir) {
            DeleteFileW(pattern);
        } else {
                /* skip self and parent */
            if(!wcscmp(L".", FindFileData.cFileName))continue;
            if(!wcscmp(L"..", FindFileData.cFileName))continue;
            file_do_delete_recursive_ucs2(pattern);
        }
    } while (FindNextFileW(hFind, &FindFileData) != 0);
    FindClose(hFind);
    return do_delete_ucs2(path);
}

static int file_do_delete_recursive(const char*path) {
    uint16_t ucs2path[MAXPDSTRING];
    u8_utf8toucs2(ucs2path, MAXPDSTRING, path, MAXPDSTRING);
    return file_do_delete_recursive_ucs2(ucs2path);
}
#else /* !_WIN32 */
static int nftw_cb(const char *path, const struct stat *s, int flag, struct FTW *f) {}

static int file_do_delete_recursive(const char*pathname) {}
#endif /* !_WIN32 */


static void file_delete_symbol(t_file_handle*x, t_symbol*path) {}

static void file_delete_recursive(t_file_handle*x, t_symbol*path) {}


    /* ================ [file copy]/[file move] ====================== */
static int file_do_copy(const char*source, const char*destination, int mode) {}
static int file_do_move(const char*source, const char*destination, int mode) {}
static void file_do_copymove(t_file_handle*x,
    const char*verb, int (*fun)(const char*,const char*,int),
    t_symbol*s, int argc, t_atom*argv) {}

static void file_copy_list(t_file_handle*x, t_symbol*s, int argc, t_atom*argv) {}
static void file_move_list(t_file_handle*x, t_symbol*s, int argc, t_atom*argv) {}

/* ================ file cwd  ====================== */
static void file_cwd_bang(t_file_handle*x) {}
static void file_cwd_symbol(t_file_handle*x, t_symbol*path) {}


/* ================ file path operations ====================== */
static void file_split_symbol(t_file_handle*x, t_symbol*path) {}

static void file_join_list(t_file_handle*x, t_symbol*s, int argc, t_atom*argv) {}

static void file_splitext_symbol(t_file_handle*x, t_symbol*path) {}
static void file_splitname_symbol(t_file_handle*x, t_symbol*path) {}

static void file_normalize_symbol(t_file_handle*x, t_symbol*path) {}


static void file_isabsolute_symbol(t_file_handle*x, t_symbol*path) {}


    /* overall creator for "file" objects - dispatch to "file handle" etc */
t_class *file_handle_class, *file_which_class, *file_glob_class, *file_patchpath_class;
t_class *file_stat_class, *file_size_class, *file_isfile_class, *file_isdirectory_class;
t_class *file_mkdir_class, *file_delete_class, *file_copy_class, *file_move_class;
t_class *file_split_class,*file_join_class,*file_splitext_class, *file_splitname_class;
t_class *file_normalize_class, *file_isabsolute_class, *file_cwd_class;

#define FILE_PD_NEW(verb, verbose, creationmode)

static void file_define_ignore(t_file_handle*x, t_symbol*s, int argc, t_atom*argv) {}
static t_file_handle*file_define_new(t_symbol*s, int argc, t_atom*argv) {}
static void file_define_free(t_file_handle*x) {}

static t_file_handle*file_handle_new(t_symbol*s, int argc, t_atom*argv) {}


FILE_PD_NEW(which, 0, 0);
FILE_PD_NEW(patchpath, 0, 0);
FILE_PD_NEW(glob, 0, 0);

FILE_PD_NEW(stat, 0, 0);
FILE_PD_NEW(size, 0, 0);
FILE_PD_NEW(isfile, 0, 0);
FILE_PD_NEW(isdirectory, 0, 0);

FILE_PD_NEW(mkdir, 0, 0777);
FILE_PD_NEW(delete, 0, 0);
FILE_PD_NEW(copy, 0, 0);
FILE_PD_NEW(move, 0, 0);

FILE_PD_NEW(cwd, 1, 0);

FILE_PD_NEW(split, 0, 0);
FILE_PD_NEW(join, 0, 0);
FILE_PD_NEW(splitext, 0, 0);
FILE_PD_NEW(splitname, 0, 0);
FILE_PD_NEW(isabsolute, 0, 0);
FILE_PD_NEW(normalize, 1, 0);

static t_pd *fileobj_new(t_symbol *s, int argc, t_atom*argv)
{}

t_filenew_flag;

static t_class*file_class_new(const char*name
    , t_file_handle* (*ctor)(t_symbol*,int,t_atom*), void (*dtor)(t_file_handle*)
    , void (*symfun)(t_file_handle*, t_symbol*)
    , t_filenew_flag flag
    ) {}

    /* ---------------- global setup function -------------------- */
void x_file_setup(void)
{}