From 7120a04f926cdf05f68fc661ed9e8a2e630757f9 Mon Sep 17 00:00:00 2001 From: Will Brown Date: Mon, 25 Nov 2024 22:32:00 -0500 Subject: [PATCH] [main.c] Complete list functionality --- arena.c | 207 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ arena.h | 52 ++++++++++++++ main.c | 172 +++++++++++++++++++++++++++++++++++----------- 3 files changed, 393 insertions(+), 38 deletions(-) create mode 100644 arena.c create mode 100644 arena.h diff --git a/arena.c b/arena.c new file mode 100644 index 0000000..94ae8ae --- /dev/null +++ b/arena.c @@ -0,0 +1,207 @@ +/* This arena allocator implementation was created by Will Brown (WCBROW01). + * Orginal source can be found at: https://github.com/WCBROW01/arena-allocator + * Licensed under the MIT License (c) 2022-2024 Will Brown */ + +#include +#include +#include +#include +#include +#include + +#include "arena.h" + +#define next_multiple(a, b) ((a) + (b) - (a) % (b)) +#define MEM_ALIGNMENT alignof(max_align_t) +#define align(n) ((n) % MEM_ALIGNMENT == 0 ? (n) : next_multiple(n, MEM_ALIGNMENT)) + +// The arena region itself is allocated after the contents of the struct. +struct Arena { + size_t size; + size_t tmp_size; + void *last_block; + void *next_block; + Arena *next_region; + bool dynamic; +}; + +// Get the start address of the arena +static inline void *Arena_start(Arena *arena) { + return arena + 1; +} + +// Allocates a fixed-size arena. Accepts the size of the arena in bytes. +Arena *Arena_new(size_t size) { + Arena *new_arena = malloc(sizeof(Arena) + size); + + *new_arena = (Arena) { + .size = size, + .tmp_size = 0, + .last_block = NULL, + .next_block = Arena_start(new_arena), + .next_region = NULL, + .dynamic = false + }; + + return new_arena; +} + +/* Allocates a dynamically-sized arena. Accepts the initial size of the arena in bytes. + * If there is not enough space in the arena for an allocation, a new region will be created. */ +Arena *Arena_new_dynamic(size_t size) { + Arena *new_arena = malloc(sizeof(Arena) + size); + + *new_arena = (Arena) { + .size = size, + .tmp_size = 0, + .last_block = NULL, + .next_block = Arena_start(new_arena), + .next_region = NULL, + .dynamic = true + }; + + return new_arena; +} + +// Frees the entire arena from memory. +void Arena_delete(Arena *arena) { + if (arena->next_region != NULL) Arena_delete(arena->next_region); + free(arena); +} + +#ifdef ENABLE_DIAG +static void print_diagnostic(Arena *arena, size_t size) { + fprintf(stderr, "Diagnostic info:\n"); + fprintf(stderr, "Arena size: %zu bytes\n", arena->size); + fprintf(stderr, "Amount currently allocated: %zu bytes\n", arena->next_block - Arena_start(arena)); + fprintf(stderr, "New block size: %zu bytes\n", size); + fprintf(stderr, "New size upon success: %zu bytes\n", arena->next_block + align(size) - Arena_start(arena)); +} +#endif + +static inline void *Arena_init_block(Arena *arena, size_t size) { + size_t blksize = align(size); + arena->tmp_size += blksize; + void *new_block = arena->next_block; + arena->last_block = new_block; + arena->next_block += blksize; + return new_block; +} + +// Will return a null pointer if you've tried allocating too much memory. +void *Arena_alloc(Arena *arena, size_t size) { + if (arena->next_block + align(size) > Arena_start(arena) + arena->size) { + if (arena->dynamic) { + if (arena->next_region == NULL) { + // If the size is too large for a region, make a special region for only that block. + size_t region_size = size > arena->size ? size : arena->size; + arena->next_region = Arena_new_dynamic(region_size); + return Arena_init_block(arena->next_region, size); + } else { + return Arena_alloc(arena->next_region, size); + } + } else { +#ifdef ENABLE_DIAG + fprintf(stderr, "Allocation too large. You've attempted to allocate a block of memory past the end of the arena.\n"); + print_diagnostic(arena, size); +#endif + return NULL; + } + } else { + return Arena_init_block(arena, size); + } +} + +// Identical to Arena_alloc but it zeros your memory +void *Arena_allocz(Arena *arena, size_t size) { + return memset(Arena_alloc(arena, size), 0, size); +} + +/* Copy a block of memory into an arena. + * Functionally equivalent to memcpy. */ +void *Arena_copy(Arena *arena, const void *src, size_t size) { + void *new_block = Arena_alloc(arena, size); + if (new_block == NULL) { + return NULL; + } else { + memcpy(new_block, src, size); + return new_block; + } +} + +/* If the pointer is to the last allocation, it will be resized. + * Otherwise, a new allocation will be created. + * Be careful with this! A null pointer will be returned upon error. + * Using this with memory outside of the arena is undefined behavior. */ +void *Arena_realloc(Arena *arena, void *ptr, size_t size) { + if (ptr == arena->last_block) { + if (arena->last_block + align(size) > Arena_start(arena) + arena->size) { + if (arena->dynamic) { + return Arena_copy(arena, ptr, size); + } else { +#ifdef ENABLE_DIAG + fprintf(stderr, "Allocation too large. You've attempted to allocate a block of memory past the end of the arena.\n"); + print_diagnostic(arena, size - (arena->next_block - arena->last_block)); +#endif + return NULL; + } + } else { + arena->next_block = arena->last_block + align(size); + return ptr; + } + } else { + return Arena_copy(arena, ptr, size); + } +} + +/* Marks the beginning of a temporary buffer that can be deallocated at any time. + * The state of the last one is saved in case you have multiple. */ +void Arena_tmp_begin(Arena *arena) { + size_t tmp_size = arena->tmp_size; + void *last_block = arena->last_block; + arena->tmp_size = 0; + + void *state = Arena_alloc(arena, sizeof(size_t) + sizeof(void*)); + if (state == NULL) return; + *(size_t*) state = tmp_size; + *(void**) (state + sizeof(void*)) = last_block; +} + +static void tmp_rewind(Arena *arena, bool *complete) { + void *stateloc = arena->next_block - arena->tmp_size; + if (arena->next_region != NULL) { + tmp_rewind(arena->next_region, complete); + arena->next_region = NULL; + } + + if (*complete) return; + + if (stateloc == Arena_start(arena)) { + Arena_delete(arena); + } else { + arena->tmp_size = *(size_t*) stateloc; + arena->last_block = *(void**) (stateloc + sizeof(size_t)); + arena->next_block = stateloc; + *complete = true; + } +} + +/* Deallocates the last temporary buffer. If there is none, + * the entire arena will be deallocated. */ +void Arena_tmp_rewind(Arena *arena) { + bool complete = false; + + if (arena->next_region != NULL) { + tmp_rewind(arena->next_region, &complete); + arena->next_region = NULL; + } + + if (!complete) { + void *stateloc = arena->next_block - arena->tmp_size; + if (stateloc != Arena_start(arena)) { + arena->tmp_size = *(size_t*) stateloc; + arena->last_block = *(void**) (stateloc + sizeof(size_t)); + } + arena->next_block = stateloc; + } +} diff --git a/arena.h b/arena.h new file mode 100644 index 0000000..8eb3fef --- /dev/null +++ b/arena.h @@ -0,0 +1,52 @@ +/* This arena allocator implementation was created by Will Brown (WCBROW01). + * Orginal source can be found at: https://github.com/WCBROW01/arena-allocator + * Licensed under the MIT License (c) 2022-2024 Will Brown */ + +#ifndef ARENA_H +#define ARENA_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct Arena Arena; + +// Allocates a fixed-size arena. Accepts the size of the arena in bytes. +Arena *Arena_new(size_t size); + +/* Allocates a dynamically-sized arena. Accepts the initial size of the arena in bytes. + * If there is not enough space in the arena for an allocation, a new region will be created. */ +Arena *Arena_new_dynamic(size_t size); + +// Frees the entire arena from memory. +void Arena_delete(Arena *arena); + +// Will return a null pointer if you've tried allocating too much memory. +void *Arena_alloc(Arena *arena, size_t size); + +// Identical to Arena_alloc but it zeros your memory +void *Arena_allocz(Arena *arena, size_t size); + +/* Copy a block of memory into an arena. + * Functionally equivalent to memcpy. */ +void *Arena_copy(Arena *arena, const void *src, size_t size); + +/* If the pointer is to the last allocation, it will be resized. + * Otherwise, a new allocation will be created. + * Be careful with this! A null pointer will be returned upon error. + * Using this with memory outside of the arena is undefined behavior. */ +void *Arena_realloc(Arena *arena, void *ptr, size_t size); + +/* Marks the beginning of a temporary buffer that can be deallocated at any time. + * The state of the last one is saved in case you have multiple. */ +void Arena_tmp_begin(Arena *arena); + +/* Deallocates the last temporary buffer. If there is none, + * the entire arena will be deallocated. */ +void Arena_tmp_rewind(Arena *arena); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/main.c b/main.c index 4e2a564..de5fa41 100644 --- a/main.c +++ b/main.c @@ -1,3 +1,4 @@ +#include #include #include #include @@ -19,6 +20,7 @@ #include "config.h" #include "feed_info.h" +#include "arena.h" // Function pointer type for commands typedef void (*command_func)(struct discord *, const struct discord_interaction *); @@ -263,48 +265,142 @@ static void bot_command_remove(struct discord *client, const struct discord_inte discord_create_interaction_response(client, event->id, event->token, &res, NULL); } +#define LIST_PAGE_SIZE 5 + +// The arena everything gets allocated to will be returned in the arena pointer +static struct discord_interaction_callback_data *list_data_create(u64snowflake channel_id, int page_number, Arena **arena) { + assert(arena && "No arena provided"); // this is programmer error + // clamp page number + page_number = page_number < 1 ? 1 : page_number; + + *arena = Arena_new(8192); // this should be more than enough + struct discord_interaction_callback_data *data = Arena_allocz(*arena, sizeof(*data)); + + int64_t count; + zblock_feed_info_err error = zblock_feed_info_count_channel(database_conn, channel_id, &count); + if (error) { + char *msg = Arena_alloc(*arena, sizeof(DISCORD_MAX_MESSAGE_LEN)); + snprintf(msg, DISCORD_MAX_MESSAGE_LEN, "Error creating list: %s", zblock_feed_info_strerror(error)); + data->content = msg; + return data; + } + + int last_page_number = count ? count % LIST_PAGE_SIZE ? count / LIST_PAGE_SIZE + 1 : count / LIST_PAGE_SIZE : 1; + + zblock_feed_info feeds[LIST_PAGE_SIZE]; + int num_retrieved; + error = zblock_feed_info_retrieve_chunk_channel(database_conn, channel_id, (page_number - 1) * LIST_PAGE_SIZE, LIST_PAGE_SIZE, feeds, &num_retrieved); + if (error) { + char *msg = Arena_alloc(*arena, sizeof(DISCORD_MAX_MESSAGE_LEN)); + snprintf(msg, DISCORD_MAX_MESSAGE_LEN, "Error creating list: %s", zblock_feed_info_strerror(error)); + data->content = msg; + return data; + } + + // create our components starting with the action row + data->components = Arena_alloc(*arena, sizeof(*data->components)); + data->components->size = 1; + data->components->array = Arena_allocz(*arena, data->components->size * sizeof(*data->components->array)); + struct discord_component *action_row = data->components->array; + action_row->type = DISCORD_COMPONENT_ACTION_ROW; + // create buttons + action_row->components = Arena_alloc(*arena, sizeof(*action_row->components)); + action_row->components->size = 2; + action_row->components->array = Arena_allocz(*arena, action_row->components->size * sizeof(*action_row->components->array)); + struct discord_component *buttons = action_row->components->array; + // create emojis + struct discord_emoji *back_arrow = Arena_allocz(*arena, sizeof(*back_arrow)); + back_arrow->name = "◀️"; + struct discord_emoji *next_arrow = Arena_allocz(*arena, sizeof(*next_arrow)); + next_arrow->name = "▶️"; + // create button ids + int back_id_size = snprintf(NULL, 0, "list_page%d", page_number - 1) + 1; + char *back_id = Arena_alloc(*arena, back_id_size); + snprintf(back_id, back_id_size, "list_page%d", page_number - 1); + int next_id_size = snprintf(NULL, 0, "list_page%d", page_number + 1) + 1; + char *next_id = Arena_alloc(*arena, next_id_size); + snprintf(next_id, next_id_size, "list_page%d", page_number + 1); + // populate buttons + buttons[0] = (struct discord_component) { + .type = DISCORD_COMPONENT_BUTTON, + .disabled = page_number == 1, + .style = DISCORD_BUTTON_SECONDARY, + .custom_id = back_id, + .label = "Back", + .emoji = back_arrow + }; + buttons[1] = (struct discord_component) { + .type = DISCORD_COMPONENT_BUTTON, + .disabled = page_number == last_page_number, + .style = DISCORD_BUTTON_SECONDARY, + .custom_id = next_id, + .label = "Next", + .emoji = next_arrow + }; + + // create embed + data->embeds = Arena_alloc(*arena, sizeof(*data->embeds)); + data->embeds->size = 1; + data->embeds->array = Arena_allocz(*arena, data->embeds->size * sizeof(*data->embeds->array)); + struct discord_embed *embed = data->embeds->array; + int embed_title_size = snprintf(NULL, 0, "Feed List (Page %d of %d)", page_number, last_page_number) + 1; + char *embed_title = Arena_alloc(*arena, embed_title_size); + snprintf(embed_title, embed_title_size, "Feed List (Page %d of %d)", page_number, last_page_number); + + // write the description + char *embed_description; + if (count) { + embed_description = Arena_alloc(*arena, 4096); // the current max size of embed descriptions + int embed_description_size = 0; + for (int i = 0; i < num_retrieved; ++i) { + // in case somebody has maliciously long text in their feed + if (embed_description_size < 4096) { + embed_description_size += snprintf(embed_description + embed_description_size, 4096 - embed_description_size, + "### %d. %s\n" // feed title + "Link: %s\n" // feed url + "Last updated: %s\n", // last_pubDate + (page_number - 1) * LIST_PAGE_SIZE + i + 1, feeds[i].title, + feeds[i].url, + feeds[i].last_pubDate + ); + } + } + } else { + embed_description = "There are no feeds in this channel."; + } + + *embed = (struct discord_embed) { + .title = embed_title, + .type = "rich", + .description = embed_description + }; + + return data; +} + static void bot_command_list(struct discord *client, const struct discord_interaction *event) { + Arena *arena; struct discord_interaction_response res = { .type = DISCORD_INTERACTION_CHANNEL_MESSAGE_WITH_SOURCE, - .data = &(struct discord_interaction_callback_data) { - .components = CREATE_COMPONENTS({ - { - .type = DISCORD_COMPONENT_ACTION_ROW, - .components = CREATE_COMPONENTS({ - { - .type = DISCORD_COMPONENT_BUTTON, - .disabled = false, - .style = DISCORD_BUTTON_SECONDARY, - .custom_id = "page_back", - .label = "Back", - .emoji = &(struct discord_emoji) { - .name = "◀️" - } - }, - { - .type = DISCORD_COMPONENT_BUTTON, - .disabled = false, - .style = DISCORD_BUTTON_SECONDARY, - .custom_id = "page_next", - .label = "Next", - .emoji = &(struct discord_emoji) { - .name = "▶️" - } - } - }) - } - }), - .embeds = CREATE_EMBEDS({ - { - .title = "Feed List", - .type = "rich", - .description = "List functionality has not been fully implemented yet." - } - }) - } + .data = list_data_create(event->channel_id, 1, &arena) }; discord_create_interaction_response(client, event->id, event->token, &res, NULL); + Arena_delete(arena); +} + +static void list_update(struct discord *client, const struct discord_interaction *event) { + int page_number; + sscanf(event->data->custom_id, "list_page%d", &page_number); + + Arena *arena; + struct discord_interaction_response res = { + .type = DISCORD_INTERACTION_UPDATE_MESSAGE, + .data = list_data_create(event->channel_id, page_number, &arena) + }; + + discord_create_interaction_response(client, event->id, event->token, &res, NULL); + Arena_delete(arena); } static void bot_command_help(struct discord *client, const struct discord_interaction *event) { @@ -409,8 +505,8 @@ static void on_interaction(struct discord *client, const struct discord_interact }; discord_create_interaction_response(client, event->id, event->token, &res, NULL); } break; - case DISCORD_INTERACTION_MESSAGE_COMPONENT: { - // nothing yet + case DISCORD_INTERACTION_MESSAGE_COMPONENT: { // only the list command is used here so far + list_update(client, event); } break; default: // nothing }