#include "e_system.h"

typedef struct
{
   char *dev, *mountpoint, *cmd, *cmd2, *cmd1;
   Ecore_Exe *exe;
   Ecore_Event_Handler *handler;
   Eina_Bool eject : 1;
   Eina_Bool mount : 1;
   Eina_Bool umount : 1;
   Eina_Bool in_progress : 1;
} Action;

static Eina_List *_pending_actions = NULL;

static Eina_Bool _store_action_do(Action *a);

static Eina_Bool
_store_device_verify(const char *dev)
{
   const char *s;
   struct stat st;

   // not even /dev/something? no way.
   if (!(!strncmp(dev, "/dev/", 5))) return EINA_FALSE;
   // if it contains /.. - even tho it could be file/..name not file/../name
   // it still looks suspicious enough so - no
   if (strstr(dev, "/..")) return EINA_FALSE;
   // any chars that cold be used in any evil ways - no way. device names
   // should not have these...
   for (s = dev; *s; s++)
     {
        if ((*s <= '*') || (*s == '`') || (*s == ';') || (*s == '<') ||
            (*s == '>') || (*s == '?') || (*s >= '{') ||
            ((*s >= '[') && (*s <= '^')))
          return EINA_FALSE;
     }
   // must exist as a file - if not - nope
   if (stat(dev, &st) != 0) return EINA_FALSE;
   // XXX: must check it's a block device, that is removable etc. etc.
   // XXX: for now we have no code for this so always fail until we do
   return EINA_FALSE;
//   return EINA_TRUE;
}

static Eina_Bool
_store_uuid_verify(const char *dev)
{
   const char *s;

   if (!(!strncmp(dev, "UUID=", 5))) return EINA_FALSE;
   for (s = dev + 5; *s; s++)
     {
        if ((!isalnum(*s)) && (*s != '-')) return EINA_FALSE;
     }
   return EINA_TRUE;
}

static Eina_Bool
_mkdir(const char *path, uid_t u, gid_t g)
{
   mode_t um;
   int ret, e;

   um = umask(0);
   errno = 0;
   ret = mkdir(path, S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
   e = errno;
   umask(um);
   if (ret != 0)
     {
        if (e != EEXIST)
          {
             ERR("Can't create mount dir [%s]\n", path);
             return EINA_FALSE;
          }
        else
          {
             struct stat st;

             if (stat(path, &st) == 0)
               {
                  if (!S_ISDIR(st.st_mode))
                    {
                       ERR("Path is not a dir [%s]\n", path);
                       return EINA_FALSE;
                    }
               }
          }
     }
   if (chown(path, u, g) != 0)
     {
        ERR("Can't own [%s] to uid.gid %i.%i\n", path, u, g);
        return EINA_FALSE;
     }
   return EINA_TRUE;
}

static Eina_Bool
_store_mount_verify(const char *mnt)
{
   char *tmnt, *p, *pp;
   const char *s;
   struct stat st;

   // XXX: we should use /run/media - possibly make this adapt
   if (!(!strncmp(mnt, "/media/", 7))) return EINA_FALSE;
   for (s = mnt; *s; s++)
     {
        if (*s == '\\') return EINA_FALSE;
        if ((*s <= '*') || (*s == '`') || (*s == ';') || (*s == '<') ||
            (*s == '>') || (*s == '?') || (*s >= '{') ||
            ((*s >= '[') && (*s <= '^')))
          return EINA_FALSE;
     }
   if (strstr(mnt, "/..")) return EINA_FALSE;
   if (strstr(mnt, "/./")) return EINA_FALSE;
   if (strstr(mnt, "//")) return EINA_FALSE;
   if (stat(mnt, &st) == 0)
     {
        if (!S_ISDIR(st.st_mode)) return EINA_FALSE;
        if (st.st_uid != 0) return EINA_FALSE;
        if (st.st_gid != 0) return EINA_FALSE;
     }
   tmnt = strdup(mnt);
   if (tmnt)
     {
        // /media <- owned by root
        p = strchr(tmnt + 1, '/');
        if (!p) goto malformed;
        *p = '\0';
        if (!_mkdir(tmnt, 0, 0)) goto err;
        *p = '/';

        // /media/username <- owned by root
        p = strchr(p + 1, '/');
        if (!p) goto malformed;
        *p = '\0';
        pp = strrchr(tmnt, '/');
        if (!pp) goto err;
        // check if dir name is name of user...
        if (strcmp(p + 1, user_name)) goto err;
        if (!_mkdir(tmnt, 0, 0)) goto err;
        *p = '/';

        // /media/username/dirname <- owned by root
        if (!_mkdir(tmnt, 0, 0)) goto err;
        free(tmnt);
     }
   return EINA_TRUE;
malformed:
   ERR("Malformed mount point [%s]\n", mnt);
err:
   free(tmnt);
   return EINA_FALSE;
}

static Eina_Bool
_store_umount_verify(const char *mnt)
{
   char *tmnt, *p;
   const char *s;
   struct stat st;

   // XXX: we should use /run/media - possibly make this adapt
   if (!(!strncmp(mnt, "/media/", 7))) return EINA_FALSE;
   for (s = mnt; *s; s++)
     {
        if (*s == '\\') return EINA_FALSE;
        if ((*s <= '*') || (*s == '`') || (*s == ';') || (*s == '<') ||
            (*s == '>') || (*s == '?') || (*s >= '{') ||
            ((*s >= '[') && (*s <= '^')))
          return EINA_FALSE;
     }
   if (strstr(mnt, "/..")) return EINA_FALSE;
   if (strstr(mnt, "/./")) return EINA_FALSE;
   if (strstr(mnt, "//")) return EINA_FALSE;
   if (stat(mnt, &st) != 0) return EINA_FALSE;
   if (!S_ISDIR(st.st_mode)) return EINA_FALSE;
   tmnt = strdup(mnt);
   if (!tmnt) return EINA_FALSE;
   p = strchr(tmnt + 7, '/');
   if (!p) goto err;
   *p = '\0';
   if (stat(tmnt, &st) != 0) goto err;
   if (st.st_uid != 0) goto err;
   if (st.st_gid != 0) goto err;
   p = tmnt + 7; // after /media/ (so username)
   if (strcmp(p + 1, user_name)) goto err; // not user named dir
   free(tmnt);
   return EINA_TRUE;
err:
   free(tmnt);
   return EINA_FALSE;
}

static void
_store_action_free(Action *a)
{
   free(a->dev);
   free(a->mountpoint);
   free(a->cmd);
   free(a->cmd2);
   free(a->cmd1);
   if (a->handler) ecore_event_handler_del(a->handler);
   if (a->exe) ecore_exe_free(a->exe);
   free(a);
}

static void
_store_pending_action_next(void)
{
   while (_pending_actions)
     {
        Action *a = _pending_actions->data;
        if (_store_action_do(a)) break;
        else _pending_actions = eina_list_remove(_pending_actions, a);
     }
}

static Eina_Bool
_cb_store_eject_exe_del(void *data, int ev_type EINA_UNUSED, void *event)
{
   Ecore_Exe_Event_Del *ev = event;
   Action *a = data;

   if (ev->exe == a->exe)
     {
        a->exe = NULL;
        if (a->eject)
          e_system_inout_command_send("store-eject", "%i %s",
                                      ev->exit_code, a->dev);
        else if ((a->mount) &&
                 ((ev->exit_code == 0) ||
                  (!((a->cmd) || (a->cmd1)  || (a->cmd2)))))
          e_system_inout_command_send("store-mount", "%i %s %s",
                                      ev->exit_code, a->dev, a->mountpoint);
        else if (a->umount)
          {
             if (ev->exit_code == 0)
               {
                  rmdir(a->mountpoint);
               }
             e_system_inout_command_send("store-umount", "%i %s",
                                         ev->exit_code, a->mountpoint);
          }
        if (((a->cmd) || (a->cmd1) || (a->cmd2)) && (ev->exit_code != 0))
          {
             if (!_store_action_do(a))
               {
                  _pending_actions = eina_list_remove(_pending_actions, a);
                  _store_pending_action_next();
               }
          }
        else
          {
             _pending_actions = eina_list_remove(_pending_actions, a);
             _store_action_free(a);
             _store_pending_action_next();
          }
     }
   return EINA_TRUE;
}

static Eina_Bool
_store_action_do(Action *a)
{
   a->handler = ecore_event_handler_add(ECORE_EXE_EVENT_DEL,
                                        _cb_store_eject_exe_del, a);
   if (!a->handler) _store_action_free(a);
   else
     {
        if (a->cmd2)
          {
             a->exe = e_system_run(a->cmd2);
             free(a->cmd2);
             a->cmd2 = NULL;
          }
        else if (a->cmd1)
          {
             a->exe = e_system_run(a->cmd1);
             free(a->cmd1);
             a->cmd1 = NULL;
          }
        else
          {
             a->exe = e_system_run(a->cmd);
             free(a->cmd);
             a->cmd = NULL;
          }
        a->in_progress = EINA_TRUE;
        if (!a->exe) _store_action_free(a);
        else return EINA_TRUE;
     }
   return EINA_FALSE;
}

static void
_store_action_queue(Action *a)
{
   if (_pending_actions)
     _pending_actions = eina_list_append(_pending_actions, a);
   else if (_store_action_do(a))
     _pending_actions = eina_list_append(_pending_actions, a);
}

static void
_cb_store_eject(void *data EINA_UNUSED, const char *params)
{
   // params:
   // /dev/xxx
   char cmd0[512];
   char cmd[4096 + 512 + 128];

   if (!_store_device_verify(params))
     {
        ERR("Invalid device [%s]\n", params);
        return;
     }
#if defined (__FreeBSD__) || defined (__OpenBSD__)
   // on bsd cdcontrol is the shnizzle for this
   if (ecore_file_app_installed("cdcontrol"))
     snprintf(cmd0, sizeof(cmd0), "cdcontrol eject");
   // otherwise regular old eject will do
   else
#endif
     snprintf(cmd0, sizeof(cmd0), "eject");
   if (snprintf(cmd, sizeof(cmd), "%s %s", cmd0, params) < (int)(sizeof(cmd) - 1))
     {
        Action *a = calloc(1, sizeof(Action));
        if (a)
          {
             a->eject = EINA_TRUE;
             a->dev = strdup(params);
             a->cmd = strdup(cmd);
             if ((!a->dev) || (!a->cmd)) _store_action_free(a);
             else _store_action_queue(a);
          }
     }
}

static void
_cb_store_mount(void *data EINA_UNUSED, const char *params)
{
   // params:
   // /dev/sdc1 - /media/user/dir
   // /dev/disk/by-uuid/d9c53a62-7fc2-4cc3-9616-4e41e065da4c - /media/user/dir
   // /dev/sdb1 x /media/user/dir
   // ...
   // DEV ARG MNT
   // ARG is - OR one or more of the below chars (-, xs, rx, ...):
   // x = noexec
   // r = ro
   // s = sync
   // d = dirsync
   // l = lazytime
   // a = noatime
   // A = relatime
   // D = diratime
   char dev[1024], arg[128], mnt[4096], opt2[512], opt1[512], opt0[256], *p;
   char cmd2[(4096 * 2) + 1024 + 500];
   char cmd1[(4096 * 2) + 1024 + 500];
   char cmd0[(4096 * 2) + 1024 + 500];

   if (sscanf(params, "%1023s %127s %4095s", dev, arg, mnt) == 3)
     {
        char *mnt2;

        if ((!_store_device_verify(dev)) && (!_store_uuid_verify(dev)))
          {
             ERR("Invalid device [%s]\n", dev);
             return;
          }
        if (!_store_mount_verify(mnt))
          {
             ERR("Invalid mount [%s]\n", mnt);
             return;
          }
        mnt2 = ecore_file_escape_name(mnt);
        if (mnt2)
          {
             Eina_Bool o_noexec = EINA_FALSE;
             Eina_Bool o_ro = EINA_FALSE;
             Eina_Bool o_sync = EINA_FALSE;
             Eina_Bool o_dirsync = EINA_FALSE;
             Eina_Bool o_lazytime = EINA_FALSE;
             Eina_Bool o_noatime = EINA_FALSE;
             Eina_Bool o_relatime = EINA_FALSE;
             Eina_Bool o_diratime = EINA_FALSE;

             for (p = arg; *p; p++)
               {
                  if      ((*p) == 'x') o_noexec = EINA_TRUE;
                  else if ((*p) == 'r') o_ro = EINA_TRUE;
                  else if ((*p) == 's') o_sync = EINA_TRUE;
                  else if ((*p) == 'd') o_dirsync = EINA_TRUE;
                  else if ((*p) == 'l') o_lazytime = EINA_TRUE;
                  else if ((*p) == 'a') o_noatime = EINA_TRUE;
                  else if ((*p) == 'A') o_relatime = EINA_TRUE;
                  else if ((*p) == 'D') o_diratime = EINA_TRUE;
               }
             snprintf(opt0, sizeof(opt0),
                      "nosuid,nodev%s%s%s%s%s%s%s%s",
                      o_noexec ? ",noexec" : "",
                      o_ro ? ",ro" : "",
                      o_sync ? ",sync" : "",
                      o_dirsync ? ",dirsync" : "",
                      o_lazytime ? ",lazytime" : "",
                      o_noatime ? ",noatime" : "",
                      o_relatime ? ",relatime" : "",
                      o_diratime ? ",diratime" : "");
             snprintf(opt2, sizeof(opt2), "%s,iocharset=utf8,uid=%i", opt0, uid);
             snprintf(opt1, sizeof(opt1), "%s,iocharset=utf8", opt0);
             // opt2, opt1, opt0
             if ((snprintf(cmd2, sizeof(cmd2), "mount -o %s %s %s",
                           opt2, dev, mnt2) < (int)(sizeof(cmd2) - 1)) &&
                 (snprintf(cmd1, sizeof(cmd1), "mount -o %s %s %s",
                           opt1, dev, mnt2) < (int)(sizeof(cmd1) - 1)) &&
                 (snprintf(cmd0, sizeof(cmd0), "mount -o %s %s %s",
                           opt0, dev, mnt2) < (int)(sizeof(cmd0) - 1)))
               {
                  Action *a = calloc(1, sizeof(Action));
                  if (a)
                    {
                       a->mount = EINA_TRUE;
                       a->dev = strdup(dev);
                       a->mountpoint = strdup(mnt);
                       a->cmd = strdup(cmd0);
                       a->cmd2 = strdup(cmd2);
                       a->cmd1 = strdup(cmd1);
                       if ((!a->dev) || (!a->mountpoint) ||
                           (!a->cmd) || (!a->cmd2) || (!a->cmd1))
                         _store_action_free(a);
                       else _store_action_queue(a);
                    }
               }
          }
        free(mnt2);
     }
}

static void
_cb_store_umount(void *data EINA_UNUSED, const char *params)
{
   // params:
   // /media/user/xxx
   char cmd[4096];

   if (!_store_umount_verify(params))
     {
        ERR("Invalid mount [%s]\n", params);
        return;
     }
   if (snprintf(cmd, sizeof(cmd), "umount %s", params) < (int)(sizeof(cmd) - 1))
     {
        Action *a = calloc(1, sizeof(Action));
        if (a)
          {
             a->umount = EINA_TRUE;
             a->mountpoint = strdup(params);
             a->cmd = strdup(cmd);
             if ((!a->mountpoint) || (!a->cmd)) _store_action_free(a);
             else _store_action_queue(a);
          }
     }
}

void
e_system_storage_init(void)
{
   e_system_inout_command_register("store-eject",  _cb_store_eject, NULL);
   e_system_inout_command_register("store-mount",  _cb_store_mount, NULL);
   e_system_inout_command_register("store-umount", _cb_store_umount, NULL);
}

void
e_system_storage_shutdown(void)
{
   // only shutdown things we really have to - no need to free mem etc.
}
