Commit message: Fixed String.prototype.trimEnd() with unicode string.

Previously, when the method was invoked with a string consisting of space
characters and at least one of them was a Unicode space separator (code
point above 127) it returned invalid string value with non-zero size
but zero length.

The fix is to update the size of the resulting string appropriately.

This closes #569 issue on Github.
CVE: None
CWE: CWE-125
Vulnerability description: None

Project: njs
File: njs_string.c

njs_string_to_number(const njs_value_t *value, njs_bool_t parse_float)
{
    double             num;
    njs_bool_t         minus;
    const u_char       *p, *start, *end;
    njs_string_prop_t  string;

    const size_t  infinity = njs_length("Infinity");

    (void) njs_string_trim(value, &string, NJS_TRIM_START);

    p = string.start;
    end = p + string.size;

    if (p == end) {
        return parse_float ? NAN : 0.0;
    }

    minus = 0;

    if (*p == '+') {
        p++;

    } else if (*p == '-') {
        p++;
        minus = 1;
    }

    if (p == end) {
        return NAN;
    }

    if (!parse_float
        && p + 2 < end && p[0] == '0' && (p[1] == 'x' || p[1] == 'X'))
    {
        p += 2;
        num = njs_number_hex_parse(&p, end, 0);

    } else {
        start = p;
        num = njs_number_dec_parse(&p, end, 0);

        if (p == start) {
            if (p + infinity > end || memcmp(p, "Infinity", infinity) != 0) {
                return NAN;
            }

            num = INFINITY;
            p += infinity;
        }
    }

    if (!parse_float) {
        while (p < end) {
            if (*p != ' ' && *p != '\t') {
                return NAN;
            }

            p++;
        }
    }

    return minus ? -num : num;
}

Project: njs
File: njs_string.c

njs_chb_write_byte_as_utf8(njs_chb_t *chain, u_char byte)
{
    njs_utf8_encode(njs_chb_current(chain), byte);
    njs_chb_written(chain, njs_utf8_size(byte));
}

Project: njs
File: njs_string.c

njs_string_prototype_from_utf8(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t unused)
{
    ssize_t            length;
    njs_int_t          ret;
    njs_slice_prop_t   slice;
    njs_string_prop_t  string;

    njs_deprecated(vm, "String.prototype.fromUTF8()");

    ret = njs_string_object_validate(vm, njs_argument(args, 0));
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    ret = njs_string_slice_prop(vm, &string, &slice, args, nargs);
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    if (string.length != 0) {
        /* ASCII or UTF8 string. */
        return njs_string_slice(vm, &vm->retval, &string, &slice);
    }

    string.start += slice.start;

    length = njs_utf8_length(string.start, slice.length);

    if (length >= 0) {
        return njs_string_new(vm, &vm->retval, string.start, slice.length,
                              length);
    }

    vm->retval = njs_value_null;

    return NJS_OK;
}

Project: njs
File: njs_string.c

njs_string_prototype_to_utf8(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused)
{
    njs_int_t          ret;
    njs_slice_prop_t   slice;
    njs_string_prop_t  string;

    njs_deprecated(vm, "String.prototype.toUTF8()");

    ret = njs_string_object_validate(vm, njs_argument(args, 0));
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    (void) njs_string_prop(&string, njs_argument(args, 0));

    string.length = 0;
    slice.string_length = string.size;

    ret = njs_string_slice_args(vm, &slice, args, nargs);
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    return njs_string_slice(vm, &vm->retval, &string, &slice);
}

Project: njs
File: njs_string.c

njs_string_btoa(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused)
{
    u_char                *dst;
    size_t                len, length;
    uint32_t              cp0, cp1, cp2;
    njs_int_t             ret;
    njs_value_t           *value, lvalue;
    const u_char          *p, *end;
    njs_string_prop_t     string;
    njs_unicode_decode_t  ctx;

    value = njs_lvalue_arg(&lvalue, args, nargs, 1);

    ret = njs_value_to_string(vm, value, value);
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    len = njs_string_prop(&string, value);

    p = string.start;
    end = string.start + string.size;

    njs_utf8_decode_init(&ctx);

    length = njs_base64_encoded_length(len);

    dst = njs_string_alloc(vm, &vm->retval, length, length);
    if (njs_slow_path(dst == NULL)) {
        return NJS_ERROR;
    }

    while (len > 2 && p < end) {
        cp0 = njs_utf8_decode(&ctx, &p, end);
        cp1 = njs_utf8_decode(&ctx, &p, end);
        cp2 = njs_utf8_decode(&ctx, &p, end);

        if (njs_slow_path(cp0 > 0xff || cp1 > 0xff || cp2 > 0xff)) {
            goto error;
        }

        *dst++ = njs_basis64_enc[cp0 >> 2];
        *dst++ = njs_basis64_enc[((cp0 & 0x03) << 4) | (cp1 >> 4)];
        *dst++ = njs_basis64_enc[((cp1 & 0x0f) << 2) | (cp2 >> 6)];
        *dst++ = njs_basis64_enc[cp2 & 0x3f];

        len -= 3;
    }

    if (len > 0) {
        cp0 = njs_utf8_decode(&ctx, &p, end);
        if (njs_slow_path(cp0 > 0xff)) {
            goto error;
        }

        *dst++ = njs_basis64_enc[cp0 >> 2];

        if (len == 1) {
            *dst++ = njs_basis64_enc[(cp0 & 0x03) << 4];
            *dst++ = '=';
            *dst++ = '=';

        } else {
            cp1 = njs_utf8_decode(&ctx, &p, end);
            if (njs_slow_path(cp1 > 0xff)) {
                goto error;
            }

            *dst++ = njs_basis64_enc[((cp0 & 0x03) << 4) | (cp1 >> 4)];
            *dst++ = njs_basis64_enc[(cp1 & 0x0f) << 2];
            *dst++ = '=';
        }

    }

    return NJS_OK;

error:

    njs_type_error(vm, "invalid character (>= U+00FF)");

    return NJS_ERROR;
}

Project: njs
File: njs_string.c

njs_encode_base64(njs_str_t *dst, const njs_str_t *src)
{
    njs_encode_base64_core(dst, src, njs_basis64_enc, 1);
}

Project: njs
File: njs_string.c

njs_string_atob(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused)
{
    size_t        i, n, len, pad;
    u_char        *dst, *tmp, *p;
    ssize_t       size;
    njs_str_t     str;
    njs_int_t     ret;
    njs_chb_t     chain;
    njs_value_t   *value, lvalue;
    const u_char  *b64, *s;

    value = njs_lvalue_arg(&lvalue, args, nargs, 1);

    ret = njs_value_to_string(vm, value, value);
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    /* Forgiving-base64 decode. */

    b64 = njs_basis64;
    njs_string_get(value, &str);

    tmp = njs_mp_alloc(vm->mem_pool, str.length);
    if (tmp == NULL) {
        njs_memory_error(vm);
        return NJS_ERROR;
    }

    p = tmp;

    for (i = 0; i < str.length; i++) {
        if (njs_slow_path(str.start[i] == ' ')) {
            continue;
        }

        *p++ = str.start[i];
    }

    pad = 0;
    str.start = tmp;
    str.length = p - tmp;

    if (str.length % 4 == 0) {
        if (str.length > 0) {
            if (str.start[str.length - 1] == '=') {
                pad += 1;
            }

            if (str.start[str.length - 2] == '=') {
                pad += 1;
            }
        }

    } else if (str.length % 4 == 1) {
        goto error;
    }

    for (i = 0; i < str.length - pad; i++) {
        if (njs_slow_path(b64[str.start[i]] == 77)) {
            goto error;
        }
    }

    len = njs_base64_decoded_length(str.length, pad);

    njs_chb_init(&chain, vm->mem_pool);

    dst = njs_chb_reserve(&chain, len * 2);
    if (njs_slow_path(dst == NULL)) {
        njs_memory_error(vm);
        return NJS_ERROR;
    }

    n = len;
    s = str.start;

    while (n >= 3) {
        njs_chb_write_byte_as_utf8(&chain, b64[s[0]] << 2 | b64[s[1]] >> 4);
        njs_chb_write_byte_as_utf8(&chain, b64[s[1]] << 4 | b64[s[2]] >> 2);
        njs_chb_write_byte_as_utf8(&chain, b64[s[2]] << 6 | b64[s[3]]);

        s += 4;
        n -= 3;
    }

    if (n >= 1) {
        njs_chb_write_byte_as_utf8(&chain, b64[s[0]] << 2 | b64[s[1]] >> 4);
    }

    if (n >= 2) {
        njs_chb_write_byte_as_utf8(&chain, b64[s[1]] << 4 | b64[s[2]] >> 2);
    }

    size = njs_chb_size(&chain);
    if (njs_slow_path(size < 0)) {
        njs_memory_error(vm);
        return NJS_ERROR;
    }

    if (size == 0) {
        njs_value_assign(&vm->retval, &njs_string_empty);
        return NJS_OK;
    }

    dst = njs_string_alloc(vm, &vm->retval, size, len);
    if (njs_slow_path(dst == NULL)) {
        return NJS_ERROR;
    }

    njs_chb_join_to(&chain, dst);
    njs_chb_destroy(&chain);

    njs_mp_free(vm->mem_pool, tmp);

    return NJS_OK;

error:

    njs_type_error(vm, "the string to be decoded is not correctly encoded");

    return NJS_ERROR;
}

Project: njs
File: njs_string.c

njs_string_trim(const njs_value_t *value, njs_string_prop_t *string,
    unsigned mode)
{
    uint32_t              cp, trim;
    const u_char          *p, *prev, *start, *end;
    njs_unicode_decode_t  ctx;

    trim = 0;

    njs_string_prop(string, value);

    start = string->start;
    end = string->start + string->size;

    if (njs_is_byte_or_ascii_string(string)) {

        if (mode & NJS_TRIM_START) {
            for ( ;; ) {
                if (start == end) {
                    break;
                }

                if (njs_is_whitespace(*start)) {
                    start++;
                    trim++;
                    continue;
                }

                break;
            }
        }

        if (mode & NJS_TRIM_END) {
            for ( ;; ) {
                if (start == end) {
                    break;
                }

                end--;

                if (njs_is_whitespace(*end)) {
                    trim++;
                    continue;
                }

                end++;
                break;
            }
        }

    } else {
        /* UTF-8 string. */

        if (mode & NJS_TRIM_START) {
            njs_utf8_decode_init(&ctx);

            for ( ;; ) {
                if (start == end) {
                    break;
                }

                p = start;
                cp = njs_utf8_decode(&ctx, &start, end);

                if (njs_utf8_is_whitespace(cp)) {
                    trim++;
                    continue;
                }

                start = p;
                break;
            }
        }

        if (mode & NJS_TRIM_END) {
            prev = end;

            njs_utf8_decode_init(&ctx);

            for ( ;; ) {
                if (start == prev) {
                    end = prev;
                    break;
                }

                prev = njs_utf8_prev(prev);
                p = prev;
                cp = njs_utf8_decode(&ctx, &p, end);

                if (njs_utf8_is_whitespace(cp)) {
                    trim++;
                    continue;
                }

                end = p;
                break;
            }
        }
    }

    if (start == end) {
        string->length = 0;
        string->size = 0;
        return trim;
    }

    string->start = (u_char *) start;
    string->size = end - start;

    if (string->length != 0) {
        string->length -= trim;
    }

    return trim;
}

Project: njs
File: njs_string.c

njs_string_prototype_trim(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t mode)
{
    uint32_t           trim;
    njs_int_t          ret;
    njs_value_t        *value;
    njs_string_prop_t  string;

    value = njs_argument(args, 0);
    ret = njs_string_object_validate(vm, value);
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    trim = njs_string_trim(value, &string, mode);

    if (trim == 0) {
        njs_value_assign(&vm->retval, value);
        return NJS_OK;
    }

    if (string.size == 0) {
        njs_value_assign(&vm->retval, &njs_string_empty);
        return NJS_OK;
    }

    return njs_string_new(vm, &vm->retval, string.start, string.size,
                          string.length);
}

Project: njs
File: njs_string.c

njs_string_bytes_from(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused)
{
    njs_value_t  *value;

    njs_deprecated(vm, "String.bytesFrom()");

    value = njs_arg(args, nargs, 1);

    if (njs_is_string(value)) {
        return njs_string_bytes_from_string(vm, value, njs_arg(args, nargs, 2));

    } else if (njs_is_object(value)) {

        if (njs_is_object_string(value)) {
            value = njs_object_value(value);
            return njs_string_bytes_from_string(vm, value,
                                                njs_arg(args, nargs, 2));
        }

        return njs_string_bytes_from_array_like(vm, value);
    }

    njs_type_error(vm, "value must be a string or array-like object");

    return NJS_ERROR;
}

Project: njs
File: njs_string.c

njs_encode_base64url(njs_str_t *dst, const njs_str_t *src)
{
    njs_encode_base64_core(dst, src, njs_basis64url_enc, 0);
}

Project: njs
File: njs_string.c

njs_string_prototype_to_bytes(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused)
{
    u_char                *p;
    size_t                length;
    uint32_t              byte;
    njs_int_t             ret;
    const u_char          *s, *end;
    njs_slice_prop_t      slice;
    njs_string_prop_t     string;
    njs_unicode_decode_t  ctx;

    njs_deprecated(vm, "String.prototype.toBytes()");

    ret = njs_string_object_validate(vm, njs_argument(args, 0));
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    ret = njs_string_slice_prop(vm, &string, &slice, args, nargs);
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    if (string.length == 0) {
        /* Byte string. */
        return njs_string_slice(vm, &vm->retval, &string, &slice);
    }

    p = njs_string_alloc(vm, &vm->retval, slice.length, 0);

    if (njs_fast_path(p != NULL)) {

        if (string.length != string.size) {
            /* UTF-8 string. */
            end = string.start + string.size;

            s = njs_string_offset(string.start, end, slice.start);

            length = slice.length;

            njs_utf8_decode_init(&ctx);

            while (length != 0 && s < end) {
                byte = njs_utf8_decode(&ctx, &s, end);

                if (njs_slow_path(byte > 0xFF)) {
                    njs_release(vm, &vm->retval);
                    vm->retval = njs_value_null;

                    return NJS_OK;
                }

                *p++ = (u_char) byte;
                length--;
            }

        } else {
            /* ASCII string. */
            memcpy(p, string.start + slice.start, slice.length);
        }

        return NJS_OK;
    }

    return NJS_ERROR;
}

Project: njs
File: njs_string.c

njs_string_prototype_from_bytes(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t unused)
{
    u_char             *p, *s, *start, *end;
    size_t             size;
    njs_int_t          ret;
    njs_slice_prop_t   slice;
    njs_string_prop_t  string;

    njs_deprecated(vm, "String.prototype.fromBytes()");

    ret = njs_string_object_validate(vm, njs_argument(args, 0));
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    ret = njs_string_slice_prop(vm, &string, &slice, args, nargs);
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    if (string.length != 0) {
        /* ASCII or UTF8 string. */
        return njs_string_slice(vm, &vm->retval, &string, &slice);
    }

    size = 0;
    string.start += slice.start;
    end = string.start + slice.length;

    for (p = string.start; p < end; p++) {
        size += (*p < 0x80) ? 1 : 2;
    }

    start = njs_string_alloc(vm, &vm->retval, size, slice.length);

    if (njs_fast_path(start != NULL)) {

        if (size == slice.length) {
            memcpy(start, string.start, size);

        } else {
            s = start;
            end = string.start + slice.length;

            for (p = string.start; p < end; p++) {
                s = njs_utf8_encode(s, *p);
            }
        }

        return NJS_OK;
    }

    return NJS_ERROR;
}