Use <cctype>
for character classification
Instead of defining is_hex()
(and depending on abcdef
and ABCDEF
being contiguously coded), better to use the standard isxdigit()
.
get_nibble
makes a similar assumption - it's arguably safer to use a switch
for this.
On the whole, the logic looks good. I'm not a fan of names beginning with _
- even if you do fully understand the rules about reserved identifiers, many of your readers will not!
Some expressions can be simplified with post-increment. Consider this fragment:
if (!_err)
tmp[_len] = esc, ++_len;
} else {
tmp[_len] = *in; ++in, ++_len;
}
That could be
if (!_err)
tmp[_len++] = esc;
} else {
tmp[_len++] = *in++;
}
That's fairly idiomatic usage, and should be familiar to anyone who's looked at strcpy
or the like. In fact, slightly more idiomatic would be to use a pointer, rather than indexing:
char *post_string(char *const in, size_t *len, const char **err) {
if (!in || !len)
return NULL;
char *out = in, *p = in;
const char *int_err = NULL;
while (*p && !int_err) {
if (*p != '\\') {
/* normal case */
*out++ = *p++;
} else {
/* escape sequence */
switch (*++p) {
case 'a': *out++ = '\a'; ++p; break;
case 'b': *out++ = '\b'; ++p; break;
case 'f': *out++ = '\f'; ++p; break;
case 'n': *out++ = '\n'; ++p; break;
case 'r': *out++ = '\r'; ++p; break;
case 't': *out++ = '\t'; ++p; break;
case 'v': *out++ = '\v'; ++p; break;
case '"': case '\'': case '\\': *out++ = *p++; case '?': break;
case 'x': case 'X':
if (!isxdigit(p[1]) || !isxdigit(p[2])) {
int_err = "Invalid character on hexadecimal escape.";
} else {
*out++ = (char)(get_nibble(p[1]) * 0x10 + get_nibble(p[2]));
p += 3;
}
break;
default:
int_err = "Unexpected '\\' with no escape sequence."; break;
}
}
}
*out = '\0'; /* Set the end of string. */
*len = p - in; /* Set the final string length. */
if (int_err && err) /* Set error message back. */
*err = int_err;
return in;
}
Testing
Some of the code appears completely untested (and I've fixed above). In particular, passing NULL
as the err
parameter - as in the sample main()
- will provoke undefined behaviour if your string is invalid).
Modified code
I've also added code to accept 1-3 octal digits, and modified the hexadecimal reader to accept 1+ digits, and included tests of each error branch.
#include <ctype.h>
#include <limits.h>
#include <stddef.h>
static unsigned int get_nibble(char c)
{
switch(c) {
case '0': return 0;
case '1': return 1;
case '2': return 2;
case '3': return 3;
case '4': return 4;
case '5': return 5;
case '6': return 6;
case '7': return 7;
case '8': return 8;
case '9': return 9;
case 'A': case 'a': return 0xa;
case 'B': case 'b': return 0xb;
case 'C': case 'c': return 0xc;
case 'D': case 'd': return 0xd;
case 'E': case 'e': return 0xe;
case 'F': case 'f': return 0xf;
}
/* shouldn't happen */
return 0;
}
char *post_string(char *const in, size_t *len, const char **err) {
if (!in || !len)
return NULL;
char *out = in, *p = in;
const char *int_err = NULL;
while (*p && !int_err) {
if (*p != '\\') {
/* normal case */
*out++ = *p++;
} else {
/* escape sequence */
switch (*++p) {
case 'a': *out++ = '\a'; ++p; break;
case 'b': *out++ = '\b'; ++p; break;
case 'f': *out++ = '\f'; ++p; break;
case 'n': *out++ = '\n'; ++p; break;
case 'r': *out++ = '\r'; ++p; break;
case 't': *out++ = '\t'; ++p; break;
case 'v': *out++ = '\v'; ++p; break;
case '"': case '\'': case '\\': case '?': *out++ = *p++; break;
case 'x': case 'X':
if (!isxdigit(*++p)) {
int_err = "Invalid character on hexadecimal escape."; break;
} else {
unsigned int x = 0; /* assuming int is larger than char */
while (isxdigit(*p)) {
x *= 0x10;
x += get_nibble(*p++);
if (x > UCHAR_MAX) {
int_err = "Out of range hexadecimal escape."; break;
}
}
*out++ = (char)x; break;
}
default:
if (isdigit(*p) && *p < '8') {
/* octal escape */
unsigned int x = 0; /* assuming int is larger than char */
for (int i = 0; i < 3 && isdigit(*p) && *p < '8'; ++i) {
x = x * 010 + (unsigned)*p++ - '0';
}
if (x > UCHAR_MAX) {
int_err = "Out of range octal escape."; break;
}
*out++ = (char)x; break;
} else {
int_err = "Unexpected '\\' with no escape sequence."; break;
}
}
}
}
*out = '\0'; /* Set the end of string. */
*len = (size_t)(p - in); /* Set the final string length. */
if (int_err && err) /* Set error message back. */
*err = int_err;
return in;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
char t[256] = "This is a \\\"string\\\" with \\x43 \\X063 hex\\"
"nnewline\\ttab\\t\\?\\'\\\"\\\\octal\\132\\538 \\0null";
size_t len = 0;
printf("Before: %s\n", t);
post_string(t, &len, NULL);
printf("After : %s\n\n", t);
const char *err = NULL;
strcpy(t, "This has a hex error: \\xg");
printf("Before: %s\n", t);
post_string(t, &len, &err);
printf("Error: %s\n\n", err);
strcpy(t, "Hex beyond ASCII: \\xfff");
printf("Before: %s\n", t);
post_string(t, &len, &err);
printf("Error: %s\n\n", err);
strcpy(t, "Octal beyond ASCII: \\777");
printf("Before: %s\n", t);
post_string(t, &len, &err);
printf("Error: %s\n\n", err);
strcpy(t, "not an escape: \\9");
printf("Before: %s\n", t);
post_string(t, &len, &err);
printf("Error: %s\n\n", err);
strcpy(t, "backslash at end: \\");
printf("Before: %s\n", t);
post_string(t, &len, &err);
printf("Error: %s\n\n", err);
}
\a
,\v
,\f
,\"
\u
and\U
? \$\endgroup\$\"
is already handled, Unicode support will come later. I just need ASCII characters for the moment, but didn't know about\a
or\f
. \$\endgroup\$\"
(spotted it when writing my answer). Here's a full list! \$\endgroup\$