| /* |
| Copyright: Boaz Segev, 2017-2018 |
| License: MIT |
| */ |
| |
| #if defined(__unix__) || defined(__APPLE__) || defined(__linux__) |
| #ifndef _GNU_SOURCE |
| #define _GNU_SOURCE |
| #endif |
| #include <unistd.h> |
| #endif |
| |
| #ifdef _SC_PAGESIZE |
| #define PAGE_SIZE sysconf(_SC_PAGESIZE) |
| #else |
| #define PAGE_SIZE 4096 |
| #endif |
| |
| #include <fiobject.h> |
| |
| #include <fio_siphash.h> |
| #include <fiobj_numbers.h> |
| #include <fiobj_str.h> |
| |
| #include <assert.h> |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <limits.h> |
| #include <string.h> |
| #include <sys/stat.h> |
| |
| #define FIO_INCLUDE_STR |
| #define FIO_STR_NO_REF |
| #include <fio.h> |
| |
| #ifndef PATH_MAX |
| #define PATH_MAX PAGE_SIZE |
| #endif |
| |
| /* ***************************************************************************** |
| String Type |
| ***************************************************************************** */ |
| |
| typedef struct { |
| fiobj_object_header_s head; |
| uint64_t hash; |
| fio_str_s str; |
| } fiobj_str_s; |
| |
| #define obj2str(o) ((fiobj_str_s *)(FIOBJ2PTR(o))) |
| |
| static inline fio_str_info_s fiobj_str_get_cstr(const FIOBJ o) { |
| return fio_str_info(&obj2str(o)->str); |
| } |
| |
| /* ***************************************************************************** |
| String VTables |
| ***************************************************************************** */ |
| |
| static fio_str_info_s fio_str2str(const FIOBJ o) { |
| return fiobj_str_get_cstr(o); |
| } |
| |
| static void fiobj_str_dealloc(FIOBJ o, void (*task)(FIOBJ, void *), void *arg) { |
| fio_str_free(&obj2str(o)->str); |
| fio_free(FIOBJ2PTR(o)); |
| (void)task; |
| (void)arg; |
| } |
| |
| static size_t fiobj_str_is_eq(const FIOBJ self, const FIOBJ other) { |
| return fio_str_iseq(&obj2str(self)->str, &obj2str(other)->str); |
| } |
| |
| static intptr_t fio_str2i(const FIOBJ o) { |
| char *pos = fio_str_data(&obj2str(o)->str); |
| return fio_atol(&pos); |
| } |
| static double fio_str2f(const FIOBJ o) { |
| char *pos = fio_str_data(&obj2str(o)->str); |
| return fio_atof(&pos); |
| } |
| |
| static size_t fio_str2bool(const FIOBJ o) { |
| return fio_str_len(&obj2str(o)->str) != 0; |
| } |
| |
| uintptr_t fiobject___noop_count(const FIOBJ o); |
| |
| const fiobj_object_vtable_s FIOBJECT_VTABLE_STRING = { |
| .class_name = "String", |
| .dealloc = fiobj_str_dealloc, |
| .to_i = fio_str2i, |
| .to_f = fio_str2f, |
| .to_str = fio_str2str, |
| .is_eq = fiobj_str_is_eq, |
| .is_true = fio_str2bool, |
| .count = fiobject___noop_count, |
| }; |
| |
| /* ***************************************************************************** |
| String API |
| ***************************************************************************** */ |
| |
| /** Creates a buffer String object. Remember to use `fiobj_free`. */ |
| FIOBJ fiobj_str_buf(size_t capa) { |
| if (capa) |
| capa = capa + 1; |
| else |
| capa = PAGE_SIZE; |
| |
| fiobj_str_s *s = fio_malloc(sizeof(*s)); |
| if (!s) { |
| perror("ERROR: fiobj string couldn't allocate memory"); |
| exit(errno); |
| } |
| *s = (fiobj_str_s){ |
| .head = |
| { |
| .ref = 1, |
| .type = FIOBJ_T_STRING, |
| }, |
| .str = FIO_STR_INIT, |
| }; |
| if (capa) { |
| fio_str_capa_assert(&s->str, capa); |
| } |
| return ((uintptr_t)s | FIOBJECT_STRING_FLAG); |
| } |
| |
| /** Creates a String object. Remember to use `fiobj_free`. */ |
| FIOBJ fiobj_str_new(const char *str, size_t len) { |
| fiobj_str_s *s = fio_malloc(sizeof(*s)); |
| if (!s) { |
| perror("ERROR: fiobj string couldn't allocate memory"); |
| exit(errno); |
| } |
| *s = (fiobj_str_s){ |
| .head = |
| { |
| .ref = 1, |
| .type = FIOBJ_T_STRING, |
| }, |
| .str = FIO_STR_INIT, |
| }; |
| if (str && len) { |
| fio_str_write(&s->str, str, len); |
| } |
| return ((uintptr_t)s | FIOBJECT_STRING_FLAG); |
| } |
| |
| /** |
| * Creates a String object. Remember to use `fiobj_free`. |
| * |
| * It's possible to wrap a previosly allocated memory block in a FIOBJ String |
| * object, as long as it was allocated using `fio_malloc`. |
| * |
| * The ownership of the memory indicated by `str` will "move" to the object and |
| * will be freed (using `fio_free`) once the object's reference count drops to |
| * zero. |
| */ |
| FIOBJ fiobj_str_move(char *str, size_t len, size_t capacity) { |
| fiobj_str_s *s = fio_malloc(sizeof(*s)); |
| if (!s) { |
| perror("ERROR: fiobj string couldn't allocate memory"); |
| exit(errno); |
| } |
| *s = (fiobj_str_s){ |
| .head = |
| { |
| .ref = 1, |
| .type = FIOBJ_T_STRING, |
| }, |
| .str = FIO_STR_INIT_EXISTING(str, len, capacity), |
| }; |
| return ((uintptr_t)s | FIOBJECT_STRING_FLAG); |
| } |
| |
| /** |
| * Returns a thread-static temporary string. Avoid calling `fiobj_dup` or |
| * `fiobj_free`. |
| */ |
| FIOBJ fiobj_str_tmp(void) { |
| static __thread fiobj_str_s tmp = { |
| .head = |
| { |
| .ref = ((~(uint32_t)0) >> 4), |
| .type = FIOBJ_T_STRING, |
| }, |
| .str = {.small = 1}, |
| }; |
| tmp.str.frozen = 0; |
| fio_str_resize(&tmp.str, 0); |
| return ((uintptr_t)&tmp | FIOBJECT_STRING_FLAG); |
| } |
| |
| /** Prevents the String object from being changed. */ |
| void fiobj_str_freeze(FIOBJ str) { |
| if (FIOBJ_TYPE_IS(str, FIOBJ_T_STRING)) |
| fio_str_freeze(&obj2str(str)->str); |
| } |
| |
| /** Confirms the requested capacity is available and allocates as required. */ |
| size_t fiobj_str_capa_assert(FIOBJ str, size_t size) { |
| |
| assert(FIOBJ_TYPE_IS(str, FIOBJ_T_STRING)); |
| if (obj2str(str)->str.frozen) |
| return 0; |
| fio_str_info_s state = fio_str_capa_assert(&obj2str(str)->str, size); |
| return state.capa; |
| } |
| |
| /** Return's a String's capacity, if any. */ |
| size_t fiobj_str_capa(FIOBJ str) { |
| assert(FIOBJ_TYPE_IS(str, FIOBJ_T_STRING)); |
| return fio_str_capa(&obj2str(str)->str); |
| } |
| |
| /** Resizes a String object, allocating more memory if required. */ |
| void fiobj_str_resize(FIOBJ str, size_t size) { |
| assert(FIOBJ_TYPE_IS(str, FIOBJ_T_STRING)); |
| fio_str_resize(&obj2str(str)->str, size); |
| obj2str(str)->hash = 0; |
| return; |
| } |
| |
| /** Deallocates any unnecessary memory (if supported by OS). */ |
| void fiobj_str_compact(FIOBJ str) { |
| assert(FIOBJ_TYPE_IS(str, FIOBJ_T_STRING)); |
| fio_str_compact(&obj2str(str)->str); |
| return; |
| } |
| |
| /** Empties a String's data. */ |
| void fiobj_str_clear(FIOBJ str) { |
| assert(FIOBJ_TYPE_IS(str, FIOBJ_T_STRING)); |
| fio_str_resize(&obj2str(str)->str, 0); |
| obj2str(str)->hash = 0; |
| } |
| |
| /** |
| * Writes data at the end of the string, resizing the string as required. |
| * Returns the new length of the String |
| */ |
| size_t fiobj_str_write(FIOBJ dest, const char *data, size_t len) { |
| assert(FIOBJ_TYPE_IS(dest, FIOBJ_T_STRING)); |
| if (obj2str(dest)->str.frozen) |
| return 0; |
| obj2str(dest)->hash = 0; |
| return fio_str_write(&obj2str(dest)->str, data, len).len; |
| } |
| |
| /** |
| * Writes a number at the end of the String using normal base 10 notation. |
| * |
| * Returns the new length of the String |
| */ |
| size_t fiobj_str_write_i(FIOBJ dest, int64_t num) { |
| assert(FIOBJ_TYPE_IS(dest, FIOBJ_T_STRING)); |
| if (obj2str(dest)->str.frozen) |
| return 0; |
| obj2str(dest)->hash = 0; |
| return fio_str_write_i(&obj2str(dest)->str, num).len; |
| } |
| |
| /** |
| * Writes data at the end of the string, resizing the string as required. |
| * Returns the new length of the String |
| */ |
| size_t fiobj_str_printf(FIOBJ dest, const char *format, ...) { |
| assert(FIOBJ_TYPE_IS(dest, FIOBJ_T_STRING)); |
| if (obj2str(dest)->str.frozen) |
| return 0; |
| obj2str(dest)->hash = 0; |
| va_list argv; |
| va_start(argv, format); |
| fio_str_info_s state = fio_str_vprintf(&obj2str(dest)->str, format, argv); |
| va_end(argv); |
| return state.len; |
| } |
| |
| size_t fiobj_str_vprintf(FIOBJ dest, const char *format, va_list argv) { |
| assert(FIOBJ_TYPE_IS(dest, FIOBJ_T_STRING)); |
| if (obj2str(dest)->str.frozen) |
| return 0; |
| obj2str(dest)->hash = 0; |
| fio_str_info_s state = fio_str_vprintf(&obj2str(dest)->str, format, argv); |
| return state.len; |
| } |
| |
| /** Dumps the `filename` file's contents at the end of a String. If `limit == |
| * 0`, than the data will be read until EOF. |
| * |
| * If the file can't be located, opened or read, or if `start_at` is beyond |
| * the EOF position, NULL is returned. |
| * |
| * Remember to use `fiobj_free`. |
| */ |
| size_t fiobj_str_readfile(FIOBJ dest, const char *filename, intptr_t start_at, |
| intptr_t limit) { |
| fio_str_info_s state = |
| fio_str_readfile(&obj2str(dest)->str, filename, start_at, limit); |
| return state.len; |
| } |
| |
| /** |
| * Writes data at the end of the string, resizing the string as required. |
| * Returns the new length of the String |
| */ |
| size_t fiobj_str_concat(FIOBJ dest, FIOBJ obj) { |
| assert(FIOBJ_TYPE_IS(dest, FIOBJ_T_STRING)); |
| if (obj2str(dest)->str.frozen) |
| return 0; |
| obj2str(dest)->hash = 0; |
| fio_str_info_s o = fiobj_obj2cstr(obj); |
| if (o.len == 0) |
| return fio_str_len(&obj2str(dest)->str); |
| return fio_str_write(&obj2str(dest)->str, o.data, o.len).len; |
| } |
| |
| /** |
| * Calculates a String's SipHash value for use as a HashMap key. |
| */ |
| uint64_t fiobj_str_hash(FIOBJ o) { |
| assert(FIOBJ_TYPE_IS(o, FIOBJ_T_STRING)); |
| // if (obj2str(o)->is_small) { |
| // return fio_siphash(STR_INTENAL_STR(o), STR_INTENAL_LEN(o)); |
| // } else |
| if (obj2str(o)->hash) { |
| return obj2str(o)->hash; |
| } |
| fio_str_info_s state = fio_str_info(&obj2str(o)->str); |
| obj2str(o)->hash = fio_siphash(state.data, state.len); |
| return obj2str(o)->hash; |
| } |
| |
| /* ***************************************************************************** |
| Tests |
| ***************************************************************************** */ |
| |
| #if DEBUG |
| void fiobj_test_string(void) { |
| fprintf(stderr, "=== Testing Strings\n"); |
| fprintf(stderr, "* Internal String Capacity %u \n", |
| (unsigned int)FIO_STR_SMALL_CAPA); |
| #define TEST_ASSERT(cond, ...) \ |
| if (!(cond)) { \ |
| fprintf(stderr, "* " __VA_ARGS__); \ |
| fprintf(stderr, "Testing failed.\n"); \ |
| exit(-1); \ |
| } |
| #define STR_EQ(o, str) \ |
| TEST_ASSERT((fiobj_str_getlen(o) == strlen(str) && \ |
| !memcmp(fiobj_str_mem_addr(o), str, strlen(str))), \ |
| "String not equal to " str) |
| FIOBJ o = fiobj_str_new("Hello", 5); |
| TEST_ASSERT(FIOBJ_TYPE_IS(o, FIOBJ_T_STRING), "Small String isn't string!\n"); |
| TEST_ASSERT(obj2str(o)->str.small, "Hello isn't small\n"); |
| fiobj_str_write(o, " World", 6); |
| TEST_ASSERT(FIOBJ_TYPE_IS(o, FIOBJ_T_STRING), |
| "Hello World String isn't string!\n"); |
| TEST_ASSERT(obj2str(o)->str.small, "Hello World isn't small\n"); |
| TEST_ASSERT(fiobj_obj2cstr(o).len == 11, |
| "Invalid small string length (%u != 11)!\n", |
| (unsigned int)fiobj_obj2cstr(o).len) |
| fiobj_str_write(o, " World, you crazy longer sleep loving person :-)", 48); |
| TEST_ASSERT(!obj2str(o)->str.small, "Crazier shouldn't be small\n"); |
| fiobj_free(o); |
| |
| o = fiobj_str_new( |
| "hello my dear friend, I hope that your are well and happy.", 58); |
| TEST_ASSERT(FIOBJ_TYPE_IS(o, FIOBJ_T_STRING), "Long String isn't string!\n"); |
| TEST_ASSERT(!obj2str(o)->str.small, |
| "Long String is small! (capa: %lu, len: %lu)\n", |
| fio_str_capa(&obj2str(o)->str), fio_str_len(&obj2str(o)->str)); |
| TEST_ASSERT(fiobj_obj2cstr(o).len == 58, |
| "Invalid long string length (%lu != 58)!\n", |
| fiobj_obj2cstr(o).len) |
| uint64_t hash = fiobj_str_hash(o); |
| TEST_ASSERT(!obj2str(o)->str.frozen, "String forzen when only hashing!\n"); |
| fiobj_str_freeze(o); |
| TEST_ASSERT(obj2str(o)->str.frozen, "String not forzen!\n"); |
| fiobj_str_write(o, " World", 6); |
| TEST_ASSERT(hash == fiobj_str_hash(o), |
| "String hash changed after hashing - not frozen?\n"); |
| TEST_ASSERT(fiobj_obj2cstr(o).len == 58, |
| "String was edited after hashing - not frozen!\n (%lu): %s", |
| (unsigned long)fiobj_obj2cstr(o).len, fiobj_obj2cstr(o).data); |
| fiobj_free(o); |
| |
| o = fiobj_str_buf(1); |
| fiobj_str_printf(o, "%u", 42); |
| TEST_ASSERT(fio_str_len(&obj2str(o)->str) == 2, |
| "fiobj_strprintf length error.\n"); |
| TEST_ASSERT(fiobj_obj2num(o), "fiobj_strprintf integer error.\n"); |
| TEST_ASSERT(!memcmp(fiobj_obj2cstr(o).data, "42", 2), |
| "fiobj_strprintf string error.\n"); |
| fiobj_free(o); |
| |
| o = fiobj_str_buf(4); |
| for (int i = 0; i < 16000; ++i) { |
| fiobj_str_write(o, "a", 1); |
| } |
| TEST_ASSERT(fio_str_len(&obj2str(o)->str) == 16000, |
| "16K fiobj_str_write not 16K.\n"); |
| TEST_ASSERT(fio_str_capa(&obj2str(o)->str) >= 16000, |
| "16K fiobj_str_write capa not enough.\n"); |
| fiobj_free(o); |
| |
| o = fiobj_str_buf(0); |
| TEST_ASSERT(fiobj_str_readfile(o, __FILE__, 0, 0), |
| "`fiobj_str_readfile` - file wasn't read!"); |
| TEST_ASSERT(!memcmp(fiobj_obj2cstr(o).data, "/*", 2), |
| "`fiobj_str_readfile` error, start of file doesn't match:\n%s", |
| fiobj_obj2cstr(o).data); |
| fiobj_free(o); |
| |
| fprintf(stderr, "* passed.\n"); |
| } |
| #endif |