diff --git a/Transmission.xcodeproj/project.pbxproj b/Transmission.xcodeproj/project.pbxproj
index 7dd9759..8324074 100644
--- a/Transmission.xcodeproj/project.pbxproj
+++ b/Transmission.xcodeproj/project.pbxproj
@@ -462,6 +462,7 @@
 		C1077A4F183EB29600634C22 /* error.h in Headers */ = {isa = PBXBuildFile; fileRef = C1077A4B183EB29600634C22 /* error.h */; };
 		C1077A50183EB29600634C22 /* file-posix.c in Sources */ = {isa = PBXBuildFile; fileRef = C1077A4C183EB29600634C22 /* file-posix.c */; };
 		C1077A51183EB29600634C22 /* file.h in Headers */ = {isa = PBXBuildFile; fileRef = C1077A4D183EB29600634C22 /* file.h */; };
+		C1305EBE186A13B100F03351 /* file.c in Sources */ = {isa = PBXBuildFile; fileRef = C1305EB8186A134000F03351 /* file.c */; };
 		D4AF3B2F0C41F7A500D46B6B /* list.c in Sources */ = {isa = PBXBuildFile; fileRef = D4AF3B2D0C41F7A500D46B6B /* list.c */; };
 		D4AF3B300C41F7A600D46B6B /* list.h in Headers */ = {isa = PBXBuildFile; fileRef = D4AF3B2E0C41F7A500D46B6B /* list.h */; };
 		E138A9780C04D88F00C5426C /* ProgressGradients.m in Sources */ = {isa = PBXBuildFile; fileRef = E138A9760C04D88F00C5426C /* ProgressGradients.m */; };
@@ -1200,6 +1201,7 @@
 		C1077A4B183EB29600634C22 /* error.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = error.h; path = libtransmission/error.h; sourceTree = "<group>"; };
 		C1077A4C183EB29600634C22 /* file-posix.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = "file-posix.c"; path = "libtransmission/file-posix.c"; sourceTree = "<group>"; };
 		C1077A4D183EB29600634C22 /* file.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = file.h; path = libtransmission/file.h; sourceTree = "<group>"; };
+		C1305EB8186A134000F03351 /* file.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = file.c; path = libtransmission/file.c; sourceTree = "<group>"; };
 		D4AF3B2D0C41F7A500D46B6B /* list.c */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.c; name = list.c; path = libtransmission/list.c; sourceTree = "<group>"; };
 		D4AF3B2E0C41F7A500D46B6B /* list.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; name = list.h; path = libtransmission/list.h; sourceTree = "<group>"; };
 		E138A9750C04D88F00C5426C /* ProgressGradients.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; name = ProgressGradients.h; path = macosx/ProgressGradients.h; sourceTree = "<group>"; };
@@ -1657,6 +1659,7 @@
 				C1077A4A183EB29600634C22 /* error.c */,
 				C1077A4B183EB29600634C22 /* error.h */,
 				C1077A4C183EB29600634C22 /* file-posix.c */,
+				C1305EB8186A134000F03351 /* file.c */,
 				C1077A4D183EB29600634C22 /* file.h */,
 				4D80185710BBC0B0008A4AF2 /* magnet.c */,
 				4D80185810BBC0B0008A4AF2 /* magnet.h */,
@@ -2696,6 +2699,7 @@
 				A2A4EA0E0DE106EB000CE197 /* ConvertUTF.c in Sources */,
 				A292A6E80DFB45FC004B9C0A /* webseed.c in Sources */,
 				A25E03E30E4015380086C225 /* tr-getopt.c in Sources */,
+				C1305EBE186A13B100F03351 /* file.c in Sources */,
 				4DB74F080E8CD75100AEB1A8 /* wildmat.c in Sources */,
 				A21FBBAC0EDA78C300BC3C51 /* bandwidth.c in Sources */,
 				A22CFCA80FC24ED80009BD3E /* tr-dht.c in Sources */,
diff --git a/libtransmission/Makefile.am b/libtransmission/Makefile.am
index e841a2d..21a085d 100644
--- a/libtransmission/Makefile.am
+++ b/libtransmission/Makefile.am
@@ -30,6 +30,7 @@ libtransmission_a_SOURCES = \
   crypto.c \
   error.c \
   fdlimit.c \
+  file.c \
   handshake.c \
   history.c \
   inout.c \
diff --git a/libtransmission/file-test.c b/libtransmission/file-test.c
index 15b8422..c4be94c 100644
--- a/libtransmission/file-test.c
+++ b/libtransmission/file-test.c
@@ -1153,6 +1153,105 @@ test_file_map (void)
 }
 
 static int
+test_file_utilities (void)
+{
+  char * const test_dir = create_test_dir (__FUNCTION__);
+  tr_error * err = NULL;
+  char * path1;
+  tr_sys_file_t fd;
+  char buffer[16];
+
+  path1 = tr_buildPath (test_dir, "a", NULL);
+
+  libtest_create_file_with_string_contents (path1, "a\nbc\r\ndef\nghij\r\n\n\nklmno\r");
+
+  fd = tr_sys_file_open (path1, TR_SYS_FILE_READ, 0, NULL);
+
+  check (tr_sys_file_read_line (fd, buffer, sizeof (buffer) / sizeof (*buffer), &err));
+  check (err == NULL);
+  check_streq ("a", buffer);
+  check (tr_sys_file_read_line (fd, buffer, sizeof (buffer) / sizeof (*buffer), &err));
+  check (err == NULL);
+  check_streq ("bc", buffer);
+  check (tr_sys_file_read_line (fd, buffer, sizeof (buffer) / sizeof (*buffer), &err));
+  check (err == NULL);
+  check_streq ("def", buffer);
+  check (tr_sys_file_read_line (fd, buffer, sizeof (buffer) / sizeof (*buffer), &err));
+  check (err == NULL);
+  check_streq ("ghij", buffer);
+  check (tr_sys_file_read_line (fd, buffer, sizeof (buffer) / sizeof (*buffer), &err));
+  check (err == NULL);
+  check_streq ("", buffer);
+  check (tr_sys_file_read_line (fd, buffer, sizeof (buffer) / sizeof (*buffer), &err));
+  check (err == NULL);
+  check_streq ("", buffer);
+  check (tr_sys_file_read_line (fd, buffer, 4, &err));
+  check (err == NULL);
+  check_streq ("klmn", buffer);
+  check (tr_sys_file_read_line (fd, buffer, sizeof (buffer) / sizeof (*buffer), &err));
+  check (err == NULL);
+  check_streq ("o", buffer);
+  check (!tr_sys_file_read_line (fd, buffer, sizeof (buffer) / sizeof (*buffer), &err));
+  check (err == NULL);
+  check_streq ("o", buffer); /* on EOF, buffer stays unchanged */
+
+  tr_sys_file_close (fd, NULL);
+
+  fd = tr_sys_file_open (path1, TR_SYS_FILE_READ | TR_SYS_FILE_WRITE | TR_SYS_FILE_TRUNCATE, 0, NULL);
+
+  check (tr_sys_file_write_line (fd, "p", &err));
+  check (err == NULL);
+  check (tr_sys_file_write_line (fd, "", &err));
+  check (err == NULL);
+  check (tr_sys_file_write_line (fd, "qr", &err));
+  check (err == NULL);
+  check (tr_sys_file_write_fmt (fd, "s%cu\r\n", &err, 't'));
+  check (err == NULL);
+  check (tr_sys_file_write_line (fd, "", &err));
+  check (err == NULL);
+  check (tr_sys_file_write_line (fd, "", &err));
+  check (err == NULL);
+  check (tr_sys_file_write_fmt (fd, "v%sy%d", &err, "wx", 2));
+  check (err == NULL);
+
+  tr_sys_file_seek (fd, 0, TR_SEEK_SET, NULL, NULL);
+
+  check (tr_sys_file_read_line (fd, buffer, sizeof (buffer) / sizeof (*buffer), &err));
+  check (err == NULL);
+  check_streq ("p", buffer);
+  check (tr_sys_file_read_line (fd, buffer, sizeof (buffer) / sizeof (*buffer), &err));
+  check (err == NULL);
+  check_streq ("", buffer);
+  check (tr_sys_file_read_line (fd, buffer, sizeof (buffer) / sizeof (*buffer), &err));
+  check (err == NULL);
+  check_streq ("qr", buffer);
+  check (tr_sys_file_read_line (fd, buffer, sizeof (buffer) / sizeof (*buffer), &err));
+  check (err == NULL);
+  check_streq ("stu", buffer);
+  check (tr_sys_file_read_line (fd, buffer, sizeof (buffer) / sizeof (*buffer), &err));
+  check (err == NULL);
+  check_streq ("", buffer);
+  check (tr_sys_file_read_line (fd, buffer, sizeof (buffer) / sizeof (*buffer), &err));
+  check (err == NULL);
+  check_streq ("", buffer);
+  check (tr_sys_file_read_line (fd, buffer, sizeof (buffer) / sizeof (*buffer), &err));
+  check (err == NULL);
+  check_streq ("vwxy2", buffer);
+  check (!tr_sys_file_read_line (fd, buffer, sizeof (buffer) / sizeof (*buffer), &err));
+  check (err == NULL);
+  check_streq ("vwxy2", buffer); /* on EOF, buffer stays unchanged */
+
+  tr_sys_file_close (fd, NULL);
+
+  tr_sys_path_remove (path1, NULL);
+
+  tr_free (path1);
+
+  tr_free (test_dir);
+  return 0;
+}
+
+static int
 test_dir_create (void)
 {
   char * const test_dir = create_test_dir (__FUNCTION__);
@@ -1309,6 +1408,7 @@ main (void)
       test_file_truncate,
       test_file_preallocate,
       test_file_map,
+      test_file_utilities,
       test_dir_create,
       test_dir_read
     };
diff --git a/libtransmission/file.c b/libtransmission/file.c
new file mode 100644
index 0000000..e0cfe42
--- /dev/null
+++ b/libtransmission/file.c
@@ -0,0 +1,123 @@
+/*
+ * This file Copyright (C) 2013-2014 Mnemosyne LLC
+ *
+ * It may be used under the GNU GPL versions 2 or 3
+ * or any future license endorsed by Mnemosyne LLC.
+ *
+ * $Id$
+ */
+
+#include <assert.h>
+#include <string.h> /* strlen () */
+
+#include "transmission.h"
+#include "file.h"
+#include "utils.h"
+
+bool
+tr_sys_file_read_line (tr_sys_file_t    handle,
+                       char           * buffer,
+                       size_t           buffer_size,
+                       tr_error      ** error)
+{
+  bool ret = false;
+  size_t offset = 0;
+  uint64_t bytes_read;
+
+  assert (handle != TR_BAD_SYS_FILE);
+  assert (buffer != NULL);
+  assert (buffer_size > 0);
+
+  while (buffer_size > 0)
+    {
+      size_t i;
+      bool found_eol = false;
+
+      ret = tr_sys_file_read (handle, buffer + offset, MIN(buffer_size, 1024u),
+                              &bytes_read, error);
+      if (!ret || (offset == 0 && bytes_read == 0))
+        {
+          ret = false;
+          break;
+        }
+
+      for (i = 0; i < bytes_read; ++i, ++offset, --buffer_size)
+        {
+          if (buffer[offset] == '\n')
+            {
+              found_eol = true;
+              break;
+            }
+        }
+
+      if (found_eol || buffer_size == 0 || bytes_read == 0)
+        {
+          const int64_t delta = -(int64_t) bytes_read + i + (found_eol ? 1 : 0);
+
+          if (delta != 0)
+            {
+              ret = tr_sys_file_seek (handle, delta, TR_SEEK_CUR, NULL, error);
+              if (!ret)
+                break;
+            }
+
+          if (offset > 0 && buffer[offset - 1] == '\r')
+            buffer[offset - 1] = '\0';
+          else
+            buffer[offset] = '\0';
+
+          break;
+        }
+    }
+
+  return ret;
+}
+
+bool
+tr_sys_file_write_line (tr_sys_file_t    handle,
+                        const char     * buffer,
+                        tr_error      ** error)
+{
+  bool ret;
+
+  assert (handle != TR_BAD_SYS_FILE);
+  assert (buffer != NULL);
+
+  ret = tr_sys_file_write (handle, buffer, strlen (buffer), NULL, error);
+
+  if (ret)
+    ret = tr_sys_file_write (handle, TR_NATIVE_EOL_STR, TR_NATIVE_EOL_STR_SIZE,
+                             NULL, error);
+
+  return ret;
+}
+
+bool
+tr_sys_file_write_fmt (tr_sys_file_t    handle,
+                       const char     * format,
+                       tr_error      ** error,
+                                        ...)
+{
+  bool ret = false;
+  char * buffer;
+  va_list args;
+
+  assert (handle != TR_BAD_SYS_FILE);
+  assert (format != NULL);
+
+  va_start (args, error);
+  buffer = tr_strdup_vprintf (format, args);
+  va_end (args);
+
+  if (buffer != NULL)
+    {
+      ret = tr_sys_file_write (handle, buffer, strlen (buffer), NULL, error);
+      tr_free (buffer);
+    }
+  else
+    {
+      tr_error_set_literal (error, 0, "Unable to format message.");
+    }
+
+  return ret;
+}
diff --git a/libtransmission/file.h b/libtransmission/file.h
index 562a4c1..e24c072 100644
--- a/libtransmission/file.h
+++ b/libtransmission/file.h
@@ -35,10 +35,16 @@ extern "C" {
  #define TR_BAD_SYS_FILE (-1)
  /** @brief Platform-specific directory descriptor type. */
  typedef void * tr_sys_dir_t;
+ /** @brief Platform-specific end-of-line sequence. */
+ #define TR_NATIVE_EOL_STR "\n"
+ /** @brief Platform-specific end-of-line sequence length. */
+ #define TR_NATIVE_EOL_STR_SIZE 1
 #else
  typedef HANDLE tr_sys_file_t;
  #define TR_BAD_SYS_FILE INVALID_HANDLE_VALUE
  typedef struct tr_sys_dir_win32 * tr_sys_dir_t;
+ #define TR_NATIVE_EOL_STR "\r\n"
+ #define TR_NATIVE_EOL_STR_SIZE 2
 #endif
 
 /** @brief Platform-specific invalid directory descriptor constant. */
@@ -509,6 +515,76 @@ bool            tr_sys_file_unmap           (const void         * address,
                                              uint64_t             size,
                                              tr_error          ** error);
 
+/* File-related wrappers (utility) */
+
+/**
+ * @brief Portability wrapper for `fgets ()`, removing EOL internally.
+ *
+ * Special care should be taken when reading from one of standard input streams
+ * (@ref tr_std_sys_file_t) since no UTF-8 conversion is currently being made.
+ *
+ * Reading from other streams (files, pipes) also leaves data untouched, so it
+ * should already be in UTF-8 encoding, or whichever else you expect.
+ *
+ * @param[in]  handle      Valid file descriptor.
+ * @param[out] buffer      Buffer to store read zero-terminated string to.
+ * @param[in]  buffer_size Buffer size in bytes, taking '\0' character into
+ *                         account.
+ * @param[out] error       Pointer to error object. Optional, pass `NULL` if you
+ *                         are not interested in error details.
+ *
+ * @return `True` on success, `false` otherwise (with `error` set accordingly).
+ *         Note that `false` will also be returned in case of end of file; if
+ *         you need to distinguish the two, check if `error` is `NULL`
+ *         afterwards.
+ */
+bool            tr_sys_file_read_line       (tr_sys_file_t        handle,
+                                             char               * buffer,
+                                             size_t               buffer_size,
+                                             tr_error          ** error);
+
+/**
+ * @brief Portability wrapper for `fputs ()`, appending EOL internally.
+ *
+ * Special care should be taken when writing to one of standard output streams
+ * (@ref tr_std_sys_file_t) since no UTF-8 conversion is currently being made.
+ *
+ * Writing to other streams (files, pipes) also leaves data untouched, so it
+ * should already be in UTF-8 encoding, or whichever else you expect.
+ *
+ * @param[in]  handle Valid file descriptor.
+ * @param[in]  buffer Zero-terminated string to write.
+ * @param[out] error  Pointer to error object. Optional, pass `NULL` if you are
+ *                    not interested in error details.
+ *
+ * @return `True` on success, `false` otherwise (with `error` set accordingly).
+ */
+bool            tr_sys_file_write_line      (tr_sys_file_t        handle,
+                                             const char         * buffer,
+                                             tr_error          ** error);
+
+/**
+ * @brief Portability wrapper for `fprintf ()`.
+ *
+ * Special care should be taken when writing to one of standard output streams
+ * (@ref tr_std_sys_file_t) since no UTF-8 conversion is currently being made.
+ *
+ * Writing to other streams (files, pipes) also leaves data untouched, so it
+ * should already be in UTF-8 encoding, or whichever else you expect.
+ *
+ * @param[in]  handle Valid file descriptor.
+ * @param[in]  format String format to write.
+ * @param[out] error  Pointer to error object. Optional, pass `NULL` if you are
+ *                    not interested in error details.
+ * @param[in]  ...    Format arguments.
+ *
+ * @return `True` on success, `false` otherwise (with `error` set accordingly).
+ */
+bool            tr_sys_file_write_fmt       (tr_sys_file_t        handle,
+                                             const char         * format,
+                                             tr_error          ** error,
+                                                                  ...);
+
 /* Directory-related wrappers */
 
 /**
