/*****************************************************************************
 * Author:   Valient Gough <vgough@pobox.com>
 *
 *****************************************************************************
 * Copyright (c) 2003-2004, Valient Gough
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser 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 Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "fs/encfs.h"

#include <sys/stat.h>
#include <sys/types.h>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#ifdef linux
#include <sys/fsuid.h>
#endif

#include <cstring>

#include "base/Error.h"
#include "base/Mutex.h"
#include "fs/Context.h"
#include "fs/DirNode.h"
#include "fs/FileUtils.h"
#include "fs/fsconfig.pb.h"

#include <glog/logging.h>

#include <iostream>

using std::list;
using std::string;

namespace encfs {

class DirDeleter {
 public:
  void operator()(DIR *d) const { ::closedir(d); }
};

DirTraverse::DirTraverse(const shared_ptr<DIR> &_dirPtr, uint64_t _iv,
                         const shared_ptr<NameIO> &_naming)
    : dir(_dirPtr), iv(_iv), naming(_naming) {}

DirTraverse::DirTraverse(const DirTraverse &src)
    : dir(src.dir), iv(src.iv), naming(src.naming) {}

DirTraverse &DirTraverse::operator=(const DirTraverse &src) {
  dir = src.dir;
  iv = src.iv;
  naming = src.naming;

  return *this;
}

DirTraverse::~DirTraverse() {
  dir.reset();
  iv = 0;
  naming.reset();
}

static bool _nextName(struct dirent *&de, const shared_ptr<DIR> &dir,
                      int *fileType, ino_t *inode) {
  de = ::readdir(dir.get());

  if (de) {
    if (fileType) {
#if defined(_DIRENT_HAVE_D_TYPE) || defined(__FreeBSD__)
      *fileType = de->d_type;
#else
#warning "struct dirent.d_type not supported"
      *fileType = 0;
#endif
    }
    if (inode) *inode = de->d_ino;
    return true;
  } else {
    if (fileType) *fileType = 0;
    return false;
  }
}

std::string DirTraverse::nextPlaintextName(int *fileType, ino_t *inode) {
  struct dirent *de = 0;
  while (_nextName(de, dir, fileType, inode)) {
    try {
      uint64_t localIv = iv;
      return naming->decodePath(de->d_name, &localIv);
    }
    catch (Error &ex) {
      // .. .problem decoding, ignore it and continue on to next name..
      VLOG(1) << "error decoding filename " << de->d_name << " : " << ex.what();
    }
  }

  return string();
}

std::string DirTraverse::nextInvalid() {
  struct dirent *de = 0;
  // find the first name which produces a decoding error...
  while (_nextName(de, dir, (int *)0, (ino_t *)0)) {
    try {
      uint64_t localIv = iv;
      naming->decodePath(de->d_name, &localIv);
      continue;
    }
    catch (Error &ex) {
      return string(de->d_name);
    }
  }

  return string();
}

struct RenameEl {
  // ciphertext names
  string oldCName;
  string newCName;  // intermediate name (not final cname)

  // plaintext names
  string oldPName;
  string newPName;

  bool isDirectory;
};

class RenameOp {
 private:
  DirNode *dn;
  shared_ptr<list<RenameEl> > renameList;
  list<RenameEl>::const_iterator last;

 public:
  RenameOp(DirNode *_dn, const shared_ptr<list<RenameEl> > &_renameList)
      : dn(_dn), renameList(_renameList) {
    last = renameList->begin();
  }

  RenameOp(const RenameOp &src)
      : dn(src.dn), renameList(src.renameList), last(src.last) {}

  ~RenameOp();

  operator bool() const { return renameList; }

  bool apply();
  void undo();
};

RenameOp::~RenameOp() {
  if (renameList) {
    // got a bunch of decoded filenames sitting in memory..  do a little
    // cleanup before leaving..
    list<RenameEl>::iterator it;
    for (it = renameList->begin(); it != renameList->end(); ++it) {
      it->oldPName.assign(it->oldPName.size(), ' ');
      it->newPName.assign(it->newPName.size(), ' ');
    }
  }
}

bool RenameOp::apply() {
  try {
    while (last != renameList->end()) {
      // backing store rename.
      VLOG(2) << "renaming " << last->oldCName << "-> " << last->newCName;

      struct stat st;
      bool preserve_mtime = ::stat(last->oldCName.c_str(), &st) == 0;

      // internal node rename..
      dn->renameNode(last->oldPName.c_str(), last->newPName.c_str());

      // rename on disk..
      if (::rename(last->oldCName.c_str(), last->newCName.c_str()) == -1) {
        LOG(WARNING) << "Error renaming " << last->oldCName << ": "
                     << strerror(errno);
        dn->renameNode(last->newPName.c_str(), last->oldPName.c_str(), false);
        return false;
      }

      if (preserve_mtime) {
        struct utimbuf ut;
        ut.actime = st.st_atime;
        ut.modtime = st.st_mtime;
        ::utime(last->newCName.c_str(), &ut);
      }

      ++last;
    }

    return true;
  }
  catch (Error &err) {
    LOG(WARNING) << "caught error in rename application: " << err.what();
    return false;
  }
}

void RenameOp::undo() {
  VLOG(1) << "in undoRename";

  if (last == renameList->begin()) {
    VLOG(1) << "nothing to undo";
    return;  // nothing to undo
  }

  // list has to be processed backwards, otherwise we may rename
  // directories and directory contents in the wrong order!
  int undoCount = 0;
  int errorCount = 0;
  list<RenameEl>::const_iterator it = last;

  while (it != renameList->begin()) {
    --it;

    VLOG(1) << "undo: renaming " << it->newCName << " -> " << it->oldCName;

    ::rename(it->newCName.c_str(), it->oldCName.c_str());
    try {
      dn->renameNode(it->newPName.c_str(), it->oldPName.c_str(), false);
    }
    catch (Error &err) {
      if (++errorCount == 1)
        LOG(WARNING) << "error in rename und: " << err.what();
      // continue on anyway...
    }
    ++undoCount;
  };

  LOG(WARNING) << "Undo rename count: " << undoCount;
}

DirNode::DirNode(EncFS_Context *_ctx, const string &sourceDir,
                 const FSConfigPtr &_config) {
  Lock _lock(mutex);

  ctx = _ctx;
  rootDir = sourceDir;
  fsConfig = _config;

  // make sure rootDir ends in '/', so that we can form a path by appending
  // the rest..
  if (rootDir[rootDir.length() - 1] != '/') rootDir.append(1, '/');

  naming = fsConfig->nameCoding;
}

DirNode::~DirNode() {}

bool DirNode::hasDirectoryNameDependency() const {
  return naming ? naming->getChainedNameIV() : false;
}

string DirNode::rootDirectory() const {
  // don't update last access here, otherwise 'du' would cause lastAccess to
  // be reset.
  // chop off '/' terminator from root dir.
  return string(rootDir, 0, rootDir.length() - 1);
}

string DirNode::cipherPath(const char *plaintextPath) {
  if (plaintextPath[0] == '/') {
    ++plaintextPath;
  }
  return rootDir + naming->encodePath(plaintextPath);
}

string DirNode::cipherPathWithoutRoot(const char *plaintextPath) {
  return naming->encodePath(plaintextPath);
}

string DirNode::plainPath(const char *cipherPath_) {
  try {
    if (!strncmp(cipherPath_, rootDir.c_str(), rootDir.length())) {
      return naming->decodePath(cipherPath_ + rootDir.length());
    } else {
      return naming->decodePath(cipherPath_);
    }
  }
  catch (Error &err) {
    LOG(ERROR) << "decode err: " << err.what();
    return string();
  }
}

string DirNode::relativeCipherPath(const char *plaintextPath) {
  try {
    return naming->encodePath(plaintextPath);
  }
  catch (Error &err) {
    LOG(ERROR) << "encode err: " << err.what();
    return string();
  }
}

DirTraverse DirNode::openDir(const char *plaintextPath) {
  string cyName = cipherPath(plaintextPath);
  // rDebug("openDir on %s", cyName.c_str() );

  DIR *dir = ::opendir(cyName.c_str());
  if (dir == NULL) {
    VLOG(1) << "opendir error " << strerror(errno);
    return DirTraverse(shared_ptr<DIR>(), 0, shared_ptr<NameIO>());
  } else {
    shared_ptr<DIR> dp(dir, DirDeleter());

    uint64_t iv = 0;
    // if we're using chained IV mode, then compute the IV at this
    // directory level..
    try {
      if (naming->getChainedNameIV()) naming->encodePath(plaintextPath, &iv);
    }
    catch (Error &err) {
      LOG(ERROR) << "encode err: " << err.what();
    }
    return DirTraverse(dp, iv, naming);
  }
}

bool DirNode::genRenameList(list<RenameEl> &renameList, const char *fromP,
                            const char *toP) {
  uint64_t fromIV = 0, toIV = 0;

  // compute the IV for both paths
  string fromCPart = naming->encodePath(fromP, &fromIV);
  string toCPart = naming->encodePath(toP, &toIV);

  // where the files live before the rename..
  string sourcePath = rootDir + fromCPart;

  // ok..... we wish it was so simple.. should almost never happen
  if (fromIV == toIV) return true;

  // generate the real destination path, where we expect to find the files..
  VLOG(1) << "opendir " << sourcePath;
  shared_ptr<DIR> dir =
      shared_ptr<DIR>(opendir(sourcePath.c_str()), DirDeleter());
  if (!dir) return false;

  struct dirent *de = NULL;
  while ((de = ::readdir(dir.get())) != NULL) {
    // decode the name using the oldIV
    uint64_t localIV = fromIV;
    string plainName;

    if ((de->d_name[0] == '.') &&
        ((de->d_name[1] == '\0') ||
         ((de->d_name[1] == '.') && (de->d_name[2] == '\0')))) {
      // skip "." and ".."
      continue;
    }

    try {
      plainName = naming->decodePath(de->d_name, &localIV);
    }
    catch (Error &ex) {
      // if filename can't be decoded, then ignore it..
      continue;
    }

    // any error in the following will trigger a rename failure.
    try {
      // re-encode using the new IV..
      localIV = toIV;
      string newName = naming->encodePath(plainName.c_str(), &localIV);

      // store rename information..
      string oldFull = sourcePath + '/' + de->d_name;
      string newFull = sourcePath + '/' + newName;

      RenameEl ren;
      ren.oldCName = oldFull;
      ren.newCName = newFull;
      ren.oldPName = string(fromP) + '/' + plainName;
      ren.newPName = string(toP) + '/' + plainName;

      bool isDir;
#if defined(_DIRENT_HAVE_D_TYPE)
      if (de->d_type != DT_UNKNOWN) {
        isDir = (de->d_type == DT_DIR);
      } else
#endif
      {
        isDir = isDirectory(oldFull.c_str());
      }

      ren.isDirectory = isDir;

      if (isDir) {
        // recurse..  We want to add subdirectory elements before the
        // parent, as that is the logical rename order..
        if (!genRenameList(renameList, ren.oldPName.c_str(),
                           ren.newPName.c_str())) {
          return false;
        }
      }

      VLOG(1) << "adding file " << oldFull << " to rename list";
      renameList.push_back(ren);
    }
    catch (Error &err) {
      // We can't convert this name, because we don't have a valid IV for
      // it (or perhaps a valid key).. It will be inaccessible..
      LOG(WARNING) << "Aborting rename: error on file "
                   << fromCPart.append(1, '/').append(de->d_name) << ":"
                   << err.what();

      // abort.. Err on the side of safety and disallow rename, rather
      // then loosing files..
      return false;
    }
  }

  return true;
}

/*
    A bit of a pain.. If a directory is renamed in a filesystem with
    directory initialization vector chaining, then we have to recursively
    rename every descendent of this directory, as all initialization vectors
    will have changed..

    Returns a list of renamed items on success, a null list on failure.
*/
shared_ptr<RenameOp> DirNode::newRenameOp(const char *fromP, const char *toP) {
  // Do the rename in two stages to avoid chasing our tail
  // Undo everything if we encounter an error!
  shared_ptr<list<RenameEl> > renameList(new list<RenameEl>);
  if (!genRenameList(*renameList.get(), fromP, toP)) {
    LOG(WARNING) << "Error during generation of recursive rename list";
    return shared_ptr<RenameOp>();
  } else
    return shared_ptr<RenameOp>(new RenameOp(this, renameList));
}

int DirNode::mkdir(const char *plaintextPath, mode_t mode, uid_t uid,
                   gid_t gid) {
  string cyName = cipherPath(plaintextPath);
  rAssert(!cyName.empty());

  VLOG(1) << "mkdir on " << cyName;

  // if uid or gid are set, then that should be the directory owner
  int olduid = -1;
  int oldgid = -1;
  if (uid != 0) olduid = setfsuid(uid);
  if (gid != 0) oldgid = setfsgid(gid);

  int res = ::mkdir(cyName.c_str(), mode);

  if (olduid >= 0) setfsuid(olduid);
  if (oldgid >= 0) setfsgid(oldgid);

  if (res == -1) {
    int eno = errno;
    LOG(WARNING) << "mkdir error on " << cyName << " mode " << mode << ": "
                 << strerror(eno);
    res = -eno;
  } else
    res = 0;

  return res;
}

int DirNode::rename(const char *fromPlaintext, const char *toPlaintext) {
  Lock _lock(mutex);

  string fromCName = cipherPath(fromPlaintext);
  string toCName = cipherPath(toPlaintext);
  rAssert(!fromCName.empty());
  rAssert(!toCName.empty());

  VLOG(1) << "rename " << fromCName << " -> " << toCName;

  shared_ptr<FileNode> toNode = findOrCreate(toPlaintext);

  shared_ptr<RenameOp> renameOp;
  if (hasDirectoryNameDependency() && isDirectory(fromCName.c_str())) {
    VLOG(1) << "recursive rename begin";
    renameOp = newRenameOp(fromPlaintext, toPlaintext);

    if (!renameOp || !renameOp->apply()) {
      if (renameOp) renameOp->undo();

      LOG(WARNING) << "rename aborted";
      return -EACCES;
    }
    VLOG(1) << "recursive rename end";
  }

  int res = 0;
  try {
    struct stat st;
    bool preserve_mtime = ::stat(fromCName.c_str(), &st) == 0;

    renameNode(fromPlaintext, toPlaintext);
    res = ::rename(fromCName.c_str(), toCName.c_str());

    if (res == -1) {
      // undo
      res = -errno;
      renameNode(toPlaintext, fromPlaintext, false);

      if (renameOp) renameOp->undo();
    } else if (preserve_mtime) {
      struct utimbuf ut;
      ut.actime = st.st_atime;
      ut.modtime = st.st_mtime;
      ::utime(toCName.c_str(), &ut);
    }
  }
  catch (Error &err) {
    // exception from renameNode, just show the error and continue..
    LOG(ERROR) << "rename err: " << err.what();
    res = -EIO;
  }

  if (res != 0) {
    VLOG(1) << "rename failed: " << strerror(errno);
    res = -errno;
  }

  return res;
}

int DirNode::link(const char *from, const char *to) {
  Lock _lock(mutex);

  string fromCName = cipherPath(from);
  string toCName = cipherPath(to);

  rAssert(!fromCName.empty());
  rAssert(!toCName.empty());

  VLOG(1) << "link " << fromCName << " -> " << toCName;

  int res = -EPERM;
  if (fsConfig->config->external_iv()) {
    VLOG(1) << "hard links not supported with external IV chaining!";
  } else {
    res = ::link(fromCName.c_str(), toCName.c_str());
    if (res == -1)
      res = -errno;
    else
      res = 0;
  }

  return res;
}

/*
    The node is keyed by filename, so a rename means the internal node names
    must be changed.
*/
shared_ptr<FileNode> DirNode::renameNode(const char *from, const char *to) {
  return renameNode(from, to, true);
}

shared_ptr<FileNode> DirNode::renameNode(const char *from, const char *to,
                                         bool forwardMode) {
  shared_ptr<FileNode> node = findOrCreate(from);

  if (node) {
    uint64_t newIV = 0;
    string cname = rootDir + naming->encodePath(to, &newIV);

    VLOG(1) << "renaming internal node " << node->cipherName() << " -> "
            << cname.c_str();

    if (node->setName(to, cname.c_str(), newIV, forwardMode)) {
      if (ctx) ctx->renameNode(from, to);
    } else {
      // rename error! - put it back
      LOG(ERROR) << "renameNode failed";
      throw Error("Internal node name change failed!");
    }
  }

  return node;
}

shared_ptr<FileNode> DirNode::findOrCreate(const char *plainName) {
  shared_ptr<FileNode> node;
  if (ctx) node = ctx->lookupNode(plainName);

  if (!node) {
    uint64_t iv = 0;
    if (plainName[0] == '/') {
      ++plainName;
    }
    string cipherName = naming->encodePath(plainName, &iv);
    node.reset(new FileNode(this, fsConfig, plainName,
                            (rootDir + cipherName).c_str()));

    if (fsConfig->config->external_iv()) node->setName(0, 0, iv);

    VLOG(1) << "created FileNode for " << node->cipherName();
  }

  return node;
}

shared_ptr<FileNode> DirNode::lookupNode(const char *plainName,
                                         const char *requestor) {
  (void)requestor;
  Lock _lock(mutex);

  shared_ptr<FileNode> node = findOrCreate(plainName);

  return node;
}

/*
    Similar to lookupNode, except that we also call open() and only return a
    node on sucess..  This is done in one step to avoid any race conditions
    with the stored state of the file.
*/
shared_ptr<FileNode> DirNode::openNode(const char *plainName,
                                       const char *requestor, int flags,
                                       int *result) {
  (void)requestor;
  rAssert(result != NULL);
  Lock _lock(mutex);

  shared_ptr<FileNode> node = findOrCreate(plainName);

  if (node && (*result = node->open(flags)) >= 0)
    return node;
  else
    return shared_ptr<FileNode>();
}

int DirNode::unlink(const char *plaintextName) {
  string cyName = cipherPath(plaintextName);
  VLOG(1) << "unlink " << cyName;

  Lock _lock(mutex);

  int res = 0;
  if (ctx && ctx->lookupNode(plaintextName)) {
    // If FUSE is running with "hard_remove" option where it doesn't
    // hide open files for us, then we can't allow an unlink of an open
    // file..
    LOG(WARNING) << "Refusing to unlink open file: " << cyName
                 << ", hard_remove option is probably in effect";
    res = -EBUSY;
  } else {
    res = ::unlink(cyName.c_str());
    if (res == -1) {
      res = -errno;
      VLOG(1) << "unlink error: " << strerror(errno);
    }
  }

  return res;
}

}  // namespace encfs