3
\$\begingroup\$

I'm trying to write an equivalent of PHP's htmlspecialchars() in C. What is my implementation missing or doing wrong?

char www_specialchars_html(char *dest, char *dest_last, char *src) {
while (*src != 0) {
    switch(*src) {
        //If a special character is found, replace with its HTML entity.
        case '&':
            if ((dest + 5) > dest_last) {
                return 1;
            }
            *dest = '&';
            dest++;
            *dest = 'a';
            dest++;
            *dest = 'm';
            dest++;
            *dest = 'p';
            dest++;
            *dest = ';';
            dest++;
            src++;
            break;
        case '"':
            if ((dest + 6) > dest_last) {
                return 1;
            }
            *dest = '&';
            dest++;
            *dest = 'q';
            dest++;
            *dest = 'u';
            dest++;
            *dest = 'o';
            dest++;
            *dest = 't';
            dest++;
            *dest = ';';
            dest++;
            src++;
            break;
        case '\'':
            if ((dest + 6) > dest_last) {
                return 1;
            }
            *dest = '&';
            dest++;
            *dest = '#';
            dest++;
            *dest = '0';
            dest++;
            *dest = '3';
            dest++;
            *dest = '9';
            dest++;
            *dest = ';';
            dest++;
            src++;
            break;
        case '<':
            if ((dest + 4) > dest_last) {
                return 1;
            }
            *dest = '&';
            dest++;
            *dest = 'l';
            dest++;
            *dest = 't';
            dest++;
            *dest = ';';
            dest++;
            src++;
            break;
        case '>':
            if ((dest + 4) > dest_last) {
                return 1;
            }
            *dest = '&';
            dest++;
            *dest = 'g';
            dest++;
            *dest = 't';
            dest++;
            *dest = ';';
            dest++;
            src++;
            break;
        case '\\': //Not required in HTML.
            if ((dest) > dest_last) {
                return 1;
            }
            src++;
            break;
        default:
            //Default for characters that don't need to be replaced.
            if ((dest + 1) > dest_last) {
                return 1;
            }
            *dest = *src;
            src++;
            dest++;
            break;
    }
}
*dest = 0;
return 0;
}

The function takes pointers to the destination, the last element of the destination and the source. It returns 1 if it can't write to the destination, and returns 0 when it has successfully copied to the destination. I'm still a relatively new C programmer.

\$\endgroup\$
  • \$\begingroup\$ Can you explain the case that handles the backslash character? \$\endgroup\$ – 200_success Oct 27 '18 at 22:07
  • \$\begingroup\$ @200_success I had put in the backslash case so that I could use the same function to escape SQL queries, but I'm pretty sure it's not required in HTML. I'll update the code to reflect that. \$\endgroup\$ – wispi Oct 27 '18 at 22:13
  • \$\begingroup\$ @wispi For what it's worth, PHP is implemented mostly in C. For example, here's the PHP declaration of htmlspecialchars(). You can then follow the function calls to see how they do it. If you're goal is to write an equivalent implementation, it would be a lot more work because of all that htmlspecialchars() does \$\endgroup\$ – esote Oct 27 '18 at 23:06
2
\$\begingroup\$
  • You shouldn't pass a char* as buffer end but a size_t size corresponding to the size of the allocated buffer. So your available length will be size - 2 ("zero-indexed" and "trailing '\0'").

  • You return either 1 or 0, why don't using bool as return type? (Or maybe a size_t corresponding to the final string length, but, that's a design decision).

  • Variables where value doesn't change in your function (as locales or parameters) should be marked const. That's not faster but it show your intends.

  • You can maybe use strcpy, strncpy or memcpy to ... copy the replacements string into destination (and replace multiple incrementation by a single add-assign).

  • For non-special chars sequences, you can maybe get them in chunk by chunk (between two special chars) with strcspn or strpbrk. (and after, copying them with same functions than former)


Edit

This is a raw try using theses functions.

I think it is safe, with no overflow, but maybe not the optimal implement. Don't be scared about the size, there is a lot of comments :)

I outed two functions from source (strlenand replace_chrstr), 'cause I think they are reusable somewhere.

#include <string.h>

// Get the position of the c char in the str string.
// -> str: a null-terminated string where to lookup
// -> needle: char to find 
// Return: Position of the char `c` or 1 past the end (strlen + 1)
size_t strpos(const char* const str, const char c)
{
    char* p = strchr(str, c);
    return p ? (p - str) : strlen(str) + 1;
}

// Scan `in_str` and replace all char occurrences from `search` 
// by corresponding string from `replace`, then place the result in `out_str`.
// -> search: A null-terminated string with all char to fiend
// -> replace: An array of null-terminated strings with index based replacement from `search`
// -> out_str: a buffer to put the result, at least as big than max_size
// -> in_str: a null-terminated string where to process replacements
// Return: length of resulting string, or length+1 in case of overflow
size_t replace_chrstr(const char* const search, const char* const* const replace,
                      char *const out_str, const char* const in_str, const size_t max_size)
{
    size_t in_offset = 0;
    size_t out_offset = 0;
    size_t index;
    size_t length;
    const size_t index_max = strlen(search);
    const size_t length_max = strlen(in_str);

    while (max_size > out_offset) {
        length = strcspn(in_str + in_offset, search);
        // buffer overflow detected, here we truncate the input string
        // we add `\0` and return max_size (equivalent to strlen + 1)
        // but maybe you want another handling
        if (max_size <= out_offset + length) {
            strncpy(out_str + out_offset, in_str + in_offset, max_size-out_offset-1);
            out_str[max_size-1] = '\0';
            return max_size;
        }
        strncpy(out_str + out_offset, in_str + in_offset, length);
        in_offset += length + 1;
        out_offset += length;
        // normal end of input string
        // add a `\0`, then return out_offset (equivalent to strlen)
        if (in_str[in_offset-1] == '\0') {
            out_str[out_offset] = '\0';
            return out_offset;
        }
        index = strpos(search, in_str[in_offset-1]);
        length = strlen(replace[index]);
        // buffer overflow detected, here we truncate the replacement string
        // we add `\0` and return max_size (equivalent to strlen + 1)
        // but maybe you want another handling        
        if (max_size <= out_offset + length) {
            strncpy(out_str + out_offset, replace[index], max_size-out_offset-1);
            out_str[max_size-1] = '\0';            
            return max_size;
        }
        strcpy(out_str + out_offset, replace[index]);
        out_offset += length;
    }
    // we never reach here, normally
    return 0;
}

size_t www_specialchars_html(char* const dest, const char* const source, size_t max_size)
{
    const char search[] = "&\"'<>";
    const char * replace[] = {"&amp;","&quot;","&#039;","&lt;","&gt;"}; 
    return replace_chrstr(search, replace, dest, source, max_size);
}
\$\endgroup\$
  • \$\begingroup\$ Using bool would require an extra #include <stdbool.h>, it's not uncommon to just use int in C for holding boolean values. \$\endgroup\$ – esote Oct 27 '18 at 23:14
  • \$\begingroup\$ I know, but what's the problem about including it? \$\endgroup\$ – Calak Oct 27 '18 at 23:20
  • \$\begingroup\$ As an aside, you might want to expound on the calculations for remaining space: Arbitrary pointer-arithmetic (meaning the displacement is not zero, nor start and end point into or at the end of the same array) is not allowed. \$\endgroup\$ – Deduplicator Oct 27 '18 at 23:27
  • \$\begingroup\$ @Calak Nothing wrong with including it. Using it would be overkill unless they use lots of boolean values elsewhere I suppose. Your answer is good in my opinion. \$\endgroup\$ – esote Oct 27 '18 at 23:45
  • \$\begingroup\$ @Deduplicator What do you mean by arbitrary pointer-arithmetic? \$\endgroup\$ – wispi Oct 27 '18 at 23:47

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.