diff --git a/src/basic/string-util.c b/src/basic/string-util.c index 774a72fd69..b2278c288e 100644 --- a/src/basic/string-util.c +++ b/src/basic/string-util.c @@ -1428,6 +1428,23 @@ size_t strspn_from_end(const char *str, const char *accept) { return n; } +size_t strnspn(const char *str, const char *accept, size_t n) { + size_t i; + + /* Like strspn(), but reads at most 'n' bytes from 'str'. Returns the length of the initial + * run of 'str' (capped at 'n') that consists solely of bytes found in 'accept'. Stops at a + * NUL byte too. Unlike strspn() this is safe on a buffer that is not NUL terminated within + * 'n' bytes. */ + + assert(str || n == 0); + assert(accept); + + for (i = 0; i < n && str[i] != '\0' && strchr(accept, str[i]); i++) + ; + + return i; +} + char* strdupspn(const char *a, const char *accept) { if (isempty(a) || isempty(accept)) return strdup(""); diff --git a/src/basic/string-util.h b/src/basic/string-util.h index d0b614f775..74d6ca1682 100644 --- a/src/basic/string-util.h +++ b/src/basic/string-util.h @@ -286,6 +286,7 @@ typedef enum MakeCStringMode { int make_cstring(const void *s, size_t n, MakeCStringMode mode, char **ret); size_t strspn_from_end(const char *str, const char *accept) _pure_; +size_t strnspn(const char *str, const char *accept, size_t n) _pure_ _nonnull_if_nonzero_(1, 3); char* strdupspn(const char *a, const char *accept); char* strdupcspn(const char *a, const char *reject); diff --git a/src/shared/udev-util.c b/src/shared/udev-util.c index f6021de569..a54720d959 100644 --- a/src/shared/udev-util.c +++ b/src/shared/udev-util.c @@ -315,7 +315,9 @@ size_t udev_replace_whitespace(const char *str, char *to, size_t len) { * * Note that only 'len' characters are read from 'str'. */ - i = strspn(str, WHITESPACE); + /* Skip leading whitespace, but read at most 'len' bytes: 'str' is not necessarily NUL + * terminated within 'len' (e.g. the space padded ATA IDENTIFY fields ata_id passes in). */ + i = strnspn(str, WHITESPACE, len); for (j = 0; j < len && i < len && str[i] != '\0'; i++) { if (isspace(str[i])) { diff --git a/src/test/test-string-util.c b/src/test/test-string-util.c index 343ddcf3cd..79b8175c34 100644 --- a/src/test/test-string-util.c +++ b/src/test/test-string-util.c @@ -1288,6 +1288,26 @@ TEST(strspn_from_end) { assert_se(strspn_from_end("aaa12aa34", DIGITS) == 2); } +TEST(strnspn) { + assert_se(strnspn(NULL, WHITESPACE, 0) == 0); + assert_se(strnspn("", WHITESPACE, 0) == 0); + assert_se(strnspn("hoge", WHITESPACE, 0) == 0); + assert_se(strnspn("hoge", WHITESPACE, 4) == 0); + assert_se(strnspn(" hoge", WHITESPACE, 7) == 3); + assert_se(strnspn(" hoge", WHITESPACE, 2) == 2); + assert_se(strnspn(" ", WHITESPACE, 4) == 4); + + /* 'str' is all whitespace and not NUL terminated within 'n': at most 'n' bytes may be read, + * otherwise this reads off the end of the buffer (caught by the sanitizers). */ + for (size_t n = 1; n <= 64; n++) { + _cleanup_free_ char *str = NULL; + + assert_se(str = new(char, n)); + memset(str, ' ', n); + assert_se(strnspn(str, WHITESPACE, n) == n); + } +} + TEST(streq_skip_trailing_chars) { /* NULL is WHITESPACE by default */ assert_se(streq_skip_trailing_chars("foo bar", "foo bar", NULL)); diff --git a/src/test/test-udev-util.c b/src/test/test-udev-util.c index e6c339bcc7..8355a94b0d 100644 --- a/src/test/test-udev-util.c +++ b/src/test/test-udev-util.c @@ -57,4 +57,43 @@ TEST(udev_replace_whitespace) { test_udev_replace_whitespace_one_len(" hoge hoge ", 0, ""); } +static void test_udev_replace_whitespace_not_terminated_one(const char *str, size_t len, const char *expected) { + _cleanup_free_ char *buf = NULL, *result = NULL; + int r; + + /* Copy exactly 'len' bytes with no trailing NUL, so any read past 'len' is caught by the + * sanitizers. */ + assert_se(buf = memdup(str, len)); + + assert_se(result = new(char, len + 1)); + r = udev_replace_whitespace(buf, result, len); + assert_se((size_t) r == strlen(expected)); + ASSERT_STREQ(result, expected); +} + +TEST(udev_replace_whitespace_not_nul_terminated) { + /* 'str' is not NUL terminated within 'len' and consists entirely of whitespace, like a + * space padded, non-terminated ATA IDENTIFY model/serial field. Only 'len' bytes may be + * read from it; otherwise this reads off the end of the buffer (caught by the sanitizers). */ + + for (size_t len = 1; len <= 64; len++) { + _cleanup_free_ char *str = NULL, *result = NULL; + + assert_se(str = new(char, len)); + memset(str, ' ', len); + + assert_se(result = new(char, len + 1)); + assert_se(udev_replace_whitespace(str, result, len) == 0); + ASSERT_STREQ(result, ""); + } + + /* Leading whitespace followed by content, also not NUL terminated within 'len': exercises + * the handoff from the bounded leading skip into the copy loop on a non-terminated buffer. */ + test_udev_replace_whitespace_not_terminated_one(" hoge", 7, "hoge"); + test_udev_replace_whitespace_not_terminated_one(" hoge hoge", 13, "hoge_hoge"); + test_udev_replace_whitespace_not_terminated_one(" hoge hoge ", 16, "hoge_hoge"); + test_udev_replace_whitespace_not_terminated_one(" hoge hoge ", 7, "hog"); + test_udev_replace_whitespace_not_terminated_one(" x", 2, "x"); +} + DEFINE_TEST_MAIN(LOG_INFO);