And fix some bugs as a side effect.
*.tar
*.zip
+# Check Framework
+*.test
+*.trs
+test-driver
+
# Temporal files
*~
# Man, GNU conventions need a 21 century overhaul badly.
AUTOMAKE_OPTIONS = foreign
-SUBDIRS = src man
+SUBDIRS = src man test
AC_CHECK_FUNCS([memset socket])
AC_SEARCH_LIBS([pthread_create], [pthread])
+# Uhhh... this one starts with "PKG_" so it's probably different.
+# No idea.
+PKG_CHECK_MODULES([CHECK], [check])
+
# Spit out the makefiles.
-AC_OUTPUT(Makefile src/Makefile man/Makefile)
+AC_OUTPUT(Makefile src/Makefile man/Makefile test/Makefile)
if (!meta)
return -ENOENT; /* TODO try to skip it anyway? */
- pdu = malloc(meta->length);
- if (!pdu)
+ *pdu = malloc(meta->length);
+ if (*pdu == NULL)
return -ENOMEM;
- err = meta->from_stream(&header, fd, pdu);
+ err = meta->from_stream(&header, fd, *pdu);
if (err) {
- free(pdu);
+ free(*pdu);
return err;
}
#include <netinet/in.h>
static int read_exact(int, unsigned char *, size_t);
-static int read_and_waste(int, unsigned char *, size_t, u_int64_t);
-static int get_octets(rtr_char);
+static int read_and_waste(int, unsigned char *, size_t, u_int32_t);
+static int get_octets(unsigned char);
static void place_null_character(rtr_char *, size_t);
static int
return err;
}
if (read_result == 0) {
- warn("Stream ended mid-PDU");
+ warnx("Stream ended mid-PDU.");
return -EPIPE;
}
}
* It is required that @str_len <= @total_len.
*/
static int
-read_and_waste(int fd, unsigned char *str, size_t str_len, u_int64_t total_len)
+read_and_waste(int fd, unsigned char *str, size_t str_len, u_int32_t total_len)
{
#define TLEN 1024 /* "Trash length" */
unsigned char trash[TLEN];
* @first_octet.
*/
static int
-get_octets(rtr_char first_octet)
+get_octets(unsigned char first_octet)
{
- if ((first_octet & 0xC0) == 0)
+ if ((first_octet & 0x80) == 0)
return 1;
if ((first_octet >> 5) == 6) /* 0b110 */
return 2;
return EINVALID_UTF8;
}
+/* This is just a cast. The barebones version is too cluttered. */
+#define UCHAR(c) ((unsigned char *)c)
+
/*
* This also sanitizes the string, BTW.
- * (Because it places the null chara in the first invalid character.
+ * (Because it overrides the first invalid character with the null chara.
* The rest is silently ignored.)
- *
- * TODO test the hell out of this.
*/
static void
place_null_character(rtr_char *str, size_t len)
null_chara_pos = str;
cursor = str;
- while (cursor < str + len) {
- octets = get_octets(*cursor);
+ while (cursor < str + len - 1) {
+ octets = get_octets(*UCHAR(cursor));
if (octets == EINVALID_UTF8)
break;
+ cursor++;
+
for (octet = 1; octet < octets; octet++) {
- if (cursor >= str + len - 1 || cursor[1] >> 6 != 0x10)
- break;
+ /* Memory ends in the middle of this code point? */
+ if (cursor >= str + len - 1)
+ goto end;
+ /* All continuation octets must begin with 0b10. */
+ if ((*(UCHAR(cursor)) >> 6) != 2 /* 0b10 */)
+ goto end;
cursor++;
}
null_chara_pos = cursor;
}
+end:
*null_chara_pos = '\0';
}
+/*
+ * Reads an RTR string from the file descriptor @fd. Returns the string as a
+ * normal UTF-8 C string (NULL-terminated).
+ *
+ * Will consume the entire string from the stream, but @result can be truncated.
+ * This is because RTR strings are technically allowed to be 4 GBs long.
+ *
+ * The result is allocated in the heap. It will length 4096 characters at most.
+ * (Including the NULL chara.)
+ */
int
read_string(int fd, rtr_char **result)
{
err = read_int32(fd, &full_length32);
if (err)
return err;
+
full_length64 = ((u_int64_t) full_length32) + 1;
alloc_length = (full_length64 > 4096) ? 4096 : full_length64;
if (!str)
return -ENOMEM;
- err = read_and_waste(fd, str, alloc_length - 1, full_length64);
+ err = read_and_waste(fd, UCHAR(str), alloc_length - 1, full_length32);
if (err) {
free(str);
return err;
#include "../common.h"
-typedef unsigned char rtr_char;
+typedef char rtr_char;
__BEGIN_DECLS
int read_int8(int, u_int8_t *);
--- /dev/null
+# Reminder: Automake will automatically add this to any targets where
+# <mumble>_CFLAGS is not defined.
+# Otherwise it must be included manually:
+# mumble_mumble_CFLAGS = $(AM_CFLAGS) flag1 flag2 flag3 ...
+AM_CFLAGS = -pedantic -Wall -std=gnu11 -I../src @CHECK_CFLAGS@
+
+# Reminder: As opposed to AM_CFLAGS, "AM_LDADD" is not idiomatic automake, and
+# autotools will even reprehend us if we declare it. Therefore, I came up with
+# "my" own "ldadd". Unlike AM_CFLAGS, it needs to be manually added to every
+# target.
+MY_LDADD = $(CHECK_LIBS)
+
+check_PROGRAMS = rtr/primitive_reader.test rtr/pdu.test
+TESTS = $(check_PROGRAMS)
+
+rtr_primitive_reader_test_SOURCES = \
+ rtr/primitive_reader_test.c \
+ rtr/stream.c
+rtr_primitive_reader_test_LDADD = $(MY_LDADD)
+
+rtr_pdu_test_SOURCES = \
+ rtr/pdu_test.c \
+ rtr/stream.c \
+ $(top_builddir)/src/rtr/primitive_reader.c \
+ $(top_builddir)/src/rtr/pdu_handler.c
+rtr_pdu_test_LDADD = $(MY_LDADD)
--- /dev/null
+# Introduction
+
+I decided to use the [Check Framework](https://libcheck.github.io/check/) for
+unit testing.
+
+This is how I understand it:
+
+- Each `main()` has one or more "Suite"s.
+- Each "Suite" has multiple "Test Case"s.
+ "Test Case" is an odd name. They should be called "Test Type"s in my
+ opinion. In the examples, one "test case" is "Core" (happy path tests),
+ another one is "Limits" (the corner cases), and I imagine that one can add
+ "Errors" and "Performance" and whatever.
+- Each "Test Case" has multiple "test"s. Each "test" is a `_test` function.
+ (Defined by `START_TEST` and `END_TEST`.)
+- Each "test" is allowed to contain more than one function call, but it should
+ normally only throw one kind of challenge to the target function.
+
+The framework seems to want each suite to test one module, but in my opinion,
+that's autotools's job. (Each `check_PROGRAM` in `Makefile.am` should test one
+module, otherwise why the F would it allow multiple entries.) So I guess the
+rule of thumb is as follows:
+
+ Each "Suite" should test one (and only one) function within a module.
+
+So then, each entry in `Makefile.am` is a module (within `/test/`) that tests
+another module (within `/src/`). (If the name of the tested module is `A.c`, then
+the name of the testing module is `A_test.c`.) Each testing module has one suite
+per function within the tested module. Each suite has one test suite per test
+type. And so on.
+
+Testing private functions is totally allowed. Simply `#include` the `.c` (not the
+`.h`) to do this.
+
+# Running
+
+The following commands are preparatory and only need to be run the first time,
+_in the current directory's parent_:
+
+ ./autogen.sh
+ ./configure
+
+Then, whenever you want to run the tests, enter the current directory and run
+
+ make check
+
+There's at least one very long test that lasts about a full minute.
--- /dev/null
+#include <check.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include "stream.h"
+#include "rtr/pdu.c"
+
+/*
+ * Just a wrapper for `buffer2fd()`. Boilerplate one-liner.
+ */
+#define BUFFER2FD(buffer, cb, obj) { \
+ struct pdu_header header; \
+ int fd, err; \
+ \
+ fd = buffer2fd(buffer, sizeof(buffer)); \
+ ck_assert_int_ge(fd, 0); \
+ init_pdu_header(&header); \
+ err = cb(&header, fd, obj); \
+ close(fd); \
+ ck_assert_int_eq(err, 0); \
+ assert_pdu_header(&(obj)->header); \
+}
+
+static void
+init_pdu_header(struct pdu_header *header)
+{
+ header->protocol_version = 0;
+ header->pdu_type = 22;
+ header->reserved = 12345;
+ header->length = 0xFFAA9955;
+}
+
+static void
+assert_pdu_header(struct pdu_header *header)
+{
+ ck_assert_uint_eq(header->protocol_version, 0);
+ ck_assert_uint_eq(header->pdu_type, 22);
+ ck_assert_uint_eq(header->reserved, 12345);
+ ck_assert_uint_eq(header->length, 0xFFAA9955);
+}
+
+START_TEST(test_pdu_header_from_stream)
+{
+ unsigned char input[] = { 0, 1, 2, 3, 4, 5, 6, 7 };
+ struct pdu_header header;
+ int fd;
+ int err;
+
+ fd = buffer2fd(input, sizeof(input));
+ ck_assert_int_ge(fd, 0);
+ err = pdu_header_from_stream(fd, &header);
+ close(fd);
+ ck_assert_int_eq(err, 0);
+
+ ck_assert_uint_eq(header.protocol_version, 0);
+ ck_assert_uint_eq(header.pdu_type, 1);
+ ck_assert_uint_eq(header.reserved, 0x0203);
+ ck_assert_uint_eq(header.length, 0x04050607);
+}
+END_TEST
+
+START_TEST(test_serial_notify_from_stream)
+{
+ unsigned char input[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
+ struct serial_notify_pdu pdu;
+
+ BUFFER2FD(input, serial_notify_from_stream, &pdu);
+ ck_assert_uint_eq(pdu.serial_number, 0x010203);
+}
+END_TEST
+
+START_TEST(test_serial_query_from_stream)
+{
+ unsigned char input[] = { 13, 14, 15, 16, 17 };
+ struct serial_query_pdu pdu;
+
+ BUFFER2FD(input, serial_query_from_stream, &pdu);
+ ck_assert_uint_eq(pdu.serial_number, 0x0d0e0f10);
+}
+END_TEST
+
+START_TEST(test_reset_query_from_stream)
+{
+ unsigned char input[] = { 18, 19 };
+ struct reset_query_pdu pdu;
+
+ BUFFER2FD(input, reset_query_from_stream, &pdu);
+}
+END_TEST
+
+START_TEST(test_cache_response_from_stream)
+{
+ unsigned char input[] = { 18, 19 };
+ struct cache_response_pdu pdu;
+
+ BUFFER2FD(input, cache_response_from_stream, &pdu);
+}
+END_TEST
+
+START_TEST(test_ipv4_prefix_from_stream)
+{
+ unsigned char input[] = { 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28,
+ 29, 30, 31, 32 };
+ struct ipv4_prefix_pdu pdu;
+
+ BUFFER2FD(input, ipv4_prefix_from_stream, &pdu);
+ ck_assert_uint_eq(pdu.flags, 18);
+ ck_assert_uint_eq(pdu.prefix_length, 19);
+ ck_assert_uint_eq(pdu.max_length, 20);
+ ck_assert_uint_eq(pdu.zero, 21);
+ ck_assert_uint_eq(pdu.ipv4_prefix.s_addr, 0x16171819);
+ ck_assert_uint_eq(pdu.asn, 0x1a1b1c1d);
+}
+END_TEST
+
+START_TEST(test_ipv6_prefix_from_stream)
+{
+ unsigned char input[] = { 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43,
+ 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57,
+ 58, 59, 60 };
+ struct ipv6_prefix_pdu pdu;
+
+ BUFFER2FD(input, ipv6_prefix_from_stream, &pdu);
+ ck_assert_uint_eq(pdu.flags, 33);
+ ck_assert_uint_eq(pdu.prefix_length, 34);
+ ck_assert_uint_eq(pdu.max_length, 35);
+ ck_assert_uint_eq(pdu.zero, 36);
+ ck_assert_uint_eq(pdu.ipv6_prefix.s6_addr32[0], 0x25262728);
+ ck_assert_uint_eq(pdu.ipv6_prefix.s6_addr32[1], 0x292a2b2c);
+ ck_assert_uint_eq(pdu.ipv6_prefix.s6_addr32[2], 0x2d2e2f30);
+ ck_assert_uint_eq(pdu.ipv6_prefix.s6_addr32[3], 0x31323334);
+ ck_assert_uint_eq(pdu.asn, 0x35363738);
+}
+END_TEST
+
+START_TEST(test_end_of_data_from_stream)
+{
+ unsigned char input[] = { 61, 62, 63, 64 };
+ struct end_of_data_pdu pdu;
+
+ BUFFER2FD(input, end_of_data_from_stream, &pdu);
+ ck_assert_uint_eq(pdu.serial_number, 0x3d3e3f40);
+}
+END_TEST
+
+START_TEST(test_cache_reset_from_stream)
+{
+ unsigned char input[] = { 65, 66, 67 };
+ struct cache_reset_pdu pdu;
+
+ BUFFER2FD(input, cache_reset_from_stream, &pdu);
+}
+END_TEST
+
+START_TEST(test_error_report_from_stream)
+{
+ unsigned char input[] = {
+ /* Sub-pdu length */
+ 0, 0, 0, 12,
+ /* Sub-pdu */
+ 1, 0, 2, 3, 0, 0, 0, 12, 1, 2, 3, 4,
+ /* Error msg length */
+ 0, 0, 0, 5,
+ /* Error msg */
+ 'h', 'e', 'l', 'l', 'o',
+ /* Garbage */
+ 1, 2, 3, 4,
+ };
+ struct error_report_pdu *pdu;
+ struct serial_notify_pdu *sub_pdu;
+
+ pdu = malloc(sizeof(struct error_report_pdu));
+ if (!pdu)
+ ck_abort_msg("PDU allocation failure");
+
+ BUFFER2FD(input, error_report_from_stream, pdu);
+
+ sub_pdu = pdu->erroneous_pdu;
+ ck_assert_uint_eq(sub_pdu->header.protocol_version, 1);
+ ck_assert_uint_eq(sub_pdu->header.pdu_type, 0);
+ ck_assert_uint_eq(sub_pdu->header.reserved, 0x0203);
+ ck_assert_uint_eq(sub_pdu->header.length, 12);
+ ck_assert_uint_eq(sub_pdu->serial_number, 0x01020304);
+ ck_assert_str_eq(pdu->error_message, "hello");
+
+ /*
+ * Yes, this test memory leaks on failure.
+ * Not sure how to fix it without making a huge mess.
+ */
+ error_report_destroy(pdu);
+}
+END_TEST
+
+START_TEST(test_interrupted)
+{
+ unsigned char input[] = { 0, 1 };
+ struct serial_notify_pdu pdu;
+ struct pdu_header header;
+ int fd, err;
+
+ fd = buffer2fd(input, sizeof(input));
+ ck_assert_int_ge(fd, 0);
+ init_pdu_header(&header);
+ err = serial_notify_from_stream(&header, fd, &pdu);
+ close(fd);
+ ck_assert_int_eq(err, -EPIPE);
+}
+END_TEST
+
+Suite *pdu_suite(void)
+{
+ Suite *suite;
+ TCase *core, *errors;
+
+ core = tcase_create("Core");
+ tcase_add_test(core, test_pdu_header_from_stream);
+ tcase_add_test(core, test_serial_notify_from_stream);
+ tcase_add_test(core, test_serial_notify_from_stream);
+ tcase_add_test(core, test_serial_query_from_stream);
+ tcase_add_test(core, test_reset_query_from_stream);
+ tcase_add_test(core, test_cache_response_from_stream);
+ tcase_add_test(core, test_ipv4_prefix_from_stream);
+ tcase_add_test(core, test_ipv6_prefix_from_stream);
+ tcase_add_test(core, test_end_of_data_from_stream);
+ tcase_add_test(core, test_cache_reset_from_stream);
+ tcase_add_test(core, test_error_report_from_stream);
+
+ errors = tcase_create("Errors");
+ tcase_add_test(errors, test_interrupted);
+
+ suite = suite_create("PDU");
+ suite_add_tcase(suite, core);
+ suite_add_tcase(suite, errors);
+ return suite;
+}
+
+int main(void)
+{
+ Suite *suite;
+ SRunner *runner;
+ int tests_failed;
+
+ suite = pdu_suite();
+
+ runner = srunner_create(suite);
+ srunner_run_all(runner, CK_NORMAL);
+ tests_failed = srunner_ntests_failed(runner);
+ srunner_free(runner);
+
+ return (tests_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
+}
--- /dev/null
+#include <check.h>
+#include <error.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "stream.h"
+#include "rtr/primitive_reader.c"
+
+/*
+ * Wrapper for `read_string()`, for easy testing.
+ */
+static int
+__read_string(unsigned char *input, size_t size, rtr_char **result)
+{
+ int fd;
+ int err;
+
+ fd = buffer2fd(input, size);
+ if (fd < 0)
+ return fd;
+
+ err = read_string(fd, result);
+ close(fd);
+ return err;
+}
+
+static void
+test_read_string_success(unsigned char *input, size_t length,
+ rtr_char *expected)
+{
+ rtr_char *actual;
+ int err;
+
+ err = __read_string(input, length, &actual);
+ ck_assert_int_eq(0, err);
+ if (!err) {
+ ck_assert_str_eq(expected, actual);
+ free(actual);
+ }
+}
+
+static void
+test_read_string_fail(unsigned char *input, size_t length, int expected)
+{
+ rtr_char *result;
+ int err;
+
+ err = __read_string(input, length, &result);
+ ck_assert_int_eq(expected, err);
+
+ if (!err)
+ free(result);
+}
+
+START_TEST(read_string_ascii)
+{
+ unsigned char input[] = { 0, 0, 0, 4, 'a', 'b', 'c', 'd' };
+ test_read_string_success(input, sizeof(input), "abcd");
+}
+END_TEST
+
+START_TEST(read_string_unicode)
+{
+ unsigned char input0[] = { 0, 0, 0, 7, 's', 'a', 'n', 'd', 0xc3, 0xad,
+ 'a' };
+ test_read_string_success(input0, sizeof(input0), "sandía");
+
+ unsigned char input1[] = { 0, 0, 0, 12,
+ 0xe1, 0x88, 0x90, 0xe1, 0x89, 0xa5, 0xe1, 0x88, 0x90, 0xe1, 0x89,
+ 0xa5 };
+ test_read_string_success(input1, sizeof(input1), "ሐብሐብ");
+
+ unsigned char input2[] = { 0, 0, 0, 12,
+ 0xd8, 0xa7, 0xd9, 0x84, 0xd8, 0xa8, 0xd8, 0xb7, 0xd9, 0x8a, 0xd8,
+ 0xae };
+ test_read_string_success(input2, sizeof(input2), "البطيخ");
+
+ unsigned char input3[] = { 0, 0, 0, 25,
+ 0xd5, 0xb1, 0xd5, 0xb4, 0xd5, 0xa5, 0xd6, 0x80, 0xd5, 0xb8, 0xd6,
+ 0x82, 0xd5, 0xaf, 0x20, 0xd0, 0xba, 0xd0, 0xb0, 0xd0, 0xb2, 0xd1,
+ 0x83, 0xd0, 0xbd };
+ test_read_string_success(input3, sizeof(input3), "ձմերուկ кавун");
+
+ unsigned char input4[] = { 0, 0, 0, 36,
+ 0xe0, 0xa6, 0xa4, 0xe0, 0xa6, 0xb0, 0xe0, 0xa6, 0xae, 0xe0, 0xa7,
+ 0x81, 0xe0, 0xa6, 0x9c, 0x20, 0xd0, 0xb4, 0xd0, 0xb8, 0xd0, 0xbd,
+ 0xd1, 0x8f, 0x20, 0xe8, 0xa5, 0xbf, 0xe7, 0x93, 0x9c, 0x20, 0xf0,
+ 0x9f, 0x8d, 0x89 };
+ test_read_string_success(input4, sizeof(input4), "তরমুজ диня 西瓜 🍉");
+}
+END_TEST
+
+START_TEST(read_string_empty)
+{
+ unsigned char input[] = { 0, 0, 0, 0 };
+ test_read_string_success(input, sizeof(input), "");
+}
+END_TEST
+
+struct thread_param {
+ int fd;
+ u_int32_t msg_size;
+ int err;
+};
+
+#define WRITER_PATTERN "abcdefghijklmnopqrstuvwxyz0123456789"
+
+/*
+ * Writes a @param_void->msg_size-sized RTR string in @param_void->fd.
+ */
+static void *
+writer_thread_cb(void *param_void)
+{
+ struct thread_param *param;
+ rtr_char *pattern;
+ size_t pattern_len;
+ unsigned char header[4];
+
+ param = param_void;
+ pattern = WRITER_PATTERN;
+ pattern_len = strlen(pattern);
+
+ /* Write the string length */
+ header[0] = (param->msg_size >> 24) & 0xFF;
+ header[1] = (param->msg_size >> 16) & 0xFF;
+ header[2] = (param->msg_size >> 8) & 0xFF;
+ header[3] = (param->msg_size >> 0) & 0xFF;
+ param->err = write_exact(param->fd, header, sizeof(header));
+ if (param->err)
+ return param;
+
+ /* Write the string */
+ for (; param->msg_size > pattern_len; param->msg_size -= pattern_len) {
+ param->err = write_exact(param->fd, UCHAR(pattern), pattern_len);
+ if (param->err)
+ return param;
+ }
+ param->err = write_exact(param->fd, UCHAR(pattern), param->msg_size);
+ return param;
+}
+
+/*
+ * Checks that the string @str is made up of @expected_len characters composed
+ * of the @WRITER_PATTERN pattern repeatedly.
+ */
+static void
+validate_massive_string(u_int32_t expected_len, rtr_char *str)
+{
+ size_t actual_len;
+ rtr_char *pattern;
+ size_t pattern_len;
+ rtr_char *cursor;
+ rtr_char *end;
+
+ actual_len = strlen(str);
+ if (expected_len != actual_len) {
+ free(str);
+ ck_abort_msg("Expected length %zu != Actual length %zu",
+ expected_len, actual_len);
+ }
+
+ pattern = WRITER_PATTERN;
+ pattern_len = strlen(pattern);
+ end = str + expected_len;
+ for (cursor = str; cursor + pattern_len < end; cursor += pattern_len) {
+ if (strncmp(pattern, cursor, pattern_len) != 0) {
+ free(str);
+ ck_abort_msg("String does not match expected pattern");
+ }
+ }
+
+ if (strncmp(pattern, cursor, strlen(cursor)) != 0) {
+ free(str);
+ ck_abort_msg("String end does not match expected pattern");
+ }
+
+ free(str);
+ /* Success */
+}
+
+/*
+ * Sends @full_string_length characters to the fd, validates the parsed string
+ * contains the first @return_length characters.
+ */
+static void
+test_massive_string(u_int32_t return_length, u_int32_t full_string_length)
+{
+ int fd[2];
+ pthread_t writer_thread;
+ struct thread_param *arg;
+ rtr_char *result_string;
+ int err, err2, err3;
+
+ if (pipe(fd) == -1)
+ ck_abort_msg("pipe(fd) threw errcode %d", errno);
+ /* Need to close @fd[0] and @fd[1] from now on */
+
+ arg = malloc(sizeof(struct thread_param));
+ if (!arg) {
+ close(fd[0]);
+ close(fd[1]);
+ ck_abort_msg("Thread parameter allocation failure");
+ }
+ /* Need to free @arg from now on */
+
+ arg->fd = fd[1];
+ arg->msg_size = full_string_length;
+ arg->err = 0;
+
+ err = pthread_create(&writer_thread, NULL, writer_thread_cb, arg);
+ if (err) {
+ close(fd[0]);
+ close(fd[1]);
+ free(arg);
+ ck_abort_msg("pthread_create() threw errcode %d", err);
+ }
+ /* The writer thread owns @arg now; do not touch it until retrievel */
+
+ err = read_string(fd[0], &result_string);
+ /* Need to free @result_string from now on */
+ err2 = pthread_join(writer_thread, (void **)&arg);
+ /* @arg is now retrieved. */
+ err3 = arg->err;
+
+ close(fd[0]);
+ close(fd[1]);
+ free(arg);
+ /* Don't need to close @fd[0], @fd[1] nor free @arg from now on */
+
+ if (err || err2 || err3) {
+ free(result_string);
+ ck_abort_msg("read_string:%d pthread_join:%d write_exact:%d",
+ err, err2, err3);
+ }
+
+ /* This function now owns @result_string */
+ validate_massive_string(return_length, result_string);
+}
+
+START_TEST(read_string_massive)
+{
+ test_massive_string(2000, 2000);
+ test_massive_string(4000, 4000);
+ test_massive_string(4094, 4094);
+ test_massive_string(4095, 4095);
+ test_massive_string(4095, 4096);
+ test_massive_string(4095, 4097);
+ test_massive_string(4095, 8000);
+ test_massive_string(4095, 16000);
+ test_massive_string(4095, 0xFFFFFFFF);
+}
+END_TEST
+
+START_TEST(read_string_null)
+{
+ test_read_string_fail(NULL, 0, -EPIPE);
+}
+END_TEST
+
+START_TEST(read_string_truncated)
+{
+ unsigned char input0[] = { 0, 0, 0, 7, 'a', 'b' };
+ test_read_string_fail(input0, sizeof(input0), -EPIPE);
+
+ unsigned char input1[] = { 0, 0 };
+ test_read_string_fail(input1, sizeof(input1), -EPIPE);
+}
+END_TEST
+
+START_TEST(read_string_unicode_mix)
+{
+ /* One octet failure */
+ unsigned char input0[] = { 0, 0, 0, 3, 'a', 0x80, 'z' };
+ test_read_string_success(input0, sizeof(input0), "a");
+
+ /* Two octets success */
+ unsigned char input1[] = { 0, 0, 0, 4, 'a', 0xdf, 0x9a, 'z' };
+ test_read_string_success(input1, sizeof(input1), "aߚz");
+ /* Two octets failure */
+ unsigned char input2[] = { 0, 0, 0, 4, 'a', 0xdf, 0xda, 'z' };
+ test_read_string_success(input2, sizeof(input2), "a");
+
+ /* Three characters success */
+ unsigned char input3[] = { 0, 0, 0, 5, 'a', 0xe2, 0x82, 0xac, 'z' };
+ test_read_string_success(input3, sizeof(input3), "a€z");
+ /* Three characters failure */
+ unsigned char input4[] = { 0, 0, 0, 5, 'a', 0xe2, 0x82, 0x2c, 'z' };
+ test_read_string_success(input4, sizeof(input4), "a");
+
+ /* Four characters success */
+ unsigned char i5[] = { 0, 0, 0, 6, 'a', 0xf0, 0x90, 0x86, 0x97, 'z' };
+ test_read_string_success(i5, sizeof(i5), "a𐆗z");
+ /* Four characters failure */
+ unsigned char i6[] = { 0, 0, 0, 6, 'a', 0xf0, 0x90, 0x90, 0x17, 'z' };
+ test_read_string_success(i6, sizeof(i6), "a");
+}
+END_TEST
+
+Suite *read_string_suite(void)
+{
+ Suite *suite;
+ TCase *core, *limits, *errors;
+
+ core = tcase_create("Core");
+ tcase_add_test(core, read_string_ascii);
+ tcase_add_test(core, read_string_unicode);
+
+ limits = tcase_create("Limits");
+ tcase_add_test(limits, read_string_empty);
+ tcase_add_test(limits, read_string_massive);
+ /* The 0xFFFFFFFF test lasts 1:02 minutes on my computer. */
+ tcase_set_timeout(limits, 120);
+
+ errors = tcase_create("Errors");
+ tcase_add_test(errors, read_string_null);
+ tcase_add_test(errors, read_string_truncated);
+ tcase_add_test(errors, read_string_unicode_mix);
+
+ suite = suite_create("read_string()");
+ suite_add_tcase(suite, core);
+ suite_add_tcase(suite, limits);
+ suite_add_tcase(suite, errors);
+ return suite;
+}
+
+int main(void)
+{
+ Suite *suite;
+ SRunner *runner;
+ int tests_failed;
+
+ /*
+ * This is it. We won't test the other functions because they are
+ * already reasonably manhandled in the PDU units.
+ */
+ suite = read_string_suite();
+
+ runner = srunner_create(suite);
+ srunner_run_all(runner, CK_NORMAL);
+ tests_failed = srunner_ntests_failed(runner);
+ srunner_free(runner);
+
+ return (tests_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
+}
--- /dev/null
+#include "stream.h"
+
+#include <err.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+/*
+ * Writes exactly @length bytes from @buffer to the file descriptor @fd.
+ * All or nothing.
+ *
+ * The result is zero on success, nonzero on failure.
+ */
+int
+write_exact(int fd, unsigned char *buffer, size_t length)
+{
+ size_t written;
+ int written_now;
+
+ for (written = 0; written < length; written += written_now) {
+ written_now = write(fd, buffer + written, length - written);
+ if (written_now == -1)
+ return errno;
+ }
+
+ return 0;
+}
+
+/*
+ * "Converts" the buffer @buffer (sized @size) to a file descriptor (FD).
+ * You will get @buffer if you `read()` the FD.
+ *
+ * If the result is not negative, then you're receiving the resulting FD.
+ * If the result is negative, it's an error code.
+ *
+ * Note that you need to close the FD when you're done reading it.
+ */
+int
+buffer2fd(unsigned char *buffer, size_t size)
+{
+ int fd[2];
+ int err;
+
+ if (pipe(fd) == -1) {
+ err = errno;
+ warn("Pipe creation failed");
+ return -abs(err);
+ }
+
+ err = write_exact(fd[1], buffer, size);
+ close(fd[1]);
+ if (err) {
+ errno = err;
+ warn("Pipe write failed");
+ close(fd[0]);
+ return -abs(err);
+ }
+
+ return fd[0];
+}
--- /dev/null
+#ifndef TEST_RTR_STREAM_H_
+#define TEST_RTR_STREAM_H_
+
+#include <stddef.h>
+
+#include "common.h"
+
+__BEGIN_DECLS
+int write_exact(int, unsigned char *, size_t);
+int buffer2fd(unsigned char *, size_t);
+__END_DECLS
+
+#endif /* TEST_RTR_STREAM_H_ */