8
\$\begingroup\$

vstrsepf is string parsing utility function born from the idea of using strsep and strtol with a sscanf-like interface.

This is an attempt to build a safer scanf for embedded system application.

Some key design choices:

  • Will destroy its input string (by adding '\0').
  • No dynamic memory allocation.
  • Reentry safe (RTOS and thread support)
  • No string copy, only pointer address are changed.
  • No floating point support.

Usage example 1:

// Parse IP string
char              test[] = "192.168.0.13";
char const* const format = "%3d.%3d.%3d.%3d";

uint32_t answer0 = 0;
uint32_t answer1 = 0;
uint32_t answer2 = 0;
uint32_t answer3 = 0;
int16_t  n = strsepf(test, format, &answer0, &answer1, &answer2, &answer3);

TEST_ASSERT_EQUAL(192, answer0);
TEST_ASSERT_EQUAL(168, answer1);
TEST_ASSERT_EQUAL(0, answer2);
TEST_ASSERT_EQUAL(13, answer3);
TEST_ASSERT_EQUAL(4, n);

Usage example 2:

// Parse NMEA (GPS) message
char              test[] = "$GPBWC,081837,,,,,,T,,M,,N,*13";
char const* const format = "$%*sBWC,%d,%*s,%*s,%*s,%*s,%*s,%s,";

uint32_t answer0 = 0;
char*    answer1 = NULL;
int16_t  n = strsepf(test, format, &answer0, &answer1);

TEST_ASSERT_EQUAL(81837, answer0);
TEST_ASSERT_EQUAL_STRING("T", answer1);
TEST_ASSERT_EQUAL(2, n);

Try it online: https://onlinegdb.com/BkTDY5EoS


strtol_s.h A safer implementation of strtol (needed for strsepf).

long
strtol_s(const char* buff, uint8_t base, int8_t* err)
{
    char* end;
    errno = 0;

    const long sl = strtol(buff, &end, base);
    if (end == buff) {
        *err = -1; //< not a decimal number

    } else if ('\0' != *end) {
        *err = -2; //< extra characters at end of input

    } else if ((LONG_MIN == sl || LONG_MAX == sl) && ERANGE == errno) {
        *err = -3; //< out of range of type long

    } else if (sl > INT_MAX) {
        *err = -4; //< greater than INT_MAX

    } else if (sl < INT_MIN) {
        *err = -5; //< less than INT_MIN

    } else {
        *err = 0; //< ok
    }
    return sl;
}

vstrsepf.h The string parser

/*
 * `vstrsepf` is string parsing utility function born from
 * the idea of using `strsep` with a `sscanf` interface.
 *
 * It is designed to be a safer `scanf` for embedded system application.
 *
 * Some key design  choices:
 * - Will destroy its input string (by adding '\0').
 * - No dynamic memory allocation
 * - No string copy, only pointer address are changed.
 * - No floating point support.
 *
 * ARGUMENTS:
 *  @param: mutStr - mutable input string.
 *  @param: fmt    - format string.
 *  @param: arg    - Aguments lists (va_list)
 *
 * FORMAT SPECIFIER:
 *
 * | Specifier   | Descriptions                                                    |
 * |-------------|-----------------------------------------------------------------|
 * | %i, %d, %u, | Any number of digits*                                           |
 * | %o          | Any number of octal digits (0-7)*                               |
 * | %x          | Any number of hexadecimal digits (0-9, a-f, A-F*                |
 * | %%          | A % followed by another % matches a single %.                   |
 * | %s          | A string with any character in it. A terminating null character |
 * |             | is automatically added at the end of the stored sequence.#      |
 *
 * FORMAT OPTIONAL SPECIFIER:
 *
 * | O Specifier | Descriptions                                                    |
 * |-------------|-----------------------------------------------------------------|
 * | *           | An optional starting asterisk indicates that the data is to be  |
 * |             | read from the stream but ignored.                               |
 * | width       | Specifies the maximum number of characters to be read in the    |
 * |             | current reading operation.                                      |
 *
 *
 */
int16_t
vstrsepf(char mutStr[], char const* fmt, va_list arg)
{
    if (mutStr == NULL || fmt == NULL) {
        return -1;
    }

#define SUPPORTED_SPECIFIER "dibouxs%"

    int count = 0;
    while (*mutStr && *fmt) {

        // printf("fmt = %c,  s=%c\n", *fmt, *mutStr);
        if (*fmt == '%') {
            fmt++;

            // A format specifier follows this prototype: [=%[*][width][modifiers]type=]
            char   specifierType = '\0';
            bool   noAssignFlag = false;
            size_t width = 0;
            for (; *fmt; fmt++) {
                if (strchr(SUPPORTED_SPECIFIER, *fmt)) {
                    specifierType = *fmt;
                    fmt++;
                    break; //< Specifier type is always the last element of a specifier string
                }

                if (*fmt == '*') {
                    noAssignFlag = true;

                } else if (*fmt >= '1' && *fmt <= '9') {
                    char const* tc;
                    for (tc = fmt; isdigit(*fmt); fmt++)
                        ;
                    width = strtol(tc, (char**)&fmt, 10);
                    fmt--;
                }
            }

            if (specifierType == '%') {
                mutStr++; //< `%%` is just the `%` character.
                continue;
            }

            char*      token;
            const bool continueUntilTheEnd = (*fmt == '\0');
            if (continueUntilTheEnd) {
                token = mutStr;
            } else {
                char termination[] = { *fmt, '\0' };
                token = strsep(&mutStr, termination);
                if (token == NULL) {
                    return -1; //< no token found
                }
                fmt++;
            }

            if (noAssignFlag) {
                continue; //< ignore it.
            }

            if (width > 0) {
                if (width < strlen(token)) {
                    return -1;
                }
            }

            int8_t strtolErr = 0;
            switch (specifierType) {
                /*********************************
                 * Scan string
                 *********************************/
                case 's': {
                    char** ptr = va_arg(arg, char**);
                    if (ptr == NULL) {
                        return -1;
                    }
                    *ptr = token;
                    count++;
                } break;

                /*********************************
                 * Scan int
                 *********************************/
                case 'd':
                /* FALLTHROUGH */
                case 'i':
                /* FALLTHROUGH */
                case 'u': {
                    uint8_t   base = 10;
                    uint32_t* ptr = va_arg(arg, uint32_t*);
                    if (ptr == NULL) {
                        return -1;
                    }
                    *ptr = strtol_s(token, base, &strtolErr);
                    if (strtolErr < 0) {
                        return -1;
                    }
                    count++;
                } break;

                case 'x': {
                    uint8_t   base = 16;
                    uint32_t* ptr = va_arg(arg, uint32_t*);
                    if (ptr == NULL) {
                        return -1;
                    }
                    *ptr = strtol_s(token, base, &strtolErr);
                    if (strtolErr < 0) {
                        return -1;
                    }
                    count++;
                } break;

                case 'o': {
                    uint8_t   base = 8;
                    uint32_t* ptr = va_arg(arg, uint32_t*);
                    if (ptr == NULL) {
                        return -1;
                    }
                    *ptr = strtol_s(token, base, &strtolErr);
                    if (strtolErr < 0) {
                        return -1;
                    }
                    count++;
                } break;

                case 'b': {
                    uint8_t   base = 2;
                    uint32_t* ptr = va_arg(arg, uint32_t*);
                    if (ptr == NULL) {
                        return -1;
                    }
                    *ptr = strtol_s(token, base, &strtolErr);
                    if (strtolErr < 0) {
                        return -1;
                    }
                    count++;
                } break;

                /*********************************
                 * We don't know.
                 *********************************/
                default: {
                    return -1;
                }
            }
        } else { /* !(*fmt == '%') */
            if (*fmt != *mutStr) {
                return -1;
            } else {
                fmt++;
                mutStr++;
            }

        } // END if (*fmt == '%')

    } // END while (*mutStr && *fmt)
    return count;
}

strsepf.h An arguments wrapper

int16_t
strsepf(char mutStr[], char const* fmt, ...)
{
    int16_t rc;
    va_list arg;
    va_start(arg, fmt);
    rc = vstrsepf(mutStr, fmt, arg);
    va_end(arg);
    return rc;
}

While robustness and speed are my priorities, all suggestions are welcome.

\$\endgroup\$
5
  • 1
    \$\begingroup\$ I see that strtol_s returns a highly specific error message and vstrsepf just projects it to a binary and returns -1 on error. Could you think of a way of including that information? \$\endgroup\$ – Neil Nov 9 '19 at 20:58
  • \$\begingroup\$ Great point. To be honest, I mostly reused strtol_s from an old project. I could return better info about what failed with an enum. \$\endgroup\$ – gberth Nov 9 '19 at 21:56
  • \$\begingroup\$ I would strongly recommend to never design APIs revolving around variable arguments. The printf/scanf familes of functions are some of the buggiest, most dangerous functions - not just in in the history of C - but in the history of programming. It is not something you should look at for inspiration! Variable argument functions is a completely superfluous feature, originating from a time in the 1960-1970s where programming languages needed to brag about having lots of features, long before good programming practice was invented and established. \$\endgroup\$ – Lundin Dec 2 '19 at 14:00
  • \$\begingroup\$ @Lundin While I do see the problem associated with variable arguments function, it seems like a valuable feature for building flexible API in c. Do you have an alternative in mind ? \$\endgroup\$ – gberth Jan 5 '20 at 18:09
  • \$\begingroup\$ @gberth Pretty much all major C APIs out there use pointers to struct. And if needed, the structs can implement some manner of polymorphism. \$\endgroup\$ – Lundin Jan 7 '20 at 7:47
3
\$\begingroup\$

Small review

Bug

case 'u': { ... *ptr = strtol_s(token, base, &strtolErr); can form the wrong value as strtol() can limit values to 231-1. Same with 'x'. Better to use strtoul().

Type conflagration

long strtol_s() looks like a function to convert a string to long yet it errors when out of int range and is curiously used in the code only for uint32_t.

I'd expect a uin32_t strtou32_s() or the like. Maybe something like strto_subrange()?

Reduce error values

In strtol_s(), as (LONG_MIN == sl || LONG_MAX == sl) && ERANGE == errno generates the same error value, I'd expect sl > INT_MAX and sl < INT_MIN to generate one same error value.

Error checking omission?

Interesting width = strtol(tc, (char**)&fmt, 10); lacks range checking.

\$\endgroup\$
1
  • \$\begingroup\$ Thanks i will make sure to improve it. \$\endgroup\$ – gberth Dec 1 '19 at 20:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.