/*
 * BRLTTY - A background process providing access to the console screen (when in
 *          text mode) for a blind person using a refreshable braille display.
 *
 * Copyright (C) 1995-2025 by The BRLTTY Developers.
 *
 * BRLTTY comes with ABSOLUTELY NO WARRANTY.
 *
 * This is free software, placed under the terms of the
 * GNU Lesser General Public License, as published by the Free Software
 * Foundation; either version 2.1 of the License, or (at your option) any
 * later version. Please see the file LICENSE-LGPL for details.
 *
 * Web Page: http://brltty.app/
 *
 * This software is maintained by Dave Mielke <dave@mielke.cc>.
 */

#include "prologue.h"

#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <limits.h>
#include <locale.h>

#ifdef HAVE_LANGINFO_H
#include <langinfo.h>
#endif /* HAVE_LANGINFO_H */

#ifdef HAVE_ICONV_H
#include <iconv.h>
#endif /* HAVE_ICONV_H */

#ifdef HAVE_SYS_FILE_H
#include <sys/file.h>
#endif /* HAVE_SYS_FILE_H */

#ifdef HAVE_TERMIOS_H
#include <termios.h>
#endif /* HAVE_TERMIOS_H */

#include "parameters.h"
#include "options.h"
#include "log.h"
#include "strfmt.h"
#include "file.h"
#include "lock.h"
#include "parse.h"
#include "async_wait.h"
#include "utf8.h"
#include "program.h"


static inline int
allowBackslashAsPathSeparator (void) {
#if defined(__MINGW32__) || defined(__MSDOS__)
  return 1;
#else /* allow backslash */
  return 0;
#endif /* allow backslash */
}

static int
isDriveLetter (char character) {
  return !!strchr("ABCDEFGHIJKLMNOPQRSTUVWXYZ", toupper(character));
}

int
isPathSeparator (const char character) {
  if (character == PATH_SEPARATOR_CHARACTER) return 1;

  if (allowBackslashAsPathSeparator()) {
    if (character == '\\') {
      return 1;
    }
  }

  return 0;
}

int
isAbsolutePath (const char *path) {
  if (isPathSeparator(path[0])) return 1;

  if (allowBackslashAsPathSeparator()) {
    if (isDriveLetter(path[0])) {
      if (path[1] == ':') {
        if (isPathSeparator(path[2])) {
          return 1;
        }
      }
    }
  }

  return 0;
}

static size_t
stripPathSeparator (const char *path, size_t length) {
  while (length) {
    if (!isPathSeparator(path[length-1])) break;
    length -= 1;
  }

  return length;
}

char *
getPathDirectory (const char *path) {
  size_t length = strlen(path);
  size_t end = stripPathSeparator(path, length);

  if (end) {
    while (--end) {
      if (isPathSeparator(path[end-1])) {
        break;
      }
    }

    if ((length = end)) {
      if ((end = stripPathSeparator(path, length))) {
        length = end;
      }
    }
  }

  if (!length) length = strlen((path = CURRENT_DIRECTORY_NAME));
  {
    char *directory = malloc(length + 1);

    if (directory) {
      memcpy(directory, path, length);
      directory[length] = 0;
    } else {
      logMallocError();
    }

    return directory;
  }
}

const char *
locatePathName (const char *path) {
  const char *name = path + strlen(path);

  while (name != path) {
    if (isPathSeparator(*--name)) {
      ++name;
      break;
    }
  }

  return name;
}

const char *
locatePathExtension (const char *path) {
  const char *name = locatePathName(path);
  const char *extension = strrchr(name, '.');

  if (extension && extension[1]) {
    const char *c = extension;

    while (c > name) {
      if (*--c != '.') {
        return extension;
      }
    }
  }

  return NULL;
}

int
isExplicitPath (const char *path) {
  return locatePathName(path) != path;
}

char *
joinPath (const char *const *components, unsigned int count) {
  const char *const *component = components + count;
  unsigned int size = (count * 2) - 1;
  const char *strings[size];
  unsigned int first = size;

  while (component != components) {
    const char *next = *--component;

    if (next && *next) {
      if ((first != size) && !isPathSeparator(next[strlen(next)-1])) {
        strings[--first] = "/";
      }

      strings[--first] = next;
      if (isAbsolutePath(next)) break;
    }
  }

  return joinStrings(&strings[first], size-first);
}

char *
makePath (const char *directory, const char *file) {
  const char *const components[] = {directory, file};

  return joinPath(components, ARRAY_COUNT(components));
}

int
anchorRelativePath (char **path, const char *anchor) {
  int ok = 0;
  char *newPath = makePath(anchor, *path);

  if (newPath) {
    if (changeStringSetting(path, newPath)) ok = 1;
    free(newPath);
  }

  return ok;
}

int
hasFileExtension (const char *path, const char *extension) {
  const char *tail = locatePathExtension(path);

  if (!tail) return 0;
  return strcmp(tail, extension) == 0;
}

char *
replaceFileExtension (const char *path, const char *extension) {
  const char *oldExtension = locatePathExtension(path);

  if (oldExtension) {
    size_t headLength = oldExtension - path;
    size_t extensionLength = strlen(extension);
    char *newPath = malloc(headLength + extensionLength + 1);

    if (newPath) {
      char *byte = newPath;

      byte = mempcpy(byte, path, headLength);
      byte = mempcpy(byte, extension, extensionLength);
      *byte = 0;

      return newPath;
    } else {
      logMallocError();
    }
  } else {
    logMessage(LOG_WARNING, "path has no extension: %s", path);
  }

  return NULL;
}

char *
ensureFileExtension (const char *path, const char *extension) {
  const char *strings[2];
  int count = 0;

  strings[count++] = path;
  if (extension && !locatePathExtension(path)) strings[count++] = extension;
  return joinStrings(strings, count);
}

char *
makeFilePath (const char *directory, const char *name, const char *extension) {
  char *path = NULL;
  char *file = ensureFileExtension(name, extension);

  if (file) {
    if (isExplicitPath(file)) return file;
    path = makePath(directory, file);
    free(file);
  }

  return path;
}

int
testPath (const char *path) {
#ifdef F_OK
  return access(path, F_OK) != -1;
#else /* F_OK */
  errno = ENOSYS;
  return 0;
#endif /* F_OK */
}

int
testFilePath (const char *path) {
#ifdef S_ISREG
  struct stat status;

  if (stat(path, &status) != -1) {
    if (S_ISREG(status.st_mode)) return 1;
    errno = EEXIST;
  }
#else /* S_ISREG */
  int result = open(path, O_RDONLY);

  if (result != -1) {
    close(result);
    return 1;
  }
#endif /* S_ISREG */

  return 0;
}

int
testProgramPath (const char *path) {
  if (!testFilePath(path)) return 0;

#if defined(__MINGW32__)
  {
    const char *extension = locatePathExtension(path);

    if (extension) {
      static char **extensions = NULL;

      if (!extensions) {
        const char *string = getenv("PATHEXT");

        {
          static char *noExtensions[] = {NULL};

          extensions = noExtensions;
        }

        if (string) {
          char **strings = splitString(string, ';', NULL);

          if (strings) extensions = strings;
        }
      }

      {
        char **x = extensions;

        while (*x) {
          if (strcasecmp(*x, extension) == 0) return 1;
          x += 1;
        }
      }
    }
  }

  return 0;

#elif defined(X_OK)
  return access(path, X_OK) != -1;

#else /* X_OK */
  errno = ENOSYS;
  return 0;
#endif /* X_OK */
}

int
testDirectoryPath (const char *path) {
#ifdef S_ISDIR
  struct stat status;

  if (stat(path, &status) != -1) {
    if (S_ISDIR(status.st_mode)) return 1;
    errno = EEXIST;
  }
#else /* S_ISDIR */
  errno = ENOSYS;
#endif /* S_ISDIR */

  return 0;
}

static LockDescriptor *
getUmaskLock (void) {
  static LockDescriptor *lock = NULL;
  return getLockDescriptor(&lock, "umask");
}

void
lockUmask (void) {
  obtainExclusiveLock(getUmaskLock());
}

void
unlockUmask (void) {
  releaseLock(getUmaskLock());
}

int
createDirectory (const char *path, int worldWritable) {
#if defined(GRUB_RUNTIME)
  errno = EROFS;

#else /* make directory */
#ifdef __MINGW32__
  if (mkdir(path) != -1) return 1;
#else /* __MINGW32__ */
  lockUmask();
  int created = mkdir(path, (S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH)) != -1;
  unlockUmask();

  if (created) {
    if (!worldWritable) return 1;
    mode_t mode = 0;

    #ifdef S_IRWXU
    mode |= S_IRWXU;
    #endif /* S_IRWXU */

    #ifdef S_IRWXG
    mode |= S_IRWXG;
    #endif /* S_IRWXG */

    #ifdef S_IRWXO
    mode |= S_IRWXO;
    #endif /* S_IRWXO */

    #ifdef S_ISVTX
    mode |= S_ISVTX;
    #endif /* S_ISVTX */

    {
      lockUmask();
      int changed = chmod(path, mode) != -1;
      unlockUmask();
      if (changed) return 1;
    }

    logMessage(LOG_WARNING,
      "%s: %s: %s",
      gettext("cannot make world writable"),
      path, strerror(errno)
    );

    return 0;
  }
#endif /* __MINGW32__ */
#endif /* make directory */

  logMessage(LOG_WARNING,
    "%s: %s: %s",
    gettext("cannot create directory"),
    path, strerror(errno)
  );

  return 0;
}

int
ensureDirectory (const char *path, int worldWritable) {
  if (testDirectoryPath(path)) return 1;

  if (errno == EEXIST) {
    logMessage(LOG_ERR, "not a directory: %s", path);
  } else if (errno != ENOENT) {
    logMessage(LOG_ERR, "cannot access directory: %s: %s", path, strerror(errno));
  } else {
    {
      char *parent = getPathDirectory(path);
      if (!parent) return 0;
      int exists = 0;

      if (strcmp(parent, path) != 0) {
        exists = ensureDirectory(parent, 0);
      }

      free(parent);
      if (!exists) return 0;
    }

    if (createDirectory(path, worldWritable)) {
      logMessage(LOG_NOTICE, "directory created: %s", path);
      return 1;
    }
  }

  return 0;
}

int
ensurePathDirectory (const char *path) {
  char *directory = getPathDirectory(path);
  if (!directory) return 0;

  {
    int exists = ensureDirectory(directory, 0);
    free(directory);
    return exists;
  }
}

static const char *
getDirectory (char *const *variable) {
  if (*variable && **variable) {
    if (ensureDirectory(*variable, 0)) {
      return *variable;
    }
  }

  return NULL;
}

static char *
makeDirectoryPath (char *const *variable, const char *file) {
  const char *directory = getDirectory(variable);
  if (directory) return makePath(directory, file);
  return NULL;
}

char *opt_updatableDirectory = UPDATABLE_DIRECTORY;

const char *
getUpdatableDirectory (void) {
  return getDirectory(&opt_updatableDirectory);
}

char *
makeUpdatablePath (const char *file) {
  return makeDirectoryPath(&opt_updatableDirectory, file);
}

char *opt_writableDirectory = WRITABLE_DIRECTORY;

const char *
getWritableDirectory (void) {
  return getDirectory(&opt_writableDirectory);
}

char *
makeWritablePath (const char *file) {
  return makeDirectoryPath(&opt_writableDirectory, file);
}

const char *
getDevicesDirectory (void) {
  static const char *devicesDirectory = NULL;

  if (!devicesDirectory) {
    const char *directory = makeWritablePath("dev");

    if (directory) {
      if (ensureDirectory(directory, 0)) {
        devicesDirectory = directory;
      }
    }

    if (devicesDirectory) logMessage(LOG_DEBUG, "devices directory: %s", devicesDirectory);
  }

  return devicesDirectory;
}

char *
makeDevicesPath (const char *file) {
  const char *directory = getDevicesDirectory();
  if (directory) return makePath(directory, file);
  return NULL;
}

char *
getWorkingDirectory (void) {
#if defined(GRUB_RUNTIME)
  errno = ENOSYS;
#else /* get working directory */
  size_t size = 0X80;
  char *buffer = NULL;

  while (1) {
    {
      char *newBuffer = realloc(buffer, size<<=1);

      if (!newBuffer) {
        logMallocError();
        break;
      }

      buffer = newBuffer;
    }

    if (getcwd(buffer, size)) return buffer;

    if (errno != ERANGE) {
      logSystemError("getcwd");
      break;
    }
  }

  if (buffer) free(buffer);
#endif /* get working directory */

  logMessage(LOG_WARNING, "%s: %s",
             gettext("cannot get working directory"),
             strerror(errno));
  return NULL;
}

int
setWorkingDirectory (const char *path) {
#if defined(GRUB_RUNTIME)
  errno = ENOSYS;
#else /* set working directory */
  if (chdir(path) != -1) return 1;
#endif /* set working directory */

  logMessage(LOG_WARNING, "%s: %s: %s",
             gettext("cannot set working directory"),
             path, strerror(errno));
  return 0;
}

char *
getHomeDirectory (void) {
#if defined(GRUB_RUNTIME)
#else /* get home directory */
  char *path = getenv("HOME");

  if (path && *path) {
    if ((path = strdup(path))) return path;
    logMallocError();
  }
#endif /* get home directory */

  return NULL;
}

static char *
makeOverridePath (const char *base, int xdg) {
  return makePath(base, (xdg? PACKAGE_TARNAME: (CURRENT_DIRECTORY_NAME PACKAGE_TARNAME)));
}

static int
addOverridePath (char **paths, size_t *index, const char *base, int xdg) {
  char *path = makeOverridePath(base, xdg);
  if (!path) return 0;

  logMessage(LOG_DEBUG, "override directory: %s", path);
  paths[(*index)++] = path;
  return 1;
}

static char **overrideDirectories = NULL;

const char *const *
getAllOverrideDirectories (void) {
  if (!overrideDirectories) {
    logMessage(LOG_DEBUG, "determining override directories");

    const char *secondaryList = getenv("XDG_CONFIG_DIRS");
    int secondaryCount;
    char **secondaryBases = splitString(((secondaryList && *secondaryList)? secondaryList: "/etc/xdg"), ':', &secondaryCount);

    if (secondaryBases) {
      size_t count = 1 + secondaryCount + 1;
      char **paths;

      if ((paths = malloc(sizeof(*paths) * (count + 1)))) {
        size_t index = 0;

        {
          const char *primary = getenv("XDG_CONFIG_HOME");

          if (primary && *primary) {
            if (!addOverridePath(paths, &index, primary, 1)) goto done;
          } else {
            char *home = getHomeDirectory();

            if (home) {
              char *base = *home? makePath(home, ".config"): NULL;

              free(home);
              home = NULL;

              if (base) {
                int added = addOverridePath(paths, &index, base, 1);

                free(base);
                base = NULL;

                if (!added) goto done;
              }
            }
          }
        }

        if (!index) {
          char *primary = strdup("");

          if (!primary) {
            logMallocError();
            goto done;
          }

          paths[index++] = primary;
        }

        {
          char **base = secondaryBases;

          while (*base) {
            if (**base) {
              if (!addOverridePath(paths, &index, *base, 1)) {
                break;
              }
            } else {
              count -= 1;
            }

            base += 1;
          }

          if (*base) goto done;
        }

        {
          int added = 0;
          char *home = getHomeDirectory();

          if (home && *home) {
            if (addOverridePath(paths, &index, home, 0)) added = 1;
          } else {
            char *current = getWorkingDirectory();

            if (current) {
              if (addOverridePath(paths, &index, current, 0)) added = 1;
              free(current);
            }
          }

          if (home) free(home);
          if (!added) goto done;
        }

done:
        paths[index] = NULL;

        if (index == count) {
          overrideDirectories = paths;
        } else {
          deallocateStrings(paths);
        }
      } else {
        logMallocError();
      }

      deallocateStrings(secondaryBases);
    }

    if (!overrideDirectories) logMessage(LOG_WARNING, "no override directories");
  }

  return (const char *const *)overrideDirectories;
}

const char *
getPrimaryOverrideDirectory (void) {
  const char *const *directories = getAllOverrideDirectories();

  if (directories) {
    const char *directory = directories[0];

    if (directory && *directory) return directory;
  }

  logMessage(LOG_WARNING, "no primary override directory");
  return NULL;
}

void
forgetOverrideDirectories (void) {
  if (overrideDirectories) {
    logMessage(LOG_DEBUG, "forgetting override directories");
    deallocateStrings(overrideDirectories);
    overrideDirectories = NULL;
  }
}

#if defined(F_SETLK)
static int
modifyFileLock (int file, int action, short type) {
  struct flock lock;

  memset(&lock, 0, sizeof(lock));
  lock.l_type = type;
  lock.l_whence = SEEK_SET;
  lock.l_start = 0;
  lock.l_len = 0;

  do {
    if (fcntl(file, action, &lock) != -1) return 1;
  } while (errno == EINTR);

  if (errno == EACCES) errno = EAGAIN;
  if (errno != EAGAIN) logSystemError("fcntl[struct flock *]");
  return 0;
}

static int
lockFile (int file, int exclusive, int wait) {
  return modifyFileLock(file, (wait? F_SETLKW: F_SETLK), (exclusive? F_WRLCK: F_RDLCK));
}

int
acquireFileLock (int file, int exclusive) {
  return lockFile(file, exclusive, 1);
}

int
attemptFileLock (int file, int exclusive) {
  return lockFile(file, exclusive, 0);
}

int
releaseFileLock (int file) {
  return modifyFileLock(file, F_SETLK, F_UNLCK);
}

#elif defined(LOCK_EX)
static int
modifyFileLock (int file, int operation) {
  do {
    if (flock(file, operation) != -1) return 1;
  } while (errno == EINTR);

#ifdef EWOULDBLOCK
  if (errno == EWOULDBLOCK) errno = EAGAIN;
#endif /* EWOULDBLOCK */

  if (errno == EACCES) errno = EAGAIN;
  if (errno != EAGAIN) logSystemError("flock");
  return 0;
}

int
acquireFileLock (int file, int exclusive) {
  return modifyFileLock(file, (exclusive? LOCK_EX: LOCK_SH));
}

int
attemptFileLock (int file, int exclusive) {
  return modifyFileLock(file, ((exclusive? LOCK_EX: LOCK_SH) | LOCK_NB));
}

int
releaseFileLock (int file) {
  return modifyFileLock(file, LOCK_UN);
}

#elif defined(F_LOCK)
static int
modifyRegionLock (int file, int command, off_t length) {
  do {
    if (lockf(file, command, length) != -1) return 1;
  } while (errno == EINTR);

  if (errno == EACCES) errno = EAGAIN;
  if (errno != EAGAIN) logSystemError("lockf");
  return 0;
}

static int
modifyFileLock (int file, int command) {
  off_t offset;

  if ((offset = lseek(file, 0, SEEK_CUR)) == -1) {
    logSystemError("lseek");
  } else if (modifyRegionLock(file, command, 0)) {
    if (!offset) return 1;
    if (modifyRegionLock(file, command, -offset)) return 1;
  }

  return 0;
}

int
acquireFileLock (int file, int exclusive) {
  return modifyFileLock(file, F_LOCK);
}

int
attemptFileLock (int file, int exclusive) {
  return modifyFileLock(file, F_TLOCK);
}

int
releaseFileLock (int file) {
  return modifyFileLock(file, F_ULOCK);
}

#elif defined(__MINGW32__)
#include <io.h>
#include <sys/locking.h>
#include <limits.h>

static int
modifyFileLock (int file, int mode) {
  int ok = 0;
  off_t offset;

  if ((offset = lseek(file, 0, SEEK_CUR)) != -1) {
    if (lseek(file, 0, SEEK_SET) != -1) {
      int wait;

      if (mode == _LK_LOCK) {
        mode = _LK_NBLCK;
        wait = 1;
      } else if (mode == _LK_RLCK) {
        mode = _LK_NBRLCK;
        wait = 1;
      } else {
        wait = 0;
      }

      while (1) {
        if (_locking(file, mode, LONG_MAX) != -1) {
          ok = 1;
          break;
        }

        if (errno != EACCES) {
          logSystemError("_locking");
          break;
        }

        if (!wait) break;
        asyncWait(WINDOWS_FILE_LOCK_RETRY_INTERVAL);
      }

      if (lseek(file, offset, SEEK_SET) == -1) {
        logSystemError("lseek");
        ok = 0;
      }
    } else {
      logSystemError("lseek");
    }
  } else {
    logSystemError("lseek");
  }

  return ok;
}

int
acquireFileLock (int file, int exclusive) {
  return modifyFileLock(file, (exclusive? _LK_LOCK: _LK_RLCK));
}

int
attemptFileLock (int file, int exclusive) {
  return modifyFileLock(file, (exclusive? _LK_NBLCK: _LK_NBRLCK));
}

int
releaseFileLock (int file) {
  return modifyFileLock(file, _LK_UNLCK);
}

#else /* file locking */
#warning file lock support not available on this platform

int
acquireFileLock (int file, int exclusive) {
  logUnsupportedFunction();
  return 0;
}

int
attemptFileLock (int file, int exclusive) {
  logUnsupportedFunction();
  return 0;
}

int
releaseFileLock (int file) {
  logUnsupportedFunction();
  return 0;
}
#endif /* file locking */

static void
exitProgramStream (void *data) {
  FILE **stream = data;

  if (*stream) {
    fclose(*stream);
    *stream = NULL;
  }
}

void
registerProgramStream (const char *name, FILE **stream) {
  onProgramExit(name, exitProgramStream, stream);
}

FILE *
openFile (const char *path, const char *mode, int optional) {
  FILE *file = fopen(path, mode);

  if (file) {
    logMessage(LOG_DEBUG, "file opened: %s fd=%d", path, fileno(file));
  } else {
    logMessage((optional && (errno == ENOENT))? LOG_DEBUG: LOG_ERR,
               "cannot open file: %s: %s", path, strerror(errno));
  }

  return file;
}

int
readLine (FILE *file, char **buffer, size_t *size, size_t *length) {
  char *line;

  if (ferror(file)) return 0;
  if (feof(file)) return 0;

  if (!*size) {
    if (!(*buffer = malloc((*size = 0X80)))) {
      logMallocError();
      return 0;
    }
  }

  if ((line = fgets(*buffer, *size, file))) {
    size_t count = strlen(line); /* Line length including new-line. */

    /* No trailing new-line means that the buffer isn't big enough. */
    while (line[count-1] != '\n') {
      /* If necessary, extend the buffer. */
      if ((*size - (count + 1)) == 0) {
        size_t newSize = *size << 1;
        char *newBuffer = realloc(*buffer, newSize);

        if (!newBuffer) {
          logMallocError();
          return 0;
        }

        *buffer = newBuffer;
        *size = newSize;
      }

      /* Read the rest of the line into the end of the buffer. */
      if (!(line = fgets(&(*buffer)[count], (*size -count), file))) {
        if (!ferror(file)) goto done;
        logSystemError("fgets");
        return 0;
      }

      count += strlen(line); /* New total line length. */
      line = *buffer; /* Point to the beginning of the line. */
    }

    if (--count > 0) {
      if (line[count-1] == '\r') {
        count -= 1;
      }
    }

    line[count] = 0; /* Remove trailing new-line. */
  done:
    if (length) *length = count;
    return 1;
  } else if (ferror(file)) {
    logSystemError("fgets");
  }

  return 0;
}

/* Process each line of an input text file safely.
 * This routine handles the actual reading of the file,
 * insuring that the input buffer is always big enough,
 * and calls a caller-supplied handler once for each line in the file.
 * The caller-supplied data pointer is passed straight through to the handler.
 */
int
processLines (FILE *file, LineHandler handleLine, void *data) {
  char *buffer = NULL;
  size_t bufferSize = 0;

  LineHandlerParameters parameters = {
    .data = data,

    .line = {
      .number = 0,
    },
  };

  while (1) {
    parameters.line.number += 1;
    if (!readLine(file, &buffer, &bufferSize, &parameters.line.length)) break;
    parameters.line.text = buffer;
    if (!handleLine(&parameters)) break;
  }

  if (buffer) free(buffer);
  return !ferror(file);
}

STR_BEGIN_FORMATTER(formatInputError, const char *file, const int *line, const char *format, va_list arguments)
  if (file) STR_PRINTF("%s", file);
  if (line) STR_PRINTF("[%d]", *line);
  if (STR_LENGTH) STR_PRINTF(": ");
  STR_VPRINTF(format, arguments);
STR_END_FORMATTER

static void
detachStandardStream (FILE *stream, const char *name, int output) {
  const char *nullDevice = "/dev/null";

  if (output) {
    fflush(stream);
  }

  if (!freopen(nullDevice, (output? "a": "r"), stream)) {
    if (errno != ENOENT) {
      char action[0X40];
      snprintf(action, sizeof(action), "freopen[%s]", name);
      logSystemError(action);
    }
  }
}

void
detachStandardInput (void) {
  detachStandardStream(stdin, "stdin", 0);
}

void
detachStandardOutput (void) {
  detachStandardStream(stdout, "stdout", 1);
}

void
detachStandardError (void) {
  detachStandardStream(stderr, "stderr", 1);
}

void
detachStandardStreams (void) {
  detachStandardInput();
  detachStandardOutput();
  detachStandardError();
}

#ifdef __MINGW32__
int
getConsoleSize (size_t *width, size_t *height) {
  HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);

  if (handle) {
    if (handle != INVALID_HANDLE_VALUE) {
      if (GetFileType(handle) == FILE_TYPE_CHAR) {
        CONSOLE_SCREEN_BUFFER_INFO info;

        if (GetConsoleScreenBufferInfo(handle, &info)) {
          COORD size = info.dwSize;
          if (width) *width = size.X;
          if (height) *height = size.Y;
          return 1;
        }
      }
    }
  }

  return 0;
}

const char *
getConsoleEncoding (void) {
  static char encoding[0X10];

  if (!encoding[0]) {
    unsigned cp = GetConsoleOutputCP();

    if (cp == CP_UTF8) {
      strcpy(encoding, "UTF-8");
    } else {
      snprintf(encoding, sizeof(encoding), "CP%u", cp);
    }

    logMessage(LOG_DEBUG, "Console Encoding: %s", encoding);
  }

  return encoding;
}

ssize_t
readFileDescriptor (FileDescriptor fileDescriptor, void *buffer, size_t size) {
  {
    DWORD count;

    if (ReadFile(fileDescriptor, buffer, size, &count, NULL)) return count;
  }

  setSystemErrno();
  return -1;
}

ssize_t
writeFileDescriptor (FileDescriptor fileDescriptor, const void *buffer, size_t size) {
  {
    DWORD count;

    if (WriteFile(fileDescriptor, buffer, size, &count, NULL)) return count;
  }

  setSystemErrno();
  return -1;
}

const char *
getNamedPipeDirectory (void) {
  return "//./pipe";
}

int
createAnonymousPipe (FileDescriptor *pipeInput, FileDescriptor *pipeOutput) {
  SECURITY_ATTRIBUTES attributes;

  ZeroMemory(&attributes, sizeof(attributes));
  attributes.nLength = sizeof(attributes);
  attributes.bInheritHandle = TRUE;
  attributes.lpSecurityDescriptor = NULL;

  if (CreatePipe(pipeOutput, pipeInput, &attributes, 0)) {
    return 1;
  } else {
    logWindowsSystemError("CreatePipe");
  }

  return 0;
}

#else /* unix file/socket descriptor operations */
#include <sys/ioctl.h>

int
getConsoleSize (size_t *width, size_t *height) {
  struct winsize size;
  if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &size) == -1) return 0;

  if (width) *width = size.ws_col;
  if (height) *height = size.ws_row;
  return 1;
}

const char *
getConsoleEncoding (void) {
  static const char *encoding = NULL;

  if (!encoding) {
    setlocale(LC_ALL, "");

#ifdef HAVE_NL_LANGINFO
    encoding = nl_langinfo(CODESET);
#endif /* HAVE_NL_LANGINFO */

    if (encoding) {
      if (!(encoding = strdup(encoding))) {
        logMallocError();
      }
    }

    if (!encoding) encoding = "";
    logMessage(LOG_DEBUG, "Console Encoding: %s", encoding);
  }

  return encoding;
}

ssize_t
readFileDescriptor (FileDescriptor fileDescriptor, void *buffer, size_t size) {
  return read(fileDescriptor, buffer, size);
}

ssize_t
writeFileDescriptor (FileDescriptor fileDescriptor, const void *buffer, size_t size) {
  return write(fileDescriptor, buffer, size);
}

const char *
getNamedPipeDirectory (void) {
  return getWritableDirectory();
}

int
createAnonymousPipe (FileDescriptor *pipeInput, FileDescriptor *pipeOutput) {
  int fileDescriptors[2];

  if (pipe(fileDescriptors) != -1) {
    *pipeInput = fileDescriptors[1];
    *pipeOutput = fileDescriptors[0];
    return 1;
  } else {
    logSystemError("pipe");
  }

  return 0;
}
#endif /* basic file/socket descriptor operations */

int
writeWithConsoleEncoding (FILE *stream, const char *bytes, size_t count) {
  static const char *consoleEncoding = NULL;
  if (!consoleEncoding) consoleEncoding = getConsoleEncoding();

  if (!consoleEncoding || isCharsetUTF8(consoleEncoding)) {
    consoleEncoding = "";
  }

#ifdef HAVE_ICONV_H
  if (*consoleEncoding) {
    static iconv_t iconvHandle = (iconv_t)-1;

    if (iconvHandle == (iconv_t)-1) {
      static const char internalEncoding[] = "UTF-8";
      const char *externalEncoding = consoleEncoding;

      if ((iconvHandle = iconv_open(externalEncoding, internalEncoding)) == (iconv_t)-1) {
        consoleEncoding = "";

        logMessage(LOG_WARNING,
          "iconv open error: %s -> %s: %s",
          internalEncoding, externalEncoding, strerror(errno)
        );

        goto ENCODING_NOT_SUPPORTED;
      }
    }

    const char *inputNext = bytes;
    size_t inputLeft = count;

    char outputBuffer[inputLeft * MB_LEN_MAX];
    char *outputNext = outputBuffer;
    size_t outputLeft = sizeof(outputBuffer);

    ssize_t result = iconv(
      iconvHandle,
      (char **)&inputNext, &inputLeft,
      &outputNext, &outputLeft
    );

    if (result == -1) return 0;
    fwrite(outputBuffer, 1, (outputNext - outputBuffer), stream);
    return !ferror(stream);
  }
ENCODING_NOT_SUPPORTED:
#endif /* HAVE_ICONV_H */

  fwrite(bytes, 1, count, stream);
  return !ferror(stream);
}

#ifdef GOT_SOCKETS
ssize_t
readSocketDescriptor (SocketDescriptor socketDescriptor, void *buffer, size_t size) {
  return recv(socketDescriptor, buffer, size, 0);
}

ssize_t
writeSocketDescriptor (SocketDescriptor socketDescriptor, const void *buffer, size_t size) {
  return send(socketDescriptor, buffer, size, 0);
}
#endif /* GOT_SOCKETS */

char *
readSymbolicLink (const char *path) {
  char *content = NULL;
  size_t size = 0X80;
  char *buffer = NULL;

  while (1) {
    {
      char *newBuffer = realloc(buffer, size<<=1);

      if (!newBuffer) {
        logMallocError();
        break;
      }

      buffer = newBuffer;
    }

    {
      int length;

      #ifdef HAVE_READLINK
      length = readlink(path, buffer, size);
      #else /* HAVE_READLINK */
      length = -1;
      errno = ENOSYS;
      #endif /* HAVE_READLINK */

      if (length == -1) {
        if (errno != ENOENT) logSystemError("readlink");
        break;
      }

      if (length < size) {
        buffer[length] = 0;
        if (!(content = strdup(buffer))) logMallocError();
        break;
      }
    }
  }

  if (buffer) free(buffer);
  return content;
}
