diff --git a/lib/util.h b/lib/util.h index 51d42ff16..11b1671b9 100644 --- a/lib/util.h +++ b/lib/util.h @@ -87,6 +87,8 @@ typedef struct int fd; /* data read from fd */ char buf[MC_PIPE_BUFSIZE]; + /* current position in @buf (used by mc_pstream_get_string()) */ + size_t pos; /* positive: length of data in buf; * MC_PIPE_STREAM_EOF: EOF of fd; * MC_PIPE_STREAM_UNREAD: there was not read from fd; @@ -221,6 +223,8 @@ mc_pipe_t *mc_popen (const char *command, gboolean read_out, gboolean read_err, void mc_pread (mc_pipe_t * p, GError ** error); void mc_pclose (mc_pipe_t * p, GError ** error); +GString *mc_pstream_get_string (mc_pipe_stream_t * ps); + void my_exit (int status); void save_stop_handler (void); diff --git a/lib/utilunix.c b/lib/utilunix.c index 3bf692a42..48b59ae63 100644 --- a/lib/utilunix.c +++ b/lib/utilunix.c @@ -277,6 +277,8 @@ mc_pread_stream (mc_pipe_stream_t * ps, const fd_set * fds) if (ps->null_term) ps->buf[(size_t) ps->len] = '\0'; } + + ps->pos = 0; } /* --------------------------------------------------------------------------------------------- */ @@ -628,6 +630,50 @@ mc_pread (mc_pipe_t * p, GError ** error) p->err.len = MC_PIPE_STREAM_UNREAD; } +/* --------------------------------------------------------------------------------------------- */ +/** + * Reads a line from @stream. Reading stops after an EOL or a newline. If a newline is read, + * it is appended to the line. + * + * @stream mc_pipe_stream_t object + * + * @return newly created GString or NULL in case of EOL; + */ + +GString * +mc_pstream_get_string (mc_pipe_stream_t * ps) +{ + char *s; + size_t size, i; + gboolean escape = FALSE; + + g_return_val_if_fail (ps != NULL, NULL); + + if (ps->len < 0) + return NULL; + + size = ps->len - ps->pos; + + if (size == 0) + return NULL; + + s = ps->buf + ps->pos; + + if (s[0] == '\0') + return NULL; + + /* find '\0' or unescaped '\n' */ + for (i = 0; i < size && !(s[i] == '\0' || (s[i] == '\n' && !escape)); i++) + escape = s[i] == '\\' ? !escape : FALSE; + + if (i != size && s[i] == '\n') + i++; + + ps->pos += i; + + return g_string_new_len (s, i); +} + /* --------------------------------------------------------------------------------------------- */ /** * Close pipe and destroy pipe descriptor. diff --git a/tests/lib/Makefile.am b/tests/lib/Makefile.am index 9e8b3d76e..ec7634617 100644 --- a/tests/lib/Makefile.am +++ b/tests/lib/Makefile.am @@ -20,6 +20,7 @@ TESTS = \ mc_build_filename \ name_quote \ serialize \ + utilunix__mc_pstream_get_string \ utilunix__my_system_fork_fail \ utilunix__my_system_fork_child_shell \ utilunix__my_system_fork_child \ @@ -46,6 +47,9 @@ name_quote_SOURCES = \ serialize_SOURCES = \ serialize.c +utilunix__mc_pstream_get_string_SOURCES = \ + utilunix__mc_pstream_get_string.c + utilunix__my_system_fork_fail_SOURCES = \ utilunix__my_system-fork_fail.c diff --git a/tests/lib/utilunix__mc_pstream_get_string.c b/tests/lib/utilunix__mc_pstream_get_string.c new file mode 100644 index 000000000..a942d1ee4 --- /dev/null +++ b/tests/lib/utilunix__mc_pstream_get_string.c @@ -0,0 +1,396 @@ +/* + lib - Read string from mc_pipe_stream + + Copyright (C) 2021 + Free Software Foundation, Inc. + + Written by: + Andrew Borodin , 2021 + + This file is part of the Midnight Commander. + + The Midnight Commander is free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + The Midnight Commander is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#define TEST_SUITE_NAME "/lib/util" + +#include "tests/mctest.h" + +#include "lib/util.h" + +/* --------------------------------------------------------------------------------------------- */ + +#define MAX_CHUNKS 8 + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pipe_stream_t stream; + +static char etalon_long_file_list[BUF_1K]; +static size_t etalon_long_file_list_pos; + +/* --------------------------------------------------------------------------------------------- */ + +/* @Before */ +static void +setup (void) +{ +} + +/* @After */ +static void +teardown (void) +{ +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @DataSource("data_source") */ +/* *INDENT-OFF* */ +static const struct data_source +{ + /* input */ + const char *buf; /* string to read */ + + /* output */ + int pos[MAX_CHUNKS]; /* ps.pos values */ + const char *str[MAX_CHUNKS]; /* chunks */ + size_t len[MAX_CHUNKS]; /* chunk lengths */ +} +data_source[] = +{ + /* 0 */ + { + .buf = "", + .pos = { 0 }, + .str = { "" }, + .len = { 0 } + }, + /* 1 */ + { + .buf = "\n", + .pos = { 0, 1 }, + .str = { "\n" }, + .len = { 1, 0 } + }, + /* 2 */ + { + .buf = "\\\n", + .pos = { 0, 2 }, + .str = { "\\\n" }, + .len = { 2, 0 } + }, + /* 3 */ + { + .buf = "\\\\\n", + .pos = { 0, 3 }, + .str = { "\\\\\n" }, + .len = { 3, 0 } + }, + /* 4 */ + { + .buf = "\\\\\\\n", + .pos = { 0, 4 }, + .str = { "\\\\\\\n" }, + .len = { 4, 0 } + }, + /* 5 */ + { + .buf = "\\\\\\\\\n", + .pos = { 0, 5 }, + .str = { "\\\\\\\\\n" }, + .len = { 5, 0 } + }, + /* 6 */ + { + .buf = "12345", + .pos = { 0, 5 }, + .str = { "12345" }, + .len = { 5, 0 } + }, + /* 7 */ + { + .buf = "12345\n", + .pos = { 0, 6 }, + .str = { "12345\n" }, + .len = { 6, 0 } + }, + /* 8 */ + { + .buf = "12345\\\n", + .pos = { 0, 7 }, + .str = { "12345\\\n" }, + .len = { 7, 0 } + }, + /* 9 */ + { + .buf = "12345\\\\\n", + .pos = { 0, 8 }, + .str = { "12345\\\\\n" }, + .len = { 8, 0 } + }, + /* 10 */ + { + .buf = "12345\nabcd", + .pos = { 0, 6, 10 }, + .str = { "12345\n", "abcd" }, + .len = { 6, 4, 0 } + }, + /* 11 */ + { + .buf = "12345\\\nabcd", + .pos = { 0, 11 }, + .str = { "12345\\\nabcd" }, + .len = { 11, 0 } + }, + /* 12 */ + { + .buf = "12345\\\\\nabcd", + .pos = { 0, 8, 12 }, + .str = { "12345\\\\\n", "abcd" }, + .len = { 8, 4, 0 } + }, + /* 13 */ + { + .buf = "12345\\\\\\\nabcd", + .pos = { 0, 13 }, + .str = { "12345\\\\\\\nabcd" }, + .len = { 13, 0 } + }, + /* 14 */ + { + .buf = "12345\\\\\\\\\nabcd", + .pos = { 0, 10, 14 }, + .str = { "12345\\\\\\\\\n", "abcd" }, + .len = { 10, 4, 0 } + }, + /* 15 */ + { + .buf = "12345\nabcd\n", + .pos = { 0, 6, 11 }, + .str = { "12345\n", "abcd\n" }, + .len = { 6, 5, 0 } + }, + /* 16 */ + { + .buf = "12345\nabcd\n~!@#$%^", + .pos = { 0, 6, 11, 18 }, + .str = { "12345\n", "abcd\n", "~!@#$%^" }, + .len = { 6, 5, 7, 0 } + }, + /* 17 */ + { + .buf = "12345\nabcd\n~!@#$%^\n", + .pos = { 0, 6, 11, 19 }, + .str = { "12345\n", "abcd\n", "~!@#$%^\n" }, + .len = { 6, 5, 8, 0 } + } +}; +/* *INDENT-ON* */ + +/* @Test(dataSource = "data_source") */ +/* *INDENT-OFF* */ +START_PARAMETRIZED_TEST (mc_pstream_get_string_test, data_source) +/* *INDENT-ON* */ +{ + /* given */ + int j = 0; + + /* when */ + memset (&stream, 0, sizeof (stream)); + stream.len = strlen (data->buf); + memmove (&stream.buf, data->buf, stream.len); + + /* then */ + do + { + GString *ret; + + mctest_assert_int_eq (stream.pos, data->pos[j]); + + ret = mc_pstream_get_string (&stream); + if (ret == NULL) + break; + + mctest_assert_int_eq (ret->len, data->len[j]); + mctest_assert_str_eq (ret->str, data->str[j]); + + g_string_free (ret, TRUE); + + j++; + } + while (TRUE); +} +/* *INDENT-OFF* */ +END_PARAMETRIZED_TEST +/* *INDENT-ON* */ + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pipe_t * +test_mc_popen (void) +{ + mc_pipe_t *p; + + p = g_try_new0 (mc_pipe_t, 1); + /* make less than sizeof (etalon_long_file_list) */ + p->out.len = 128; + + etalon_long_file_list_pos = 0; + + return p; +} + +static void +test_mc_pread (mc_pipe_t * p) +{ + size_t len; + + p->out.pos = 0; + + if (etalon_long_file_list_pos >= sizeof (etalon_long_file_list)) + { + etalon_long_file_list_pos = sizeof (etalon_long_file_list); + p->out.len = MC_PIPE_STREAM_EOF; + return; + } + + len = sizeof (etalon_long_file_list) - etalon_long_file_list_pos; + len = MIN (len, (size_t) p->out.len); + memmove (p->out.buf, etalon_long_file_list + etalon_long_file_list_pos, len); + p->out.len = (ssize_t) len; + + etalon_long_file_list_pos += len; +} + +/* *INDENT-OFF* */ +START_TEST (mc_pstream_get_long_file_list_test) +/* *INDENT-ON* */ + +{ + /* given */ + GString *result_long_file_list = NULL; + mc_pipe_t *pip; + GString *remain_file_name = NULL; + + /* when */ + /* fill the list */ + memset (etalon_long_file_list, 'a', sizeof (etalon_long_file_list) - 1); + /* create an \n-separated list */ + etalon_long_file_list[5] = '\n'; + etalon_long_file_list[25] = '\n'; + etalon_long_file_list[50] = '\n'; + etalon_long_file_list[75] = '\n'; + etalon_long_file_list[127] = '\n'; + etalon_long_file_list[200] = '\n'; + etalon_long_file_list[310] = '\n'; + etalon_long_file_list[325] = '\n'; + etalon_long_file_list[360] = '\n'; + etalon_long_file_list[512] = '\n'; + etalon_long_file_list[701] = '\n'; + etalon_long_file_list[725] = '\n'; + etalon_long_file_list[800] = '\n'; + etalon_long_file_list[sizeof (etalon_long_file_list) - 2] = '\n'; + etalon_long_file_list[sizeof (etalon_long_file_list) - 1] = '\0'; + + /* then */ + /* read file list */ + pip = test_mc_popen (); + + while (TRUE) + { + GString *line; + + test_mc_pread (pip); + + if (pip->out.len == MC_PIPE_STREAM_EOF) + break; + + while ((line = mc_pstream_get_string (&pip->out)) != NULL) + { + /* handle an \n-separated file list */ + + if (line->str[line->len - 1] == '\n') + { + /* entire file name or last chunk */ + + g_string_truncate (line, line->len - 1); + + /* join filename chunks */ + if (remain_file_name != NULL) + { + g_string_append_len (remain_file_name, line->str, line->len); + g_string_free (line, TRUE); + line = remain_file_name; + remain_file_name = NULL; + } + } + else + { + /* first or middle chunk of file name */ + if (remain_file_name == NULL) + remain_file_name = line; + else + { + g_string_append_len (remain_file_name, line->str, line->len); + g_string_free (line, TRUE); + } + + line = NULL; + } + + /* collect file names to assemble the result string */ + if (line == NULL) + continue; + + if (result_long_file_list == NULL) + result_long_file_list = line; + else + { + g_string_append_len (result_long_file_list, line->str, line->len); + g_string_free (line, TRUE); + } + + g_string_append_c (result_long_file_list, '\n'); + } + } + + mctest_assert_str_eq (etalon_long_file_list, result_long_file_list->str); + g_string_free (result_long_file_list, TRUE); + +} +/* *INDENT-OFF* */ +END_TEST +/* *INDENT-ON* */ + +/* --------------------------------------------------------------------------------------------- */ + +int +main (void) +{ + TCase *tc_core; + + tc_core = tcase_create ("Core"); + + tcase_add_checked_fixture (tc_core, setup, teardown); + + /* Add new tests here: *************** */ + mctest_add_parameterized_test (tc_core, mc_pstream_get_string_test, data_source); + tcase_add_test (tc_core, mc_pstream_get_long_file_list_test); + /* *********************************** */ + + return mctest_run_all (tc_core); +} + +/* --------------------------------------------------------------------------------------------- */