about summary refs log blame commit diff stats
path: root/archive/2.transect/compiler9
blob: 26becf48a8b12fcf4099f29c438d7305e3b9297c (plain) (tree)





























































































































































































































































                                                                                                                                             
=== Goal

A memory-safe language with a simple translator to x86 that can be feasibly
written without itself needing a translator.

Memory-safe: it should be impossible to:
  a) create a pointer out of arbitrary data, or
  b) to access heap memory after it's been freed.

Simple: do all the work in a 2-pass translator:
  Pass 1: check each instruction's types in isolation.
  Pass 2: emit code for each instruction in isolation.

=== Overview of the language

A program consists of a series of type, function and global variable declarations.
(Also constants and tests, but let's focus on these.)

Type declarations basically follow Hindley-Milner with product and (tagged) sum
types. Types are written in s-expression form. There's a `ref` type that's a
type-safe fat pointer, with an alloc id that gets incremented after each
allocation. Memory allocation and reclamation is manual. Dereferencing a ref
after its underlying memory is reclaimed (pointer alloc id no longer matches
payload alloc id) is guaranteed to immediately kill the program (like a
segfault).

  # product type
  type foo [
    x : int
    y : (ref int)
    z : bar
  ]

  # sum type
  choice bar [
    x : int
    y : point
  ]

Functions have a header and a series of instructions in the body:

  fn f a : int -> b : int [
    ...
  ]

Instructions have the following format:

  io1, io2, ... <- operation i1, i2, ...

i1, i2 operands on the right hand side are immutable. io1, io2 are in-out
operands. They're written to, and may also be read.

User-defined functions will be called with the same syntax. They'll translate
to a sequence of push instructions (one per operand, both in and in-out), a
call instruction, and a sequence of pop instructions, either to a black hole
(in operands) or a location (in-out operands). This follows the standard Unix
calling convention. Each operand needs to be something push/pop can accept.

Primitive operations depend on the underlying processor. We'd like each primitive
operation supported by the language to map to a single instruction in the ISA.
Sometimes we have to violate that (see below), but we definitely won't be
writing to any temporary locations behind the scenes. The language affords
control over registers, and tracking unused registers gets complex, and
besides we may have no unused registers at a specific point. Instructions only
modify their operands.

In most ISAs, instructions operate on at most a word of data at a time. They
also tend to not have more than 2-3 operands, and not modify more than 2
locations in memory.

Since the number of reads from memory is limited, we break up complex high-level
operations using a special type called `address`. Addresses are strictly
short-term entities. They can't be stored in a compound type, and they can't
be passed into or returned from a user-defined function. They also can't be
used after a function call (because it could free the underlying memory) or
label (because it gets complex to check control flow, and we want to translate
each instruction simply and in isolation).

=== Compilation to 32-bit x86

Values can be stored:
  in code (literals)
  in registers
  on the stack
  on the global segment

Variables on the stack are stored at *(ESP+n)
Global variables are stored at *disp32, where disp32 is statically known

Address variables have to be in a register.
  - You need them in a register to do a lookup, and
  - Saving them to even the stack increases the complexity of checks needed on
    function calls or labels.

Compilation proceeds by pattern matching over an instruction along with
knowledge about the types of its operands, as well as where they're stored
(register/stack/global). We now enumerate mappings for various categories of
instructions, based on the type and location of their operands.

Where types of operands aren't mentioned below, all operands of an instruction
should have the same (word-length) type.

Lots of special cases because of limitations of the x86 ISA. Beware.

A. x : int <- add y

  Requires y to be scalar. Result will always be an int. No pointer arithmetic.

  reg <- add literal    => 81 0/subop 3/mod                                                                                           ...(0)
  reg <- add reg        => 01 3/mod                                                                                                   ...(1)
  reg <- add stack      => 03 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8 reg/r32                                        ...(2)
  reg <- add global     => 03 0/mod 5/rm32/include-disp32 global/disp32 reg/r32                                                       ...(3)
  stack <- add literal  => 81 0/subop 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8 literal/imm32                          ...(4)
  stack <- add reg      => 01 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8 reg/r32                                        ...(5)
  stack <- add stack    => disallowed
  stack <- add global   => disallowed
  global <- add literal => 81 0/subop 0/mod 5/rm32/include-disp32 global/disp32 literal/imm32                                         ...(6)
  global <- add reg     => 01 0/mod 5/rm32/include-disp32 global/disp32 reg/r32                                                       ...(7)
  global <- add stack   => disallowed
  global <- add global  => disallowed

Similarly for sub, and, or, xor and even copy. Replace the opcodes above with corresponding ones from this table:

                            add             sub           and           or            xor         copy/mov
  reg <- op literal         81 0/subop      81 5/subop    81 4/subop    81 1/subop    81 6/subop  c7
  reg <- op reg             01 or 03        29 or 2b      21 or 23      09 or 0b      31 or 33    89 or 8b
  reg <- op stack           03              2b            23            0b            33          8b
  reg <- op global          03              2b            23            0b            33          8b
  stack <- op literal       81 0/subop      81 5/subop    81 4/subop    81 1/subop    81 6/subop  c7
  stack <- op reg           01              29            21            09            31          89
  global <- op literal      81 0/subop      81 5/subop    81 4/subop    81 1/subop    81 6/subop  c7
  global <- op reg          01              29            21            09            31          89

B. x/reg : int <- mul y

  Requires both y to be scalar.
  x must be in a register. Multiplies can't write to memory.

  reg <- mul literal    => 69                                                                                                         ...(8)
  reg <- mul reg        => 0f af 3/mod                                                                                                ...(9)
  reg <- mul stack      => 0f af 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8 reg/r32                                     ...(10)
  reg <- mul global     => 0f af 0/mod 5/rm32/include-disp32 global/disp32 reg/r32                                                    ...(11)

C. x/EAX/quotient : int, y/EDX/remainder : int <- idiv z     # divide EAX by z; store the result in EAX and EDX

  Requires source x and z to both be scalar.
  x must be in EAX and y must be in EDX. Divides can't write anywhere else.

  First clear EDX (we don't support ints larger than 32 bits):
  31/xor 3/mod 2/rm32/EDX 2/r32/EDX

  then:
  EAX, EDX <- idiv literal  => disallowed
  EAX, EDX <- idiv reg      => f7 7/subop 3/mod                                                                                       ...(12)
  EAX, EDX <- idiv stack    => f7 7/subop 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8                                    ...(13)
  EAX, EDX <- idiv global   => f7 7/subop 0/mod 5/rm32/include-disp32 global/disp32 reg/r32                                           ...(14)

D. x : int <- not

  Requires x to be an int.

  reg <- not                => f7 3/mod                                                                                               ...(15)
  stack <- not              => f7 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8                                            ...(16)
  global <- not             => f7 0/mod 5/rm32/include-disp32 global/disp32 reg/r32                                                   ...(17)

E. x : (address t) <- get o : T, %f

  (Assumes T.f has type t.)

  o can't be on a register since it's a non-primitive (likely larger than a word)
  f is a literal
  x must be in a register (by definition for an address)

  below '*' works on either address or ref types

  For raw stack values we want to read *(ESP+n)
  For raw global values we want to read *disp32
  For address stack values we want to read *(ESP+n)+
    *(ESP+n) contains an address
    so we want to compute *(ESP+n) + literal

  reg1 <- get reg2, literal       => 8d/lea 1/mod reg2/rm32 literal/disp8 reg1/r32                                                    ...(18)
  reg <- get stack, literal       => 8d/lea 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n+literal/disp8 reg/r32                  ...(19)
    (simplifying assumption: stack frames can't be larger than 256 bytes)
  reg <- get global, literal      => 8d/lea 0/mod 5/rm32/include-disp32 global+literal/disp32, reg/r32                                ...(20)

F. x : (offset T) <- index i : int, %size(T)

  reg1 <- index reg2, literal       => 69/mul 3/mod reg2/rm32 literal/imm32 -> reg1/r32
                                    or 68/mul 3/mod reg2/rm32 literal/imm8 -> reg1/r32                                                ...(21)
  reg1 <- index stack, literal      => 69/mul 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8 literal/imm32 -> reg1/r32      ...(22)
  reg1 <- index global, literal     => 69/mul 0/mod 5/rm32/include-disp32 global/disp32 literal/imm32 -> reg1/r32                     ...(23)

  optimization: avoid multiply if literal is a power of 2
    use SIB byte if literal is 2, 4 or 8
    or left shift

G. x : (address T) <- advance o : (array T), idx : (offset T)

  reg <- advance a/reg, idx/reg   => 8d/lea 0/mod 4/rm32/SIB a/base idx/index 0/scale reg/r32                                         ...(24)
  reg <- advance stack, literal   => 8d/lea 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n+literal/disp8 reg/r32                  ...(25)
  reg <- advance stack, reg2      => 8d/lea 1/mod 4/rm32/SIB 4/base/ESP reg2/index 0/scale n/disp8 reg/r32                            ...(26)
  reg <- advance global, literal  => 8d/lea 0/mod 5/rm32/include-disp32 global+literal/disp32, reg/r32                                ...(27)

  also instructions for runtime bounds checking

=== Example

Putting it all together: code generation for `a[i].y = 4` where a is an array
of 2-d points with x, y coordinates.

If a is allocated on the stack, say of type (array point 6) at (ESP+4):

  offset/EAX : (offset point) <- index i, 8  # (22)
  tmp/EBX : (address point) <- advance a : (array point 6), offset/EAX  # (26)
  tmp2/ECX : (address number) <- get tmp/EBX : (address point), 4/y  # (18)
  *tmp2/ECX <- copy 4  # (5 for copy/mov with 0 disp8)

Many instructions, particularly variants of 'get' and 'advance' -- end up encoding the exact same instructions.
But the types differ, and the type-checker checks them differently.

=== Advanced checks

Couple of items require inserting mapping to multiple instructions:
  bounds checking against array length in 'advance'
  dereferencing 'ref' types (see type list up top)

A. Dereferencing a ref

    tmp/EDX <- advance *s, tmp0/EDI
      => compare (ESP+4), *(ESP+8)  ; '*' from compiler2
         jump-unless-equal panic
         EDX <- add ESP, 8
         EDX <- copy *EDX
         EDX <- add EDX, 4
         EDX <- 8d/lea EDX + result

=== More speculative ideas

Initialize data segment with special extensible syntax for literals. All
literals except numbers and strings start with %.

  %size(type) => compiler replaces with size of type
  %point(3, 4) => two words

and so on.

=== Credits

Forth
C
Rust
Lisp
qhasm
ef='#n847'>847 848 849
/*
 * log.c
 *
 * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
 * Copyright (C) 2018 - 2019 Michael Vetter <jubalh@idoru.org>
 *
 * This file is part of Profanity.
 *
 * Profanity is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Profanity is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Profanity.  If not, see <https://www.gnu.org/licenses/>.
 *
 * In addition, as a special exception, the copyright holders give permission to
 * link the code of portions of this program with the OpenSSL library under
 * certain conditions as described in each individual source file, and
 * distribute linked combinations including the two.
 *
 * You must obey the GNU General Public License in all respects for all of the
 * code used other than OpenSSL. If you modify file(s) with this exception, you
 * may extend this exception to your version of the file(s), but you are not
 * obligated to do so. If you do not wish to do so, delete this exception
 * statement from your version. If you delete this exception statement from all
 * source files in the program, then also delete it here.
 *
 */

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "glib.h"
#include "glib/gstdio.h"

#include "log.h"
#include "common.h"
#include "config/files.h"
#include "config/preferences.h"
#include "xmpp/xmpp.h"
#include "xmpp/muc.h"

#define PROF "prof"

static FILE *logp;
GString *mainlogfile;

static GTimeZone *tz;
static GDateTime *dt;
static log_level_t level_filter;

static GHashTable *logs;
static GHashTable *groupchat_logs;
static GDateTime *session_started;

enum {
    STDERR_BUFSIZE = 4000,
    STDERR_RETRY_NR = 5,
};
static int stderr_inited;
static log_level_t stderr_level;
static int stderr_pipe[2];
static char *stderr_buf;
static GString *stderr_msg;

struct dated_chat_log {
    gchar *filename;
    GDateTime *date;
};

static gboolean _log_roll_needed(struct dated_chat_log *dated_log);
static struct dated_chat_log* _create_log(const char *const other, const char *const login);
static struct dated_chat_log* _create_groupchat_log(const char *const room, const char *const login);
static void _free_chat_log(struct dated_chat_log *dated_log);
static gboolean _key_equals(void *key1, void *key2);
static char* _get_log_filename(const char *const other, const char *const login, GDateTime *dt, gboolean create);
static char* _get_groupchat_log_filename(const char *const room, const char *const login, GDateTime *dt,
    gboolean create);
static void _rotate_log_file(void);
static char* _log_string_from_level(log_level_t level);
static void _chat_log_chat(const char *const login, const char *const other, const gchar *const msg,
    chat_log_direction_t direction, GDateTime *timestamp);
static void _groupchat_log_chat(const gchar *const login, const gchar *const room, const gchar *const nick,
    const gchar *const msg);

void
log_debug(const char *const msg, ...)
{
    va_list arg;
    va_start(arg, msg);
    GString *fmt_msg = g_string_new(NULL);
    g_string_vprintf(fmt_msg, msg, arg);
    log_msg(PROF_LEVEL_DEBUG, PROF, fmt_msg->str);
    g_string_free(fmt_msg, TRUE);
    va_end(arg);
}

void
log_info(const char *const msg, ...)
{
    va_list arg;
    va_start(arg, msg);
    GString *fmt_msg = g_string_new(NULL);
    g_string_vprintf(fmt_msg, msg, arg);
    log_msg(PROF_LEVEL_INFO, PROF, fmt_msg->str);
    g_string_free(fmt_msg, TRUE);
    va_end(arg);
}

void
log_warning(const char *const msg, ...)
{
    va_list arg;
    va_start(arg, msg);
    GString *fmt_msg = g_string_new(NULL);
    g_string_vprintf(fmt_msg, msg, arg);
    log_msg(PROF_LEVEL_WARN, PROF, fmt_msg->str);
    g_string_free(fmt_msg, TRUE);
    va_end(arg);
}

void
log_error(const char *const msg, ...)
{
    va_list arg;
    va_start(arg, msg);
    GString *fmt_msg = g_string_new(NULL);
    g_string_vprintf(fmt_msg, msg, arg);
    log_msg(PROF_LEVEL_ERROR, PROF, fmt_msg->str);
    g_string_free(fmt_msg, TRUE);
    va_end(arg);
}

void
log_init(log_level_t filter)
{
    level_filter = filter;
    tz = g_time_zone_new_local();
    char *log_file = files_get_log_file();
    logp = fopen(log_file, "a");
    g_chmod(log_file, S_IRUSR | S_IWUSR);
    mainlogfile = g_string_new(log_file);
    free(log_file);
}

void
log_reinit(void)
{
    log_close();
    log_init(level_filter);
}

char*
get_log_file_location(void)
{
    return mainlogfile->str;
}

log_level_t
log_get_filter(void)
{
    return level_filter;
}

void
log_close(void)
{
    g_string_free(mainlogfile, TRUE);
    g_time_zone_unref(tz);
    if (logp) {
        fclose(logp);
    }
}

void
log_msg(log_level_t level, const char *const area, const char *const msg)
{
    if (level >= level_filter && logp) {
        dt = g_date_time_new_now(tz);

        char *level_str = _log_string_from_level(level);

        gchar *date_fmt = g_date_time_format(dt, "%d/%m/%Y %H:%M:%S");

        fprintf(logp, "%s: %s: %s: %s\n", date_fmt, area, level_str, msg);
        g_date_time_unref(dt);

        fflush(logp);
        g_free(date_fmt);

        if (prefs_get_boolean(PREF_LOG_ROTATE)) {
            long result = ftell(logp);
            if (result != -1 && result >= prefs_get_max_log_size()) {
                _rotate_log_file();
            }
        }
    }
}

log_level_t
log_level_from_string(char *log_level)
{
    assert(log_level != NULL);
    if (strcmp(log_level, "DEBUG") == 0) {
        return PROF_LEVEL_DEBUG;
    } else if (strcmp(log_level, "INFO") == 0) {
        return PROF_LEVEL_INFO;
    } else if (strcmp(log_level, "WARN") == 0) {
        return PROF_LEVEL_WARN;
    } else if (strcmp(log_level, "ERROR") == 0) {
        return PROF_LEVEL_ERROR;
    } else { // default to info
        return PROF_LEVEL_INFO;
    }
}

static void
_rotate_log_file(void)
{
    gchar *log_file = files_get_log_file();
    size_t len = strlen(log_file);
    gchar *log_file_new = malloc(len + 4);
    int i = 1;

    // find an empty name. from .log -> log.01 -> log.99
    for(; i<100; i++) {
        g_sprintf(log_file_new, "%s.%02d", log_file, i);
        if (!g_file_test(log_file_new, G_FILE_TEST_EXISTS))
            break;
    }

    log_close();
    rename(log_file, log_file_new);
    log_init(log_get_filter());

    free(log_file_new);
    free(log_file);
    log_info("Log has been rotated");
}

void
chat_log_init(void)
{
    session_started = g_date_time_new_now_local();
    log_info("Initialising chat logs");
    logs = g_hash_table_new_full(g_str_hash, (GEqualFunc) _key_equals, free,
        (GDestroyNotify)_free_chat_log);
}

void
groupchat_log_init(void)
{
    log_info("Initialising groupchat logs");
    groupchat_logs = g_hash_table_new_full(g_str_hash, (GEqualFunc) _key_equals, free,
        (GDestroyNotify)_free_chat_log);
}

void
chat_log_msg_out(const char *const barejid, const char *const msg)
{
    if (prefs_get_boolean(PREF_CHLOG)) {
        const char *jid = connection_get_fulljid();
        Jid *jidp = jid_create(jid);
        _chat_log_chat(jidp->barejid, barejid, msg, PROF_OUT_LOG, NULL);
        jid_destroy(jidp);
    }
}

void
chat_log_otr_msg_out(const char *const barejid, const char *const msg)
{
    if (prefs_get_boolean(PREF_CHLOG)) {
        const char *jid = connection_get_fulljid();
        Jid *jidp = jid_create(jid);
        char *pref_otr_log = prefs_get_string(PREF_OTR_LOG);
        if (strcmp(pref_otr_log, "on") == 0) {
            _chat_log_chat(jidp->barejid, barejid, msg, PROF_OUT_LOG, NULL);
        } else if (strcmp(pref_otr_log, "redact") == 0) {
            _chat_log_chat(jidp->barejid, barejid, "[redacted]", PROF_OUT_LOG, NULL);
        }
        prefs_free_string(pref_otr_log);
        jid_destroy(jidp);
    }
}

void
chat_log_pgp_msg_out(const char *const barejid, const char *const msg)
{
    if (prefs_get_boolean(PREF_CHLOG)) {
        const char *jid = connection_get_fulljid();
        Jid *jidp = jid_create(jid);
        char *pref_pgp_log = prefs_get_string(PREF_PGP_LOG);
        if (strcmp(pref_pgp_log, "on") == 0) {
            _chat_log_chat(jidp->barejid, barejid, msg, PROF_OUT_LOG, NULL);
        } else if (strcmp(pref_pgp_log, "redact") == 0) {
            _chat_log_chat(jidp->barejid, barejid, "[redacted]", PROF_OUT_LOG, NULL);
        }
        prefs_free_string(pref_pgp_log);
        jid_destroy(jidp);
    }
}

void
chat_log_omemo_msg_out(const char *const barejid, const char *const msg)
{
    if (prefs_get_boolean(PREF_CHLOG)) {
        const char *jid = connection_get_fulljid();
        Jid *jidp = jid_create(jid);
        char *pref_omemo_log = prefs_get_string(PREF_OMEMO_LOG);
        if (strcmp(pref_omemo_log, "on") == 0) {
            _chat_log_chat(jidp->barejid, barejid, msg, PROF_OUT_LOG, NULL);
        } else if (strcmp(pref_omemo_log, "redact") == 0) {
            _chat_log_chat(jidp->barejid, barejid, "[redacted]", PROF_OUT_LOG, NULL);
        }
        prefs_free_string(pref_omemo_log);
        jid_destroy(jidp);
    }
}

void
chat_log_otr_msg_in(ProfMessage *message)
{
    if (prefs_get_boolean(PREF_CHLOG)) {
        const char *jid = connection_get_fulljid();
        Jid *jidp = jid_create(jid);
        char *pref_otr_log = prefs_get_string(PREF_OTR_LOG);
        if (message->enc == PROF_MSG_ENC_PLAIN || (strcmp(pref_otr_log, "on") == 0)) {
            _chat_log_chat(jidp->barejid, message->jid->barejid, message->plain, PROF_IN_LOG, message->timestamp);
        } else if (strcmp(pref_otr_log, "redact") == 0) {
            _chat_log_chat(jidp->barejid, message->jid->barejid, "[redacted]", PROF_IN_LOG, message->timestamp);
        }
        prefs_free_string(pref_otr_log);
        jid_destroy(jidp);
    }
}

void
chat_log_pgp_msg_in(ProfMessage *message)
{
    if (prefs_get_boolean(PREF_CHLOG)) {
        const char *jid = connection_get_fulljid();
        Jid *jidp = jid_create(jid);
        char *pref_pgp_log = prefs_get_string(PREF_PGP_LOG);
        if (strcmp(pref_pgp_log, "on") == 0) {
            _chat_log_chat(jidp->barejid, message->jid->barejid, message->plain, PROF_IN_LOG, message->timestamp);
        } else if (strcmp(pref_pgp_log, "redact") == 0) {
            _chat_log_chat(jidp->barejid, message->jid->barejid, "[redacted]", PROF_IN_LOG, message->timestamp);
        }
        prefs_free_string(pref_pgp_log);
        jid_destroy(jidp);
    }
}

void
chat_log_omemo_msg_in(ProfMessage *message)
{
    if (prefs_get_boolean(PREF_CHLOG)) {
        const char *jid = connection_get_fulljid();
        Jid *jidp = jid_create(jid);
        char *pref_omemo_log = prefs_get_string(PREF_OMEMO_LOG);
        if (strcmp(pref_omemo_log, "on") == 0) {
            _chat_log_chat(jidp->barejid, message->jid->barejid, message->plain, PROF_IN_LOG, message->timestamp);
        } else if (strcmp(pref_omemo_log, "redact") == 0) {
            _chat_log_chat(jidp->barejid, message->jid->barejid, "[redacted]", PROF_IN_LOG, message->timestamp);
        }
        prefs_free_string(pref_omemo_log);
        jid_destroy(jidp);
    }
}

void
chat_log_msg_in(ProfMessage *message)
{
    if (prefs_get_boolean(PREF_CHLOG)) {
        const char *jid = connection_get_fulljid();
        Jid *jidp = jid_create(jid);
        _chat_log_chat(jidp->barejid, message->jid->barejid, message->plain, PROF_IN_LOG, message->timestamp);
        jid_destroy(jidp);
    }
}

static void
_chat_log_chat(const char *const login, const char *const other, const char *const msg,
    chat_log_direction_t direction, GDateTime *timestamp)
{
    struct dated_chat_log *dated_log = g_hash_table_lookup(logs, other);

    // no log for user
    if (dated_log == NULL) {
        dated_log = _create_log(other, login);
        g_hash_table_insert(logs, strdup(other), dated_log);

    // log entry exists but file removed
    } else if (!g_file_test(dated_log->filename, G_FILE_TEST_EXISTS)) {
        dated_log = _create_log(other, login);
        g_hash_table_replace(logs, strdup(other), dated_log);

    // log file needs rolling
    } else if (_log_roll_needed(dated_log)) {
        dated_log = _create_log(other, login);
        g_hash_table_replace(logs, strdup(other), dated_log);
    }

    if (timestamp == NULL) {
        timestamp = g_date_time_new_now_local();
    } else {
        g_date_time_ref(timestamp);
    }

    gchar *date_fmt = g_date_time_format(timestamp, "%H:%M:%S");
    FILE *logp = fopen(dated_log->filename, "a");
    g_chmod(dated_log->filename, S_IRUSR | S_IWUSR);
    if (logp) {
        if (direction == PROF_IN_LOG) {
            if (strncmp(msg, "/me ", 4) == 0) {
                fprintf(logp, "%s - *%s %s\n", date_fmt, other, msg + 4);
            } else {
                fprintf(logp, "%s - %s: %s\n", date_fmt, other, msg);
            }
        } else {
            if (strncmp(msg, "/me ", 4) == 0) {
                fprintf(logp, "%s - *me %s\n", date_fmt, msg + 4);
            } else {
                fprintf(logp, "%s - me: %s\n", date_fmt, msg);
            }
        }
        fflush(logp);
        int result = fclose(logp);
        if (result == EOF) {
            log_error("Error closing file %s, errno = %d", dated_log->filename, errno);
        }
    }

    g_free(date_fmt);
    g_date_time_unref(timestamp);
}

void
groupchat_log_msg_out(const gchar *const room, const gchar *const msg)
{
    if (prefs_get_boolean(PREF_GRLOG)) {
        const char *jid = connection_get_fulljid();
        Jid *jidp = jid_create(jid);
        char *mynick = muc_nick(room);
        _groupchat_log_chat(jidp->barejid, room, mynick, msg);
        jid_destroy(jidp);
    }
}

void
groupchat_log_msg_in(const gchar *const room, const gchar *const nick, const gchar *const msg)
{
    if (prefs_get_boolean(PREF_GRLOG)) {
        const char *jid = connection_get_fulljid();
        Jid *jidp = jid_create(jid);
        _groupchat_log_chat(jidp->barejid, room, nick, msg);
        jid_destroy(jidp);
    }
}

void
groupchat_log_omemo_msg_out(const gchar *const room, const gchar *const msg)
{
    if (prefs_get_boolean(PREF_CHLOG)) {
        const char *jid = connection_get_fulljid();
        Jid *jidp = jid_create(jid);
        char *pref_omemo_log = prefs_get_string(PREF_OMEMO_LOG);
        char *mynick = muc_nick(room);
        if (strcmp(pref_omemo_log, "on") == 0) {
            _groupchat_log_chat(jidp->barejid, room, mynick, msg);
        } else if (strcmp(pref_omemo_log, "redact") == 0) {
            _groupchat_log_chat(jidp->barejid, room, mynick, "[redacted]");
        }
        prefs_free_string(pref_omemo_log);
        jid_destroy(jidp);
    }
}

void
groupchat_log_omemo_msg_in(const gchar *const room, const gchar *const nick, const gchar *const msg)
{
    if (prefs_get_boolean(PREF_CHLOG)) {
        const char *jid = connection_get_fulljid();
        Jid *jidp = jid_create(jid);
        char *pref_omemo_log = prefs_get_string(PREF_OMEMO_LOG);
        if (strcmp(pref_omemo_log, "on") == 0) {
            _groupchat_log_chat(jidp->barejid, room, nick, msg);
        } else if (strcmp(pref_omemo_log, "redact") == 0) {
            _groupchat_log_chat(jidp->barejid, room, nick, "[redacted]");
        }
        prefs_free_string(pref_omemo_log);
        jid_destroy(jidp);
    }
}

void
_groupchat_log_chat(const gchar *const login, const gchar *const room, const gchar *const nick,
    const gchar *const msg)
{
    struct dated_chat_log *dated_log = g_hash_table_lookup(groupchat_logs, room);

    // no log for room
    if (dated_log == NULL) {
        dated_log = _create_groupchat_log(room, login);
        g_hash_table_insert(groupchat_logs, strdup(room), dated_log);

    // log exists but needs rolling
    } else if (_log_roll_needed(dated_log)) {
        dated_log = _create_groupchat_log(room, login);
        g_hash_table_replace(logs, strdup(room), dated_log);
    }

    GDateTime *dt = g_date_time_new_now_local();

    gchar *date_fmt = g_date_time_format(dt, "%H:%M:%S");

    FILE *logp = fopen(dated_log->filename, "a");
    g_chmod(dated_log->filename, S_IRUSR | S_IWUSR);
    if (logp) {
        if (strncmp(msg, "/me ", 4) == 0) {
            fprintf(logp, "%s - *%s %s\n", date_fmt, nick, msg + 4);
        } else {
            fprintf(logp, "%s - %s: %s\n", date_fmt, nick, msg);
        }

        fflush(logp);
        int result = fclose(logp);
        if (result == EOF) {
            log_error("Error closing file %s, errno = %d", dated_log->filename, errno);
        }
    }

    g_free(date_fmt);
    g_date_time_unref(dt);
}


GSList*
chat_log_get_previous(const gchar *const login, const gchar *const recipient)
{
    GSList *history = NULL;
    GDateTime *now = g_date_time_new_now_local();
    GDateTime *log_date = g_date_time_new(tz,
        g_date_time_get_year(session_started),
        g_date_time_get_month(session_started),
        g_date_time_get_day_of_month(session_started),
        g_date_time_get_hour(session_started),
        g_date_time_get_minute(session_started),
        g_date_time_get_second(session_started));

    // get data from all logs from the day the session was started to today
    while (g_date_time_compare(log_date, now) != 1) {
        char *filename = _get_log_filename(recipient, login, log_date, FALSE);

        FILE *logp = fopen(filename, "r");
        if (logp) {
            GString *header = g_string_new("");
            g_string_append_printf(header, "%d/%d/%d:",
                g_date_time_get_day_of_month(log_date),
                g_date_time_get_month(log_date),
                g_date_time_get_year(log_date));
            history = g_slist_append(history, header->str);
            g_string_free(header, FALSE);

            char *line;
            while ((line = file_getline(logp)) != NULL) {
                history = g_slist_append(history, line);
            }

            fclose(logp);
        }

        free(filename);

        GDateTime *next = g_date_time_add_days(log_date, 1);
        g_date_time_unref(log_date);
        log_date = next;
    }

    g_date_time_unref(log_date);
    g_date_time_unref(now);

    return history;
}

void
chat_log_close(void)
{
    g_hash_table_destroy(logs);
    g_hash_table_destroy(groupchat_logs);
    g_date_time_unref(session_started);
}

static struct dated_chat_log*
_create_log(const char *const other, const char *const login)
{
    GDateTime *now = g_date_time_new_now_local();
    char *filename = _get_log_filename(other, login, now, TRUE);

    struct dated_chat_log *new_log = malloc(sizeof(struct dated_chat_log));
    new_log->filename = strdup(filename);
    new_log->date = now;

    free(filename);

    return new_log;
}

static struct dated_chat_log*
_create_groupchat_log(const char * const room, const char *const login)
{
    GDateTime *now = g_date_time_new_now_local();
    char *filename = _get_groupchat_log_filename(room, login, now, TRUE);

    struct dated_chat_log *new_log = malloc(sizeof(struct dated_chat_log));
    new_log->filename = strdup(filename);
    new_log->date = now;

    free(filename);

    return new_log;
}

static gboolean
_log_roll_needed(struct dated_chat_log *dated_log)
{
    gboolean result = FALSE;
    GDateTime *now = g_date_time_new_now_local();
    if (g_date_time_get_day_of_year(dated_log->date) !=
            g_date_time_get_day_of_year(now)) {
        result = TRUE;
    }
    g_date_time_unref(now);

    return result;
}

static void
_free_chat_log(struct dated_chat_log *dated_log)
{
    if (dated_log) {
        if (dated_log->filename) {
            g_free(dated_log->filename);
            dated_log->filename = NULL;
        }
        if (dated_log->date) {
            g_date_time_unref(dated_log->date);
            dated_log->date = NULL;
        }
        free(dated_log);
    }
}

static
gboolean _key_equals(void *key1, void *key2)
{
    gchar *str1 = (gchar *) key1;
    gchar *str2 = (gchar *) key2;

    return (g_strcmp0(str1, str2) == 0);
}

static char*
_get_log_filename(const char *const other, const char *const login, GDateTime *dt, gboolean create)
{
    char *chatlogs_dir = files_get_data_path(DIR_CHATLOGS);
    GString *log_file = g_string_new(chatlogs_dir);
    free(chatlogs_dir);

    gchar *login_dir = str_replace(login, "@", "_at_");
    g_string_append_printf(log_file, "/%s", login_dir);
    if (create) {
        create_dir(log_file->str);
    }
    free(login_dir);

    gchar *other_file = str_replace(other, "@", "_at_");
    g_string_append_printf(log_file, "/%s", other_file);
    if (create) {
        create_dir(log_file->str);
    }
    free(other_file);

    gchar *date = g_date_time_format(dt, "/%Y_%m_%d.log");
    g_string_append(log_file, date);
    g_free(date);

    char *result = strdup(log_file->str);
    g_string_free(log_file, TRUE);

    return result;
}

static char*
_get_groupchat_log_filename(const char *const room, const char *const login, GDateTime *dt, gboolean create)
{
    char *chatlogs_dir = files_get_data_path(DIR_CHATLOGS);
    GString *log_file = g_string_new(chatlogs_dir);
    free(chatlogs_dir);

    gchar *login_dir = str_replace(login, "@", "_at_");
    g_string_append_printf(log_file, "/%s", login_dir);
    if (create) {
        create_dir(log_file->str);
    }
    free(login_dir);

    g_string_append(log_file, "/rooms");
    if (create) {
        create_dir(log_file->str);
    }

    gchar *room_file = str_replace(room, "@", "_at_");
    g_string_append_printf(log_file, "/%s", room_file);
    if (create) {
        create_dir(log_file->str);
    }
    free(room_file);

    gchar *date = g_date_time_format(dt, "/%Y_%m_%d.log");
    g_string_append(log_file, date);
    g_free(date);

    char *result = strdup(log_file->str);
    g_string_free(log_file, TRUE);

    return result;
}

static char*
_log_string_from_level(log_level_t level)
{
    switch (level)
    {
        case PROF_LEVEL_ERROR:
            return "ERR";
        case PROF_LEVEL_WARN:
            return "WRN";
        case PROF_LEVEL_INFO:
            return "INF";
        case PROF_LEVEL_DEBUG:
            return "DBG";
        default:
            return "LOG";
    }
}

void
log_stderr_handler(void)
{
    GString * const s = stderr_msg;
    char * const buf = stderr_buf;
    ssize_t size;
    int retry = 0;
    int i;

    if (!stderr_inited)
        return;

    do {
        size = read(stderr_pipe[0], buf, STDERR_BUFSIZE);
        if (size == -1 && errno == EINTR && retry++ < STDERR_RETRY_NR)
            continue;
        if (size <= 0 || retry++ >= STDERR_RETRY_NR)
            break;

        for (i = 0; i < size; ++i) {
            if (buf[i] == '\n') {
                log_msg(stderr_level, "stderr", s->str);
                g_string_assign(s, "");
            } else
                g_string_append_c(s, buf[i]);
        }
    } while (1);

    if (s->len > 0 && s->str[0] != '\0') {
        log_msg(stderr_level, "stderr", s->str);
        g_string_assign(s, "");
    }
}

void
log_stderr_init(log_level_t level)
{
    int rc;
    int flags;

    rc = pipe(stderr_pipe);
    if (rc != 0)
        goto err;

    flags = fcntl(stderr_pipe[0], F_GETFL);
    rc = fcntl(stderr_pipe[0], F_SETFL, flags | O_NONBLOCK);
    if (rc != 0)
        goto err_close;

    close(STDERR_FILENO);
    rc = dup2(stderr_pipe[1], STDERR_FILENO);
    if (rc < 0)
        goto err_close;

    stderr_buf = malloc(STDERR_BUFSIZE);
    stderr_msg = g_string_sized_new(STDERR_BUFSIZE);
    stderr_level = level;
    stderr_inited = 1;

    if (stderr_buf == NULL || stderr_msg == NULL) {
        errno = ENOMEM;
        goto err_free;
    }
    return;

err_free:
    if (stderr_msg != NULL)
        g_string_free(stderr_msg, TRUE);
    free(stderr_buf);
err_close:
    close(stderr_pipe[0]);
    close(stderr_pipe[1]);
err:
    stderr_inited = 0;
    log_error("Unable to init stderr log handler: %s", strerror(errno));
}

void
log_stderr_close(void)
{
    if (!stderr_inited)
        return;

    /* handle remaining logs before close */
    log_stderr_handler();
    stderr_inited = 0;
    free(stderr_buf);
    g_string_free(stderr_msg, TRUE);
    close(stderr_pipe[0]);
    close(stderr_pipe[1]);
}