/*
    inotify - File-system notifications service for Tcl
    A Tcl interface to the Linux kernel's Inotify service.

    Copyright (C) 2008  Alexandros Stergiakis

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

/*
 * inotify.c
 *
 */

#include <stdio.h>          // snprintf
#include <sys/ioctl.h>      // ioctl, ..
#include <errno.h>          // errno, EMFILE, ENFILE, EINTR
#include <string.h>         // strlen, strcpy
#include <unistd.h>         // read

#ifdef OWN_INOTIFY
#include "inotify-nosys.h"
#else
#include <sys/inotify.h>
#endif

#include <tcl.h>

#include "inotify.h"

/*
 * Function Prototypes
 */

static int InotifyCmd(ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[]);

static void InotifyCmd_CleanUp(ClientData clientData);

static int WatchCmd(ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[]);

static void WatchCmd_CleanUp(ClientData clientData);

static void inotify_handler(ClientData clientData, int mask);

static void inotify_mtof(CONST unsigned int mask, char *flags);

static int inotify_ftom(char *flags, unsigned int *mask);

static void itoa(int n, char *s);

/*
 * Function Bodies
 */

int Inotify_Init(Tcl_Interp *interp) {
    if (Tcl_InitStubs(interp, "8.5", 0) == NULL) {
        return TCL_ERROR;
    }

    Tcl_CreateObjCommand(interp, "inotify", InotifyCmd, (ClientData) NULL, InotifyCmd_CleanUp);

    Tcl_PkgProvide(interp, "inotify", "1.3");
    return TCL_OK;
}

static int InotifyCmd(ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[]) {
    static char* cmds[] = { "create", "info", NULL };
    int index;

    if (objc < 2) {
        Tcl_WrongNumArgs(interp, 1, objv, "option ?arg? ...");
        return TCL_ERROR;
    }

    if (Tcl_GetIndexFromObj(interp, objv[1], cmds, "option", 0, &index) != TCL_OK)
        return TCL_ERROR;

    // TODO: move initialization on Inotify_Init
    extern Tcl_HashTable Instances;
    extern int Instances_initialized;
    extern Tcl_Mutex InstancesAccess;
    Tcl_MutexLock (&InstancesAccess);
    if(! Instances_initialized) {
        Tcl_InitHashTable(&Instances, TCL_ONE_WORD_KEYS); // We will use integers (int) for key on this array.
                                                          // Casting should be ok because: sizeof(int) < WordSize.
        Instances_initialized = 1;
    }
    Tcl_MutexUnlock (&InstancesAccess);

    switch (index) {
        case 0: /* create */
        {
            if (objc != 4) {
                Tcl_WrongNumArgs(interp, 2, objv, "watch handler");
                return TCL_ERROR;
            }

            char *name;
            name = Tcl_GetString(objv[2]);

            char *handler;
            handler = Tcl_GetString(objv[3]);

            int instanceId;
            instanceId = inotify_init();
            if (instanceId < 0) { /* an error occured */
                switch (instanceId) {
                    case EMFILE:
                        Tcl_SetResult(interp, "The per-user open file limit was reached", TCL_STATIC);
                        break;
                    case ENFILE:
                        Tcl_SetResult(interp, "The system-wide open file limit was reached", TCL_STATIC);
                        break;
                    default:
                        Tcl_SetResult(interp, "inotify_init error", TCL_STATIC);
                        break;
                }
                return TCL_ERROR;
            }

            int unique;
            Tcl_HashEntry *entryPtr;
            extern Tcl_Mutex InstancesAccess;
            Tcl_MutexLock (&InstancesAccess);
            {
            entryPtr = Tcl_CreateHashEntry(&Instances, (char *) instanceId, &unique); 
                // Casting should be ok becaue sizeof(int) < word-size
            }
            Tcl_MutexUnlock (&InstancesAccess);
            if(! unique) { /* This is practically impossible to happen, but we check anyway */
                Tcl_SetResult(interp, "A watch with same id already exists", TCL_STATIC);
                return TCL_ERROR;
            }

            struct instance_state *data;
            data = (struct instance_state *) ckalloc(sizeof(struct instance_state));
            bzero(data, sizeof(struct instance_state));

            /* Instance Id */
            data->fd = instanceId;

            /* Handler proc */
            data->handler = (char *) ckalloc(strlen(handler) + 1);
            strcpy(data->handler, handler);

            /* Watch name */
            data->name = (char *) ckalloc(strlen(name) + 1);
            strcpy(data->name, name);

            /* Interpreter */
            data->interp = interp;

            Tcl_SetHashValue(entryPtr, (ClientData) data);

            Tcl_CreateFileHandler(instanceId, TCL_READABLE, inotify_handler, (ClientData) data);

            Tcl_CreateObjCommand(interp, name, WatchCmd, (ClientData) data, WatchCmd_CleanUp);

            Tcl_Obj *result = Tcl_NewObj();
            Tcl_SetIntObj(result, instanceId);
            Tcl_SetObjResult(interp, result);

            break; /* create */
        }

        case 1: /* info */
        {
            if (objc != 2) {
                Tcl_WrongNumArgs(interp, 2, objv, NULL);
                return TCL_ERROR;
            }

            Tcl_Obj *objPtr;
            int instanceId;
            struct instance_state *data;

            Tcl_Obj *listPtr;
            listPtr = Tcl_NewListObj(0, NULL);

            Tcl_HashSearch search;
            Tcl_HashEntry *entryPtr;
            extern Tcl_Mutex InstancesAccess;
            Tcl_MutexLock (&InstancesAccess);
            {
            entryPtr = Tcl_FirstHashEntry(&Instances, &search);    
            }
            Tcl_MutexUnlock (&InstancesAccess);

            while (entryPtr != NULL) {
                /* InstanceId */
                Tcl_MutexLock (&InstancesAccess);
                {
                instanceId = (int) Tcl_GetHashKey(&Instances, entryPtr); // Casting should be ok becaue sizeof(int) < word-size
                }
                Tcl_MutexUnlock (&InstancesAccess);
                char s[INT_LEN];
                itoa(instanceId, s);
                objPtr = Tcl_NewStringObj(s, -1);
                if (Tcl_ListObjAppendElement(interp, listPtr, objPtr) != TCL_OK) {
                    Tcl_DecrRefCount(objPtr);
                    Tcl_DecrRefCount(listPtr);
                    return TCL_ERROR;
                }

                data = (struct instance_state *) Tcl_GetHashValue(entryPtr);

                /* Handler proc */
                objPtr = Tcl_NewStringObj(data->handler, -1);
                if (Tcl_ListObjAppendElement(interp, listPtr, objPtr) != TCL_OK) {
                    Tcl_DecrRefCount(objPtr);
                    Tcl_DecrRefCount(listPtr);
                    return TCL_ERROR;
                }

                /* Watch name */
                objPtr = Tcl_NewStringObj(data->name, -1);
                if (Tcl_ListObjAppendElement(interp, listPtr, objPtr) != TCL_OK) {
                    Tcl_DecrRefCount(objPtr);
                    Tcl_DecrRefCount(listPtr);
                    return TCL_ERROR;
                }
                
                entryPtr = Tcl_NextHashEntry(&search);
            }
            Tcl_SetObjResult(interp, listPtr);
            break; /* info */
        }
    } /* switch */

    return TCL_OK;
}

static void InotifyCmd_CleanUp(ClientData clientData) {
    extern Tcl_HashTable Instances;
    extern int Instances_initialized;
    extern Tcl_Mutex InstancesAccess;

    Tcl_MutexLock (&InstancesAccess);
    {
    Tcl_DeleteHashTable(&Instances);
    Instances_initialized = 0;
    }
    Tcl_MutexUnlock (&InstancesAccess);
}

static int WatchCmd(ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[]) {
    static char* cmds[] = { "add", "remove", "read", "queue", "info", NULL };
    int index;

    if (Tcl_GetIndexFromObj(interp, objv[1], cmds, "option", 0, &index) != TCL_OK)
        return TCL_ERROR;

    struct instance_state *data;
    data = (struct instance_state *) clientData;

    if(! data->initialized) {
        Tcl_InitHashTable(&data->Watches, TCL_STRING_KEYS);
        data->initialized = 1;
    }

    switch (index) {
        case 0: /* add */
        {
            if (objc != 4) {
                Tcl_WrongNumArgs(interp, 2, objv, "pathname flags");
                return TCL_ERROR;
            }

            char *pathname = Tcl_GetString(objv[2]);
            char *flags = Tcl_GetString(objv[3]);

            unsigned int mask = 0;
            int ret;
            ret = inotify_ftom(flags, &mask);
            if(ret) {
                Tcl_SetResult(interp, "Error while parsing flags", TCL_STATIC);
                return TCL_ERROR;
            }

            int watchId;
            watchId = inotify_add_watch(data->fd, pathname, mask);
            if(watchId == -1) {
                Tcl_AppendResult (interp, "Error while registering watch: ", Tcl_PosixError(interp), NULL);
                return TCL_ERROR;
            }

            int unique;
            Tcl_HashEntry *entryPtr;
            entryPtr = Tcl_CreateHashEntry(&data->Watches, pathname, &unique);
            /* If the file is already being monitored, then we silently
               overwrite the previous watch, with the new settings */

            struct watch_state *wdata;
            wdata = (struct watch_state *) ckalloc(sizeof(struct watch_state));
            bzero(wdata, sizeof(struct watch_state));

            wdata->wd = watchId;

            wdata->flags = (char *) ckalloc(strlen(flags) + 1);
            strcpy(wdata->flags, flags);

            Tcl_SetHashValue(entryPtr, (ClientData) wdata);

            Tcl_Obj *result = Tcl_NewObj();
            Tcl_SetIntObj(result, watchId);
            Tcl_SetObjResult(interp, result);

            break; /* add */
        }

        case 1: /* remove */
        {
            if (objc != 3) {
                Tcl_WrongNumArgs(interp, 2, objv, "pathname");
                return TCL_ERROR;
            }

            char *pathname = Tcl_GetString(objv[2]);

            Tcl_HashEntry *entryPtr;
            entryPtr = Tcl_FindHashEntry(&data->Watches, pathname);
            if (entryPtr == NULL) {
                Tcl_SetResult(interp, "Specified pathname is not watched", TCL_STATIC);
                return TCL_ERROR;
            }

            struct watch_state *wdata;
            wdata = (struct watch_state *) Tcl_GetHashValue(entryPtr);

            int ret;
            ret = inotify_rm_watch(data->fd, wdata->wd);
            if(ret == -1) {
                Tcl_AppendResult (interp, "Error while removing watch: ", Tcl_PosixError(interp), NULL);
                return TCL_ERROR;
            }

            ckfree(wdata->flags);
            ckfree((char *) wdata);
            Tcl_DeleteHashEntry(entryPtr);

            break; /* remove */
        }

        case 2: /* read */
        {
            if (objc != 2) {
                Tcl_WrongNumArgs(interp, 2, objv, NULL);
                return TCL_ERROR;
            }

            Tcl_Obj *objPtr;
            Tcl_Obj *listPtr;
            listPtr = Tcl_NewListObj(0, NULL);

            struct inotify_event *event;
            char flags[FLAGS_LEN];
            int i = 0;
            char *buf = data->buf;
            /* Parse the cached events in chronological order and generate a list to return */
            while (i < data->buflen) {
                Tcl_Obj *dictPtr;
                dictPtr = Tcl_NewDictObj();

                event = (struct inotify_event *) &buf[i];
                /* Convert mask to flags */
                inotify_mtof(event->mask, flags);

                char s[INT_LEN]; 

                /* WatchId */
                itoa(event->wd, s);
                objPtr = Tcl_NewStringObj(s, -1);
                if (Tcl_DictObjPut(interp, dictPtr, Tcl_NewStringObj("watchid", -1), objPtr) != TCL_OK) {
                    Tcl_DecrRefCount(objPtr);
                    Tcl_DecrRefCount(dictPtr);
                    Tcl_DecrRefCount(listPtr);
                    return TCL_ERROR;
                }

                /* Flags */
                objPtr = Tcl_NewStringObj(flags, -1);
                if (Tcl_DictObjPut(interp, dictPtr, Tcl_NewStringObj("flags", -1), objPtr) != TCL_OK) {
                    Tcl_DecrRefCount(objPtr);
                    Tcl_DecrRefCount(dictPtr);
                    Tcl_DecrRefCount(listPtr);
                    return TCL_ERROR;
                }

                /* Cookie */
                itoa(event->cookie, s);
                objPtr = Tcl_NewStringObj(s, -1);
                if (Tcl_DictObjPut(interp, dictPtr, Tcl_NewStringObj("cookie", -1), objPtr) != TCL_OK) {
                    Tcl_DecrRefCount(objPtr);
                    Tcl_DecrRefCount(dictPtr);
                    Tcl_DecrRefCount(listPtr);
                    return TCL_ERROR;
                }

                /* Name */
                if (event->len) {
                    objPtr = Tcl_NewStringObj(event->name, -1);
                } else {
                    objPtr = Tcl_NewStringObj("", -1);
                }
                if (Tcl_DictObjPut(interp, dictPtr, Tcl_NewStringObj("filename", -1), objPtr) != TCL_OK) {
                    Tcl_DecrRefCount(objPtr);
                    Tcl_DecrRefCount(dictPtr);
                    Tcl_DecrRefCount(listPtr);
                    return TCL_ERROR;
                }

                if (Tcl_ListObjAppendElement(interp, listPtr, dictPtr) != TCL_OK) {
                    Tcl_DecrRefCount(objPtr);
                    Tcl_DecrRefCount(dictPtr);
                    Tcl_DecrRefCount(listPtr);
                    return TCL_ERROR;
                }

                /* The following code is safe with respect to alignment requirements */
                i += EVENT_SIZE + event->len;
            }
            Tcl_SetObjResult(interp, listPtr);
            break; /* read */
        }

        case 3: /* queue */
        {
            if (objc != 2) {
                Tcl_WrongNumArgs(interp, 2, objv, NULL);
                return TCL_ERROR;
            }

            /* Obtaining the Size of the Queue maintained by the kernel
               (the accumulated space the pending events take in the queue) */
            unsigned int queue_len = 0;
            int ret;
            ret = ioctl(data->fd, FIONREAD, &queue_len);
            if(ret == -1) {
                Tcl_AppendResult (interp, "Error while retrieving queue size: ", Tcl_PosixError(interp), NULL);
                return TCL_ERROR;
            }

            Tcl_Obj *result = Tcl_NewObj();
            Tcl_SetIntObj(result, queue_len);
            Tcl_SetObjResult(interp, result);
            break; /* size */
        }

        case 4: /* info */
        {
            if (objc != 2) {
                Tcl_WrongNumArgs(interp, 2, objv, NULL);
                return TCL_ERROR;
            }

            Tcl_Obj *objPtr;
            char *pathname;
            struct watch_state *wdata;

            Tcl_Obj *listPtr;
            listPtr = Tcl_NewListObj(0, NULL);

            Tcl_HashSearch search;
            Tcl_HashEntry *entryPtr;
            entryPtr = Tcl_FirstHashEntry(&data->Watches, &search);    

            while (entryPtr != NULL) {
                /* Pathname */
                pathname = Tcl_GetHashKey(&data->Watches, entryPtr);
                objPtr = Tcl_NewStringObj(pathname, -1);
                if (Tcl_ListObjAppendElement(interp, listPtr, objPtr) != TCL_OK) {
                    Tcl_DecrRefCount(objPtr);
                    Tcl_DecrRefCount(listPtr);
                    return TCL_ERROR;
                }

                wdata = (struct watch_state *) Tcl_GetHashValue(entryPtr);

                char s[INT_LEN]; 

                /* WatchId */
                itoa(wdata->wd, s);
                objPtr = Tcl_NewStringObj(s, -1);
                if (Tcl_ListObjAppendElement(interp, listPtr, objPtr) != TCL_OK) {
                    Tcl_DecrRefCount(objPtr);
                    Tcl_DecrRefCount(listPtr);
                    return TCL_ERROR;
                }

                /* Flags */
                objPtr = Tcl_NewStringObj((wdata->flags), -1);
                if (Tcl_ListObjAppendElement(interp, listPtr, objPtr) != TCL_OK) {
                    Tcl_DecrRefCount(objPtr);
                    Tcl_DecrRefCount(listPtr);
                    return TCL_ERROR;
                }

                entryPtr = Tcl_NextHashEntry(&search);
            }
            Tcl_SetObjResult(interp, listPtr);
            break; /* info */
        }

    } /* switch */

    return TCL_OK;
}

static void WatchCmd_CleanUp(ClientData clientData) {
    struct instance_state *data;
    struct watch_state *wdata;
    data = (struct instance_state *) clientData;

    Tcl_HashSearch search;
    Tcl_HashEntry *entryPtr;

    extern Tcl_HashTable Instances;
    extern Tcl_Mutex InstancesAccess;
    Tcl_MutexLock (&InstancesAccess);
    {
    entryPtr = Tcl_FindHashEntry(&Instances, (char *) data->fd); // Casting should be ok becaue sizeof(int) < word-size
    }
    Tcl_MutexUnlock (&InstancesAccess);
    if (entryPtr == NULL) { /* Unlikely to happen, but we check anyway */
        Tcl_SetResult(data->interp, "instanceId does not exist", TCL_STATIC);
        Tcl_BackgroundError(data->interp);
        return;
    }
    Tcl_DeleteHashEntry(entryPtr);

    Tcl_DeleteFileHandler(data->fd);

    /* The following removes all watches and close the inotify instance */
    int ret;
    ret = close(data->fd);
    if(ret == -1) {
        Tcl_AppendResult (data->interp, "Error while destroying instance: ", Tcl_PosixError(data->interp), NULL);
        Tcl_BackgroundError(data->interp);
        return;
    }

    if (data->initialized) {
        entryPtr = Tcl_FirstHashEntry(&data->Watches, &search);    

        while (entryPtr != NULL) {
            wdata = (struct watch_state *) Tcl_GetHashValue(entryPtr);

            ckfree(wdata->flags);
            ckfree((char *) wdata);
            Tcl_DeleteHashEntry(entryPtr);

            entryPtr = Tcl_NextHashEntry(&search);
        }
    }
    Tcl_DeleteHashTable(&data->Watches);

    ckfree(data->name);
    ckfree(data->handler);
    ckfree((char *) data);
}

/* Copy whole/part-of kernel buffer to local buffer, and execute handler Tcl procedure
 * We don't have the copying operation as part of iget because then we
 * would risk a runaway Tcl process, in the case where the implementation of the
 * handler is faulty (and doesn't read the kernel buffer, or the interpreter is
 * so that the handler cannot properly empty the buffer (eg read is renamed). */
static void inotify_handler(ClientData clientData, int mask) {
    struct instance_state *data;
    data = (struct instance_state *) clientData;

    /* Synchronous read of cached events */
    data->buflen = read(data->fd, data->buf, BUF_LEN);
    if (data->buflen < 0) {
        if (errno == EINTR) {
            Tcl_SetResult(data->interp, "Need to reissue system call", TCL_STATIC);
            Tcl_BackgroundError(data->interp);
            return;
        } else {
            Tcl_SetResult(data->interp, "Error while reading cached events", TCL_STATIC);
            Tcl_BackgroundError(data->interp);
            return;
        }
    } else if (data->buflen == 0) {
        Tcl_SetResult(data->interp, "Implementation error: BUF_LEN too small", TCL_STATIC);
        Tcl_BackgroundError(data->interp);
        return;
    }

    char buffer[INT_LEN];
    snprintf(buffer, INT_LEN, "%d", data->fd);

    /* We don't need to use Tcl_Preserve here (and Tcl_EventuallyFree in 'instance delete')
       because we don't use data->handler or data after this point, and till the end of the
       function. */
    int ret;
    ret = Tcl_VarEval(data->interp, data->handler, " ", buffer, NULL);
    if(ret != TCL_OK) {
        /* Error message in data->interp's result*/
        Tcl_BackgroundError(data->interp);
        return;
    }
}

/* One-Shot Support
If the IN_ONESHOT value is OR'ed into the event mask at watch addition,
the watch is automatically removed during generation of the first event.
Subsequent events will not be generated against the file until the watch
is added back.
*/

/* On Unmount
inotify takes this one step further, though, and sends out the IN_UNMOUNT
event when the filesystem on which a file resides is unmounted. It also
automatically destroys the watch and cleanup.
*/

/* Moves
Move events are complicated because inotify may be watching the directory 
that the file is moved to or from, but not the other. Because of this, it 
is not always possible to alert the user of the source and destination of 
a file involved in a move. inotify is able to alert the application to both 
only if the application is watching both directories. In that case, inotify 
emits an IN_MOVED_FROM from the watch descriptor of the source directory, 
and it emits an IN_MOVED_TO from the watch descriptor of the destination 
directory. If watching only one or the other, only the one event will be sent.
To tie together two disparate moved to/from events, inotify sets the cookie 
field in the inotify_event structure to a unique nonzero value. Two events with
matching cookies are thus related, one showing the source and one showing the 
destination of the move.
*/

/* inotify Limits
inotify is configurable via procfs and sysctl.
     /proc/sys/filesystem/inotify/max_queued_events 
is the maximum number of events that can be queued at once. If the queue reaches 
this size, new events are dropped, but the IN_Q_OVERFLOW event is always sent. 
With a significantly large queue, overflows are rare even if watching many objects. 
The default value is 16,384 events per queue.
     /proc/sys/filesystem/inotify/max_user_instances 
is the maximum number of inotify instances that a given user can instantiate. 
The default value is 128 instances, per user.
     /proc/sys/filesystem/inotify/max_user_watches 
is the maximum number of watches per instance. The default value is 8,192 watches, 
per instance.
*/

/* The following event triggers are supported:
     n IN_CREATE         File was created.
     r IN_ACCESS         File was read from.
     w IN_MODIFY         File was written to.
     a IN_ATTRIB         File's metadata (inode or xattr) was changed.
     C IN_CLOSE_WRITE    File was closed (and was open for writing).
     c IN_CLOSE_NOWRITE  File was closed (and was not open for writing).
     o IN_OPEN           File was opened.
     S IN_MOVE_SELF      Watched file/directory was itself moved
     m IN_MOVED_FROM     File was moved away from watch.
     M IN_MOVED_TO       File was moved to watch.
     d IN_DELETE         File was deleted.
     s IN_DELETE_SELF    The watch itself was deleted.

     u IN_UNMOUNT        The backing filesystem was unmounted.
     f IN_Q_OVERFLOW     The inotify queue overflowed.
     i IN_IGNORED        The watch was automatically removed, because the file was deleted or its filesystem was unmounted.
     D IN_ISDIR          The event occurred against a directory. */
static void inotify_mtof(CONST unsigned int mask, char *flags) {
    if (mask & IN_CREATE) *flags++ = 'n';
    if (mask & IN_ACCESS) *flags++ = 'r';
    if (mask & IN_MODIFY) *flags++ = 'w';
    if (mask & IN_ATTRIB) *flags++ = 'a';
    if (mask & IN_CLOSE_WRITE) *flags++ = 'C';
    if (mask & IN_CLOSE_NOWRITE) *flags++ = 'c';
    if (mask & IN_OPEN) *flags++ = 'o';
    if (mask & IN_MOVE_SELF) *flags++ = 'S';
    if (mask & IN_MOVED_FROM) *flags++ = 'm';
    if (mask & IN_MOVED_TO) *flags++ = 'M';
    if (mask & IN_DELETE) *flags++ = 'd';
    if (mask & IN_DELETE_SELF) *flags++ = 's';

    if (mask & IN_UNMOUNT) *flags++ = 'u';
    if (mask & IN_Q_OVERFLOW) *flags++ = 'f';
    if (mask & IN_IGNORED) *flags++ = 'i';
    if (mask & IN_ISDIR) *flags++ = 'D';
    *flags = '\0';
}

/* In addition to most of the above, the following are also supported:
     1 IN_ONESHOT        The watch will be automatically removed during generation of the first event.
     _ IN_CLOSE          IN_CLOSE_WRITE | IN_CLOSE_NOWRITE
     > IN_MOVE           IN_MOVED_FROM | IN_MOVED_TO
     * IN_ALL_EVENTS     Bitwise OR of all events. */
static int inotify_ftom(char *flags, unsigned int *mask) {
    for (; *flags != '\0'; flags++)
        switch (*flags) {
            case 'n':
                *mask |= IN_CREATE; break;
            case 'r':
                *mask |= IN_ACCESS; break;
            case 'w':
                *mask |= IN_MODIFY; break;
            case 'a':
                *mask |= IN_ATTRIB; break;
            case 'C':
                *mask |= IN_CLOSE_WRITE; break;
            case 'c':
                *mask |= IN_CLOSE_NOWRITE; break;
            case 'o':
                *mask |= IN_OPEN; break;
            case 'S':
                *mask |= IN_MOVE_SELF; break;
            case 'm':
                *mask |= IN_MOVED_FROM; break;
            case 'M':
                *mask |= IN_MOVED_TO; break;
            case 'd':
                *mask |= IN_DELETE; break;
            case 's':
                *mask |= IN_DELETE_SELF; break;

            case '_':
                *mask |= IN_CLOSE; break;
            case '>':
                *mask |= IN_MOVE; break;
            case '*':
                *mask |= IN_ALL_EVENTS; break;
            case '1':
                *mask |= IN_ONESHOT; break;
            default:
                return 1;
        }
    return 0;
}

/* itoa:  convert n to characters in s */
static void itoa(int n, char *s)
{
    snprintf(s, INT_LEN, "%d", n);
}
