/**
    file handling, including .torrent and XML parsing (interface to expat library)

    Copyright (c) 2020-2023 The Creators of Simphone

    See the file COPYING.LESSER.txt for copying permission.
**/

#define INITGUID /* shlobj.h */

#ifndef _BSD_SOURCE
#define _BSD_SOURCE /* Linux: strtoull, futimes */
#endif

#ifndef __LONG_LONG_SUPPORTED
#define __LONG_LONG_SUPPORTED /* FreeBSD: strtoull */
#endif

#ifndef __unix__
#undef __STRICT_ANSI__ /* Windows: _wrename, _wremove. Mac OS: strtoull */
#endif

#include "config.h"

#include "logger.h"
#include "table.h"
#include "error.h"
#include "utils.h"
#include "system.h"
#include "crypto.h"
#include "file.h"
#include "keygen.h"
#include "network.h"
#include "contact.h"
#include "param.h"
#include "limit.h"
#include "msg.h"
#include "api.h"

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

#ifdef _WIN32
#define FILE_TIME_0 0x19DB1DED53E8000LL
#define FILE_CALC_TIME(filetime) ((simnumber) (filetime).dwHighDateTime << 32 | (filetime).dwLowDateTime)

#include <shlobj.h>
#include <io.h>
#include <direct.h>

#ifdef _MSC_VER
#define strtoull _strtoui64
#define lseek _lseeki64

DEFINE_GUID (CLSID_ShellLink, 0x00021401, 0x0000, 0x0000, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46);
#elif ! defined(__IPersistFile_INTERFACE_DEFINED__)
#define __IPersistFile_INTERFACE_DEFINED__
DEFINE_GUID (IID_IPersistFile, 0x0000010b, 0x0000, 0x0000, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46);
#endif

#ifndef S_ISDIR
#define S_ISDIR(mode) ((mode) & (_S_IFDIR))
#endif
#else /* _WIN32 */
#ifndef __USE_BSD
#define __USE_BSD /* ftruncate */
#endif

#ifndef __USE_SVID
#define __USE_SVID /* endpwent */
#endif

#if HAVE_FSTATFS
#if HAVE_SYS_STATFS_H
#include <sys/statfs.h>
#else
#include <sys/mount.h>
#endif
#endif

#include <sys/time.h>
#include <sys/file.h>
#include <unistd.h>
#include <pwd.h>
#include <arpa/inet.h>

#if HAVE_MALLOC_USABLE_SIZE
#if HAVE_MALLOC_NP_H
#include <malloc_np.h>
#else
#include <malloc.h>
#endif
#endif

#ifndef O_TEXT
#define O_TEXT 0
#endif

#ifndef O_BINARY
#define O_BINARY 0
#endif

#define GetCurrentProcessId getpid
#endif

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

#if HAVE_LIBEXPAT
#include <expat.h>
#endif

#define SIM_MODULE "file"

#define FILE_LOCK "pid"
#define FILE_CONFIG_DIRECTORY ".simphone"
#define FILE_USER_DIRECTORY "user"

#define FILE_MODE_FILE 0600
#define FILE_MODE_DIRECTORY 0700

#define FILE_PREFIX "SIM"
#define FILE_KEY_SALT "/ST"
#define FILE_KEY_IV "/IV"
#define FILE_KEY_CIPHER "/CR"
#define FILE_KEY_HASH "/HH"
#define FILE_KEY_VERSION "_V"

#define FILE_DHT_START "5:nodesll"
#define FILE_DHT_START_LENGTH (int) SIM_STRING_GET_LENGTH_CONST (FILE_DHT_START)
#define FILE_DHT_STOP "eee"
#define FILE_DHT_STOP_LENGTH (int) SIM_STRING_GET_LENGTH_CONST (FILE_DHT_STOP)

#define FILE_MAX_LINK_SIZE 21
#define FILE_MAX_TORRENT_SIZE 1048576
#define FILE_MAX_TORRENTS_SIZE 16777216

struct file_txt_buffer {
  struct file_txt_buffer *next; /* next buffer in linked list */
  char *data;                   /* pointer to first data byte */
  unsigned len;                 /* number of data bytes */
  char buffer[sizeof ("<" MSG_HISTORY_ENTRY ">") + SIM_FILE_BUFFER_SIZE];
};

struct file_context {
  int fd;         /* file descriptor */
  int err;        /* error code if failed */
  void *crypt;    /* cipher context */
  void *md;       /* hash context */
  simtype buffer; /* file data loaded or to be saved */
};

static char *file_user_dir = NULL, *file_app_dir, *file_home_dir;
static simtype file_key_strings[2]; /* [0] = private key or nil if none, [1] = password or seed, nil if none */
static unsigned file_key_type;

#ifndef _WIN32
#define _stricmp(s1, s2) strcmp (s1, s2)
#define wremove(pathname, mode) remove (pathname)

int sim_file_truncate (int fd) {
  return ! ftruncate (fd, lseek (fd, 0, SEEK_CUR)) ? SIM_OK : errno ? errno : ENOENT;
}

#define FILE_FUNCTION(FUNCTION, TYPE) FUNCTION (int fd, TYPE *buffer, unsigned length)
#define FILE_CALL_UTF(TYPE, error, ascii, unicode, ...) \
  return pathname ? ascii (pathname, __VA_ARGS__) : ((errno = SIM_FILE_BAD_NAME), (error))

#define FILE_CALL_LOOP(FUNCTION)         \
  int ret;                               \
  do {                                   \
    ret = FUNCTION (fd, buffer, length); \
  } while (ret < 0 && errno == EINTR);   \
  return ret

void *sim_file_fopen_utf (const char *pathname, const char *mode) {
  FILE_CALL_UTF (file_ptr, NULL, fopen, fopen, mode);
}
#else
#define wrename(newname, oldname) _wrename (oldname, newname)
#define wremove(pathname, mode) _wremove (pathname)
#define wmkdir(pathname, mode) _wmkdir (pathname)

int sim_file_truncate (int fd) {
  return SetEndOfFile ((HANDLE) _get_osfhandle (fd)) ? SIM_OK : GetLastError ();
}

#define FILE_FUNCTION(FUNCTION, TYPE) FUNCTION (int fd, TYPE *buffer, unsigned length)
#define FILE_CALL_LOOP(FUNCTION) return _##FUNCTION (fd, buffer, length)

#define FILE_CALL_UTF(TYPE, error, ascii, unicode, ...)   \
  TYPE ret = (error);                                     \
  int err = SIM_FILE_BAD_NAME;                            \
  unsigned len = pathname ? strlen (pathname) : 0;        \
  simtype name = sim_convert_utf_to_ucs (pathname, &len); \
  if (pathname && name.typ != SIMNIL) {                   \
    ret = unicode (name.ptr, __VA_ARGS__);                \
    err = errno;                                          \
  }                                                       \
  string_free (name);                                     \
  errno = err;                                            \
  return ret

typedef FILE *file_ptr;

void *sim_file_fopen_utf (const char *pathname, const unsigned short *mode) {
  FILE_CALL_UTF (file_ptr, NULL, _wfopen, _wfopen, mode);
}

static int sim_file_rename_utf (const wchar_t *oldname, const char *pathname) {
  FILE_CALL_UTF (int, -1, wrename, wrename, oldname);
}
#endif

int sim_file_rename (const char *pathname, const char *newname) {
  FILE_CALL_UTF (int, -1, rename, sim_file_rename_utf, newname);
}

int sim_file_remove (const char *pathname) {
  FILE_CALL_UTF (int, -1, wremove, wremove, 0);
}

int sim_file_mkdir (const char *pathname) {
  FILE_CALL_UTF (int, -1, mkdir, wmkdir, FILE_MODE_DIRECTORY);
}

int sim_file_lstat (const char *pathname, void *output) {
  struct stat buf;
  FILE_CALL_UTF (int, -1, lstat, _wstati64, output ? output : &buf);
}

int sim_file_stat (const char *pathname, void *output) {
  FILE_CALL_UTF (int, -1, stat, _wstati64, output);
}

static int sim_file_open_utf (const char *pathname, int mode, int flags) {
  FILE_CALL_UTF (int, -1, open, _wopen, flags, mode);
}

int sim_file_open (const char *pathname, int bits, simnumber *size) {
  int err = errno = 0, fd, flags = bits & FILE_BIT_TEXT ? O_TEXT : O_BINARY;
  if (bits & FILE_BIT_READ) {
    flags |= bits & FILE_BIT_WRITE ? O_RDWR : O_RDONLY;
  } else if (bits & FILE_BIT_WRITE)
    flags |= O_WRONLY;
  if (bits & FILE_BIT_APPEND)
    flags |= O_APPEND;
  if (bits & FILE_BIT_CREATE)
    flags |= O_CREAT;
  if (bits & FILE_BIT_TRUNCATE)
#ifdef O_SYNC
    flags |= O_TRUNC | O_SYNC;
#else
    flags |= O_TRUNC;
#endif
#ifdef _WIN32
  if ((fd = sim_file_open_utf (pathname, FILE_MODE_FILE, flags)) < 0) {
    if ((bits & (FILE_BIT_READ | FILE_BIT_WRITE | FILE_BIT_TEXT)) == (FILE_BIT_READ | FILE_BIT_WRITE | FILE_BIT_TEXT))
      fd = sim_file_open_utf (pathname, FILE_MODE_FILE, (flags & ~O_TEXT) | O_BINARY);
    if (fd >= 0 && _setmode (fd, O_TEXT) < 0)
      SIM_FILE_CLOSE (&fd);
  }
#else
  fd = sim_file_open_utf (pathname, bits & FILE_BIT_EXEC ? FILE_MODE_DIRECTORY : FILE_MODE_FILE, flags);
#endif
  if (fd >= 0 && size) {
    struct stat buf;
    if (fstat (fd, &buf) || S_ISDIR (buf.st_mode)) {
      err = errno ? errno : SIM_FILE_BAD_TYPE;
    } else if ((err = sim_file_get_size (fd, size)) == SIM_OK && ! SIM_CHECK_LONG (*size))
      err = SIM_FILE_BAD_SIZE;
    if (err != SIM_OK) {
      SIM_FILE_CLOSE (&fd);
      errno = err;
    }
  } else if (! errno && fd < 0)
    errno = ENOENT;
  return fd;
}

int sim_file_create (const char *filename, int bits) {
  int fd = -1, err = 0;
  simtype pathname = sim_file_name_new (filename, FILE_DIRECTORY_USER), name;
  if (pathname.typ != SIMNIL && (fd = sim_file_open (pathname.ptr, bits, NULL)) < 0) {
    char *slash = strchr (filename, *FILE_DIRECTORY_SLASH);
    sim_file_mkdir ((name = sim_file_name_new ("", FILE_DIRECTORY_LOGIN)).ptr);
    STRING_FREE (&name, sim_file_name_new ("", FILE_DIRECTORY_USER));
    sim_file_mkdir (name.ptr);
    string_free (name);
    if (slash) {
      *slash = 0;
      sim_file_mkdir ((name = sim_file_name_new (filename, FILE_DIRECTORY_USER)).ptr);
      string_free (name);
      *slash = *FILE_DIRECTORY_SLASH;
    }
    if ((fd = sim_file_open (pathname.ptr, bits, NULL)) < 0)
      err = errno;
  }
  string_free (pathname);
  errno = err;
  return fd;
}

simbool sim_file_create_link (const char *oldname, const char *newname) {
#ifdef _WIN32
  simbool ok = false;
  HRESULT hr = CoInitializeEx (0, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
  if (SUCCEEDED (hr) || hr == RPC_E_CHANGED_MODE) {
    IShellLinkW *link;
    unsigned oldlen = strlen (oldname), newlen = strlen (newname);
    simtype oldpath = sim_convert_utf_to_ucs (oldname, &oldlen), newpath = sim_convert_utf_to_ucs (newname, &newlen);
    if (oldpath.typ != SIMNIL && newpath.typ != SIMNIL) {
      hr = CoCreateInstance (&CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, &IID_IShellLinkW, (void **) &link);
      if (SUCCEEDED (hr)) {
        IPersistFile *file;
        link->lpVtbl->SetPath (link, oldpath.ptr);
        hr = link->lpVtbl->QueryInterface (link, &IID_IPersistFile, (void **) &file);
        if (SUCCEEDED (hr)) {
          hr = file->lpVtbl->Save (file, newpath.ptr, TRUE);
          if (SUCCEEDED (hr))
            ok = true;
          file->lpVtbl->Release (file);
        }
        link->lpVtbl->Release (link);
      }
    }
    string_free (newpath);
    string_free (oldpath);
    CoUninitialize ();
  }
  errno = hr;
  return ok;
#else
  return ! symlink (oldname, newname);
#endif
}

#ifdef _WIN32
static int sim_file_link_utf (const wchar_t *oldname, const char *pathname) {
  FILE_CALL_UTF (int, -1, link, ! CreateHardLinkW, oldname, NULL);
}
#endif

int sim_file_link (const char *pathname, const char *newname) {
  FILE_CALL_UTF (int, -1, link, sim_file_link_utf, newname);
}

simtype sim_file_read_link (const char *pathname) {
#ifdef _WIN32
  simtype name = nil ();
  HRESULT hr = CoInitializeEx (0, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
  if (SUCCEEDED (hr) || hr == RPC_E_CHANGED_MODE) {
    unsigned len = strlen (pathname);
    simtype tmp = sim_convert_utf_to_ucs (pathname, &len);
    if (tmp.typ != SIMNIL) {
      IShellLinkW *link;
      hr = CoCreateInstance (&CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, &IID_IShellLinkW, (void **) &link);
      if (SUCCEEDED (hr)) {
        IPersistFile *file;
        hr = link->lpVtbl->QueryInterface (link, &IID_IPersistFile, (void **) &file);
        if (SUCCEEDED (hr)) {
          wchar_t path[MAX_PATH + 1];
          path[MAX_PATH] = 0;
          hr = file->lpVtbl->Load (file, tmp.ptr, STGM_READ);
          if (SUCCEEDED (hr))
            hr = link->lpVtbl->GetPath (link, path, MAX_PATH, NULL, 0);
          if (SUCCEEDED (hr)) {
            len = wcslen (path);
            name = sim_convert_ucs_to_utf (path, &len);
          }
          file->lpVtbl->Release (file);
        }
        link->lpVtbl->Release (link);
      }
      string_free (tmp);
    }
    CoUninitialize ();
  }
#else
  simtype name = string_new (FILE_MAX_LINK_SIZE);
  int hr = errno = 0, len;
  if ((len = readlink (pathname, name.ptr, name.len)) < 0) {
    hr = errno;
    STRING_FREE (&name, nil ());
  } else
    name.str[len] = 0;
#endif
  errno = hr;
  return name;
}

int FILE_FUNCTION (sim_file_read, void) {
  FILE_CALL_LOOP (read);
}

int FILE_FUNCTION (sim_file_write, const void) {
  FILE_CALL_LOOP (write);
}

static simbool sim_file_callback_write (void *ctxt, simtype input) {
  struct file_context *ctx = ctxt;
  unsigned len = sim_file_write (ctx->fd, input.str, input.len);
  if (len == input.len)
    return true;
  ctx->err = FILE_CASE_ERROR (len, SIM_FILE_NO_SPACE);
  return false;
}

static simbool sim_file_callback_read (void *ctxt, simtype output) {
  struct file_context *ctx = ctxt;
  unsigned len = sim_file_read (ctx->fd, output.str, output.len);
  if (len == output.len)
    return true;
  ctx->err = FILE_CASE_ERROR (len, SIM_FILE_END);
  return false;
}

static simbool sim_file_callback_encrypt (void *ctxt, simtype input) {
  struct file_context *ctx = ctxt;
  if (input.len > ctx->buffer.len)
    return false;
  if (! ctx->crypt) {
    memcpy (ctx->buffer.str, input.str, input.len);
    sim_crypt_md_update (ctx->md, input.str, input.len);
  } else
    sim_crypt_encrypt (ctx->crypt, input, ctx->buffer.str);
  ctx->buffer.len -= input.len;
  ctx->buffer.str += input.len;
  return true;
}

static simbool sim_file_callback_decrypt (void *ctxt, simtype output) {
  struct file_context *ctx = ctxt;
  if (output.len > ctx->buffer.len)
    return false;
  if (ctx->crypt) {
    sim_crypt_decrypt (ctx->crypt, pointer_new_len (ctx->buffer.str, output.len), output.str);
  } else
    memcpy (output.str, ctx->buffer.str, output.len);
  ctx->buffer.len -= output.len;
  ctx->buffer.str += output.len;
  return true;
}

static simtype file_convert_key (const simtype header, int type) {
  simtype key = file_key_strings[type & FILE_TYPE_KEY], salt;
  if (! (type & FILE_TYPE_KEY) || file_key_type != SIM_KEY_NONE)
    return sim_crypt_md_hash (sim_crypt_md_new (CRYPT_MD_WHIRLPOOL), nil (), nil (), key);
  salt = table_get_string (header, FILE_KEY_SALT);
  return salt.len < RANDOM_SIZE_BLOCK ? nil () : sim_crypt_convert_password (key.ptr, salt);
}

static simtype sim_file_new_key (const char *filename) {
  simtype name = string_cat ("./", filename);
  char *s;
  for (s = name.ptr; *s; s++)
    if (*s == *FILE_DIRECTORY_SLASH)
      *s = '/';
  if (s - (char *) name.ptr > (int) SIM_STRING_GET_LENGTH_CONST (SIM_FILE_BACKUP))
    if (! strcmp (s - SIM_STRING_GET_LENGTH_CONST (SIM_FILE_BACKUP), SIM_FILE_BACKUP)) {
      simtype tmp = name;
      s[-(int) SIM_STRING_GET_LENGTH_CONST (SIM_FILE_BACKUP)] = 0;
      name = string_copy (name.ptr);
      string_free (tmp);
    }
  return name;
}

simtype sim_file_name_new (const char *filename, int directory) {
  if (filename[0] == *FILE_DIRECTORY_SLASH || filename[0] == '/')
    return string_copy (filename);
#ifdef _WIN32
  if (filename[0] && filename[1] == ':')
    return string_copy (filename);
#endif
  if (directory == FILE_DIRECTORY_USER)
    return file_user_dir ? string_concat (file_user_dir, FILE_DIRECTORY_SLASH, filename, NULL) : nil ();
  if (directory == FILE_DIRECTORY_APP)
    return string_concat (file_app_dir, FILE_DIRECTORY_SLASH, filename, NULL);
  return string_concat (file_home_dir, FILE_DIRECTORY_SLASH FILE_CONFIG_DIRECTORY FILE_DIRECTORY_SLASH, filename, NULL);
}

simtype sim_file_name_copy (const simtype pathname) {
  simtype str = nil ();
  unsigned len = pathname.len;
#ifdef _WIN32
  wchar_t *ucs = NULL;
  simtype buf = sim_convert_utf (pathname.ptr, &len);
  errno = SIM_FILE_BAD_NAME;
  if (buf.typ != SIMNIL) {
    ucs = _wfullpath (NULL, buf.ptr, 0);
    len = errno;
    string_free (buf);
    errno = len;
  }
  if (! ucs)
    return nil ();
  len = wcslen (ucs);
  buf = sim_convert_ucs_to_utf (ucs, &len);
  free (ucs);
  str = buf.typ != SIMNIL ? string_copy_len (buf.str, len + 1) : nil ();
  string_free (buf);
#else
  string_free (sim_convert_utf (pathname.ptr, &len));
  errno = SIM_FILE_BAD_NAME;
  if (len) {
#if HAVE_REALPATH
    char *s = realpath (pathname.ptr, NULL);
    if (s) {
      str = string_copy_len (s, strlen (s) + 1);
      free (s);
    } else
#endif
    {
      if (pathname.str[0] != *FILE_DIRECTORY_SLASH) {
        simtype buf = string_buffer_new (SIM_MAX_MSG_SIZE + 2);
        if (getcwd (buf.ptr, buf.len)) {
          string_buffer_append (&buf, len = strlen (buf.ptr), pathname.len + 2);
          buf.str[len++] = *FILE_DIRECTORY_SLASH;
          memcpy (buf.str + len, pathname.str, pathname.len + 1);
          str = string_buffer_truncate (buf, len + pathname.len + 1);
        } else
          string_buffer_free (buf);
      }
    }
  } else
    return nil ();
#endif /* _WIN32 */
  if (str.typ == SIMNIL)
    str = FILE_NAME_COPY (pathname);
  if (sim_file_name_split (str.ptr, str.len - 1).len > SIM_MAX_MSG_SIZE) {
    STRING_FREE (&str, nil ());
    errno = SIM_FILE_BAD_NAME;
  }
  return str;
}

simbool sim_file_name_check (const char *filename, const char *extensions[]) {
  unsigned l;
  const char **p;
  for (l = 0; isdigit (*filename++); l++) {}
  if (l != 19 || ! extensions)
    return true;
  if (filename[-1] == '_')
    while (isdigit (*filename++)) {}
  for (p = extensions; *p && _stricmp (filename - 1, *p); p++) {}
  return ! *p;
}

simtype sim_file_name_parse (const simtype filename, const char *extensions[]) {
  unsigned len = filename.len;
  simtype str = string_new (len);
#ifdef _WIN32
  char *s;
#endif
  char *d = str.ptr;
  if (! sim_file_name_check (filename.ptr, extensions))
    *d++ = '_';
#ifdef _WIN32
  for (s = filename.ptr; --len; s++)
    *d++ = (simbyte) *s >= ' ' && ! strchr ("\"*:<>?\\|", *s) ? *s : '_';
  *d = 0;
#else
  memcpy (d, filename.str, len);
#endif
  return str;
}

simtype sim_file_name_split (const char *pathname, unsigned length) {
  const char *slash = strrchr (pathname, '/');
#ifdef _WIN32
  const char *s = strrchr (pathname, *FILE_DIRECTORY_SLASH);
  if (s > slash)
    slash = s;
  if (! slash && pathname[0] && pathname[1] == ':')
    slash = (char *) &pathname[1];
#endif
  return slash ? pointer_new_len (slash + 1, pathname + length - slash - 1) : pointer_new_len (pathname, length);
}

int sim_file_get_stat (const simtype pathname, simunsigned *byte, simunsigned *bytes,
                       simunsigned *node, simunsigned *nodes) {
  int err = SIM_OK;
#ifdef _WIN32
  ULARGE_INTEGER avail, total;
  unsigned len = pathname.len;
  simtype name = sim_convert_utf_to_ucs (pathname.ptr, &len);
  if (name.typ != SIMNIL && GetDiskFreeSpaceExW (name.ptr, &avail, &total, NULL)) {
    *byte = avail.QuadPart;
    *bytes = total.QuadPart;
    *node = *nodes = 0;
  } else
    err = name.typ != SIMNIL ? GetLastError () : SIM_FILE_BAD_NAME;
  string_free (name);
#elif HAVE_FSTATFS && ! defined(__NetBSD__)
  struct statfs fsbuf;
  if (pathname.ptr && ! statfs (pathname.ptr, &fsbuf)) {
    *byte = (simunsigned) fsbuf.f_bsize * fsbuf.f_bavail;
    *bytes = (simunsigned) fsbuf.f_bsize * fsbuf.f_blocks;
    *node = fsbuf.f_ffree;
    *nodes = fsbuf.f_files;
  } else
    err = ! pathname.ptr ? SIM_FILE_BAD_NAME : errno ? errno : EINVAL;
#else
  err = EINVAL;
#endif
  return err;
}

simtype sim_file_get_id (int fd) {
  simtype id = nil ();
#ifndef _WIN32
  struct stat buf;
  if (! fstat (fd, &buf)) {
#if HAVE_FSTATFS && ! defined(__NetBSD__)
    struct statfs fsbuf;
    memset (&fsbuf.f_fsid, 0, sizeof (fsbuf.f_fsid));
    if (fstatfs (fd, &fsbuf))
      memset (&fsbuf.f_fsid, 0, sizeof (fsbuf.f_fsid));
    id = string_buffer_new (sizeof (buf.st_ino) + sizeof (buf.st_dev) + sizeof (fsbuf.f_fsid));
    memcpy (id.str, &buf.st_ino, sizeof (buf.st_ino));
    memcpy (id.str + sizeof (buf.st_ino), &fsbuf.f_fsid, sizeof (fsbuf.f_fsid));
#else
    id = string_buffer_new (sizeof (buf.st_ino) + sizeof (buf.st_dev));
    memcpy (id.str, &buf.st_ino, sizeof (buf.st_ino));
#endif
    memcpy (id.str + id.len - sizeof (buf.st_dev), &buf.st_dev, sizeof (buf.st_dev));
  }
#else
  BY_HANDLE_FILE_INFORMATION buf;
  if (GetFileInformationByHandle ((HANDLE) _get_osfhandle (fd), &buf)) {
    const int l = sizeof (buf.nFileIndexLow) + sizeof (buf.nFileIndexHigh) + sizeof (buf.dwVolumeSerialNumber);
    simbyte *s = (id = string_buffer_new (l)).str;
    memcpy (s, &buf.nFileIndexLow, sizeof (buf.nFileIndexLow));
    memcpy (s += sizeof (buf.nFileIndexLow), &buf.nFileIndexHigh, sizeof (buf.nFileIndexHigh));
    memcpy (s + sizeof (buf.nFileIndexHigh), &buf.dwVolumeSerialNumber, sizeof (buf.dwVolumeSerialNumber));
  } else
    errno = GetLastError ();
#endif
  return id;
}

static simbool sim_file_get_times (int fd, const struct stat *buf, simnumber *filetimes) {
  if (filetimes) {
#ifdef _WIN32
    FILETIME ctime, atime, mtime;
    if (fd < 0 || ! GetFileTime ((HANDLE) _get_osfhandle (fd), &ctime, &atime, &mtime))
      return false;
    filetimes[0] = FILE_CALC_TIME (mtime) > FILE_TIME_0 ? (FILE_CALC_TIME (mtime) - FILE_TIME_0) * 100 : 1000000000;
    filetimes[1] = FILE_CALC_TIME (ctime) > FILE_TIME_0 ? (FILE_CALC_TIME (ctime) - FILE_TIME_0) * 100 : 1000000000;
    filetimes[2] = FILE_CALC_TIME (atime) > FILE_TIME_0 ? (FILE_CALC_TIME (atime) - FILE_TIME_0) * 100 : 1000000000;
#elif defined(__APPLE__)
    filetimes[0] = (simnumber) buf->st_mtimespec.tv_sec * 1000000000 + buf->st_mtimespec.tv_nsec;
    filetimes[1] = (simnumber) buf->st_ctimespec.tv_sec * 1000000000 + buf->st_ctimespec.tv_nsec;
    filetimes[2] = (simnumber) buf->st_atimespec.tv_sec * 1000000000 + buf->st_atimespec.tv_nsec;
#elif defined(st_mtime)
    filetimes[0] = (simnumber) buf->st_mtim.tv_sec * 1000000000 + buf->st_mtim.tv_nsec;
    filetimes[1] = (simnumber) buf->st_ctim.tv_sec * 1000000000 + buf->st_ctim.tv_nsec;
    filetimes[2] = (simnumber) buf->st_atim.tv_sec * 1000000000 + buf->st_atim.tv_nsec;
#else
    filetimes[0] = (simnumber) buf->st_mtime * 1000000000;
    filetimes[1] = (simnumber) buf->st_ctime * 1000000000;
    filetimes[2] = (simnumber) buf->st_atime * 1000000000;
#endif
  }
  return true;
}

int sim_file_get_type (int fd, simnumber *filetimes) {
  struct stat buf;
#ifdef _WIN32
  if (! filetimes || ! sim_file_get_times (fd, &buf, filetimes))
    return filetimes ? -1 : 0;
  filetimes[1] = filetimes[0];
#else
  if (fstat (fd, &buf)) {
    LOG_WARN_ ("fstat:%d error %d\n", fd, errno);
    return -1;
  }
  sim_file_get_times (fd, &buf, filetimes);
  if (buf.st_mode & S_IXUSR)
    return 1;
#endif
  return 0;
}

int sim_file_get_time (const char *pathname, simnumber *filetime) {
  simnumber ftimes[3];
#ifndef _WIN32
  struct stat buf;
  if (sim_file_lstat (pathname, &buf))
    return errno ? errno : ENOENT;
  sim_file_get_times (-1, &buf, ftimes);
#else
  if (sim_file_size (pathname, NULL, ftimes))
    return errno ? errno : ENOENT;
#endif
  *filetime = ftimes[1];
  return SIM_OK;
}

int sim_file_get_size (int fd, simnumber *size) {
  simnumber pos = lseek (fd, 0, SEEK_CUR);
  if (pos >= 0 && (*size = lseek (fd, 0, SEEK_END)) >= 0 && lseek (fd, pos, SEEK_SET) == pos)
    return SIM_OK;
  LOG_WARN_ ("lseek:%d error %d\n", fd, errno);
  return errno ? errno : SIM_FILE_NO_SEEK;
}

int sim_file_size (const char *pathname, simnumber *size, simnumber *filetimes) {
  struct stat buf;
#ifdef _WIN32
  int err, ret = -1, fd = sim_file_open_utf (pathname, FILE_MODE_FILE, O_RDONLY | O_BINARY);
  if (fd >= 0) {
    ret = size ? fstat (fd, &buf) : 0, err = errno;
    if (! sim_file_get_times (fd, &buf, ret ? NULL : filetimes))
      ret = -1, err = GetLastError ();
    sim_file_close (fd);
    errno = err;
  }
#else
  int ret = sim_file_stat (pathname, &buf);
  sim_file_get_times (-1, &buf, ret ? NULL : filetimes);
#endif
  if (size)
    *size = ret ? -1 : buf.st_size;
  return ret;
}

int sim_file_utimes (const char *pathname, void *input) {
#ifdef _WIN32
  int err, ret = -1, fd = sim_file_open_utf (pathname, FILE_MODE_FILE, O_WRONLY | O_BINARY);
  if (fd >= 0) {
    ret = sim_file_set_times (fd, input);
    err = errno;
    sim_file_close (fd);
    errno = err;
  }
  return ret;
#else
  FILE_CALL_UTF (int, -1, utimes, _wutime, input);
#endif
}

int sim_file_set_times (int fd, void *input) {
#ifdef _WIN32
  struct timeval *tv = input;
  simunsigned atime = (simunsigned) tv[0].tv_sec * 10000000 + (simunsigned) tv[0].tv_usec * 10 + FILE_TIME_0;
  simunsigned mtime = (simunsigned) tv[1].tv_sec * 10000000 + (simunsigned) tv[1].tv_usec * 10 + FILE_TIME_0;
  FILETIME filetimes[2];
  filetimes[0].dwLowDateTime = (DWORD) atime, filetimes[0].dwHighDateTime = (DWORD) (atime >> 32);
  filetimes[1].dwLowDateTime = (DWORD) mtime, filetimes[1].dwHighDateTime = (DWORD) (mtime >> 32);
  if (SetFileTime ((HANDLE) _get_osfhandle (fd), NULL, &filetimes[0], &filetimes[1]))
    return 0;
  errno = GetLastError ();
#elif HAVE_FUTIMES
  return futimes (fd, input);
#endif
  return -1;
}

const char *file_set_user (const char *user) {
#ifdef _WIN32
  const wchar_t *home;
  wchar_t path[MAX_PATH];
  if (! file_app_dir) {
    int err = SHGetFolderPathW (NULL, CSIDL_PROFILE, NULL, 0, path);
    if (err == S_OK) {
      unsigned len = wcslen (path);
      simtype str = sim_convert_ucs_to_utf (path, &len);
      file_app_dir = string_copy (str.ptr).ptr;
      string_free (str);
    }
    if (! file_app_dir) {
      LOG_ERROR_ ("SHGetFolderPath error %u\n", err);
      file_app_dir = string_copy (".").ptr;
    }
    if ((home = _wgetenv (L"SIMHOME")) != NULL) {
      unsigned len = wcslen (home);
      simtype str = sim_convert_ucs_to_utf (home, &len);
      file_home_dir = string_copy (str.ptr).ptr;
      string_free (str);
    }
#else
  if (! file_app_dir) {
    struct passwd *pw = getpwuid (getuid ());
    const char *home = pw ? pw->pw_dir : NULL;
    if (! home)
      home = getenv ("HOME");
    file_app_dir = string_copy (home ? home : ".").ptr;
    endpwent ();
    if (! home)
      LOG_ERROR_ ("getenv HOME\n");
    if ((home = getenv ("SIMHOME")) != NULL)
      file_home_dir = string_copy (home).ptr;
#endif
    if (! file_home_dir || ! file_home_dir[0])
      SIM_FREE_STRING (&file_home_dir, string_copy (file_app_dir).ptr);
  }
  if (user && user[0] == '.' && ! user[1]) {
    SIM_FREE_STRING (&file_home_dir, NULL);
    SIM_FREE_STRING (&file_app_dir, NULL);
    user = NULL;
  }
  sim_free_string (file_user_dir);
  if (user) {
    if (! user[0]) {
      file_user_dir = sim_file_name_new (FILE_USER_DIRECTORY, FILE_DIRECTORY_LOGIN).ptr;
    } else if (user[0] != '.' || (user[1] != *FILE_DIRECTORY_SLASH && user[1] != '/')) {
      file_user_dir = sim_file_name_new (user, FILE_DIRECTORY_LOGIN).ptr;
    } else
      file_user_dir = string_copy (user).ptr;
  } else
    file_user_dir = NULL;
  return file_user_dir;
}

simtype file_set_password (simtype key, simbool password) {
  simtype seed = nil ();
  if (file_key_strings[password].ptr)
    string_free (file_key_strings[password]);
  if (password) {
    seed = convert_string_to_seed (key.ptr, &file_key_type);
    if (file_key_type != SIM_KEY_NONE) {
      STRING_FREE (&key, seed);
    } else
      STRING_FREE (&seed, nil ());
  }
  file_key_strings[password] = key;
  return seed;
}

int file_check_password (void) {
  simtype ec, rsa;
  return ! file_user_dir || key_get (false, &ec, &rsa).typ == SIMNIL ? -1 : ! file_key_strings[1].len;
}

simbool sim_file_check_exists (const char *filename) {
  simtype name = sim_file_name_new (filename, FILE_DIRECTORY_USER);
  simbool exists = ! sim_file_lstat (name.ptr, NULL);
  string_free (name);
  return exists;
}

int file_wipe (simcontact contact, const char *pathname, int (*callback) (int, const void *, unsigned)) {
  int err = SIM_FILE_NO_SPACE, fd, len = SIM_FILE_BUFFER_SIZE, i, buflen = param_get_number ("file.wipe");
  if (buflen <= 0) {
    err = SIM_OK;
  } else if ((fd = sim_file_open (pathname, FILE_BIT_WRITE, NULL)) >= 0) {
    simnumber size = lseek (fd, 0, SEEK_END);
    if (size >= 0) {
      simtype buf = string_buffer_new (len = (buflen < len ? len : buflen) + (unsigned short) rand ());
      for (i = 0; i < len && i < size; i++)
        buf.str[i] = (simbyte) rand ();
      if (! lseek (fd, 0, SEEK_SET)) {
        for (err = SIM_OK; size > 0; size -= len)
          if ((len = callback (fd, buf.str, buf.len < size ? buf.len : (unsigned) size)) <= 0) {
            err = FILE_CASE_ERROR (len, SIM_FILE_NO_SPACE);
            break;
          }
      } else
        err = errno ? errno : EINVAL;
      string_buffer_free (buf);
    } else
      err = errno ? errno : EINVAL;
    if (sim_file_close (fd) && err == SIM_OK)
      err = errno ? errno : ENOSPC;
    if (err == SIM_CLIENT_OFFLINE)
      return err;
  } else {
    err = errno;
    if (sim_file_lstat (pathname, NULL))
      err = SIM_OK;
  }
  if (err != SIM_OK) {
    sim_error_set_text (" [", pathname, "]", err);
    event_send_name_number (contact, SIM_EVENT_ERROR, SIM_EVENT_ERROR_FILE_WIPE, err);
  }
  if ((err = ! sim_file_remove (pathname) ? SIM_OK : errno ? errno : EINVAL) != SIM_OK)
    LOG_WARN_ ("delete %s error %d\n", pathname, err);
  return err;
}

int file_copy (const char *oldfilename, const char *newfilename) {
  simtype name = sim_file_name_new (oldfilename, FILE_DIRECTORY_USER);
  int err = SIM_OK, fd;
  unsigned len;
  simnumber size;
  if (name.typ == SIMNIL)
    return SIM_KEY_NO_SAVE;
  if ((fd = sim_file_open (name.ptr, FILE_BIT_READ, NULL)) < 0) {
    LOG_WARN_ ("copy %s (error %d)\n", oldfilename, errno);
    sim_error_set_text (" [", oldfilename, "]", err = errno);
    string_free (name);
    event_send_name_number (NULL, SIM_EVENT_ERROR, SIM_EVENT_ERROR_FILE_LOAD, err);
    return err;
  }
  STRING_FREE (&name, sim_file_name_new (newfilename, FILE_DIRECTORY_USER));
  file_wipe (NULL, name.ptr, sim_file_write);
  if ((size = lseek (fd, 0, SEEK_END)) >= 0 && size <= FILE_MAX_TORRENT_SIZE && ! lseek (fd, 0, SEEK_SET)) {
    simtype buf = string_buffer_new ((unsigned) size);
    int fd2 = sim_file_open (name.ptr, FILE_BIT_WRITE | FILE_BIT_CREATE | FILE_BIT_TRUNCATE, NULL);
    if (fd2 < 0) {
      err = errno;
    } else if ((len = sim_file_read (fd, buf.str, buf.len)) != buf.len) {
      err = FILE_CASE_ERROR (len, SIM_FILE_END);
    } else if ((len = sim_file_write (fd2, buf.str, buf.len)) != buf.len)
      err = FILE_CASE_ERROR (len, SIM_FILE_NO_SPACE);
#ifdef _WIN32
    if (_commit (fd2) && err == SIM_OK)
      err = errno ? errno : ENOSPC;
#endif
    if (sim_file_close (fd2) && err == SIM_OK)
      err = errno ? errno : ENOSPC;
    string_buffer_free (buf);
  } else
    err = FILE_CASE_ERROR (size < 0 ? -1 : 0, SIM_FILE_ERROR);
  string_free (name);
  sim_file_close (fd);
  LOG_INFO_ ("copy %s to %s (error %d)\n", oldfilename, newfilename, err);
  if (err != SIM_OK) {
    sim_error_set_text (" [", newfilename, "]", err);
    event_send_name_number (NULL, SIM_EVENT_ERROR, SIM_EVENT_ERROR_FILE_COPY, err);
  }
  return err;
}

int file_lock (simbool lock) {
  static int file_lock_fd = -1;
  simtype name, pid;
  unsigned len;
  int err = SIM_FILE_LOCKED;
  if (! lock) {
    if (file_lock_fd >= 0) {
      SIM_FILE_CLOSE (&file_lock_fd);
      sim_file_remove ((name = sim_file_name_new (FILE_LOCK, FILE_DIRECTORY_USER)).ptr);
      string_free (name);
    }
    err = SIM_OK;
  } else if (sim_error_reset_text (), file_lock_fd >= 0) {
    LOG_ERROR_ ("flock error\n");
#ifdef _WIN32
  } else if ((file_lock_fd = sim_file_create (FILE_LOCK, FILE_BIT_WRITE | FILE_BIT_CREATE | FILE_BIT_TEXT)) >= 0) {
    HANDLE handle = (HANDLE) _get_osfhandle (file_lock_fd);
    OVERLAPPED ov;
    memset (&ov, 0, sizeof (ov));
    ov.Offset = 100;
    if (! LockFileEx (handle, LOCKFILE_FAIL_IMMEDIATELY | LOCKFILE_EXCLUSIVE_LOCK, 0, 1, 0, &ov)) {
      LOG_WARN_ ("flock error = %d\n", GetLastError ());
      SIM_FILE_CLOSE (&file_lock_fd);
      goto fail;
    }
#elif HAVE_FLOCK
  } else if ((file_lock_fd = sim_file_create (FILE_LOCK, FILE_BIT_WRITE | FILE_BIT_CREATE | FILE_BIT_TEXT)) >= 0) {
    if (flock (file_lock_fd, LOCK_EX | LOCK_NB)) {
      LOG_WARN_ ("flock error = %d\n", errno);
      if (lseek (file_lock_fd, 0, SEEK_END)) {
        SIM_FILE_CLOSE (&file_lock_fd);
        goto fail;
      }
    }
#else
  } else if (sim_file_check_exists (FILE_LOCK)) {
    SIM_FILE_CLOSE (&file_lock_fd);
  } else if ((file_lock_fd = sim_file_create (FILE_LOCK, FILE_BIT_WRITE | FILE_BIT_CREATE | FILE_BIT_TEXT)) >= 0) {
#endif
    pid = sim_convert_simunsigned_to_string ("", GetCurrentProcessId (), "\n");
    if ((len = sim_file_write (file_lock_fd, pid.str, pid.len)) != pid.len) {
      err = FILE_CASE_ERROR (len, SIM_FILE_NO_SPACE);
    } else
      err = sim_file_truncate (file_lock_fd);
    string_free (pid);
    if (err != SIM_OK) {
      LOG_WARN_ ("flock write error = %d\n", err);
      file_lock (false);
    }
  } else if ((err = errno) != 0)
    LOG_WARN_ ("flock error %d\n", err);
fail:
  if (err != SIM_OK && err != SIM_FILE_LOCKED)
    sim_error_set_text (" [", FILE_LOCK, "]", err);
  return err;
}

int file_save (simtype table, const char *filename, int type) {
  simunsigned cpu = sim_system_cpu_get (SYSTEM_CPU_TIME_THREAD, NULL);
  int err = SIM_OK, cipher = -2;
  unsigned size, len;
  struct file_context ctx;
  simtype name = sim_file_name_new (filename, FILE_DIRECTORY_USER), bak = nil (), signature = nil ();
  sim_error_reset_text ();
  if (name.typ == SIMNIL)
    return type & FILE_TYPE_KEY ? SIM_KEY_NO_SAVE : SIM_OK;
  if (! (type & FILE_TYPE_TEMPORARY)) {
    simbool exists = ! sim_file_lstat (name.ptr, NULL);
    if (exists || table.typ == SIMNIL) {
      file_wipe (NULL, (bak = string_cat (name.ptr, SIM_FILE_BACKUP)).ptr, sim_file_write);
      if (! exists || sim_file_rename (name.ptr, bak.ptr))
        STRING_FREE (&bak, nil ());
    }
  }
  if (table.typ == SIMNIL) {
    if (bak.typ != SIMNIL)
      err = file_wipe (NULL, bak.ptr, sim_file_write);
  } else if ((ctx.fd = sim_file_create (filename, FILE_BIT_WRITE | FILE_BIT_CREATE)) >= 0) {
    simbyte *buf;
    simtype header = table_new (0), iv, salt;
    if (type & FILE_TYPE_ENCRYPTED)
      table_add_pointer (table, FILE_KEY_VERSION, PACKAGE_VERSION);
    buf = (ctx.buffer = string_buffer_new (size = table_size (table, 0))).str;
    ctx.err = SIM_FILE_NO_SPACE;
    if (type & FILE_TYPE_ENCRYPTED && file_key_strings[type & FILE_TYPE_KEY].len) {
      simtype filecipher = contact_list.me ? param_get_string ("file.cipher") : nil ();
      if ((cipher = crypt_cipher_find (filecipher.ptr, 0, NULL)) < 0)
        if (! filecipher.len || type & FILE_TYPE_KEY || string_check_diff (filecipher, "NONE"))
          cipher = 0;
    } else if (! (type & FILE_TYPE_KEY) && type & FILE_TYPE_ENCRYPTED)
      err = SIM_FILE_NO_KEY;
    if (err == SIM_OK && (len = sim_file_write (ctx.fd, FILE_PREFIX, sizeof (FILE_PREFIX))) != sizeof (FILE_PREFIX))
      err = FILE_CASE_ERROR (len, SIM_FILE_NO_SPACE);
    if (err == SIM_OK) {
      if (cipher >= 0) {
        struct _ssl_master master;
        random_get (random_public, iv = string_new (crypt_cipher_size_max ("file.tagsize")));
        table_add (header, FILE_KEY_IV, iv);
        if (type & FILE_TYPE_KEY) {
          random_get (random_public, salt = string_new (RANDOM_SIZE_BLOCK));
          table_add (header, FILE_KEY_SALT, salt);
        }
        master.key = file_convert_key (header, type);
        table_add (header, FILE_KEY_CIPHER, crypt_cipher_encrypt (cipher, &master, 0));
        sim_crypt_auth_start (ctx.crypt = sim_crypt_new (cipher, master.key, true), 0, iv.ptr, iv.len, 1);
        string_free (master.key);
        err = table_write (table, sim_file_callback_encrypt, &ctx, 0) && ! ctx.buffer.len ? SIM_OK : SIM_FILE_BAD_TABLE;
        random_get (random_public, signature = string_new (crypt_cipher_size_max ("file.tagsize")));
        sim_crypt_auth_stop (ctx.crypt, signature.str);
        sim_crypt_free (ctx.crypt, true);
      } else {
        if (cipher == -1)
          table_add_pointer (header, FILE_KEY_CIPHER, "");
        ctx.crypt = NULL;
        ctx.md = sim_crypt_md_new (CRYPT_MD_RIPEMD);
        err = table_write (table, sim_file_callback_encrypt, &ctx, 0) && ! ctx.buffer.len ? SIM_OK : SIM_FILE_BAD_TABLE;
        signature = sim_crypt_md_free (ctx.md);
      }
    }
    if (type & FILE_TYPE_ENCRYPTED)
      table_delete (table, FILE_KEY_VERSION);
    table_add_key_pointer_len (header, sim_file_new_key (filename), buf, size);
    if (signature.typ != SIMNIL)
      table_add (header, FILE_KEY_HASH, signature);
    if (err == SIM_OK && ! table_write (header, sim_file_callback_write, &ctx, 0))
      err = ctx.err;
    sim_free (buf, size);
    table_free (header);
    if (err == SIM_OK) /* only important for temporary files (unless initial rename failed) */
      err = sim_file_truncate (ctx.fd);
    if (sim_file_close (ctx.fd) && err == SIM_OK)
      err = errno ? errno : ENOSPC;
    if (err != SIM_OK)
      LOG_WARN_ ("save %s error %d\n", filename, err);
  } else {
    err = errno ? errno : ENOENT;
    LOG_WARN_ ("creat %s error %d\n", filename, errno);
  }
  if (err != SIM_OK) {
    file_wipe (NULL, name.ptr, sim_file_write);
    if (bak.typ != SIMNIL)
      sim_file_rename (bak.ptr, name.ptr);
    sim_error_set_text (" [", filename, "]", err);
  }
  string_free (bak);
  string_free (name);
  limit_set_cpu (sim_system_cpu_get (SYSTEM_CPU_TIME_THREAD, NULL) - cpu, 0);
  return err;
}

int _file_load (const char *filename, int type, simtype *table, const char *file, unsigned line) {
  int err = SIM_OK, cipher;
  unsigned len;
  simbool retry = false;
  struct file_context ctx;
  simtype name = line ? sim_file_name_new (filename, FILE_DIRECTORY_USER) : string_copy (filename), bak, header;
  *table = nil ();
  sim_error_reset_text ();
  if (name.typ == SIMNIL) {
    if (type & FILE_TYPE_KEY)
      return SIM_KEY_NO_KEY;
    *table = _sim_table_new (23, SIMTABLE_NORMAL, file, line);
    return SIM_OK;
  }
  if ((ctx.fd = sim_file_open (name.ptr, FILE_BIT_READ, NULL)) < 0) {
  retry:
    bak = string_cat (name.ptr, SIM_FILE_BACKUP);
    LOG_INFO_ ("open %s error %d\n", filename, errno);
    ctx.fd = -1;
    if ((type & FILE_TYPE_TEMPORARY || (ctx.fd = sim_file_open (bak.ptr, FILE_BIT_READ, NULL)) < 0) && ! retry) {
      err = SIM_KEY_NO_KEY;
      if (! (type & FILE_TYPE_KEY)) {
        err = errno;
        if (sim_file_lstat (name.ptr, NULL) && (type & FILE_TYPE_TEMPORARY || sim_file_lstat (bak.ptr, NULL))) {
          *table = _sim_table_new (23, SIMTABLE_NORMAL, file, line);
          if (line)
            err = SIM_OK;
        }
      }
    }
    STRING_FREE (&name, bak);
    retry = true;
  }
  string_free (name);
  if (ctx.fd >= 0) {
    simbool encrypted = type & FILE_TYPE_ENCRYPTED && file_key_strings[type & FILE_TYPE_KEY].len;
    simbyte prefix[sizeof (FILE_PREFIX)];
    ctx.err = SIM_FILE_ERROR;
    if (! encrypted && ! (type & FILE_TYPE_KEY) && type & FILE_TYPE_ENCRYPTED) {
      err = SIM_FILE_NO_KEY;
    } else if ((len = sim_file_read (ctx.fd, prefix, sizeof (prefix))) != sizeof (prefix)) {
      err = FILE_CASE_ERROR (len, SIM_FILE_END);
    } else if (memcmp (prefix, FILE_PREFIX, sizeof (prefix))) {
      err = SIM_FILE_ERROR;
    } else if ((header = _sim_table_read (nil (), nil (), sim_file_callback_read, &ctx, file, line)).typ != SIMNIL) {
      if (file) {
        simtype filecipher = table_get_string (header, FILE_KEY_CIPHER);
        simtype iv = table_get_string (header, FILE_KEY_IV), signature = table_get_string (header, FILE_KEY_HASH);
        ctx.err = err = SIM_FILE_ERROR;
        ctx.crypt = NULL;
        ctx.buffer = table_get_key_string (header, name = sim_file_new_key (line ? filename : file));
        string_free (name);
        if (encrypted && (filecipher.len || type & FILE_TYPE_KEY)) {
          struct _ssl_master master;
          master.key = file_convert_key (header, type);
          if ((cipher = sim_crypt_cipher_decrypt (filecipher, &master, 0)) >= 0) {
            unsigned size = sim_crypt_auth_size (ctx.crypt = sim_crypt_new (cipher, master.key, false));
            if (iv.len >= size && signature.len >= size)
              if (sim_crypt_auth_check (ctx.crypt, ctx.buffer, iv, 1, signature.str))
                err = SIM_OK;
          }
          string_free (master.key);
          if (err != SIM_OK && type & FILE_TYPE_KEY)
            err = SIM_FILE_BAD_KEY;
        } else if (! encrypted || filecipher.typ != SIMNIL) {
          simtype hash = sim_crypt_md_hash (sim_crypt_md_new (CRYPT_MD_RIPEMD), nil (), nil (), ctx.buffer);
          if (! string_check_diff_len (hash, signature.str, signature.len)) {
            err = SIM_OK;
          } else if (type & FILE_TYPE_KEY)
            err = SIM_FILE_BAD_KEY;
          string_free (hash);
        }
        if (err == SIM_OK) {
          if ((*table = _sim_table_read (nil (), nil (), sim_file_callback_decrypt, &ctx, file, line)).typ == SIMNIL) {
            err = ctx.err;
          } else if (ctx.buffer.len) {
            TABLE_FREE (table, nil ());
            err = SIM_FILE_ERROR;
          } else
            table_delete (*table, FILE_KEY_VERSION);
        }
        sim_crypt_free (ctx.crypt, false);
        table_free (header);
      } else {
        *table = header;
        err = SIM_OK;
      }
    } else
      err = ctx.err;
    sim_file_close (ctx.fd);
    if (err != SIM_OK) {
      if (type & FILE_TYPE_KEY) {
        LOG_NOTE_ ("load %s error %d\n", filename, err);
      } else
        LOG_ANY_ (retry ? SIM_LOG_WARN : SIM_LOG_NOTE, "load %s error %d\n", filename, err);
      if (! retry) {
        retry = true;
        if ((name = line ? sim_file_name_new (filename, FILE_DIRECTORY_USER) : string_copy (filename)).typ != SIMNIL)
          goto retry;
      }
    } else if (retry && line) {
      file_wipe (NULL, (name = sim_file_name_new (filename, FILE_DIRECTORY_USER)).ptr, sim_file_write);
      sim_file_rename ((bak = string_cat (name.ptr, SIM_FILE_BACKUP)).ptr, name.ptr);
      string_free (bak);
      string_free (name);
    }
  } else
    LOG_NOTE_ ("open %s error %d\n", filename, err);
  if (err != SIM_OK)
    sim_error_set_text (" [", filename, "]", err);
  return err;
}

static simbool sim_file_load_torrent_node (simtype nodes, simtype torrents, const struct _network_ip_port *node) {
  if (table_set_key_number (torrents, string_copy_len (node, CONTACT_SIZE_IP_PORT), true).typ == SIMNIL)
    if (table_add_key_pointer (nodes, string_copy_len (node, CONTACT_SIZE_IP_PORT), "").typ == SIMNIL)
      return true;
  return false;
}

static int sim_file_load_torrent (simtype nodes, simtype torrents, int fd, int nodesize, int length, unsigned *size) {
  long l;
  int j = 0, k = 0, i, m = 0, n = 0;
  char *s, *p;
  struct _network_ip_port node;
  simtype buffer = nil ();
  char buf[BUFSIZ];
  for (lseek (fd, 0, SEEK_SET); (i = sim_file_read (fd, buf + j, sizeof (buf) - j) + j) > j;) {
    *size += i - j;
    if (! m) {
      for (j = 0; j <= i - length; j++)
        if (! memcmp (buf + j, FILE_DHT_START, length)) {
          m = i - j;
          break;
        }
    } else
      m = i;
    if (m) {
      string_buffer_append (&buffer, n, m + 1);
      memcpy (buffer.str + n, buf + j, m);
      if (length == FILE_DHT_START_LENGTH)
        for (k = n < FILE_DHT_STOP_LENGTH - 1 ? 0 : 1 - FILE_DHT_STOP_LENGTH; k <= m - FILE_DHT_STOP_LENGTH; k++)
          if (! memcmp (buffer.str + n + k, FILE_DHT_STOP, FILE_DHT_STOP_LENGTH)) {
            m = k + FILE_DHT_STOP_LENGTH;
            k = -1;
            break;
          }
      n += m;
      if (k < 0)
        break;
      j = 0;
    } else
      memcpy (buf, buf + sizeof (buf) - length, j = length);
  }
  if (n)
    buffer.str[n] = 0;
  j = FILE_DHT_START_LENGTH - 1;
  k = 0;
  if (length == FILE_DHT_START_LENGTH) {
    while (j < n && buffer.str[j++] == 'l' && (p = strstr ((char *) buffer.str + j, "ee")) != NULL) {
      if ((l = strtol ((char *) buffer.str + j, &s, 10)) > 0)
        if (*s++ == ':' && (char *) buffer.str + n > s + l && s[l] == 'i') {
          unsigned ip;
          s[l] = 0;
          l = strtol (s + l + 1, NULL, 10);
          if (SIM_CHECK_SHORT (l) && (ip = sim_network_parse_ip (s)) != 0) {
            node.ip = htonl (ip), node.port = htons ((short) l);
            k += sim_file_load_torrent_node (nodes, torrents, &node);
          }
          /*LOG_XTRA_ ("node %s:%ld\n", network_convert_ip (ip), l);*/
        }
      j = p + 2 - (char *) buffer.str;
    }
  } else if ((l = strtol ((char *) buffer.str + j - 1, &s, 10)) > 0)
    if (*s++ == ':' && (char *) buffer.str + n > s + l && s[l] == 'e')
      for (; l >= nodesize; l -= nodesize) {
        memcpy (&node, s += nodesize - CONTACT_SIZE_IP_PORT, CONTACT_SIZE_IP_PORT);
        if (node.ip && node.port)
          k += sim_file_load_torrent_node (nodes, torrents, &node);
        /*LOG_XTRA_ ("node %s:%d\n", network_convert_ip (ntohl (node.ip)), ntohs (node.port));*/
        s += CONTACT_SIZE_IP_PORT;
      }
  string_buffer_free (buffer);
  return k;
}

void file_load_torrents (simtype mainline) {
  int fd, count;
  unsigned i, j, size = 0;
  simtype dirs = param_get_strings ("main.boot.dirs"), torrents = table_get_table (mainline, FILE_KEY_MAIN_TORRENTS);
  simtype key, nodes;
  if (torrents.typ != SIMNIL) {
    simwalker ctx;
    for (table_walk_first (&ctx, torrents); table_walk_next_number (&ctx, &key).typ != SIMNIL;)
      table_set_key_number (torrents, string_copy_string (key), false);
  } else
    table_add (mainline, FILE_KEY_MAIN_TORRENTS, torrents = table_new (101));
  if ((nodes = table_get_table (mainline, FILE_KEY_MAIN_NODES)).typ == SIMNIL)
    table_add (mainline, FILE_KEY_MAIN_NODES, nodes = table_new (101));
  for (i = 1; i <= dirs.len; i++) {
    simtype name, dirname = sim_file_name_new (dirs.arr[i].ptr, FILE_DIRECTORY_APP);
    void *dir = dirname.typ == SIMNIL ? 0 : sim_system_dir_open (dirname.ptr);
    if (dir) {
      const char *filename;
      int nodesize = param_get_number ("main.boot.size");
      simtype files = param_get_strings ("main.boot.files");
      for (j = 1; j <= files.len; j++) {
        name = string_concat (dirname.ptr, FILE_DIRECTORY_SLASH, files.arr[j].ptr, NULL);
        if ((fd = sim_file_open (name.ptr, FILE_BIT_READ, NULL)) >= 0) {
          if (lseek (fd, 0, SEEK_END) <= FILE_MAX_TORRENT_SIZE)
            if ((count = sim_file_load_torrent (nodes, torrents, fd, nodesize, FILE_DHT_START_LENGTH - 2, &size)) > 0)
              LOG_CODE_ANY_ (SIM_MODULE_MAIN, SIM_LOG_DEBUG, "added %d torrent nodes from %s\n", count, name.str);
          sim_file_close (fd);
        }
        string_free (name);
      }
      while (size <= FILE_MAX_TORRENTS_SIZE && (filename = sim_system_dir_read (dir)) != NULL) {
        name = string_concat (dirname.ptr, FILE_DIRECTORY_SLASH, filename, NULL);
        if ((fd = sim_file_open (name.ptr, FILE_BIT_READ, NULL)) >= 0) {
          if (lseek (fd, 0, SEEK_END) <= FILE_MAX_TORRENT_SIZE)
            if ((count = sim_file_load_torrent (nodes, torrents, fd, nodesize, FILE_DHT_START_LENGTH, &size)) > 0)
              LOG_CODE_ANY_ (SIM_MODULE_MAIN, SIM_LOG_DEBUG, "added %d torrent nodes from %s\n", count, name.str);
          sim_file_close (fd);
        }
        string_free (name);
      }
      sim_system_dir_close (dir);
    }
    string_free (dirname);
  }
}

#define FILE_XML_CHECK_TAG(tag, string)                    \
  ((tag)[0] == '<' && tolower ((tag)[1]) == (string)[0] && \
   ((tag)[2] == '>' || (tolower ((tag)[2]) == (string)[1] && tolower ((tag)[3]) == (string)[2] && (tag)[4] == '>')))
#define FILE_XML_CALC_LIMIT(size, maxsize) (((maxsize) ? (maxsize) / 2 + 2 * (size) : (unsigned) 1 << 31) / (size))

static int sim_file_load_new (struct file_txt_buffer *head, simnumber *lines, int fd, unsigned maxsize,
                              simnumber *offset) {
  simbool start = false;
  int err = SIM_OK;
  unsigned len, memsize = 0, numsize;
  simnumber num = *lines, size, ofs, pos;
  if ((numsize = FILE_XML_CALC_LIMIT (offset ? sizeof (struct _message) : sizeof (simtype), maxsize)) <= num)
    num = numsize - 1;
  head->next = NULL;
  head->data = head->buffer;
  *lines = 0;
  if ((int) (head->len = sim_file_read (fd, head->buffer, SIM_FILE_BUFFER_SIZE)) <= 0) {
    err = FILE_CASE_ERROR (head->len ? -1 : 0, SIM_FILE_END);
  } else if ((size = lseek (fd, 0, SEEK_END)) < 0 || lseek (fd, 0, SEEK_SET)) {
    err = errno ? errno : SIM_FILE_END;
  } else {
    char *body;
    head->buffer[head->len] = 0;
    if ((body = offset ? strstr (head->buffer, MSG_XML_BODY) : head->buffer) != NULL) {
      if (offset) {
        *offset = head->len = body - head->buffer + SIM_STRING_GET_LENGTH_CONST (MSG_XML_BODY);
      } else
        head->len = 0;
      start = true;
      while (size > head->len) {
        struct file_txt_buffer *buf = sim_new (sizeof (*buf));
        memsize += SIM_POINTER_SIZE (buf, sizeof (*buf));
        if (size - head->len < SIM_FILE_BUFFER_SIZE) {
          len = (unsigned) (size - (ofs = head->len));
        } else
          ofs = size - (len = SIM_FILE_BUFFER_SIZE);
        buf->next = head->next;
        buf->data = buf->buffer;
        head->next = buf;
        if ((pos = lseek (fd, ofs, SEEK_SET)) != ofs) {
          size = 0;
          err = FILE_CASE_ERROR (pos < 0 ? -1 : 0, SIM_FILE_END);
        } else if ((buf->len = sim_file_read (fd, buf->buffer, len)) == len) {
          int count = 0;
          size = buf->len < size ? size - buf->len : 0;
          err = SIM_OK;
          body = buf->buffer + buf->len;
          if (! buf->next) {
            if (body[-1] == 0x1A) {
              body--;
              buf->len--;
            }
          } else if (offset) { /* append first characters of next chunk up to a first tag (if any) to this chunk */
            char *overlap = buf->next->buffer;
            len = sizeof (buf->buffer) - SIM_FILE_BUFFER_SIZE;
            for (; --len && *overlap && *overlap != '<'; *body++ = *overlap++) {}
          }
          *body = 0;
          body = buf->buffer;
          for (len = buf->len; len--; body++)
            if (offset ? FILE_XML_CHECK_TAG (body, MSG_HISTORY_ENTRY) : *body == '\n')
              count++;
          if (count >= num || (maxsize && memsize >= maxsize)) {
            if (offset)
              *offset = ofs;
            *lines += num;
            count -= (int) num;
            body = buf->buffer;
            for (len = buf->len; len--; body++)
              if ((offset ? FILE_XML_CHECK_TAG (body, MSG_HISTORY_ENTRY) : *body == '\n') && count-- <= 0) {
                buf->len -= len = body + ! offset - buf->buffer;
                buf->data = buf->buffer + len;
                if (offset)
                  *offset += len;
                break;
              }
            if ((! maxsize || memsize < maxsize) && numsize > num)
              start = false;
            break;
          }
          *lines += count;
          num -= count;
        } else {
          size = 0;
          err = FILE_CASE_ERROR ((int) buf->len < 0 ? -1 : 0, SIM_FILE_END);
        }
      }
    } else
      err = SIM_FILE_END;
  }
  return err == SIM_OK && start ? SIM_FILE_START : err;
}

static struct file_txt_buffer *sim_file_load_free (struct file_txt_buffer *head) {
  struct file_txt_buffer *buf = head->next;
  sim_free (head, sizeof (*buf));
  return buf;
}

int sim_file_load_log (simnumber lines, unsigned maxsize) {
  unsigned size = 0, sizes = 0, count = 0, len;
  simtype name = sim_file_name_new (FILE_LOG_CORE, FILE_DIRECTORY_USER), str, line;
  int err = SIM_OK, fd = -1, level, loglevel = log_get_level_ ("console");
  if (name.typ == SIMNIL) {
    err = SIM_FILE_START;
  } else if ((fd = lines++ ? sim_file_open (name.ptr, FILE_BIT_READ, NULL) : -1) >= 0) {
    char *s = NULL, *first = NULL, *last = NULL;
    struct file_txt_buffer *head = sim_new (sizeof (*head)), *buf = head;
    err = sim_file_load_new (head, &lines, fd, maxsize, NULL);
    sim_file_close (fd);
    if (err == SIM_OK || err == SIM_FILE_START)
      while ((buf = buf->next) != NULL) {
        if ((last = last || first == s ? NULL : s) == NULL)
          first = buf->data;
        s = buf->data;
        while (buf->len--)
          if (*s++ == '\n') {
            if (last) {
              str = string_cat_len (first, last - first, buf->data, s - buf->data);
            } else
              str = pointer_new_len (first, s - first);
            size += str.len + 1;
            if ((len = str.len) != 0 && str.str[len - 1] == '\n')
              if (--len && str.str[len - 1] == '\r')
                len--;
            line = pointer_new_len (str.str, len);
            level = sim_log_get_level (&line);
            if (level >= loglevel && level != SIM_LOG_UNKNOWN) {
              simunsigned linetime = sim_convert_string_to_time (str.ptr, len);
              line = log_put_string_ (linetime ? level : SIM_LOG_UNKNOWN, line, linetime);
              sizes += SIM_POINTER_SIZE (line.ptr, line.len + 1);
              count++;
            }
            string_free (str);
            first = s;
            last = NULL;
          }
        head = sim_file_load_free (head);
      }
    for (; head; head = sim_file_load_free (head)) {}
  }
  string_free (name);
  LOG_DEBUG_ ("loaded %u/%u bytes (%u lines)\n", sizes + (count + 2) * (unsigned) sizeof (line), size, count);
  return err;
}

#if HAVE_LIBEXPAT

#define FILE_XML_FAKE_TAG "tag"
#define FILE_XML_FAKE_HEAD "<html><body>"

#define FILE_XML_FLAG_NONE 0
#define FILE_XML_FLAG_UTF 1  /* <pre> */
#define FILE_XML_FLAG_HTML 2 /* <p> */
#define FILE_XML_FLAG_DATE 4 /* <code> */
#define FILE_XML_FLAG_MSG 8  /* no tag (message text) */

#define FILE_XML_FLAG_UTC 16    /* <u> */
#define FILE_XML_FLAG_TIME 32   /* <t> */
#define FILE_XML_FLAG_HANDLE 64 /* <n> */
#define FILE_XML_FLAG_EDIT 128  /* <e> */
#define FILE_XML_FLAG_XFER 256  /* <h> */

#define FILE_XML_FLAGS \
  (FILE_XML_FLAG_UTC | FILE_XML_FLAG_TIME | FILE_XML_FLAG_HANDLE | FILE_XML_FLAG_EDIT | FILE_XML_FLAG_XFER)
#define FILE_XML_FLAG_IGNORE 512 /* message removed */

struct file_xml_buffer {
  int flags;             /* FILE_XML_FLAG_xxx */
  unsigned len;          /* length of current message */
  int tags;              /* number of currently open tags inside message text */
  simbool textdate;      /* flag to store ASCII date/time string to msgs->tonick */
  simnumber offset;      /* file offset of first XML message in buffer */
  int err;               /* error code returned from inside handler */
  long errbyte;          /* byte index where error code was returned */
  XML_Parser parser;     /* main parser */
  XML_Parser xml;        /* comment parser */
  simtype buffer;        /* message buffer */
  unsigned maxsize;      /* maximal size allowed to allocate or zero */
  unsigned size;         /* total allocated size */
  unsigned count;        /* number of messages in buffer */
  unsigned index;        /* index of last message unloaded from buffer */
  struct _message *msgs; /* pointer to message buffer (equal to buffer.ptr) */
  unsigned buflen;       /* number of bytes in temporary buffer below */
  char buf[SIM_SIZE_NICK + SIM_SIZE_TIME + 2];
};

#define FILE_XML_CHECK_UTF(tag, chr) ((chr) == MSG_HISTORY_ENTRY[0] && tolower ((tag)[1]) == MSG_HISTORY_ENTRY[1] && \
                                      tolower ((tag)[2]) == MSG_HISTORY_ENTRY[2] && ! (tag)[3])
#define FILE_XML_CHECK_HTML(tag, chr) ((chr) == MSG_HISTORY_ENTRY[0] && ! (tag)[1])
#define FILE_XML_CHECK_DATE(tag, chr)                                                        \
  ((chr) == MSG_HISTORY_DATE[0] &&                                                           \
   tolower ((tag)[1]) == MSG_HISTORY_DATE[1] && tolower ((tag)[2]) == MSG_HISTORY_DATE[2] && \
   tolower ((tag)[3]) == MSG_HISTORY_DATE[3] && ! (tag)[4])
#define FILE_XML_CHECK_DIFF_CONST(text, length, conststr) \
  ((length) <= sizeof (conststr) || memcmp (text, conststr, SIM_STRING_GET_LENGTH_CONST (conststr)))

#define FILE_DESTROY_MSG(message, DECREMENT) \
  string_free ((message)->nick), string_buffer_free ((message)->text), DECREMENT
#define FILE_SIZE_MSG(text) ((text).ptr ? SIM_POINTER_SIZE ((text).ptr, (text).len + 1) : 0)

static void sim_file_xml_append (struct file_xml_buffer *xml, const XML_Char *text, int length) {
  struct _message *msg = &xml->msgs[xml->count];
  const unsigned l = SIM_STRING_GET_LENGTH_CONST (FILE_XML_FAKE_HEAD);
  if (! msg->offset)
    msg->offset = xml->offset + XML_GetCurrentByteIndex (xml->parser) - l;
  string_buffer_append (&msg->text, xml->len, length + 1);
  memcpy (msg->text.str + xml->len, text, length);
  msg->text.str[xml->len += length] = 0;
}

static void sim_file_xml_append_buffer (struct file_xml_buffer *xml, const XML_Char *text, int length) {
  if (xml->buflen + length >= sizeof (xml->buf))
    if ((length = sizeof (xml->buf) - 1 - xml->buflen) <= 0) /* else leave space for terminating zero byte */
      return;
  memcpy (xml->buf + xml->buflen, text, length);
  xml->buflen += length;
}

static void sim_file_xml_callback_start (void *userData, const XML_Char *tag, const XML_Char **atts) {
  int ch;
  struct file_xml_buffer *xml = userData;
  if (xml->flags & (FILE_XML_FLAG_MSG | FILE_XML_FLAG_HTML)) {
    xml->tags++;
    if (xml->flags & FILE_XML_FLAG_MSG) {
      sim_file_xml_append (xml, "<", 1);
      sim_file_xml_append (xml, tag, strlen (tag));
      sim_file_xml_append (xml, ">", 1);
    }
    return;
  }
  ch = tolower (tag[0]);
  if (xml->flags & FILE_XML_FLAG_UTF) {
    struct _message *msg = &xml->msgs[xml->count];
    if (ch == MSG_HISTORY_INCOMING[0] && ! tag[1])
      msg->status = SIM_MSG_INCOMING;
    if (ch == MSG_HISTORY_SYSTEM[0] && ! tag[1])
      msg->type = SIM_MSG_TYPE_SYSTEM;
    if (FILE_XML_CHECK_DATE (tag, ch)) {
      xml->flags |= FILE_XML_FLAG_DATE;
      xml->buflen = 0;
    }
  } else if (FILE_XML_CHECK_UTF (tag, ch)) {
    struct _message *msg;
    if (xml->maxsize) {
      unsigned flush = 0, size = SIM_POINTER_SIZE (xml->buffer.ptr, xml->buffer.len);
      if (size >= xml->maxsize) {
        xml->index = 0;
        flush = sizeof (*msg);
      }
      while (size + xml->size >= xml->maxsize && xml->index < xml->count) {
        msg = &xml->msgs[++xml->index];
        xml->size -= FILE_SIZE_MSG (msg->nick) + FILE_SIZE_MSG (msg->text);
        FILE_DESTROY_MSG (msg, size -= flush), msg->nick = msg->text = nil ();
      }
      if (flush) {
        simtype buf = xml->buffer;
        xml->buffer = string_buffer_new (xml->buffer.len - sizeof (*msg) * xml->index);
        memcpy (xml->buffer.str + sizeof (*msg), xml->msgs + xml->index, (xml->count -= xml->index) * sizeof (*msg));
        xml->index = 0;
        string_buffer_free (buf);
      }
    }
    string_buffer_append (&xml->buffer, ++xml->count * sizeof (*msg), sizeof (*msg));
    xml->msgs = xml->buffer.ptr;
    memset (msg = &xml->msgs[xml->count], 0, sizeof (*msg));
    msg->type = SIM_MSG_TYPE_UTF;
    msg->status = SIM_MSG_DELIVERED;
    msg->nick = msg->text = nil ();
    msg->tonick = NULL;
    xml->flags = FILE_XML_FLAG_UTF;
    xml->tags = xml->len = 0;
  } else if (FILE_XML_CHECK_HTML (tag, ch)) {
    xml->flags = FILE_XML_FLAG_HTML;
    xml->tags = 0;
  }
}

static void sim_file_xml_callback_end (void *userData, const XML_Char *tag) {
  struct file_xml_buffer *xml = userData;
  if (xml->flags & (FILE_XML_FLAG_MSG | FILE_XML_FLAG_HTML) && xml->tags) {
    xml->tags--;
    if (xml->flags & FILE_XML_FLAG_MSG) {
      sim_file_xml_append (xml, "</", 2);
      sim_file_xml_append (xml, tag, strlen (tag));
      sim_file_xml_append (xml, ">", 1);
    }
  } else if (xml->flags & FILE_XML_FLAG_UTF) {
    int ch = tolower (tag[0]);
    if (FILE_XML_CHECK_DATE (tag, ch)) {
      if (xml->flags & FILE_XML_FLAG_DATE && xml->buflen >= SIM_SIZE_TIME - 1) {
        struct _message *msg = &xml->msgs[xml->count];
        if (xml->buf[xml->buflen - 2] == ':' && xml->buf[xml->buflen - 1] == ' ' && xml->buflen >= SIM_SIZE_TIME + 2)
          msg->nick = string_copy_len (xml->buf + SIM_SIZE_TIME, xml->buflen - SIM_SIZE_TIME - 2);
        if (xml->textdate)
          msg->tonick = string_copy_len (xml->buf, SIM_SIZE_TIME - 1).ptr;
      }
      xml->flags = (xml->flags & ~FILE_XML_FLAG_DATE) | FILE_XML_FLAG_MSG | FILE_XML_FLAG_IGNORE;
    } else if (FILE_XML_CHECK_UTF (tag, ch) || FILE_XML_CHECK_HTML (tag, ch)) {
      if (xml->flags & FILE_XML_FLAG_MSG) {
        struct _message *msg = &xml->msgs[xml->count];
        /*msg->text.len = xml->len;*/
        if (xml->flags & FILE_XML_FLAG_IGNORE) {
          FILE_DESTROY_MSG (&xml->msgs[xml->count], xml->count--);
#ifdef DONOT_DEFINE
        } else if (msg->type == SIM_MSG_TYPE_SYSTEM && FILE_XML_CHECK_DIFF_CONST (msg->text.ptr, xml->len, "CALL ") &&
                   FILE_XML_CHECK_DIFF_CONST (msg->text.ptr, xml->len, "FILE ") &&
                   FILE_XML_CHECK_DIFF_CONST (msg->text.ptr, xml->len, "STATUS ")) {
          FILE_DESTROY_MSG (&xml->msgs[xml->count], xml->count--);
#endif
        } else if (msg->text.typ == SIMNIL) {
          msg->text = string_buffer_new (1);
          msg->text.str[0] = 0;
        } else
          xml->size += FILE_SIZE_MSG (msg->nick) + FILE_SIZE_MSG (msg->text);
      } else if (xml->flags & FILE_XML_FLAG_DATE)
        FILE_DESTROY_MSG (&xml->msgs[xml->count], xml->count--);
      xml->flags = FILE_XML_FLAG_NONE;
    }
  } else if (xml->flags & FILE_XML_FLAG_HTML)
    xml->flags = FILE_XML_FLAG_NONE;
}

static void sim_file_xml_callback_data (void *userData, const XML_Char *text, int length) {
  struct file_xml_buffer *xml = userData;
  if (xml->flags & FILE_XML_FLAG_DATE) {
    sim_file_xml_append_buffer (xml, text, length);
  } else if (xml->flags & FILE_XML_FLAG_MSG)
    sim_file_xml_append (xml, text, length);
}

static void sim_file_xml_callback_comment (void *userData, const XML_Char *data) {
  unsigned len = strlen (data);
  struct file_xml_buffer *xml = userData;
  if (XML_Parse (xml->xml, data, len, false)) {
    xml->errbyte += len;
  } else if (xml->err == XML_ERROR_NONE) {
    xml->err = XML_GetErrorCode (xml->xml);
    xml->errbyte = XML_GetCurrentByteIndex (xml->xml) - xml->errbyte + XML_GetCurrentByteIndex (xml->parser) - 1;
    XML_StopParser (xml->parser, XML_FALSE);
  }
}

static int sim_file_xml_comment_get_flag (const XML_Char *tag) {
  int ch = tolower (tag[0]);
  if (! tag[1]) {
    if (ch == MSG_HISTORY_UTC[0])
      return FILE_XML_FLAG_UTC;
    if (ch == MSG_HISTORY_TIME[0])
      return FILE_XML_FLAG_TIME;
    if (ch == MSG_HISTORY_HANDLE[0])
      return FILE_XML_FLAG_HANDLE;
    if (ch == MSG_HISTORY_EDIT[0])
      return FILE_XML_FLAG_EDIT;
    if (ch == MSG_HISTORY_XFER[0])
      return FILE_XML_FLAG_XFER;
  }
  return FILE_XML_FLAG_NONE;
}

static void sim_file_xml_comment_callback_start (void *userData, const XML_Char *tag, const XML_Char **atts) {
  struct file_xml_buffer *xml = userData;
  if (xml->flags & FILE_XML_FLAG_MSG) {
    struct _message *msg = &xml->msgs[xml->count];
    const unsigned l = SIM_STRING_GET_LENGTH_CONST (FILE_XML_FAKE_HEAD);
    if (! msg->offset)
      msg->offset = xml->offset + XML_GetCurrentByteIndex (xml->parser) - l;
    xml->flags |= sim_file_xml_comment_get_flag (tag);
    xml->buflen = 0;
  }
}

static void sim_file_xml_comment_callback_end (void *userData, const XML_Char *tag) {
  struct file_xml_buffer *xml = userData;
  if (xml->flags & FILE_XML_FLAG_MSG) {
    struct _message *msg = &xml->msgs[xml->count];
    xml->buf[xml->buflen] = 0;
    switch (xml->flags & FILE_XML_FLAGS) {
      case FILE_XML_FLAG_TIME:
        msg->rcvtime = strtoul (xml->buf, NULL, 10);
        break;
      case FILE_XML_FLAG_HANDLE:
        sim_convert_string_to_simunsigned (xml->buf, &msg->handle);
        break;
      case FILE_XML_FLAG_EDIT:
        if (msg->type != SIM_MSG_TYPE_SYSTEM)
          sim_convert_string_to_simunsigned (xml->buf, &msg->oldhandle);
        break;
      case FILE_XML_FLAG_XFER:
        if (msg->type == SIM_MSG_TYPE_SYSTEM)
          sim_convert_string_to_simunsigned (xml->buf, &msg->oldhandle);
        break;
      case FILE_XML_FLAG_UTC:
        msg->sndtime = strtoul (xml->buf, NULL, 10);
        xml->flags &= ~FILE_XML_FLAG_IGNORE;
    }
    xml->flags &= ~sim_file_xml_comment_get_flag (tag);
  }
}

static void sim_file_xml_comment_callback_data (void *userData, const XML_Char *text, int length) {
  struct file_xml_buffer *xml = userData;
  if (xml->flags & FILE_XML_FLAG_MSG && xml->flags & FILE_XML_FLAGS)
    sim_file_xml_append_buffer (xml, text, length);
}

static void sim_file_xml_test_error (int *error, simnumber errbyte, const char *nick, int err) {
  LOG_WARN_ ("xml BYTE %lld %s '%s'\n", errbyte - SIM_STRING_GET_LENGTH_CONST (FILE_XML_FAKE_HEAD),
             XML_ErrorString (err), nick);
  if (error && *error == SIM_OK) {
    char errbuf[SIM_SIZE_ERROR];
    sprintf (errbuf, " @ BYTE %" SIM_FORMAT_64 "d", errbyte - SIM_STRING_GET_LENGTH_CONST (FILE_XML_FAKE_HEAD));
    sim_error_set_text (errbuf, NULL, NULL, *error = ERROR_BASE_EXPAT - err);
  }
}

int sim_file_load_history (const char *pathname, simnumber lines, unsigned maxsize, const char *nick,
                           simtype *messages, unsigned *count) {
  unsigned len;
  simbool start = false;
  struct file_txt_buffer *txt, *buf;
  struct file_xml_buffer buffer;
  int err = SIM_OK, fd, *errptr = lines < 0 ? &err : NULL;
  buffer.maxsize = maxsize;
  *count = 0;
  if ((fd = sim_file_open (pathname, FILE_BIT_READ, NULL)) < 0)
    return ENOENT;
  if (lines < 0)
    lines = -lines;
  buf = sim_new (sizeof (*buf));
  if ((err = sim_file_load_new (buf, &lines, fd, buffer.maxsize, &buffer.offset)) == SIM_FILE_START) {
    start = true;
    err = SIM_OK;
  }
  sim_file_close (fd);
#ifndef DONOT_DEFINE
  if (err == SIM_OK) {
    const unsigned l = SIM_STRING_GET_LENGTH_CONST (MSG_XML_TAIL);
    for (txt = buf; txt->next; txt = txt->next) {}
    for (len = txt->len; len && isspace (txt->data[len - 1]); len--) {}
    if (len < l || SIM_STRING_CHECK_DIFF_CONST (txt->data + len - l, MSG_XML_TAIL)) {
      (txt->next = sim_new (sizeof (*txt->next)))->next = NULL;
      txt->next->len = strlen (strcpy (txt->next->data = txt->next->buffer, MSG_XML_TAIL));
    }
  }
#endif
  if (err == SIM_OK) {
    XML_Parser xml = buffer.parser = XML_ParserCreate (NULL);
    buffer.textdate = pathname == nick;
    if (xml && (buffer.xml = XML_ParserCreate (NULL)) != NULL) {
      unsigned n, l = buffer.index = buffer.count = buffer.size = 0;
      if ((len = FILE_XML_CALC_LIMIT (sizeof (*buffer.msgs), buffer.maxsize)) > lines)
        len = (unsigned) lines + 1;
      buffer.msgs = (buffer.buffer = string_buffer_new (sizeof (*buffer.msgs) * len)).ptr;
      buf = sim_file_load_free (buf);
    reset:
      buffer.flags = FILE_XML_FLAG_NONE;
      buffer.err = XML_ERROR_NONE;
      buffer.errbyte = 0;
      XML_SetUserData (xml, &buffer);
      XML_Parse (xml, FILE_XML_FAKE_HEAD, len = SIM_STRING_GET_LENGTH_CONST (FILE_XML_FAKE_HEAD), false);
      XML_SetElementHandler (xml, sim_file_xml_callback_start, sim_file_xml_callback_end);
      XML_SetCharacterDataHandler (xml, sim_file_xml_callback_data);
      XML_SetCommentHandler (xml, sim_file_xml_callback_comment);
      XML_SetUserData (buffer.xml, &buffer);
      XML_Parse (buffer.xml, "<" FILE_XML_FAKE_TAG ">", SIM_STRING_GET_LENGTH_CONST ("<" FILE_XML_FAKE_TAG ">"), false);
      XML_SetElementHandler (buffer.xml, sim_file_xml_comment_callback_start, sim_file_xml_comment_callback_end);
      XML_SetCharacterDataHandler (buffer.xml, sim_file_xml_comment_callback_data);
      while (buf) {
        if (! XML_Parse (xml, buf->data + l, buf->len - l, ! buf->next)) {
          int e = buffer.err;
          simnumber i = buffer.offset + (e != XML_ERROR_NONE ? buffer.errbyte : XML_GetCurrentByteIndex (xml));
          if (e == XML_ERROR_NONE)
            e = XML_GetErrorCode (xml);
          sim_file_xml_test_error (errptr, i, nick, e);
          if ((i -= buffer.offset + len) < 0) {
            buffer.offset -= i;
          } else if ((l += (unsigned) i) > buf->len) {
            LOG_ERROR_ ("xml load overflow %d + %d > %d '%s'\n", (int) (l - i), (int) i, buf->len, nick);
            l = buf->len;
          }
          do {
            for (n = l; l < buf->len && buf->data[l] != '\n'; l++) {}
            buffer.offset += l - n;
            if (l < buf->len)
              break;
            l = 0;
          } while ((buf = sim_file_load_free (buf)) != NULL);
          if (buffer.flags & (FILE_XML_FLAG_UTF | FILE_XML_FLAG_DATE | FILE_XML_FLAG_MSG))
            FILE_DESTROY_MSG (&buffer.msgs[buffer.count], buffer.count--);
          buffer.offset += (unsigned) ((int) len + (int) i) - SIM_STRING_GET_LENGTH_CONST (FILE_XML_FAKE_HEAD);
          if (XML_ParserReset (xml, NULL) && XML_ParserReset (buffer.xml, NULL))
            goto reset;
          break;
        }
        len += buf->len - l;
        l = 0;
        buf = sim_file_load_free (buf);
      }
      if (! XML_Parse (buffer.xml, "</" FILE_XML_FAKE_TAG ">",
                       SIM_STRING_GET_LENGTH_CONST ("</" FILE_XML_FAKE_TAG ">"), true)) {
        int e = XML_GetErrorCode (buffer.xml);
        sim_file_xml_test_error (errptr, buffer.offset + XML_GetCurrentByteIndex (xml), nick, e);
      }
      if (buffer.flags & (FILE_XML_FLAG_UTF | FILE_XML_FLAG_DATE | FILE_XML_FLAG_MSG))
        FILE_DESTROY_MSG (&buffer.msgs[buffer.count], buffer.count--);
      *messages = buffer.buffer;
      *count = buffer.count;
      LOG_DEBUG_ ("loaded %u bytes (%u/%lld messages) '%s'\n",
                  SIM_POINTER_SIZE (messages->ptr, messages->len) + buffer.size, *count, lines, nick);
      XML_ParserFree (buffer.xml);
    } else
      err = ENOMEM;
    XML_ParserFree (xml);
  }
  for (; buf; buf = sim_file_load_free (buf)) {}
  return start && err == SIM_OK ? SIM_FILE_START : err;
}

int file_load_msg_ (simcontact contact, simnumber lines) {
  simunsigned cpu;
  simbool reload = lines >= 0, start = false;
  int err = SIM_OK, err2 = SIM_OK;
  unsigned count = 0, maxkb = param_get_number ("msg.maxmb") << 10;
  simtype str = sim_convert_simunsigned_to_string (FILE_DIRECTORY_HISTORY, contact->id, FILE_EXTENSION_HISTORY);
  simtype name = sim_file_name_new (str.ptr, FILE_DIRECTORY_USER), messages = nil ();
  string_free (str);
  sim_error_reset_text ();
  if (! reload) {
    if ((lines = param_get_number ("msg.load")) < maxkb)
      maxkb = (unsigned) lines;
  } else if ((err = msg_wait_ (contact)) != SIM_OK) {
    string_free (name);
    return err;
  }
  cpu = sim_system_cpu_get (SYSTEM_CPU_TIME_THREAD, NULL);
  if (name.typ == SIMNIL) {
    err = SIM_FILE_START;
  } else if (lines > 0) {
    msg_close (contact);
    if (param_get_number ("msg.errors"))
      lines = -lines;
    err = sim_file_load_history (name.ptr, lines, maxkb << 10, contact->nick.ptr, &messages, &count);
    if (err == ENOENT || err == SIM_FILE_START) {
      err = SIM_OK;
      start = true;
    }
  }
  string_free (name);
  if (err != SIM_OK && err != SIM_FILE_START && ! reload)
    event_send_name_number (contact, SIM_EVENT_ERROR, SIM_EVENT_ERROR_FILE_OPEN, err);
  if ((err2 = msg_load (contact, messages, count, reload)) != SIM_OK && (! reload || err != SIM_OK))
    event_send_name_number (contact, SIM_EVENT_ERROR, SIM_EVENT_ERROR_FILE_LOAD, err2);
  limit_set_cpu (sim_system_cpu_get (SYSTEM_CPU_TIME_THREAD, NULL) - cpu, 0);
  return err != SIM_OK ? err : err2 != SIM_OK ? err2 : start ? SIM_FILE_START : SIM_OK;
}

#else

int file_load_msg_ (simcontact contact, simnumber lines) {
  int err = SIM_OK;
  sim_error_reset_text ();
  if (lines < 0 && (err = msg_load (contact, nil (), 0, false)) != SIM_OK)
    event_send_name_number (contact, SIM_EVENT_ERROR, SIM_EVENT_ERROR_FILE_LOAD, err);
  return err;
}

#endif /* HAVE_LIBEXPAT */

simbool sim_convert_string_to_simunsigned (const char *string, simnumber *number) {
  char *endptr;
  *number = strtoull (string, &endptr, 10);
  return *string && ! *endptr; /* does not check for overflow so may accept numbers of 'any' length */
}

void file_log_password (const char *module, int level, simbool numeric) {
  if (file_key_strings[1].ptr) {
    if (file_key_type != SIM_KEY_NONE) {
      simtype seed = convert_seed_to_string (file_key_strings[1], file_key_type, numeric);
      log_any_ (module, level, "%s: %s\n", numeric ? "numeric" : "seed", seed.str);
      string_free (seed);
    } else if (! numeric)
      log_any_ (module, level, "password: %.*s\n", file_key_strings[1].len, file_key_strings[1].str);
  }
}
