8 ex__nstr_8c
gullradriel edited this page 2026-10-29 22:06:20 +01:00

examples/ex_nstr.c

Functions

Name
int main(void )

Defines

Name
UTF8_V(str, ok)

Functions Documentation

function main

int main(
    void 
)

Macros Documentation

define UTF8_V

#define UTF8_V(
    str,
    ok
)
{                            \
    str, sizeof(str) - 1, ok \
}

Source code

/*
 * Nilorea Library
 * Copyright (C) 2005-2026 Castagnier Mickael
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * permissions and limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 */


#include "nilorea/n_str.h"
#include "nilorea/n_log.h"

int main(void) {
    set_log_level(LOG_DEBUG);

    char* chardest = NULL;
    NSTRBYTE written = 0,
             length = 0;

    write_and_fit(&chardest, &length, &written, "Hello");
    n_log(LOG_INFO, "charstr (%zu/%zu): %s", written, length, chardest);
    write_and_fit(&chardest, &length, &written, " ");
    n_log(LOG_INFO, "charstr (%zu/%zu): %s", written, length, chardest);
    write_and_fit(&chardest, &length, &written, "world !");
    n_log(LOG_INFO, "charstr (%zu/%zu): %s", written, length, chardest);
    write_and_fit(&chardest, &length, &written, "world ! ");
    n_log(LOG_INFO, "charstr (%zu/%zu): %s", written, length, chardest);
    write_and_fit(&chardest, &length, &written, "world ! ");
    n_log(LOG_INFO, "charstr (%zu/%zu): %s", written, length, chardest);
    write_and_fit(&chardest, &length, &written, "world ! ");
    n_log(LOG_INFO, "charstr (%zu/%zu): %s", written, length, chardest);

    Free(chardest);
    written = length = 0;

    write_and_fit_ex(&chardest, &length, &written, "Hello", 5, 0);
    n_log(LOG_INFO, "charstr (%zu/%zu): %s", written, length, chardest);
    write_and_fit_ex(&chardest, &length, &written, " ", 1, 0);
    n_log(LOG_INFO, "charstr (%zu/%zu): %s", written, length, chardest);
    write_and_fit_ex(&chardest, &length, &written, "world !", 7, 0);
    n_log(LOG_INFO, "charstr (%zu/%zu): %s", written, length, chardest);
    write_and_fit_ex(&chardest, &length, &written, "Hello", 5, 0);
    n_log(LOG_INFO, "charstr (%zu/%zu): %s", written, length, chardest);
    write_and_fit_ex(&chardest, &length, &written, " ", 1, 10);  // alocate 10 more byte if resize needed
    n_log(LOG_INFO, "charstr (%zu/%zu): %s", written, length, chardest);
    write_and_fit_ex(&chardest, &length, &written, "world !", 7, 0);
    n_log(LOG_INFO, "charstr (%zu/%zu): %s", written, length, chardest);

    Free(chardest);

    N_STR* nstr = NULL;

    n_log(LOG_INFO, "NULL str:%s", _nstr(nstr));

    nstrprintf(nstr, "Hello, file is %s line %d date %s", __FILE__, __LINE__, __TIME__);

    n_log(LOG_INFO, "str:%s", _nstr(nstr));

    nstrprintf_cat(nstr, " - This will be added at file %s line %d date %s", __FILE__, __LINE__, __TIME__);

    n_log(LOG_INFO, "str:%s", _nstr(nstr));

    free_nstr(&nstr);

    nstr = new_nstr(0);

    n_log(LOG_INFO, "EMPTY str:%s", _nstr(nstr));

    nstrprintf(nstr, "Hello, file is %s line %d date %s", __FILE__, __LINE__, __TIME__);

    n_log(LOG_INFO, "str:%s", _nstr(nstr));

    nstrprintf_cat(nstr, " - This will be added at file %s line %d date %s", __FILE__, __LINE__, __TIME__);

    n_log(LOG_INFO, "str:%s", _nstr(nstr));

    nstrprintf_cat(nstr, " - some more texte");

    N_STR* nstr2 = nstrdup(nstr);

    n_log(LOG_INFO, "str: %s\n str2: %s", _nstr(nstr), _nstr(nstr2));

    N_STR* nstr3 = NULL;

    nstrcat(nstr3, nstr);
    nstrcat(nstr3, nstr2);

    n_log(LOG_INFO, "str:%s", _nstr(nstr3));

    free_nstr(&nstr3);

    nstr3 = new_nstr(10);

    nstrcat(nstr3, nstr);
    nstrcat(nstr3, nstr2);

    n_log(LOG_INFO, "str:%s", _nstr(nstr3));

    free_nstr(&nstr);
    free_nstr(&nstr2);
    free_nstr(&nstr3);

    nstr = new_nstr(128);
    char data[1048576] = "";

    for (int it = 0; it < 1048575; it++) {
        data[it] = (char)(32 + rand() % 63);
    }
    data[1048574] = '\0';

    for (int it = 0; it < 100; it++) {
        write_and_fit(&nstr->data, &nstr->length, &nstr->written, data);
    }

    free_nstr(&nstr);

    /* test char_to_nstr and empty_nstr */
    nstr = char_to_nstr("char_to_nstr test");
    n_log(LOG_INFO, "char_to_nstr: %s", _nstr(nstr));
    empty_nstr(nstr);
    n_log(LOG_INFO, "after empty_nstr: '%s' written=%zu", _nstr(nstr), nstr->written);
    free_nstr(&nstr);

    /* test trim */
    char* trimmed = trim("   spaces around   ");
    if (trimmed) {
        n_log(LOG_INFO, "trim result: '%s'", trimmed);
        Free(trimmed);
    }
    /* test trim_nocopy */
    char trimbuf[] = "  hello  ";
    char* trimptr = trim_nocopy(trimbuf);
    n_log(LOG_INFO, "trim_nocopy result: '%s'", trimptr);

    /* test str_to_int, str_to_long, str_to_long_long */
    int ival = 0;
    if (str_to_int("12345", &ival, 10) == TRUE) {
        n_log(LOG_INFO, "str_to_int: %d", ival);
    }
    long int lval = 0;
    if (str_to_long("9876543210", &lval, 10) == TRUE) {
        n_log(LOG_INFO, "str_to_long: %ld", lval);
    }
    long long int llval = 0;
    if (str_to_long_long("123456789012345", &llval, 10) == TRUE) {
        n_log(LOG_INFO, "str_to_long_long: %lld", llval);
    }

    /* test strup, strlo */
    char upper[32] = "";
    char lower[32] = "";
    strup("hello world", upper);
    n_log(LOG_INFO, "strup: %s", upper);
    strlo("HELLO WORLD", lower);
    n_log(LOG_INFO, "strlo: %s", lower);

    /* test split, split_count, join, free_split_result */
    char** parts = split("one:two:three:four", ":", 0);
    if (parts) {
        int count = split_count(parts);
        n_log(LOG_INFO, "split count: %d", count);
        for (int it = 0; it < count; it++) {
            n_log(LOG_INFO, "  split[%d]: %s", it, parts[it]);
        }
        char* joined = join(parts, " - ");
        if (joined) {
            n_log(LOG_INFO, "join result: %s", joined);
            Free(joined);
        }
        free_split_result(&parts);
    }

    /* test wildmat, wildmatcase */
    n_log(LOG_INFO, "wildmat(\"hello.txt\", \"*.txt\"): %d", wildmat("hello.txt", "*.txt"));
    n_log(LOG_INFO, "wildmat(\"hello.txt\", \"*.csv\"): %d", wildmat("hello.txt", "*.csv"));
    n_log(LOG_INFO, "wildmatcase(\"Hello.TXT\", \"*.txt\"): %d", wildmatcase("Hello.TXT", "*.txt"));

    /* test str_natcasecmp: every pair must come out in the order listed */
    {
        const char* ordered[] = {"", "1", "01", "2", "10", "a", "A2",
                                 "a10", "a010", "b", "img2.cr3", "IMG10.cr3", "img10x", "photo 9",
                                 "photo 10", "x99999999999999999999", "x100000000000000000000"};
        size_t n = sizeof(ordered) / sizeof(ordered[0]);
        size_t k = 0, m = 0;

        for (k = 0; k < n; k++) {
            for (m = 0; m < n; m++) {
                int got = str_natcasecmp(ordered[k], ordered[m]);
                int want = k < m ? -1 : (k > m ? 1 : 0);

                if ((got < 0 ? -1 : (got > 0 ? 1 : 0)) != want) {
                    n_log(LOG_ERR, "FAIL: str_natcasecmp(\"%s\", \"%s\") gave %d, not %d", ordered[k], ordered[m], got,
                          want);
                    exit(1);
                }
            }
        }
        if (str_natcasecmp(NULL, "a") >= 0 || str_natcasecmp("a", NULL) <= 0 || str_natcasecmp(NULL, NULL) != 0 ||
            str_natcasecmp("abc", "ABC") == 0 || str_natncasecmp("img10xyz", 5, "img10", 5) != 0 ||
            str_natncasecmp("img2zz", 4, "img10", 5) >= 0 || str_natncasecmp("a1", 2, "a", 1) <= 0) {
            n_log(LOG_ERR, "FAIL: str_natcasecmp with NULL, case-only differences or bounded lengths");
            exit(1);
        }
        n_log(LOG_INFO, "PASS: str_natcasecmp orders %zu strings naturally", n);
    }

    /* test str_replace: every case checked, the example fails on any */
    {
        static const struct {
            const char* string;
            const char* substr;
            const char* replacement;
            const char* expected;
        } cases[] = {
            {"hello world hello", "hello", "bye", "bye world bye"},
            {"aaa", "a", "aa", "aaaaaa"}, /* the replacement is not searched again */
            {"aaaa", "aa", "b", "bb"},    /* occurrences do not overlap */
            {"abcabc", "abc", "", ""},    /* replaced by nothing */
            {"nothing here", "xyz", "q", "nothing here"},
            {"", "a", "b", ""},
            {"a=b=c", "=", " ", "a b c"}, /* what n_config_file asks of it */
            {"line\r\nnext\r\n", "\r", "", "line\nnext\n"},
            {"same", "", "x", "same"}, /* an empty search changes nothing */
        };
        size_t k = 0;

        for (k = 0; k < sizeof(cases) / sizeof(cases[0]); k++) {
            char* replaced = str_replace(cases[k].string, cases[k].substr, cases[k].replacement);

            if (!replaced || strcmp(replaced, cases[k].expected) != 0) {
                n_log(LOG_ERR, "FAIL: str_replace(\"%s\", \"%s\", \"%s\") gave \"%s\", not \"%s\"", cases[k].string,
                      cases[k].substr, cases[k].replacement, replaced ? replaced : "(null)", cases[k].expected);
                Free(replaced);
                exit(1);
            }
            Free(replaced);
        }
        {
            char* replaced = str_replace("keep", "k", NULL);

            if (!replaced || strcmp(replaced, "keep") != 0 || str_replace(NULL, "a", "b") != NULL) {
                n_log(LOG_ERR, "FAIL: str_replace with a NULL argument");
                Free(replaced);
                exit(1);
            }
            Free(replaced);
        }
        n_log(LOG_INFO, "str_replace: %zu cases ok", sizeof(cases) / sizeof(cases[0]));
    }

    /* test str_sanitize */
    char sanitize_buf[] = "hello; rm -rf /";
    str_sanitize(sanitize_buf, ";/", '_');
    n_log(LOG_INFO, "str_sanitize: %s", sanitize_buf);

    /* test file_to_nstr and nstr_to_file */
    N_STR* file_content = char_to_nstr("file content test\nsecond line\n");
    nstr_to_file(file_content, "nilorea_nstr_test.txt");
    free_nstr(&file_content);
    file_content = file_to_nstr("nilorea_nstr_test.txt");
    if (file_content) {
        n_log(LOG_INFO, "file_to_nstr: %s", _nstr(file_content));
        free_nstr(&file_content);
    }

    /* an empty (0-byte) file is a valid read: file_to_nstr must return an empty
       N_STR (data != NULL, written == 0), not NULL, and must log no error */
    FILE* empty_fp = fopen("nilorea_nstr_empty.txt", "wb");
    if (empty_fp)
        fclose(empty_fp);
    N_STR* empty_content = file_to_nstr("nilorea_nstr_empty.txt");
    if (empty_content && empty_content->data && empty_content->written == 0)
        n_log(LOG_INFO, "file_to_nstr on an empty file: OK (empty string, no error)");
    else
        n_log(LOG_ERR, "file_to_nstr on an empty file: FAILED (expected an empty N_STR)");
    if (empty_content)
        free_nstr(&empty_content);

    /* test skipw, skipu */
    char skipbuf[] = "   hello";
    NSTRBYTE it = 0;
    skipw(skipbuf, ' ', &it, 1);
    n_log(LOG_INFO, "skipw past spaces: pos=%zu char='%c'", it, skipbuf[it]);
    it = 0;
    skipu(skipbuf, 'h', &it, 1);
    n_log(LOG_INFO, "skipu to 'h': pos=%zu char='%c'", it, skipbuf[it]);

    /* test resize_nstr */
    nstr = new_nstr(16);
    n_log(LOG_INFO, "before resize: length=%zu", nstr->length);
    resize_nstr(nstr, 256);
    n_log(LOG_INFO, "after resize: length=%zu", nstr->length);
    free_nstr(&nstr);

    /* test scan_dir */
    LIST* dir_list = new_generic_list(0);
    if (scan_dir(".", dir_list, 0) == TRUE) {
        n_log(LOG_INFO, "scan_dir found %zu entries", dir_list->nb_items);
    }
    list_destroy(&dir_list);

    /* test n_str_url_encode */
    int url_encode_errors = 0;

    /* NULL input -> NULL */
    N_STR* encoded = n_str_url_encode(NULL);
    if (encoded != NULL) {
        n_log(LOG_ERR, "FAIL: n_str_url_encode(NULL) expected NULL, got \"%s\"", _nstr(encoded));
        url_encode_errors++;
        free_nstr(&encoded);
    } else {
        n_log(LOG_INFO, "PASS: n_str_url_encode(NULL) == NULL");
    }

    /* empty string -> empty result */
    encoded = n_str_url_encode("");
    if (!encoded || strcmp(_nstr(encoded), "") != 0) {
        n_log(LOG_ERR, "FAIL: n_str_url_encode(\"\") expected \"\", got \"%s\"", encoded ? _nstr(encoded) : "NULL");
        url_encode_errors++;
    } else {
        n_log(LOG_INFO, "PASS: n_str_url_encode(\"\") == \"\"");
    }
    free_nstr(&encoded);

    /* unreserved characters (A-Z a-z 0-9 - _ . ~) must be kept verbatim */
    encoded = n_str_url_encode("Hello-World_1.0~");
    if (!encoded || strcmp(_nstr(encoded), "Hello-World_1.0~") != 0) {
        n_log(LOG_ERR, "FAIL: n_str_url_encode(\"Hello-World_1.0~\") expected verbatim, got \"%s\"", encoded ? _nstr(encoded) : "NULL");
        url_encode_errors++;
    } else {
        n_log(LOG_INFO, "PASS: n_str_url_encode(\"Hello-World_1.0~\") == \"Hello-World_1.0~\"");
    }
    free_nstr(&encoded);

    /* reserved / special characters must be percent-encoded */
    encoded = n_str_url_encode("foo bar&baz=qux/path?q=1#frag");
    const char* expected_reserved = "foo%20bar%26baz%3Dqux%2Fpath%3Fq%3D1%23frag";
    if (!encoded || strcmp(_nstr(encoded), expected_reserved) != 0) {
        n_log(LOG_ERR, "FAIL: n_str_url_encode(reserved chars) expected \"%s\", got \"%s\"", expected_reserved, encoded ? _nstr(encoded) : "NULL");
        url_encode_errors++;
    } else {
        n_log(LOG_INFO, "PASS: n_str_url_encode(\"foo bar&baz=qux/path?q=1#frag\") == \"%s\"", expected_reserved);
    }
    free_nstr(&encoded);

    /* bytes >= 0x80 (high-byte / non-ASCII) must be percent-encoded */
    encoded = n_str_url_encode("\xC3\xA9\xFF");
    if (!encoded || strcmp(_nstr(encoded), "%C3%A9%FF") != 0) {
        n_log(LOG_ERR, "FAIL: n_str_url_encode(high bytes) expected \"%s\", got \"%s\"", "%C3%A9%FF", encoded ? _nstr(encoded) : "NULL");
        url_encode_errors++;
    } else {
        n_log(LOG_INFO, "PASS: n_str_url_encode(\"\\xC3\\xA9\\xFF\") == \"%s\"", "%C3%A9%FF");
    }
    free_nstr(&encoded);

    if (url_encode_errors > 0) {
        n_log(LOG_ERR, "n_str_url_encode: %d test(s) failed", url_encode_errors);
        exit(1);
    }

    /* n_str_utf8_valid: the verdicts match PHP's mb_check_encoding(..., 'UTF-8') */
    {
#define UTF8_V(str, ok)          \
    {                            \
        str, sizeof(str) - 1, ok \
    }
        static const struct {
            const char* s;
            size_t len;
            int valid;
        } u[] = {
            UTF8_V("", 1),
            UTF8_V("ascii", 1),
            UTF8_V("caf\xc3\xa9 \xe2\x82\xac \xf0\x9f\x98\x80", 1),
            UTF8_V("\xf4\x8f\xbf\xbf", 1), /* U+10FFFF */
            UTF8_V("a\0b", 1),             /* NUL is valid UTF-8 */
            UTF8_V("\x80", 0),             /* stray continuation */
            UTF8_V("\xc0\xaf", 0),         /* overlong 2-byte */
            UTF8_V("\xe0\x80\xaf", 0),     /* overlong 3-byte */
            UTF8_V("\xf0\x80\x80\xaf", 0), /* overlong 4-byte */
            UTF8_V("\xed\xa0\x80", 0),     /* surrogate U+D800 */
            UTF8_V("\xf4\x90\x80\x80", 0), /* above U+10FFFF */
            UTF8_V("\xe2\x82", 0),         /* truncated */
            UTF8_V("\xf5\x80\x80\x80", 0), /* lead above F4 */
        };
#undef UTF8_V
        size_t k;
        for (k = 0; k < sizeof(u) / sizeof(u[0]); k++) {
            if ((n_str_utf8_valid(u[k].s, u[k].len) ? 1 : 0) != u[k].valid) {
                n_log(LOG_ERR, "FAIL: n_str_utf8_valid vector %zu", k);
                exit(1);
            }
        }
        if (n_str_utf8_valid(NULL, 0) != TRUE || n_str_utf8_valid(NULL, 1) != FALSE) {
            n_log(LOG_ERR, "FAIL: n_str_utf8_valid NULL");
            exit(1);
        }
        n_log(LOG_INFO, "PASS: n_str_utf8_valid vectors");
    }

    /* new_nstr allocates size + 1 for the terminator, so the maximum representable
       size wraps that sum to 0 and calloc(0, 1) hands back a one-element buffer for
       what the caller believes is a huge string. Anything writing size bytes into it
       then runs off the end. The size is refused instead. */
    {
        N_STR* wrapped = new_nstr((NSTRBYTE)-1);
        if (wrapped != NULL) {
            n_log(LOG_ERR, "FAIL: new_nstr(NSTRBYTE max) returned a string instead of NULL");
            free_nstr(&wrapped);
            exit(1);
        }
        n_log(LOG_INFO, "PASS: new_nstr(NSTRBYTE max) refused (terminator would wrap the allocation)");
    }
    {
        /* an ordinary size is unaffected: length still reports the usable bytes,
           and writing all of them plus the terminator stays in bounds (ASan would
           say otherwise if the guard had changed the allocation) */
        N_STR* ok = new_nstr(16);
        if (!ok || ok->length != 16 || !ok->data) {
            n_log(LOG_ERR, "FAIL: new_nstr(16) did not return a 16-byte string");
            free_nstr(&ok);
            exit(1);
        }
        memset(ok->data, 'x', 16);
        ok->data[16] = '\0';
        ok->written = 16;
        if (strlen(ok->data) != 16) {
            n_log(LOG_ERR, "FAIL: new_nstr(16) left no room for the terminator");
            free_nstr(&ok);
            exit(1);
        }
        n_log(LOG_INFO, "PASS: new_nstr(16) holds 16 bytes plus its terminator");
        free_nstr(&ok);
    }

    exit(0);
}