fileio: add new helper call read_line() as bounded getline() replacement
authorLennart Poettering <lennart@poettering.net>
Fri, 22 Sep 2017 15:55:53 +0000 (17:55 +0200)
committerLennart Poettering <lennart@poettering.net>
Fri, 22 Sep 2017 18:34:15 +0000 (20:34 +0200)
read_line() is much like getline(), and returns a line read from a
FILE*, of arbitrary sizes. In contrast to gets() it will grow the buffer
dynamically, and in contrast to getline() it will place a user-specified
boundary on the line.

src/basic/fileio.c
src/basic/fileio.h
src/test/test-fileio.c

index 6220d1c..fc8776d 100644 (file)
@@ -1526,3 +1526,80 @@ int mkdtemp_malloc(const char *template, char **ret) {
         *ret = p;
         return 0;
 }
+
+int read_line(FILE *f, size_t limit, char **ret) {
+        _cleanup_free_ char *buffer = NULL;
+        size_t n = 0, allocated = 0, count = 0;
+        int r;
+
+        assert(f);
+
+        /* Something like a bounded version of getline().
+         *
+         * Considers EOF, \n and \0 end of line delimiters, and does not include these delimiters in the string
+         * returned.
+         *
+         * Returns the number of bytes read from the files (i.e. including delimiters — this hence usually differs from
+         * the number of characters in the returned string). When EOF is hit, 0 is returned.
+         *
+         * The input parameter limit is the maximum numbers of characters in the returned string, i.e. excluding
+         * delimiters. If the limit is hit we fail and return -ENOBUFS.
+         *
+         * If a line shall be skipped ret may be initialized as NULL. */
+
+        if (ret) {
+                if (!GREEDY_REALLOC(buffer, allocated, 1))
+                        return -ENOMEM;
+        }
+
+        flockfile(f);
+
+        for (;;) {
+                int c;
+
+                if (n >= limit) {
+                        funlockfile(f);
+                        return -ENOBUFS;
+                }
+
+                errno = 0;
+                c = fgetc_unlocked(f);
+                if (c == EOF) {
+                        /* if we read an error, and have no data to return, then propagate the error */
+                        if (ferror_unlocked(f) && n == 0) {
+                                r = errno > 0 ? -errno : -EIO;
+                                funlockfile(f);
+                                return r;
+                        }
+
+                        break;
+                }
+
+                count++;
+
+                if (IN_SET(c, '\n', 0)) /* Reached a delimiter */
+                        break;
+
+                if (ret) {
+                        if (!GREEDY_REALLOC(buffer, allocated, n + 2)) {
+                                funlockfile(f);
+                                return -ENOMEM;
+                        }
+
+                        buffer[n] = (char) c;
+                }
+
+                n++;
+        }
+
+        funlockfile(f);
+
+        if (ret) {
+                buffer[n] = 0;
+
+                *ret = buffer;
+                buffer = NULL;
+        }
+
+        return (int) count;
+}
index b423874..9422bc7 100644 (file)
@@ -101,3 +101,5 @@ int link_tmpfile(int fd, const char *path, const char *target);
 int read_nul_string(FILE *f, char **ret);
 
 int mkdtemp_malloc(const char *template, char **ret);
+
+int read_line(FILE *f, size_t limit, char **ret);
index b1d688c..375b7a8 100644 (file)
@@ -663,6 +663,49 @@ static void test_tempfn(void) {
         free(ret);
 }
 
+static void test_read_line(void) {
+        _cleanup_fclose_ FILE *f = NULL;
+        _cleanup_free_ char *line = NULL;
+
+        char buffer[] =
+                "Some test data\n"
+                "With newlines, and a NUL byte\0"
+                "\n"
+                "an empty line\n"
+                "an ignored line\n"
+                "and a very long line that is supposed to be truncated, because it is so long\n";
+
+        f = fmemopen(buffer, sizeof(buffer), "re");
+        assert_se(f);
+
+        assert_se(read_line(f, (size_t) -1, &line) == 15 && streq(line, "Some test data"));
+        line = mfree(line);
+
+        assert_se(read_line(f, 1024, &line) == 30 && streq(line, "With newlines, and a NUL byte"));
+        line = mfree(line);
+
+        assert_se(read_line(f, 1024, &line) == 1 && streq(line, ""));
+        line = mfree(line);
+
+        assert_se(read_line(f, 1024, &line) == 14 && streq(line, "an empty line"));
+        line = mfree(line);
+
+        assert_se(read_line(f, (size_t) -1, NULL) == 16);
+
+        assert_se(read_line(f, 16, &line) == -ENOBUFS);
+        line = mfree(line);
+
+        /* read_line() stopped when it hit the limit, that means when we continue reading we'll read at the first
+         * character after the previous limit. Let's make use of tha to continue our test. */
+        assert_se(read_line(f, 1024, &line) == 61 && streq(line, "line that is supposed to be truncated, because it is so long"));
+        line = mfree(line);
+
+        assert_se(read_line(f, 1024, &line) == 1 && streq(line, ""));
+        line = mfree(line);
+
+        assert_se(read_line(f, 1024, &line) == 0 && streq(line, ""));
+}
+
 int main(int argc, char *argv[]) {
         log_set_max_level(LOG_DEBUG);
         log_parse_environment();
@@ -684,6 +727,7 @@ int main(int argc, char *argv[]) {
         test_search_and_fopen_nulstr();
         test_writing_tmpfile();
         test_tempfn();
+        test_read_line();
 
         return 0;
 }