Commit Diff


commit - 927554b6d8fdd8c1e3783f422bf8ca53a16220d5
commit + 5a4f90240975e0a23735d9c1f935ff0215c4aaa0
blob - 4e026e1b6b1eb5b9c15191dc857f1480a90ff976
blob + 8303569b2df55dc82e79c7364b66c0a9bd3bd602
--- CMakeLists.txt
+++ CMakeLists.txt
@@ -5,7 +5,8 @@ include(CheckFunctionExists)
 project (unreliablefs
          DESCRIPTION "A FUSE-based fault injection filesystem.")
 
-set(UNRELIABLEFS_SRC unreliablefs.c
+set(UNRELIABLEFS_SRC conf.c
+		unreliablefs.c
 		unreliablefs_errinj.c
 		unreliablefs_ops.c)
 
@@ -56,7 +57,7 @@ add_subdirectory(tests)
 add_test(NAME pytest COMMAND pytest -c ${PROJECT_SOURCE_DIR}/tests/pytest.ini ${PROJECT_SOURCE_DIR}/tests/
          WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})
 
-set(MANUAL_PAGES "unreliablefs.1")
+set(MANUAL_PAGES "unreliablefs.1;unreliablefs.conf.5")
 add_custom_target(check DEPENDS check-mandoc)
 add_custom_target(check-mandoc DEPENDS ${MANUAL_PAGES})
 add_custom_command(TARGET check-mandoc
blob - e035e63cf465225688df2394fb4293fdff55ba09
blob + 7279944faea1fd360a270e4a5296980a9b5f2e45
--- README.md
+++ README.md
@@ -2,8 +2,15 @@
 
 [![Build Status](https://api.cirrus-ci.com/github/ligurio/unreliablefs.svg)](https://cirrus-ci.com/github/ligurio/unreliablefs)
 
-is a FUSE-based fault injection filesystem.
+is a FUSE-based fault injection filesystem that allows to change
+fault-injections in runtime.
 
+Supported fault injections are:
+
+- `errinj_noop` - replace file operation with no operation
+  (similar to [libeatmydata](https://github.com/stewartsmith/libeatmydata),
+  but applicable to any file operation).
+
 ### Building
 
 Prerequisites:
@@ -22,7 +29,14 @@ $ cmake -DCMAKE_BUILD_TYPE=Debug .. && make -j
 ### Using
 
 ```sh
-$ ./build/unreliablefs ~/Downloads/mnt/ -omodules=subdir,subdir=/tmp
-$ ls ~/Downloads/mnt/
-$ umount /tmp/unreliable
+$ mkdir /tmp/fs
+$ unreliablefs /tmp/fs -base_dir=/tmp -seed=1618680646
+$ cat << EOF > /tmp/fs/unreliablefs.conf
+[errinj_noop]
+op_regexp = .*
+path_regexp = .*
+probability = 30
+EOF
+$ ls -la
+$ umount /tmp/fs
 ```
blob - b758f3fa627bd4fdfc8673c01b1241a6a66afe25
blob + 449810c16ac5e06dcc96aa3e11aba1a6e4665eea
--- tests/test_unreliablefs.py
+++ tests/test_unreliablefs.py
@@ -43,8 +43,8 @@ def setup_unreliablefs(tmpdir):
     mnt_dir = str(tmpdir.mkdir('mnt'))
     src_dir = str(tmpdir.mkdir('src'))
 
-    modules = "-omodules=subdir,subdir={}".format(src_dir)
-    cmdline = base_cmdline + [ pjoin(basename, 'build/unreliablefs'), mnt_dir, modules]
+    options = "-basedir={}".format(src_dir)
+    cmdline = base_cmdline + [ pjoin(basename, 'build/unreliablefs'), mnt_dir, options ]
     mount_process = subprocess.Popen(cmdline)
     wait_for_mount(mount_process, mnt_dir)
 
blob - /dev/null
blob + b3c6dbcdf02d42abf6748168bd8a8ba57122e615 (mode 644)
--- /dev/null
+++ conf.c
@@ -0,0 +1,298 @@
+/* inih -- simple .INI file parser
+
+SPDX-License-Identifier: BSD-3-Clause
+
+Copyright (C) 2009-2020, Ben Hoyt
+
+inih is released under the New BSD license (see LICENSE.txt). Go to the project
+home page for more info:
+
+https://github.com/benhoyt/inih
+
+*/
+
+#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS)
+#define _CRT_SECURE_NO_WARNINGS
+#endif
+
+#include <stdio.h>
+#include <ctype.h>
+#include <string.h>
+
+#include "conf.h"
+
+#if !INI_USE_STACK
+#if INI_CUSTOM_ALLOCATOR
+#include <stddef.h>
+void* ini_malloc(size_t size);
+void ini_free(void* ptr);
+void* ini_realloc(void* ptr, size_t size);
+#else
+#include <stdlib.h>
+#define ini_malloc malloc
+#define ini_free free
+#define ini_realloc realloc
+#endif
+#endif
+
+#define MAX_SECTION 50
+#define MAX_NAME 50
+
+/* Used by ini_parse_string() to keep track of string parsing state. */
+typedef struct {
+    const char* ptr;
+    size_t num_left;
+} ini_parse_string_ctx;
+
+/* Strip whitespace chars off end of given string, in place. Return s. */
+static char* rstrip(char* s)
+{
+    char* p = s + strlen(s);
+    while (p > s && isspace((unsigned char)(*--p)))
+        *p = '\0';
+    return s;
+}
+
+/* Return pointer to first non-whitespace char in given string. */
+static char* lskip(const char* s)
+{
+    while (*s && isspace((unsigned char)(*s)))
+        s++;
+    return (char*)s;
+}
+
+/* Return pointer to first char (of chars) or inline comment in given string,
+   or pointer to NUL at end of string if neither found. Inline comment must
+   be prefixed by a whitespace character to register as a comment. */
+static char* find_chars_or_comment(const char* s, const char* chars)
+{
+#if INI_ALLOW_INLINE_COMMENTS
+    int was_space = 0;
+    while (*s && (!chars || !strchr(chars, *s)) &&
+           !(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) {
+        was_space = isspace((unsigned char)(*s));
+        s++;
+    }
+#else
+    while (*s && (!chars || !strchr(chars, *s))) {
+        s++;
+    }
+#endif
+    return (char*)s;
+}
+
+/* Similar to strncpy, but ensures dest (size bytes) is
+   NUL-terminated, and doesn't pad with NULs. */
+static char* strncpy0(char* dest, const char* src, size_t size)
+{
+    /* Could use strncpy internally, but it causes gcc warnings (see issue #91) */
+    size_t i;
+    for (i = 0; i < size - 1 && src[i]; i++)
+        dest[i] = src[i];
+    dest[i] = '\0';
+    return dest;
+}
+
+/* See documentation in header file. */
+int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,
+                     void* user)
+{
+    /* Uses a fair bit of stack (use heap instead if you need to) */
+#if INI_USE_STACK
+    char line[INI_MAX_LINE];
+    int max_line = INI_MAX_LINE;
+#else
+    char* line;
+    size_t max_line = INI_INITIAL_ALLOC;
+#endif
+#if INI_ALLOW_REALLOC && !INI_USE_STACK
+    char* new_line;
+    size_t offset;
+#endif
+    char section[MAX_SECTION] = "";
+    char prev_name[MAX_NAME] = "";
+
+    char* start;
+    char* end;
+    char* name;
+    char* value;
+    int lineno = 0;
+    int error = 0;
+
+#if !INI_USE_STACK
+    line = (char*)ini_malloc(INI_INITIAL_ALLOC);
+    if (!line) {
+        return -2;
+    }
+#endif
+
+#if INI_HANDLER_LINENO
+#define HANDLER(u, s, n, v) handler(u, s, n, v, lineno)
+#else
+#define HANDLER(u, s, n, v) handler(u, s, n, v)
+#endif
+
+    /* Scan through stream line by line */
+    while (reader(line, (int)max_line, stream) != NULL) {
+#if INI_ALLOW_REALLOC && !INI_USE_STACK
+        offset = strlen(line);
+        while (offset == max_line - 1 && line[offset - 1] != '\n') {
+            max_line *= 2;
+            if (max_line > INI_MAX_LINE)
+                max_line = INI_MAX_LINE;
+            new_line = ini_realloc(line, max_line);
+            if (!new_line) {
+                ini_free(line);
+                return -2;
+            }
+            line = new_line;
+            if (reader(line + offset, (int)(max_line - offset), stream) == NULL)
+                break;
+            if (max_line >= INI_MAX_LINE)
+                break;
+            offset += strlen(line + offset);
+        }
+#endif
+
+        lineno++;
+
+        start = line;
+#if INI_ALLOW_BOM
+        if (lineno == 1 && (unsigned char)start[0] == 0xEF &&
+                           (unsigned char)start[1] == 0xBB &&
+                           (unsigned char)start[2] == 0xBF) {
+            start += 3;
+        }
+#endif
+        start = lskip(rstrip(start));
+
+        if (strchr(INI_START_COMMENT_PREFIXES, *start)) {
+            /* Start-of-line comment */
+        }
+#if INI_ALLOW_MULTILINE
+        else if (*prev_name && *start && start > line) {
+            /* Non-blank line with leading whitespace, treat as continuation
+               of previous name's value (as per Python configparser). */
+            if (!HANDLER(user, section, prev_name, start) && !error)
+                error = lineno;
+        }
+#endif
+        else if (*start == '[') {
+            /* A "[section]" line */
+            end = find_chars_or_comment(start + 1, "]");
+            if (*end == ']') {
+                *end = '\0';
+                strncpy0(section, start + 1, sizeof(section));
+                *prev_name = '\0';
+#if INI_CALL_HANDLER_ON_NEW_SECTION
+                if (!HANDLER(user, section, NULL, NULL) && !error)
+                    error = lineno;
+#endif
+            }
+            else if (!error) {
+                /* No ']' found on section line */
+                error = lineno;
+            }
+        }
+        else if (*start) {
+            /* Not a comment, must be a name[=:]value pair */
+            end = find_chars_or_comment(start, "=:");
+            if (*end == '=' || *end == ':') {
+                *end = '\0';
+                name = rstrip(start);
+                value = end + 1;
+#if INI_ALLOW_INLINE_COMMENTS
+                end = find_chars_or_comment(value, NULL);
+                if (*end)
+                    *end = '\0';
+#endif
+                value = lskip(value);
+                rstrip(value);
+
+                /* Valid name[=:]value pair found, call handler */
+                strncpy0(prev_name, name, sizeof(prev_name));
+                if (!HANDLER(user, section, name, value) && !error)
+                    error = lineno;
+            }
+            else if (!error) {
+                /* No '=' or ':' found on name[=:]value line */
+#if INI_ALLOW_NO_VALUE
+                *end = '\0';
+                name = rstrip(start);
+                if (!HANDLER(user, section, name, NULL) && !error)
+                    error = lineno;
+#else
+                error = lineno;
+#endif
+            }
+        }
+
+#if INI_STOP_ON_FIRST_ERROR
+        if (error)
+            break;
+#endif
+    }
+
+#if !INI_USE_STACK
+    ini_free(line);
+#endif
+
+    return error;
+}
+
+/* See documentation in header file. */
+int ini_parse_file(FILE* file, ini_handler handler, void* user)
+{
+    return ini_parse_stream((ini_reader)fgets, file, handler, user);
+}
+
+/* See documentation in header file. */
+int ini_parse(const char* filename, ini_handler handler, void* user)
+{
+    FILE* file;
+    int error;
+
+    file = fopen(filename, "r");
+    if (!file)
+        return -1;
+    error = ini_parse_file(file, handler, user);
+    fclose(file);
+    return error;
+}
+
+/* An ini_reader function to read the next line from a string buffer. This
+   is the fgets() equivalent used by ini_parse_string(). */
+static char* ini_reader_string(char* str, int num, void* stream) {
+    ini_parse_string_ctx* ctx = (ini_parse_string_ctx*)stream;
+    const char* ctx_ptr = ctx->ptr;
+    size_t ctx_num_left = ctx->num_left;
+    char* strp = str;
+    char c;
+
+    if (ctx_num_left == 0 || num < 2)
+        return NULL;
+
+    while (num > 1 && ctx_num_left != 0) {
+        c = *ctx_ptr++;
+        ctx_num_left--;
+        *strp++ = c;
+        if (c == '\n')
+            break;
+        num--;
+    }
+
+    *strp = '\0';
+    ctx->ptr = ctx_ptr;
+    ctx->num_left = ctx_num_left;
+    return str;
+}
+
+/* See documentation in header file. */
+int ini_parse_string(const char* string, ini_handler handler, void* user) {
+    ini_parse_string_ctx ctx;
+
+    ctx.ptr = string;
+    ctx.num_left = strlen(string);
+    return ini_parse_stream((ini_reader)ini_reader_string, &ctx, handler,
+                            user);
+}
blob - cc0826a6abbfa403b1d8518fe2c2ec157978d90c
blob + 51fdc822a71ffae9f3a772beaa456a669f63adc8
--- unreliablefs.1
+++ unreliablefs.1
@@ -9,13 +9,15 @@
 .Sh SYNOPSIS
 .Nm
 mountpoint
-.Op Fl h
+.Op Fl basedir Ar path
+.Op Fl seed Ar number
+.Op Fl hvdf
 .Sh DESCRIPTION
 The
 .Nm
 is a filesystem that allows to inject errors on file operations.
-It works as pass-through filesystem and redirects file operations to a file
-objects on real filesystem.
+Without configuration it works as pass-through filesystem and redirects file
+operations to a file objects on a real filesystem.
 .Pp
 .Nm
 uses Filesystem in Userspace (FUSE) that allows easy setup without requiring a
@@ -25,6 +27,26 @@ available unchanged.
 To mount filesystem it is required to specify mountpoint and after mount it
 will contain the same file tree as a root filesystem.
 .Pp
+The options are as follows:
+.Bl -tag -width Ds
+.It Fl basedir Ar path
+Specify path to a directory that should be mount.
+.It Fl seed Ar number
+Specify a seed.
+.It Fl f
+Do not daemonize.
+.It Fl d
+Do not daemonize.
+If this option is specified,
+.Nm
+will run in the foreground and log to
+.Em stderr .
+.It Fl v
+Show version.
+.It Fl h
+Show usage.
+.El
+.Pp
 Supported file operations are:
 .Xr access 2 ,
 .Xr chmod 2 ,
@@ -69,16 +91,24 @@ Following functions are unsupported on OpenBSD:
 .Sh EXAMPLES
 .Bd -literal
 
-$ mkdir /tmp/unreliable
-$ unreliablefs /tmp/unreliable
-$ umount /tmp/unreliable
+$ mkdir /tmp/fs
+$ unreliablefs /tmp/fs -basedir=/tmp -seed=1618680646
+$ cat << EOF > /tmp/fs/unreliablefs.conf
+[errinj_noop]
+op_regexp = .*
+path_regexp = .*
+probability = 30
+EOF
+$ ls -la
+$ umount /tmp/fs
 
 .Ed
 .Sh SEE ALSO
 .Xr fusermount 1 ,
 .Xr errno 2 ,
 .Xr fuse 4 ,
-.Xr re_format 7 ,
+.Xr fuse 8 ,
+.Xr unreliablefs.conf 5 ,
 .Xr mount.fuse 8
 .Sh AUTHORS
 .An -nosplit
blob - /dev/null
blob + 78015d141e6878398e001790f28c480d66679edd (mode 644)
--- /dev/null
+++ conf.h
@@ -0,0 +1,157 @@
+/* inih -- simple .INI file parser
+
+SPDX-License-Identifier: BSD-3-Clause
+
+Copyright (C) 2009-2020, Ben Hoyt
+
+inih is released under the New BSD license (see LICENSE.txt). Go to the project
+home page for more info:
+
+https://github.com/benhoyt/inih
+
+*/
+
+#ifndef INI_H
+#define INI_H
+
+/* Make this header file easier to include in C++ code */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdio.h>
+
+/* Nonzero if ini_handler callback should accept lineno parameter. */
+#ifndef INI_HANDLER_LINENO
+#define INI_HANDLER_LINENO 0
+#endif
+
+/* Typedef for prototype of handler function. */
+#if INI_HANDLER_LINENO
+typedef int (*ini_handler)(void* user, const char* section,
+                           const char* name, const char* value,
+                           int lineno);
+#else
+typedef int (*ini_handler)(void* user, const char* section,
+                           const char* name, const char* value);
+#endif
+
+/* Typedef for prototype of fgets-style reader function. */
+typedef char* (*ini_reader)(char* str, int num, void* stream);
+
+/* Parse given INI-style file. May have [section]s, name=value pairs
+   (whitespace stripped), and comments starting with ';' (semicolon). Section
+   is "" if name=value pair parsed before any section heading. name:value
+   pairs are also supported as a concession to Python's configparser.
+
+   For each name=value pair parsed, call handler function with given user
+   pointer as well as section, name, and value (data only valid for duration
+   of handler call). Handler should return nonzero on success, zero on error.
+
+   Returns 0 on success, line number of first error on parse error (doesn't
+   stop on first error), -1 on file open error, or -2 on memory allocation
+   error (only when INI_USE_STACK is zero).
+*/
+int ini_parse(const char* filename, ini_handler handler, void* user);
+
+/* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't
+   close the file when it's finished -- the caller must do that. */
+int ini_parse_file(FILE* file, ini_handler handler, void* user);
+
+/* Same as ini_parse(), but takes an ini_reader function pointer instead of
+   filename. Used for implementing custom or string-based I/O (see also
+   ini_parse_string). */
+int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,
+                     void* user);
+
+/* Same as ini_parse(), but takes a zero-terminated string with the INI data
+instead of a file. Useful for parsing INI data from a network socket or
+already in memory. */
+int ini_parse_string(const char* string, ini_handler handler, void* user);
+
+/* Nonzero to allow multi-line value parsing, in the style of Python's
+   configparser. If allowed, ini_parse() will call the handler with the same
+   name for each subsequent line parsed. */
+#ifndef INI_ALLOW_MULTILINE
+#define INI_ALLOW_MULTILINE 1
+#endif
+
+/* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of
+   the file. See https://github.com/benhoyt/inih/issues/21 */
+#ifndef INI_ALLOW_BOM
+#define INI_ALLOW_BOM 1
+#endif
+
+/* Chars that begin a start-of-line comment. Per Python configparser, allow
+   both ; and # comments at the start of a line by default. */
+#ifndef INI_START_COMMENT_PREFIXES
+#define INI_START_COMMENT_PREFIXES ";#"
+#endif
+
+/* Nonzero to allow inline comments (with valid inline comment characters
+   specified by INI_INLINE_COMMENT_PREFIXES). Set to 0 to turn off and match
+   Python 3.2+ configparser behaviour. */
+#ifndef INI_ALLOW_INLINE_COMMENTS
+#define INI_ALLOW_INLINE_COMMENTS 1
+#endif
+#ifndef INI_INLINE_COMMENT_PREFIXES
+#define INI_INLINE_COMMENT_PREFIXES ";"
+#endif
+
+/* Nonzero to use stack for line buffer, zero to use heap (malloc/free). */
+#ifndef INI_USE_STACK
+#define INI_USE_STACK 1
+#endif
+
+/* Maximum line length for any line in INI file (stack or heap). Note that
+   this must be 3 more than the longest line (due to '\r', '\n', and '\0'). */
+#ifndef INI_MAX_LINE
+#define INI_MAX_LINE 200
+#endif
+
+/* Nonzero to allow heap line buffer to grow via realloc(), zero for a
+   fixed-size buffer of INI_MAX_LINE bytes. Only applies if INI_USE_STACK is
+   zero. */
+#ifndef INI_ALLOW_REALLOC
+#define INI_ALLOW_REALLOC 0
+#endif
+
+/* Initial size in bytes for heap line buffer. Only applies if INI_USE_STACK
+   is zero. */
+#ifndef INI_INITIAL_ALLOC
+#define INI_INITIAL_ALLOC 200
+#endif
+
+/* Stop parsing on first error (default is to keep parsing). */
+#ifndef INI_STOP_ON_FIRST_ERROR
+#define INI_STOP_ON_FIRST_ERROR 0
+#endif
+
+/* Nonzero to call the handler at the start of each new section (with
+   name and value NULL). Default is to only call the handler on
+   each name=value pair. */
+#ifndef INI_CALL_HANDLER_ON_NEW_SECTION
+#define INI_CALL_HANDLER_ON_NEW_SECTION 0
+#endif
+
+/* Nonzero to allow a name without a value (no '=' or ':' on the line) and
+   call the handler with value NULL in this case. Default is to treat
+   no-value lines as an error. */
+#ifndef INI_ALLOW_NO_VALUE
+#define INI_ALLOW_NO_VALUE 0
+#endif
+
+/* Nonzero to use custom ini_malloc, ini_free, and ini_realloc memory
+   allocation functions (INI_USE_STACK must also be 0). These functions must
+   have the same signatures as malloc/free/realloc and behave in a similar
+   way. ini_realloc is only needed if INI_ALLOW_REALLOC is set. */
+#ifndef INI_CUSTOM_ALLOCATOR
+#define INI_CUSTOM_ALLOCATOR 0
+#endif
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* INI_H */
blob - 8d5d93bb0ed6d1e0e4293a0a3ed38987a23ada8f
blob + c64750ff018d6a77dfceaf9bf7b20d0a9f53f40b
--- unreliablefs.c
+++ unreliablefs.c
@@ -1,12 +1,21 @@
 #define FUSE_USE_VERSION 29
 
 #include <errno.h>
+#include <stddef.h>
 #include <stdlib.h>
 #include <stdio.h>
 #include <string.h>
+#include <limits.h>
+#include <stdlib.h>
 
+#include <fuse.h>
+
 #include "unreliablefs_ops.h"
+#include "unreliablefs.h"
 
+extern struct err_inj_q *config_init(const char* conf_path);
+extern void config_delete(struct err_inj_q *config);
+
 static struct fuse_operations unreliable_ops = {
     .getattr     = unreliable_getattr,
     .readlink    = unreliable_readlink,
@@ -60,30 +69,120 @@ static struct fuse_operations unreliable_ops = {
 #endif /* HAVE_UTIMENSAT */
 };
 
+enum {
+     KEY_HELP,
+     KEY_VERSION,
+     KEY_DEBUG,
+};
+
+#define UNRELIABLEFS_OPT(t, p, v) { t, offsetof(struct unreliablefs_config, p), v }
+#define UNRELIABLEFS_VERSION "0.1"
+
+static struct fuse_opt unreliablefs_opts[] = {
+    UNRELIABLEFS_OPT("-seed=%u",           seed, 0),
+    UNRELIABLEFS_OPT("-basedir=%s",        basedir, 0),
+
+    FUSE_OPT_KEY("-d",             KEY_DEBUG),
+    FUSE_OPT_KEY("-V",             KEY_VERSION),
+    FUSE_OPT_KEY("-v",             KEY_VERSION),
+    FUSE_OPT_KEY("--version",      KEY_VERSION),
+    FUSE_OPT_KEY("-h",             KEY_HELP),
+    FUSE_OPT_KEY("--help",         KEY_HELP),
+    FUSE_OPT_KEY("subdir",         FUSE_OPT_KEY_DISCARD),
+    FUSE_OPT_KEY("modules=",       FUSE_OPT_KEY_DISCARD),
+    FUSE_OPT_END
+};
+
+static int unreliablefs_opt_proc(void *data, const char *arg, int key, struct fuse_args *outargs)
+{
+    switch (key) {
+    case KEY_HELP:
+        fprintf(stderr,
+            "usage: unreliablefs mountpoint [options]\n\n"
+            "general options:\n"
+            "    -h   --help            print help\n"
+            "    -v   --version         print version\n"
+            "    -d                     enable debug output (implies -f)\n"
+            "    -f                     foreground operation\n\n"
+            "unreliablefs options:\n"
+            "    -seed=NUM              random seed\n"
+            "    -basedir=STRING        directory to mount\n\n");
+        exit(1);
+
+    case KEY_VERSION:
+        fprintf(stderr, "unreliablefs version %s\n", UNRELIABLEFS_VERSION);
+        fuse_opt_add_arg(outargs, "--version");
+        fuse_main(outargs->argc, outargs->argv, &unreliable_ops, NULL);
+        exit(1);
+    }
+    return 1;
+}
+
+int is_dir(const char *path) {
+    struct stat statbuf;
+    if (stat(path, &statbuf) != 0) {
+        return 0;
+    }
+
+    return S_ISDIR(statbuf.st_mode);
+}
+
 int main(int argc, char *argv[])
 {
-#if defined(DEBUG)
-    int fuse_argc = 3;
-    char *fuse_argv[fuse_argc];
+    struct fuse_args args = FUSE_ARGS_INIT(argc, argv);
+    memset(&conf, 0, sizeof(conf));
+    conf.seed = time(0);
+    conf.basedir = "/";
+    fuse_opt_parse(&args, &conf, unreliablefs_opts, unreliablefs_opt_proc);
+    srand(conf.seed);
+    fprintf(stdout, "random seed = %d\n", conf.seed);
 
-    if (argc != 2) {
-	fprintf(stderr, "Usage: %s MOUNTPOINT\n", argv[0]);
-	return EXIT_FAILURE;
+    if (is_dir(conf.basedir) == 0) {
+       fprintf(stderr, "basedir ('%s') is not a directory\n", conf.basedir);
+       fuse_opt_free_args(&args);
+       return EXIT_FAILURE;
     }
+    char subdir_option[PATH_MAX];
+    sprintf(subdir_option, "-omodules=subdir,subdir=%s", conf.basedir);
+    fuse_opt_add_arg(&args, subdir_option);
+    /* build config_path */
+    char *real_path = realpath(conf.basedir, NULL);
+    if (!real_path) {
+        perror("realpath");
+        fuse_opt_free_args(&args);
+        return EXIT_FAILURE;
+    }
+    conf.basedir = real_path;
+    size_t sz = strlen(DEFAULT_CONF_NAME) + strlen(conf.basedir) + 2;
+    conf.config_path = malloc(sz);
+    if (!conf.config_path) {
+        perror("malloc");
+        fuse_opt_free_args(&args);
+        return EXIT_FAILURE;
+    }
+    /* read configuration file on start */
+    snprintf(conf.config_path, sz, "%s/%s", conf.basedir, DEFAULT_CONF_NAME);
+    conf.errors = config_init(conf.config_path);
+    if (!conf.errors) {
+        fprintf(stdout, "error injections are not configured!\n");
+    }
+    if (pthread_mutex_init(&conf.mutex, NULL) != 0) {
+        fuse_opt_free_args(&args);
+        perror("pthread_mutex_init");
+        return EXIT_FAILURE;
+    }
 
-    fuse_argv[0] = argv[0];
-    fuse_argv[1] = argv[1];
-    #if FUSE_USE_VERSION < 30
-	fuse_argv[2] = "-ononempty,suid,dev,allow_other,default_permissions";
-    #else
-	fuse_argv[2] = "-osuid,dev,allow_other,default_permissions";
-    #endif
-    fuse_argv[3] = NULL;
+    fprintf(stdout, "starting FUSE filesystem unreliablefs\n");
+    int ret = fuse_main(args.argc, args.argv, &unreliable_ops, NULL);
 
-    fprintf(stdout, "Starting FUSE filesystem\n");
-    return fuse_main(fuse_argc, fuse_argv, &unreliable_ops, NULL);
-#else
-    fprintf(stdout, "Starting FUSE filesystem\n");
-    return fuse_main(argc, argv, &unreliable_ops, NULL);
-#endif /* DEBUG */
+    /* cleanup */
+    fuse_opt_free_args(&args);
+    config_delete(conf.errors);
+    if (conf.config_path)
+        free(conf.config_path);
+    if (!ret) {
+        fprintf(stdout, "random seed = %d\n", conf.seed);
+    }
+
+    return ret;
 }
blob - 81ead53e35061f31ac181a6bc17c9241bca107f8
blob + ca7bc1fd4dd53ea18989ab28d6cbd9f2b3830f8d
--- unreliablefs_errinj.c
+++ unreliablefs_errinj.c
@@ -1,16 +1,17 @@
 #define FUSE_USE_VERSION 29
 
-#include <errno.h>
+#include <fuse.h>
+#include <libgen.h> /* basename() and dirname() */
+#include <regex.h>
 #include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
 
-#if !defined(__OpenBSD__) && !defined(__FreeBSD__) && !defined(__APPLE__)
-#define MAX_ERR EXFULL
-#else
-#define MAX_ERR ELAST
-#endif /* __OpenBSD__ */
-#define MIN_ERR E2BIG
+#include <sys/queue.h>
 
-#define PROBABILITY 5
+#include "conf.h"
+#include "unreliablefs.h"
+#include "unreliablefs_errinj.h"
 
 static int rand_range(int min_n, int max_n)
 {
@@ -19,15 +20,157 @@ static int rand_range(int min_n, int max_n)
 
 int error_inject(const char* path, char* operation)
 {
-#if defined(DEBUG)
-    int err_no = 0;
-    int p = rand_range(0, 100);
-    if ((p >= 0) && (p <= PROBABILITY)) {
-        err_no = rand_range(MIN_ERR, MAX_ERR);
+    int rc = -0;
+    struct errinj_conf *err;
+    /* read configuration file on change */
+    pthread_mutex_lock(&conf.mutex);
+    if (strcmp(path, conf.config_path) == 0) {
+        config_delete(conf.errors);
+        conf.errors = config_init(path);
     }
+    if (!conf.errors) {
+        goto cleanup;
+    }
 
-    return -err_no;
-#else
-    return 0;
-#endif /* DEBUG */
+    /* apply error injections defined in configuration one by one */
+    TAILQ_FOREACH(err, conf.errors, entries) {
+	if (is_regex_matched(err->path_regexp, path) != 0) {
+	    fprintf(stderr, "errinj '%s' skipped: path_regexp (%s) is not matched\n",
+                            errinj_name[err->type], err->path_regexp);
+	    continue;
+	}
+	if (is_regex_matched(err->op_regexp, operation) != 0) {
+	    fprintf(stderr, "errinj '%s' skipped: op_regexp (%s) is not matched\n",
+                            errinj_name[err->type], err->op_regexp);
+	    continue;
+	}
+        int p = rand_range(MIN_PROBABLITY, MAX_PROBABLITY);
+        if (!((p >= 0) && (p <= err->probability))) {
+            fprintf(stderr, "errinj '%s' skipped: probability (%d) is not matched\n",
+                            errinj_name[err->type], err->probability);
+            continue;
+        }
+	switch (err->type) {
+        case ERRINJ_NOOP:
+            fprintf(stderr, "%s triggered on operation '%s', %s\n",
+                            errinj_name[err->type], operation, path);
+            rc = 0;
+            break;
+        }
+    }
+
+cleanup:
+    pthread_mutex_unlock(&conf.mutex);
+    return rc;
 }
+
+static errinj_type errinj_type_by_name(const char *name)
+{
+    int idx = 0;
+    int n_elem = sizeof(errinj_name)/sizeof(errinj_name[0]);
+    for (int i = 0; i < n_elem; i++) {
+        if (strcmp(errinj_name[i], name) == 0)
+            idx = i;
+    }
+
+    return (errinj_type)idx;
+}
+
+struct err_inj_q *config_init(const char* conf_path) {
+    fprintf(stdout, "read configuration %s\n", conf_path);
+    struct err_inj_q *errors = calloc(1, sizeof(struct err_inj_q));
+    if (!errors) {
+        perror("calloc");
+        return NULL;
+    }
+    TAILQ_INIT(errors);  /* initialize queue */
+    if (access(conf_path, F_OK ) == 0) {
+        if (ini_parse(conf_path, conf_option_handler, errors) < 0) {
+            fprintf(stderr, "can't load '%s'\n", conf_path);
+            return NULL;
+        }
+    }
+
+    return errors;
+}
+
+void config_delete(struct err_inj_q *errors) {
+    if (!errors) {
+        return;
+    }
+    errinj_conf *err;
+    /* delete configuration of error injections */
+    while ((err= TAILQ_FIRST(errors))) {
+        if (err->path_regexp)
+	    free((char*)err->path_regexp);
+        if (err->op_regexp)
+	    free((char*)err->op_regexp);
+	TAILQ_REMOVE(errors, err, entries);
+	free(err);
+    }
+    free(errors);
+}
+
+int conf_option_handler(void* cfg, const char* section,
+                        const char* key, const char* value)
+{
+    errinj_conf *err = NULL;
+    errinj_type cur_type = errinj_type_by_name(section);
+    errinj_conf *np;
+    int is_errinj_found = 0;
+    TAILQ_FOREACH(np, (struct err_inj_q *)cfg, entries) {
+        if (np->type == cur_type) {
+            err = np;
+            is_errinj_found = 1;
+            break;
+        }
+    }
+    if (!err) {
+        if ((err = calloc(1, sizeof(struct errinj_conf))) == NULL) {
+            perror("calloc");
+            return -1;
+        }
+        err->type = cur_type;
+    }
+
+    if (is_errinj_found != 1) {
+        TAILQ_INSERT_TAIL((struct err_inj_q *)cfg, err, entries);
+        fprintf(stdout, "enabled error injection %s\n", section);
+    }
+    fprintf(stdout, "[%s] %s = %s\n", section, key, value);
+    if (strcmp(key, "path_regexp") == 0) {
+        if (err->path_regexp)
+            free(err->path_regexp);
+        err->path_regexp = strdup(value);
+    } else if (strcmp(key, "op_regexp") == 0) {
+        if (err->op_regexp)
+            free(err->op_regexp);
+        err->op_regexp = strdup(value);
+    } else if (strcmp(key, "probability") == 0) {
+        err->probability = atoi(value);
+    } else {
+        fprintf(stderr, "unknown option '%s' in configuration file\n", key);
+        return 0;
+    }
+
+    return 1;
+}
+
+int is_regex_matched(const char *regex, const char *string) {
+    if (!regex || !string)
+        return 0;
+    regex_t reg;
+    regmatch_t match[1];
+    int rc = regcomp(&reg, regex, REG_ICASE | REG_EXTENDED);
+    if (rc != 0) {
+        perror("regcomp");
+        regfree(&reg);
+        return rc;
+    }
+    rc = regexec(&reg, string, 1, match, 0);
+    if (rc != 0) {
+        perror("regexec");
+    }
+    regfree(&reg);
+    return rc;
+}
blob - 0fcd5ef17344fb7ceb8744475e73e40ea1b54457
blob + 8f3a081777a733e4d6dd713f3d88eb1e31f5cc02
--- unreliablefs_errinj.h
+++ unreliablefs_errinj.h
@@ -1,6 +1,48 @@
 #ifndef ERRINJ_HH
 #define ERRINJ_HH
 
+#include <signal.h>
+
+#define MAX_ERRINJ_NAME_LENGTH 20
+
+#if !defined(__OpenBSD__) && !defined(__FreeBSD__) && !defined(__APPLE__)
+#define MAX_ERRNO EXFULL
+#else
+#define MAX_ERRNO ELAST
+#endif /* __OpenBSD__ */
+#define MIN_ERRNO E2BIG
+
+#define MIN_PROBABLITY 0
+#define MAX_PROBABLITY 100
+
 int error_inject(const char* path, char* operation);
+struct err_inj_q *config_init(const char* conf_path);
+void config_delete(struct err_inj_q *config);
+int conf_option_handler(void* cfg, const char* section,
+                        const char* name, const char* value);
+int is_regex_matched(const char *regex, const char *string);
 
+const char *errinj_name[] =
+{
+    "errinj_noop",
+};
+
+typedef enum {
+    ERRINJ_NOOP,
+} errinj_type;
+
+typedef struct errinj_conf errinj_conf;
+
+struct errinj_conf {
+    char *err_injection_name;
+    char *op_regexp;
+    char *path_regexp;
+    int probability;
+    errinj_type type;
+
+    TAILQ_ENTRY(errinj_conf) entries;
+};
+
+TAILQ_HEAD(err_inj_q, errinj_conf);
+
 #endif /* ERRINJ_HH */
blob - d69f69870fab9e88a902ea7c428d8ebf01f3543e
blob + d9e2c0ff05d0733be8dd43c85f57154ef9ae5f96
--- unreliablefs_ops.c
+++ unreliablefs_ops.c
@@ -16,9 +16,10 @@
 #define _XOPEN_SOURCE 700
 #endif
 
-#include "unreliablefs_errinj.h"
 #include "unreliablefs_ops.h"
 
+extern int error_inject(const char* path, char* operation);
+
 int unreliable_lstat(const char *path, struct stat *buf)
 {
     int ret = error_inject(path, "lstat");
blob - /dev/null
blob + a27f32bbc1825578e040a54c9326b06fea056b87 (mode 644)
--- /dev/null
+++ unreliablefs.conf.5
@@ -0,0 +1,73 @@
+.\" Copyright (c) 2021 Sergey Bronnikov
+.\"
+.Dd $Mdocdate: April 15 2021 $
+.Dt UNRELIABLEFS.CONF 5
+.Os
+.Sh NAME
+.Nm unreliablefs.conf
+.Nd format of the configuration file used by
+.Xr unreliablefs 1
+.Sh DESCRIPTION
+The configuration file format is quite simple.
+Sections are delimited by square brackets:
+.Pp
+.Rs
+[Section]
+.Re
+.Pp
+And options within brackets sections are simple key value pairs:
+.Pp
+.Rs
+Option = Value
+.Re
+.Sh OPTIONS
+Per-fault-injection customizable variables are specified within sections
+with section names matching the fault-injection name.
+.Pp
+Supported fault injections are:
+.Bl -tag -width Ds
+.It Cm errinj_noop
+File operation replaced with no-op.
+.El
+.Pp
+The options are:
+.Bl -tag -width Ds
+.It Cm op_regexp
+Sets the regular expression that matches file operation for what fault injection is applicable.
+Option uses format of regular expressions described in
+.Xr re_format 7 .
+POSIX Extended Regular Expression syntax is supported and regular expressions do not differentiate case.
+.It Cm path_regexp
+Sets the regular expression that matches paths where fault injection is applicable.
+Option uses format of regular expressions described in
+.Xr re_format 7 .
+POSIX Extended Regular Expression syntax is supported and regular expressions do not differentiate case.
+.It Cm probability
+Sets the probability in percents.
+Probability equal to 0 means that error injection will never happen.
+Probability equal to 100 means that error injection will happen on each file operation.
+.El
+.Sh EXAMPLES
+.Bd -literal
+
+[errinj_noop]
+path_regexp = .*
+op_regexp = .*
+probability = 70
+
+.Ed
+.Sh SEE ALSO
+.Xr unreliablefs 1 ,
+.Xr errno 2 ,
+.Xr syslog 3 ,
+.Xr re_format 7
+.Sh AUTHORS
+.An -nosplit
+The
+.Xr unreliablefs 1
+utility was written by
+.An Sergey
+.An Bronnikov .
+.\" .Sh HISTORY
+.\" .Sh BUGS
+.\" .Sh CAVEATS
blob - /dev/null
blob + 90b3eb1b21a0708a0631e25873275fe3ecdd591f (mode 644)
--- /dev/null
+++ unreliablefs.h
@@ -0,0 +1,20 @@
+#ifndef UNRELIABLEFS_HH
+#define UNRELIABLEFS_HH
+
+#include <limits.h> /* PATH_MAX */
+#include <pthread.h>
+
+#define DEFAULT_CONF_NAME "unreliablefs.conf"
+
+typedef struct unreliablefs_config {
+     struct err_inj_q *errors;
+     char             *basedir;
+     char             *config_path;
+     unsigned int      seed;
+     unsigned int      debug;
+     pthread_mutex_t   mutex;
+} unreliablefs_config;
+
+struct unreliablefs_config conf;
+
+#endif /* UNRELIABLEFS_HH */