diff options
author | Mole Shang <[email protected]> | 2023-07-25 09:27:26 +0800 |
---|---|---|
committer | Mole Shang <[email protected]> | 2023-08-05 23:19:46 +0800 |
commit | ed8f6df90b0c39835198d5b7af4bbd391362f180 (patch) | |
tree | 907ba31bac854eb5dc8a2781825e24c049b10580 /src | |
download | hinata-ed8f6df90b0c39835198d5b7af4bbd391362f180.tar.gz hinata-ed8f6df90b0c39835198d5b7af4bbd391362f180.tar.bz2 hinata-ed8f6df90b0c39835198d5b7af4bbd391362f180.zip |
hinata: initial commit
Diffstat (limited to 'src')
-rw-r--r-- | src/constants.h | 18 | ||||
-rw-r--r-- | src/extractors/bilibili.c | 475 | ||||
-rw-r--r-- | src/extractors/bilibili.h | 94 | ||||
-rw-r--r-- | src/extractors/extractor.c | 24 | ||||
-rw-r--r-- | src/extractors/extractor.h | 32 | ||||
-rw-r--r-- | src/logger.c | 32 | ||||
-rw-r--r-- | src/logger.h | 30 | ||||
-rw-r--r-- | src/main.c | 163 | ||||
-rw-r--r-- | src/main.h | 8 | ||||
-rw-r--r-- | src/process_url.c | 526 | ||||
-rw-r--r-- | src/process_url.h | 59 | ||||
-rw-r--r-- | src/style.h | 133 | ||||
-rw-r--r-- | src/ui.c | 98 | ||||
-rw-r--r-- | src/ui.h | 16 | ||||
-rw-r--r-- | src/utils.c | 204 | ||||
-rw-r--r-- | src/utils.h | 61 |
16 files changed, 1973 insertions, 0 deletions
diff --git a/src/constants.h b/src/constants.h new file mode 100644 index 0000000..5891d7a --- /dev/null +++ b/src/constants.h @@ -0,0 +1,18 @@ +#ifndef CONSTANTS_H_ +#define CONSTANTS_H_ + +#define APP_NAME "Hinata" +#define MAX_VALUE 100 + +#define CEIL_DIV(a, b) (((a) + (b)-1) / (b)) +#define MAX(x, y) ((x) > (y)) ? (x) : (y) + +#ifdef _WIN32 +#define SPLITTER_CHAR '\\' +#define SPLITTER_STR "\\" +#else +#define SPLITTER_CHAR '/' +#define SPLITTER_STR "/" +#endif + +#endif diff --git a/src/extractors/bilibili.c b/src/extractors/bilibili.c new file mode 100644 index 0000000..874a605 --- /dev/null +++ b/src/extractors/bilibili.c @@ -0,0 +1,475 @@ +#include <cjson/cJSON.h> +#include <limits.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#ifdef _WIN32 +#include "c11threads.h" +#else +#include <threads.h> +#endif + +#include "../logger.h" +#include "../process_url.h" +#include "../utils.h" +#include "bilibili.h" +#include "extractor.h" + +static int get_multipagedata(char *pagedata, Multipage *multipage_struct, + bool *is_page) { + const char *patterns_str[1] = {"window.__INITIAL_STATE__=(.+?);\\(function"}; + const str_array_t patterns = {(char **)patterns_str, 1}; + str_array_t results = create_str_array(0); + + int r = regex_match(pagedata, patterns, &results); + if (!r) { + for (unsigned short i = 0; i < results.n; i++) { + // DEBUG_PRINT("%s\n", results.str[i]); + if (results.str[i]) { + multipage_struct->json = cJSON_Parse(get_str_element(&results, i)); + } + } + free_str_array(&results); + + cJSON *aid_obj = cJSON_GetObjectItem(multipage_struct->json, "aid"); + cJSON *bvid_obj = cJSON_GetObjectItem(multipage_struct->json, "bvid"); + cJSON *sections_obj = + cJSON_GetObjectItem(multipage_struct->json, "sections"); + cJSON *videoData_obj = + cJSON_GetObjectItem(multipage_struct->json, "videoData"); + if (aid_obj && bvid_obj && sections_obj && videoData_obj) { + multipage_struct->aid = aid_obj->valueint; + multipage_struct->bvid = bvid_obj->valuestring; + multipage_struct->sections = create_array( + sizeof(Multi_episode_data), cJSON_GetArraySize(sections_obj)); + + /* sections */ + if (!cJSON_GetArraySize(sections_obj)) { + DEBUG_PRINT("This video does not have sections, meaning that it is a " + "multi-p video with only one av/bvid\n"); + *is_page = 1; + } + + cJSON *e; + int i = 0; + cJSON_ArrayForEach(e, sections_obj) { + Multi_episode_data *section = + get_element(&multipage_struct->sections, i); + cJSON *season_id_obj = cJSON_GetObjectItem(e, "season_id"); + cJSON *episodes_obj = cJSON_GetObjectItem(e, "episodes"); + if (season_id_obj && episodes_obj) { + section->season_id = season_id_obj->valueint; + DEBUG_PRINT("sections[%d] season_id: %d\n", i, section->season_id); + + section->episodes = + create_array(sizeof(Episode), cJSON_GetArraySize(episodes_obj)); + + cJSON *e; + int j = 0; + cJSON_ArrayForEach(e, episodes_obj) { + cJSON *aid_obj = cJSON_GetObjectItem(e, "aid"); + cJSON *bvid_obj = cJSON_GetObjectItem(e, "bvid"); + cJSON *cid_obj = cJSON_GetObjectItem(e, "cid"); + cJSON *title_obj = cJSON_GetObjectItem(e, "title"); + if (aid_obj && bvid_obj && cid_obj && title_obj) { + Episode *episode = get_element(§ion->episodes, j); + episode->aid = aid_obj->valueint; + episode->bvid = bvid_obj->valuestring; + episode->cid = cid_obj->valueint; + episode->title = title_obj->valuestring; + DEBUG_PRINT("sections[%d].episodes[%d] aid: %d\n", i, j, + episode->aid); + DEBUG_PRINT("sections[%d].episodes[%d] bvid: %s\n", i, j, + episode->bvid); + DEBUG_PRINT("sections[%d].episodes[%d] cid: %d\n", i, j, + episode->cid); + DEBUG_PRINT("sections[%d].episodes[%d] title: %s\n", i, j, + episode->title); + + j++; + continue; + } + r = 1; + LOG("cJSON", "Read JSON.sections[%d].episodes[%d] failed.\n", i, j); + return r; + } + + i++; + continue; + } + r = 1; + LOG("cJSON", "Read JSON.sections[%d] failed.\n", i); + return r; + } + + /* videoData */ + Multipage_video_data *videoData = &multipage_struct->videoData; + cJSON *title_obj = cJSON_GetObjectItem(videoData_obj, "title"); + cJSON *pages_obj = cJSON_GetObjectItem(videoData_obj, "pages"); + if (title_obj && pages_obj) { + videoData->title = title_obj->valuestring; + DEBUG_PRINT("videoData.title: %s\n", videoData->title); + + videoData->pages = create_array(sizeof(Video_pages_data), + cJSON_GetArraySize(pages_obj)); + int i = 0; + cJSON *e; + cJSON_ArrayForEach(e, pages_obj) { + cJSON *cid_obj = cJSON_GetObjectItem(e, "cid"); + cJSON *part_obj = cJSON_GetObjectItem(e, "part"); + cJSON *page_obj = cJSON_GetObjectItem(e, "page"); + if (cid_obj && part_obj && page_obj) { + Video_pages_data *page = get_element(&videoData->pages, i); + page->cid = cid_obj->valueint; + page->part = part_obj->valuestring; + page->page = page_obj->valueint; + DEBUG_PRINT("videoData.pages[%d].cid: %d\n", i, page->cid); + DEBUG_PRINT("videoData.pages[%d].part: %s\n", i, page->part); + DEBUG_PRINT("videoData.pages[%d].page: %d\n", i, page->page); + + i++; + continue; + } + LOG("cJSON", "Read JSON.videodata.pages[%d] failed.\n", i); + return 1; + } + } else { + LOG("cJSON", "Read JSON.videodata failed.\n"); + return 1; + } + } else { + r = 1; + LOG("cJSON", "Parse pagedata JSON failed.\n"); + } + } + return r; +} + +static int get_dash(const char *api_resp, Dash *dash) { + dash->json = cJSON_Parse(api_resp); + + cJSON *code_obj = cJSON_GetObjectItem(dash->json, "code"); + cJSON *message_obj = cJSON_GetObjectItem(dash->json, "message"); + cJSON *dashinfo_obj = cJSON_GetObjectItem(dash->json, "data"); + if (cJSON_IsInvalid(dashinfo_obj)) { + dashinfo_obj = cJSON_GetObjectItem(dash->json, "result"); + } + + if (!code_obj || !message_obj || !dashinfo_obj) { + LOG("cJSON", "Parse API resp_json failed.\n"); + return 1; + } + dash->code = code_obj->valueint; + dash->message = code_obj->valuestring; + + /* dashinfo: "data" or "result" */ + DEBUG_PRINT("Key of dashinfo: %s\n", dashinfo_obj->string); + Dash_info *dashinfo = &dash->dashinfo; + cJSON *quality_obj = cJSON_GetObjectItem(dashinfo_obj, "quality"); + cJSON *accept_description_obj = + cJSON_GetObjectItem(dashinfo_obj, "accept_description"); + cJSON *accept_quality_obj = + cJSON_GetObjectItem(dashinfo_obj, "accept_quality"); + cJSON *dash_streams_obj = cJSON_GetObjectItem(dashinfo_obj, "dash"); + cJSON *format_obj = cJSON_GetObjectItem(dashinfo_obj, "format"); + cJSON *durl_obj = cJSON_GetObjectItem(dashinfo_obj, "durl"); // NOTE: Optional + + if (!quality_obj || !accept_description_obj || !accept_quality_obj || + !dash_streams_obj || !format_obj) { + LOG("cJSON", "Read API resp_json.%s failed.\n", dashinfo_obj->string); + return 1; + } + + dashinfo->quality = quality_obj->valueint; + DEBUG_PRINT("quality: %d\n", dashinfo->quality); + + dashinfo->format = format_obj->valuestring; + DEBUG_PRINT("format: %s\n", dashinfo->format); + + dashinfo->accept_description = + create_str_array(cJSON_GetArraySize(accept_quality_obj)); + str_array_t *ac_d = &dashinfo->accept_description; + for (unsigned char n = 0; n < cJSON_GetArraySize(accept_description_obj); + n++) { + cJSON *i = cJSON_GetArrayItem(accept_description_obj, n); + + if (!i) { + LOG("cJSON", "Read API resp_json.%s.accept_description failed.\n", + dashinfo_obj->string); + return 1; + } + set_str_element(ac_d, n, i->valuestring); + DEBUG_PRINT("accept_description[%hhu]: %s\n", n, get_str_element(ac_d, n)); + } + + dashinfo->accept_quality = + create_array(sizeof(int), cJSON_GetArraySize(accept_quality_obj)); + generic_array_t *ac_q = &dashinfo->accept_quality; + for (unsigned char n = 0; n < cJSON_GetArraySize(accept_quality_obj); n++) { + cJSON *i = cJSON_GetArrayItem(accept_quality_obj, n); + + if (!i) { + LOG("cJSON", "Read API resp_json.%s.accept_quality failed.\n", + dashinfo_obj->string); + return 1; + } + int *v = get_element(ac_q, n); + *v = i->valueint; + DEBUG_PRINT("accept_quality[%hhu]: %d\n", n, *v); + } + + cJSON *video_obj = cJSON_GetObjectItem(dash_streams_obj, "video"); + cJSON *audio_obj = cJSON_GetObjectItem(dash_streams_obj, "audio"); + + if (!video_obj || !audio_obj) { + LOG("cJSON", "Read API resp_json.%s.dash failed.\n", dashinfo_obj->string); + return 1; + } + + dashinfo->dash.video = + create_array(sizeof(Dash_stream), cJSON_GetArraySize(video_obj)); + dashinfo->dash.audio = + create_array(sizeof(Dash_stream), cJSON_GetArraySize(audio_obj)); + generic_array_t *target; + cJSON *dash_stream_obj; + for (dash_stream_obj = video_obj, target = &dashinfo->dash.video;;) { + int i = 0; + cJSON *e; + cJSON_ArrayForEach(e, dash_stream_obj) { + cJSON *id_obj = cJSON_GetObjectItem(e, "id"); + cJSON *baseUrl_obj = cJSON_GetObjectItem(e, "baseUrl"); + cJSON *bandwidth_obj = cJSON_GetObjectItem(e, "bandwidth"); + cJSON *mimeType_obj = cJSON_GetObjectItem(e, "mimeType"); + cJSON *codecid_obj = cJSON_GetObjectItem(e, "codecid"); + cJSON *codecs_obj = cJSON_GetObjectItem(e, "codecs"); + + if (!id_obj || !baseUrl_obj || !bandwidth_obj || !mimeType_obj || + !codecid_obj || !codecs_obj) { + LOG("cJSON", "Read API resp_json.%s.dash.%s[%d] failed.\n", + dashinfo_obj->string, dash_stream_obj->string, i); + return 1; + } + Dash_stream *ds = get_element(target, i); + ds->id = id_obj->valueint; + ds->baseUrl = baseUrl_obj->valuestring; + ds->bandwidth = bandwidth_obj->valueint; + ds->mimeType = mimeType_obj->valuestring; + ds->codecid = codecid_obj->valueint; + ds->codecs = codecs_obj->valuestring; + + DEBUG_PRINT("%s[%d].id: %d\n", dash_stream_obj->string, i, ds->id); + DEBUG_PRINT("%s[%d].baseUrl: %s\n", dash_stream_obj->string, i, + ds->baseUrl); + DEBUG_PRINT("%s[%d].bandwidth: %d\n", dash_stream_obj->string, i, + ds->bandwidth); + DEBUG_PRINT("%s[%d].mimeType: %s\n", dash_stream_obj->string, i, + ds->mimeType); + DEBUG_PRINT("%s[%d].codecid: %d\n", dash_stream_obj->string, i, + ds->codecid); + DEBUG_PRINT("%s[%d].codecs: %s\n", dash_stream_obj->string, i, + ds->codecs); + + i++; + } + + if (dash_stream_obj == video_obj) { + dash_stream_obj = audio_obj; + target = &dashinfo->dash.audio; + } else { + break; + } + } + + return 0; +} + +static int get_page_in_query(char *query, int *page) { + const char *pattern = "p=(\\d+)"; + str_array_t results = {0}; + int r = regex_match(query, (str_array_t){(char **)&pattern, 1}, &results); + if (!r) { + // for (unsigned short i = 0; i < results.n; i++) { + // DEBUG_PRINT("%s\n", results.str[i]); + // } + *page = results.n ? atoi(results.str[0]) : 1; // Download p1 by default + } + return r; +} + +static int generate_api(Bilibili_options *bilibili_options, const int quality) { + char params[UCHAR_MAX]; + snprintf(params, sizeof(params), + "avid=%d&cid=%d&bvid=%s&qn=%d&type=&otype=json&fourk=1&fnver=0&" + "fnval=2000", + bilibili_options->aid, bilibili_options->cid, bilibili_options->bvid, + quality); + bilibili_options->api = malloc(strlen(BILIBILI_API) + strlen(params) + 1); + strcpy(bilibili_options->api, BILIBILI_API); + strcat(bilibili_options->api, params); + return 0; +} + +static const char *mimeType2ext(const char *mimeType) { + static char mimeType_l[CHAR_MAX]; + strcpy(mimeType_l, mimeType); + const char *exts[2]; + size_t extsCount = 0; + + char *token = strtok(mimeType_l, "/"); + while (token != NULL && extsCount < 2) { + exts[extsCount++] = token; + token = strtok(NULL, "/"); + } + + if (extsCount == 2) { + return exts[1]; + } + + return "mp4"; // Cannot parse, use default +} + +static const char *id2quality_desc(int id) { + const char *desc; + switch (id) { + case 127: + desc = "超高清 8K"; + break; + case 120: + desc = "超清 4K"; + break; + case 112: + desc = "高清 1080P+"; + break; + case 80: + desc = "高清 1080P"; + break; + case 48: + desc = "高清 720P"; + break; + case 32: + desc = "清晰 480P"; + break; + case 16: + desc = "流畅 360P"; + break; + default: + desc = "Unknown resolution"; + break; + } + return desc; +} + +static void multipage_cleanup(Multipage *multipage_struct) { + for (unsigned short i = 0; i < multipage_struct->sections.n; i++) { + // free_and_nullify(multipage_struct->sections[i].episodes); + Multi_episode_data *section = get_element(&multipage_struct->sections, i); + free_array(§ion->episodes); + } + free_array(&multipage_struct->sections); + free_array(&multipage_struct->videoData.pages); + cJSON_Delete(multipage_struct->json); + multipage_struct->json = NULL; +} + +static void dash_cleanup(Dash *dash) { + cJSON_Delete(dash->json); + free_str_array(&dash->dashinfo.accept_description); + free_array(&dash->dashinfo.accept_quality); + free_array(&dash->dashinfo.dash.audio); + free_array(&dash->dashinfo.dash.video); +} + +static int download(Bilibili_options *bilibili_options) { + Dash dash = {0}; + char *resp; + get(bilibili_options->api, &resp); + if (get_dash(resp, &dash)) { + LOG("Bilibili", "Get dash failed."); + free_and_nullify(resp); + dash_cleanup(&dash); + return 1; + }; + + // Download the highest resolution + Dash_stream *video = get_element(&dash.dashinfo.dash.video, 0); + Dash_stream *audio = get_element(&dash.dashinfo.dash.audio, 0); + const char *quality_desc = id2quality_desc(video->id); + + { + char fn[USHRT_MAX]; + sprintf(fn, "%s[%s]-%s.%s", bilibili_options->title, quality_desc, "video", + mimeType2ext(video->mimeType)); + add_url(video->baseUrl, NULL, fn, "https://www.bilibili.com"); + } + + { + char fn[USHRT_MAX]; + sprintf(fn, "%s[%s]-%s.%s", bilibili_options->title, + quality_desc, "audio", mimeType2ext(audio->mimeType)); + add_url(audio->baseUrl, NULL, fn, "https://www.bilibili.com"); + } + + free_and_nullify(resp); + dash_cleanup(&dash); + return 0; +} + +void bilibili_extract(struct options *options) { + Multipage multipage_struct = {0}; + Bilibili_options bilibili_options = {options->URL}; + int p = 1; + char *api; + + if (get(options->URL, &options->pagedata)) { + append_log("[Bilibili] Download pagedata failed.\n"); + return; + } + bilibili_options.html = options->pagedata; + + if (get_multipagedata(options->pagedata, &multipage_struct, + &bilibili_options.is_page)) { + multipage_cleanup(&multipage_struct); + append_log("[Bilibili] Parse pagedata failed.\n"); + return; + }; + + if (get_page_in_query(options->query, &p) || p < 1 || + p > multipage_struct.videoData.pages.n) { + multipage_cleanup(&multipage_struct); + append_log("[Bilibili] Parse query failed.\n"); + return; + } + + Video_pages_data *page = + get_element(&multipage_struct.videoData.pages, p - 1); + + bilibili_options.aid = multipage_struct.aid; + bilibili_options.bvid = multipage_struct.bvid; + bilibili_options.cid = page->cid; + bilibili_options.page = p; + bilibili_options.title = multipage_struct.videoData.title; + + DEBUG_PRINT("aid: %d\n", bilibili_options.aid); + DEBUG_PRINT("bvid: %s\n", bilibili_options.bvid); + DEBUG_PRINT("cid: %d\n", bilibili_options.cid); + DEBUG_PRINT("is_page: %s\n", bilibili_options.is_page ? "yes" : "no"); + DEBUG_PRINT("page: %d\n", bilibili_options.page); + DEBUG_PRINT("title: %s\n", bilibili_options.title); + + if (generate_api(&bilibili_options, 127)) { + free_and_nullify(bilibili_options.api); + multipage_cleanup(&multipage_struct); + return; + } + DEBUG_PRINT("Generated API: %s\n", bilibili_options.api); + + if (download(&bilibili_options)) { + free_and_nullify(bilibili_options.api); + multipage_cleanup(&multipage_struct); + return; + } + + free_and_nullify(bilibili_options.api); + multipage_cleanup(&multipage_struct); +} diff --git a/src/extractors/bilibili.h b/src/extractors/bilibili.h new file mode 100644 index 0000000..5d609b0 --- /dev/null +++ b/src/extractors/bilibili.h @@ -0,0 +1,94 @@ +#ifndef BILIBILI_H_ +#define BILIBILI_H_ + +#include "../utils.h" +#include "extractor.h" +#include <stddef.h> + +#define BILIBILI_API "https://api.bilibili.com/x/player/playurl?" +#define BILIBILI_BANGUMI_API "https://api.bilibili.com/pgc/player/web/playurl?" +#define BILIBILI_TOKEN_API "https://api.bilibili.com/x/player/playurl/token?" + +typedef struct video_pages_data { + int cid; + char *part; + int page; +} Video_pages_data; + +typedef struct multipage_video_data { + char *title; + generic_array_t pages; +} Multipage_video_data; + +typedef struct episode { + int aid; + char *bvid; + int cid; + char *title; +} Episode; + +typedef struct multi_episode_data { + int season_id; + generic_array_t episodes; +} Multi_episode_data; + +typedef struct multipage { + int aid; + char *bvid; + generic_array_t sections; + Multipage_video_data videoData; + cJSON *json; +} Multipage; + +typedef struct bilibili_options { + char *url; + char *html; + char *api; + char *cookie; + bool is_bangumi; + bool is_page; + int aid; + int cid; + char *bvid; + int page; + char *title; +} Bilibili_options; + +typedef struct durl { + char *url; + size_t size; +} Durl; + +typedef struct dash_stream { + int id; + char *baseUrl; + int bandwidth; + char *mimeType; + int codecid; + char *codecs; +} Dash_stream; + +typedef struct dash_streams { + generic_array_t video; + generic_array_t audio; +} Dash_streams; + +typedef struct dash_info { + int quality; + str_array_t accept_description; + generic_array_t accept_quality; + Dash_streams dash; + char *format; + generic_array_t durl; +} Dash_info; + +typedef struct dash { + int code; + char *message; + Dash_info dashinfo; + cJSON *json; +} Dash; + +void bilibili_extract(struct options *); + +#endif diff --git a/src/extractors/extractor.c b/src/extractors/extractor.c new file mode 100644 index 0000000..91f34d2 --- /dev/null +++ b/src/extractors/extractor.c @@ -0,0 +1,24 @@ +#include <stdlib.h> + +#include "bilibili.h" +#include "extractor.h" + +Site_map site_map = {{{"www.bilibili.com", SITE_BILIBILI}}, 1}; + +void options_cleanup(Options *options) { + free_and_nullify(options->URL); + free_and_nullify(options->path); + free_and_nullify(options->query); + free_and_nullify(options->pagedata); +} + +int extract(void *v) { + Options *options = (Options *)v; + switch (options->site) { + case SITE_BILIBILI: + bilibili_extract(options); + break; + } + options_cleanup(options); + return 0; +} diff --git a/src/extractors/extractor.h b/src/extractors/extractor.h new file mode 100644 index 0000000..d3ebeec --- /dev/null +++ b/src/extractors/extractor.h @@ -0,0 +1,32 @@ +#ifndef EXTRACTOR_H_ +#define EXTRACTOR_H_ + +#include <cjson/cJSON.h> +#include <limits.h> +#include <stdbool.h> +#include <stddef.h> + +enum site { SITE_BILIBILI }; +typedef enum site site_t; + +typedef struct site_map { + struct { + char domain[SHRT_MAX]; + site_t site; + } pairs[1]; + unsigned char size; +} Site_map; + +typedef struct options { + site_t site; + char *URL; + char *path; + char *query; + char *pagedata; +} Options; + +void options_cleanup(Options*); + +int extract(void *); + +#endif diff --git a/src/logger.c b/src/logger.c new file mode 100644 index 0000000..0fef20a --- /dev/null +++ b/src/logger.c @@ -0,0 +1,32 @@ +#include <stdarg.h> +#include <stdio.h> + +#include "logger.h" +#include "nuklear.h" + +static struct logger logger = {0}; + +struct logger *setup_logger(void) { + return &logger; +} + +void append_log(const char *fmt, ...) { + va_list ap1; + va_start(ap1, fmt); + va_list ap2; + va_copy(ap2, ap1); + char buf[vsnprintf(NULL, 0, fmt, ap1) + 1]; + va_end(ap1); + vsnprintf(buf, sizeof(buf), fmt, ap2); + va_end(ap2); + nk_str_append_str_char(&logger.text_edit->string, buf); + logger.box_lines++; + logger.scrollbar->y += logger.extend_box ? (logger.font_height + 2) : 0; + DEBUG_PRINT("scrollbar w: %u, h: %u\n", logger.scrollbar->x, + logger.scrollbar->y); +} + +void clear_log() { + nk_textedit_delete(logger.text_edit, 0, logger.text_edit->string.len); + logger.box_lines = 0; +} diff --git a/src/logger.h b/src/logger.h new file mode 100644 index 0000000..dbc059f --- /dev/null +++ b/src/logger.h @@ -0,0 +1,30 @@ +#ifndef LOGGER_H_ +#define LOGGER_H_ + +#include <stdbool.h> + +#ifdef DEBUG +#define DEBUG_PRINT(fmt, args...) \ + fprintf(stderr, "DEBUG: %s:%d:%s(): " fmt, __FILE__, __LINE__, __func__, \ + ##args) +#else +#define DEBUG_PRINT(fmt, args...) /* Don't do anything in release builds */ +#endif + +#define LOG(component, fmt, args...) append_log("[%s] " fmt, component, ##args) + +struct logger { + struct nk_text_edit *text_edit; + unsigned long box_lines; + struct nk_scroll *scrollbar; + bool extend_box; + int font_height; +}; + +struct logger *setup_logger(void); + +void append_log(const char *fmt, ...); + +void clear_log(); + +#endif diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..d60112e --- /dev/null +++ b/src/main.c @@ -0,0 +1,163 @@ +#include <GL/glew.h> +#include <GLFW/glfw3.h> +#include <curl/curl.h> +#include <nfd.h> +#include <stdio.h> +#include <stdlib.h> + +#define MAX_VERTEX_BUFFER 512 * 1024 +#define MAX_ELEMENT_BUFFER 128 * 1024 +#define NK_INCLUDE_FIXED_TYPES +#define NK_INCLUDE_STANDARD_IO +#define NK_INCLUDE_STANDARD_VARARGS +#define NK_INCLUDE_DEFAULT_ALLOCATOR +#define NK_INCLUDE_VERTEX_BUFFER_OUTPUT +#define NK_INCLUDE_FONT_BAKING +#define NK_INCLUDE_DEFAULT_FONT +#define NK_IMPLEMENTATION +#define NK_GLFW_GL3_IMPLEMENTATION +#define NK_KEYSTATE_BASED_INPUT +#include "nuklear.h" +#include "nuklear_glfw_gl3.h" + +#include "constants.h" +#include "unifont.h" +#include "logger.h" +#include "main.h" +#include "process_url.h" +#include "style.h" +#include "ui.h" + +extern int win_width, win_height; +extern void load_ui(struct ui_struct *); +static void error_callback(int e, const char *d) { + printf("Error %d: %s\n", e, d); +} + +int main(void) { + struct ui_struct ui; + + /* Curl */ + curl_init(); + + /* GLFW */ + struct nk_glfw glfw = {0}; + static GLFWwindow *win; + glfwSetErrorCallback(error_callback); + if (!glfwInit()) { + fprintf(stdout, "[GFLW] failed to init!\n"); + exit(EXIT_FAILURE); + } + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); +#ifdef __APPLE__ + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); +#endif + + win = glfwCreateWindow(win_width, win_height, APP_NAME, NULL, NULL); + + if (!win) { + // Window creation failed + fprintf(stdout, "[GLFW] failed to create window. Does this platform " + "support OpenGL 3.3+?\n"); + printf("GL_VERSION_3_3: %s", + glewIsSupported("GL_VERSION_3_3") ? "yes" : "no"); + exit(EXIT_FAILURE); + } + + glfwMakeContextCurrent(win); + + /* Glew */ + glewExperimental = GL_TRUE; + GLenum err; + if ((err = glewInit()) != GLEW_OK) { + /* Problem: glewInit failed, something is seriously wrong. */ + fprintf(stderr, "Error: %s\n", glewGetErrorString(err)); + exit(EXIT_FAILURE); + } + + /* Native File Dialog */ + NFD_Init(); + + /* Logger setup */ + struct logger *logger = setup_logger(); + // Put it in ui_struct + ui.logger = logger; + + /* UI stat */ + status_t stat = {0}; + stat.is_done = true; + ui.stat = &stat; + + /* create context */ + ui.ctx = nk_glfw3_init(&glfw, win, NK_GLFW3_INSTALL_CALLBACKS); + + /* Font */ + ui.logger->font_height = 24; + { + struct nk_font_atlas *atlas; + struct nk_font_config cfg = nk_font_config(0); + /* NOTICE: Some CJK fonts may have incorrect glyph indexes. + https://github.com/Immediate-Mode-UI/Nuklear/issues/399 + https://github.com/Immediate-Mode-UI/Nuklear/issues/542 + FIX: https://github.com/Immediate-Mode-UI/Nuklear/pull/531 + */ + cfg.range = nk_font_chinese_glyph_ranges(); + // const nk_rune ranges[] = {0x4E00, 0x9FAF, 0x0020, 0x00FF, 0x3000, + // 0x30FF, 0x31F0, 0x31FF, 0xFF00, 0xFFEF, + // + // 0}; + // cfg.range = ranges; + cfg.oversample_h = 1; + cfg.oversample_v = 1; + nk_glfw3_font_stash_begin(&glfw, &atlas); + atlas->default_font = nk_font_atlas_add_compressed_base85( + atlas, unifont_compressed_data_base85, ui.logger->font_height, &cfg); + nk_glfw3_font_stash_end(&glfw); + } + + /* TextEdit */ + struct nk_text_edit text_edit; + nk_textedit_init_default(&text_edit); + + logger->text_edit = &text_edit; + + /* Scrollbar */ + struct nk_scroll scrollbar = {0}; + logger->scrollbar = &scrollbar; + + /* Theming */ + set_style(ui.ctx, THEME_DARK); + + while (!glfwWindowShouldClose(win)) { + /* Input */ + glfwPollEvents(); + nk_glfw3_new_frame(&glfw); + + /* Response to window resize */ + glfwGetWindowSize(win, &win_width, &win_height); + nk_window_set_bounds(ui.ctx, APP_NAME, + nk_rect(0, 0, win_width, win_height)); + + /* GUI */ + if (nk_begin(ui.ctx, APP_NAME, nk_rect(0, 0, win_width, win_height), + NK_WINDOW_BORDER | NK_WINDOW_MOVABLE | NK_WINDOW_TITLE)) { + load_ui(&ui); + } + nk_end(ui.ctx); + + /* Draw */ + glfwGetWindowSize(win, &win_width, &win_height); + glViewport(0, 0, win_width, win_height); + glClear(GL_COLOR_BUFFER_BIT); + nk_glfw3_render(&glfw, NK_ANTI_ALIASING_ON, MAX_VERTEX_BUFFER, + MAX_ELEMENT_BUFFER); + glfwSwapBuffers(win); + } + NFD_Quit(); + curl_cleanup(&stat); + nk_glfw3_shutdown(&glfw); + glfwTerminate(); + return 0; +} diff --git a/src/main.h b/src/main.h new file mode 100644 index 0000000..6a319d4 --- /dev/null +++ b/src/main.h @@ -0,0 +1,8 @@ +#ifndef MAIN_H_ +#define MAIN_H_ + +#include <stdio.h> + +int win_width = 800, win_height = 600; + +#endif diff --git a/src/process_url.c b/src/process_url.c new file mode 100644 index 0000000..4bfce8d --- /dev/null +++ b/src/process_url.c @@ -0,0 +1,526 @@ +#include <curl/curl.h> +#include <curl/easy.h> +#include <curl/header.h> +#include <curl/system.h> +#include <curl/urlapi.h> +#include <limits.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#ifdef _WIN32 +#include "c11threads.h" +#else +#include <threads.h> +#endif + +#include "nuklear.h" + +#include "constants.h" +#include "extractors/extractor.h" +#include "logger.h" +#include "process_url.h" + +/* NOTICE: the global curl_conf pointer will only stay valid during downloading, + * otherwise, ALWAYS point it to NULL. */ +static curl_conf_t *curl_conf; +extern Site_map site_map; +Options options; +static queue_t dl_queue; + +thrd_t tid[MAX_THREAD]; +mtx_t mtx; +cnd_t cnd; +bool corrupted; +static const char *outdir_g, *referer_g; +static CURLU *h; + +/*NOTE: Use logger(X) (defined as a generic macro) to log errors. */ +static bool logerr_b(CURLcode r) { + if (r && !corrupted) { + LOG("libcurl", "Error %d: %s\n", r, ERRTOSTRING(r)); + corrupted = true; + } + return r; +} + +static bool logerr_h(CURLHcode r) { + if (r) { + const char *err_str; + switch (r) { + case CURLHE_BADINDEX: + err_str = "header exists but not with this index"; + break; + case CURLHE_MISSING: + // Allow no headers + err_str = "no such header exists"; + DEBUG_PRINT("Header Error %d: %s\n", r, err_str); + return r; + break; + case CURLHE_NOHEADERS: + err_str = "no headers at all exist (yet)"; + break; + case CURLHE_NOREQUEST: + err_str = "no request with this number was used"; + break; + case CURLHE_OUT_OF_MEMORY: + err_str = "out of memory while processing"; + break; + case CURLHE_BAD_ARGUMENT: + err_str = "a function argument was not okay"; + break; + case CURLHE_NOT_BUILT_IN: + err_str = "if API was disabled in the build"; + break; + default: + err_str = "unknown error"; + break; + } + LOG("libcurl", "Header Error %d: %s\n", r, err_str); + corrupted = true; + } + return r; +} + +static bool logerr_u(CURLUcode r) { + switch (r) { + case CURLUE_NO_QUERY: + // Accept no query + DEBUG_PRINT("The URL has no query.\n"); + break; + case 0: + break; + default: + LOG("libcurl", "Parse Error %d: Invalid URL\n", r); + break; + } + return r; +} + +static void curl_easy_setcommonopts(CURL *curl) { + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_AUTOREFERER, 1L); + curl_easy_setopt( + curl, CURLOPT_USERAGENT, + "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/116.0"); + curl_easy_setopt(curl, CURLOPT_REFERER, referer_g); + /* enable all supported built-in compressions, + * since serveral sites enable gzip encoding */ + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); +} + +static int progress_callback(void *clientp, curl_off_t dltotal, + curl_off_t dlnow, curl_off_t ultotal, + curl_off_t ulnow) { + thrd_info_t *ti = (thrd_info_t *)clientp; + ti->curl_c->dlnow_per_thrd[ti->no] = dlnow; + if (ti->curl_c->total_thrd == 1) { + ti->curl_c->dltotal = dltotal; + } + // Return non-zero to abort download + return corrupted; +} + +static size_t write2str(void *ptr, size_t size, size_t nmemb, str_data_t *s) { + size_t new_len = s->len + size * nmemb; + s->string = realloc(s->string, new_len + 1); + memcpy(s->string + s->len, ptr, size * nmemb); + s->string[new_len] = '\0'; + s->len = new_len; + + return size * nmemb; +} + +static int parse_url(const char *URL, const char *outdir, char *fn) { + CURLUcode ue = logerr(curl_url_set(h, CURLUPART_URL, URL, 0)); + if (ue && ue != CURLUE_NO_QUERY) { + return 1; + } + char *domain, *path, *query; + + if (ue == CURLUE_NO_QUERY) { + query = NULL; + } else { + ue = logerr(curl_url_get(h, CURLUPART_QUERY, &query, 0)); + } + ue = curl_url_get(h, CURLUPART_HOST, &domain, 0); + if (ue) { + return 1; + } + ue = logerr(curl_url_get(h, CURLUPART_PATH, &path, 0)); + if (ue) { + return 1; + } + + DEBUG_PRINT("Domain: %s\n", domain); + DEBUG_PRINT("Path: %s\n", path); + DEBUG_PRINT("Query: %s\n", query); + + for (unsigned short i = 0; i < site_map.size; i++) { + if (!strcmp(domain, site_map.pairs[i].domain)) { + append_log("Got site: %s\n", domain); + thrd_t t; + options.site = site_map.pairs[i].site; + options.URL = malloc(strlen(domain) + strlen(path) + 10); + sprintf(options.URL, "https://%s%s", domain, path); + options.path = malloc(strlen(path) + 1); + strcpy(options.path, path); + if (query) { + options.query = malloc(strlen(query) + 1); + strcpy(options.query, query); + } else { + options.query = calloc(1, sizeof(char)); + } + + append_log("pagedata URL: %s\n", options.URL); + + thrd_create(&t, extract, &options); + thrd_detach(t); + + curl_free(domain); + curl_free(path); + curl_free(query); + return 0; + }; + } + + curl_conf_t *curl_c = malloc(sizeof(curl_conf_t)); + curl_c->URL = malloc(strlen(URL) + 1); + strcpy(curl_c->URL, URL); + + /* filename */ + + if (fn == NULL) { + const char *patterns_str[1] = {"(?:.+\\/)([^#/?]+)"}; + str_array_t results = create_str_array(0); + const str_array_t patterns = {(char **)patterns_str, 1}; + regex_match(path, patterns, &results); + for (unsigned short i = 0; i < results.n; i++) { + if (results.str[i]) { + DEBUG_PRINT("[%d] %s\n", i, results.str[i]); + sprintf(curl_c->outfn, "%s%s%s", outdir, + outdir[strlen(outdir) - 1] == SPLITTER_CHAR ? "" : SPLITTER_STR, + results.str[i]); + } + } + free_str_array(&results); + if (curl_c->outfn[0] == '\0') { + // sprintf(curl_c->outfn, "%s%c%s", outdir, SPLITTER, + // "test"); + LOG("libcurl", + "Infer filename failed, please specify a valid filename.\n"); + curl_free(domain); + curl_free(path); + curl_free(query); + return 1; + } + } else { + sprintf(curl_c->outfn, "%s%s%s", outdir, + outdir[strlen(outdir) - 1] == SPLITTER_CHAR ? "" : SPLITTER_STR, + fn); + free_and_nullify(fn); + } + DEBUG_PRINT("File will be saved as: %s\n", curl_c->outfn); + DEBUG_PRINT("Got regular URL: %s\n", curl_c->URL); + + enqueue(&dl_queue, (void *)curl_c); + + curl_free(domain); + curl_free(path); + curl_free(query); + + return 0; +} + +static bool get_info(const char *URL, long *psize) { + CURL *curl; + long resp_code; + bool support_range = false; + struct curl_header *pch; + curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, URL); + curl_easy_setcommonopts(curl); + curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NULL); + CURLcode r = curl_easy_perform(curl); + if (logerr(r)) { + curl_easy_cleanup(curl); + return support_range; + } + r = curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, + (curl_off_t *)psize); + if (logerr(r)) { + curl_easy_cleanup(curl); + return support_range; + } + CURLHcode rh = + curl_easy_header(curl, "Accept-Ranges", 0, CURLH_HEADER, -1, &pch); + if (logerr(rh) || strcmp(pch->value, "bytes")) { + curl_easy_cleanup(curl); + return support_range; + } + char *ct = NULL; + r = curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct); + if (logerr(r)) { + curl_easy_cleanup(curl); + return support_range; + } + + support_range = true; + curl_easy_cleanup(curl); + return support_range; +} + +static int pull_part(void *a) { + CURLcode res; + thrd_info_t *ti = (thrd_info_t *)a; + curl_conf_t *curl_c = ti->curl_c; + unsigned char n = ti->no; + // Here we need to manually control str_array_t + curl_c->partfn.str[n] = malloc(strlen(curl_c->outfn) + 4); + sprintf(curl_c->partfn.str[n], "%s.%d", curl_c->outfn, n); + DEBUG_PRINT("[THRD %hhu] partfn: %s, range: %s\n", n, + get_str_element(&curl_c->partfn, n), ti->range); + { + curl_c->fplist[n] = fopen(get_str_element(&curl_c->partfn, n), "wb+"); + CURL *curl; + + curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, curl_c->URL); + curl_easy_setcommonopts(curl); + curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 60L); + curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 30L); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, curl_c->fplist[n]); + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L); + if (ti->curl_c->total_thrd != 1) { + curl_easy_setopt(curl, CURLOPT_RANGE, ti->range); + } + curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, progress_callback); + curl_easy_setopt(curl, CURLOPT_XFERINFODATA, ti); + res = curl_easy_perform(curl); + rewind(curl_c->fplist[n]); + append_log("[THRD %hhu] File downloaded.\n", n); + curl_easy_cleanup(curl); + logerr(res); + } + mtx_lock(&mtx); + curl_c->success_thrd += 1; + cnd_signal(&cnd); // Unblocks the waiting cleanup thread. If no threads are + // blocked, does nothing and returns thrd_success. + + mtx_unlock(&mtx); + return (int)res; +} + +static int merge_and_cleanup(curl_conf_t *curl_c) { + if (corrupted) { + append_log("Cancelling...\n"); + } else { + append_log("Merging files...\n"); + } + + FILE *fop; + fop = fopen(curl_c->outfn, "wb"); + if (fop == NULL) { + // User quitted before downloading, return directly + return 1; + } + for (unsigned short i = 0; i < curl_c->total_thrd; i++) { + if (!corrupted) { + char buffer[1024]; + size_t bytesRead = 0; + while ((bytesRead = fread(buffer, 1, sizeof(buffer), curl_c->fplist[i])) > + 0) { + fwrite(buffer, 1, bytesRead, fop); + } + } + fclose(curl_c->fplist[i]); + if (remove(get_str_element(&curl_c->partfn, i)) != 0) { + append_log("Error deleting partial file %s\n", + get_str_element(&curl_c->partfn, i)); + } + } + fclose(fop); + + if (corrupted) { + // Also delete dst file + if (remove(curl_c->outfn) != 0) { + append_log("Error deleting file %s\n", curl_c->outfn); + } + } + // Reset stat + corrupted = false; + curl_c->success_thrd = 0; + curl_c->total_thrd = 0; + free_and_nullify(curl_c->URL); + + return 0; +} + +static int download(curl_conf_t *curl_c) { + /* Reset thrd info. */ + // if (curl_c->success_thrd == curl_c->total_thrd) { + curl_c->success_thrd = 0; + // } + + CURL *curl; + curl_off_t cl = 0L, begin = 0L, end; + + static thrd_info_t thrd_info[MAX_THREAD] = {0}; + + bool support_range = get_info(curl_c->URL, &cl); + DEBUG_PRINT("Size: %ld bytes.\n", cl); + if (support_range && cl > 0L) { + curl_c->dltotal = cl; + curl_c->total_thrd = (unsigned char)CEIL_DIV(cl, MAX_THREAD_SIZE); + if (curl_c->total_thrd > MAX_THREAD) { + curl_c->total_thrd = MAX_THREAD; + } + LOG("libcurl", "Server supports range header, setting threads to %hhu\n", + curl_c->total_thrd); + } else { + LOG("libcurl", "Server doesn't claim range header " + "support, falling back to single thread.\n"); + curl_c->total_thrd = 1; + } + curl_off_t size_per_thrd = (cl / curl_c->total_thrd); + + curl_c->partfn = create_str_array(curl_c->total_thrd); + + for (unsigned char i = 0; i < curl_c->total_thrd; i++) { + curl_off_t chunk_size; + thrd_info[i].no = i; + if (i + 1 == curl_c->total_thrd) + chunk_size = cl - (curl_c->total_thrd - 1) * size_per_thrd; + else + chunk_size = size_per_thrd; + end = begin + chunk_size - 1; + if (curl_c->total_thrd != 1) { + sprintf(thrd_info[i].range, + "%" CURL_FORMAT_CURL_OFF_T "-%" CURL_FORMAT_CURL_OFF_T, begin, + end); + } + thrd_info[i].curl_c = curl_c; + int error = thrd_create(&tid[i], pull_part, &thrd_info[i]); + if (error) + append_log("Couldn't run thread number %d, errno %d\n", i, error); + begin = end + 1; + } + return 0; +} + +void curl_init(curl_conf_t *curl) { + curl_global_init(CURL_GLOBAL_ALL); + h = curl_url(); + dl_queue = create_queue(); + mtx_init(&mtx, mtx_plain); + cnd_init(&cnd); +} + +void curl_cleanup(status_t *stat) { + /* We only need to cleanup + * the currently active thread. */ + if (curl_conf) { + + corrupted = true; // In case libcurl is still downloading + /* Now Wait for all threads to cancel... */ + mtx_lock(&mtx); + while (curl_conf->success_thrd != curl_conf->total_thrd) { + cnd_wait(&cnd, &mtx); + } + mtx_unlock(&mtx); + if (!stat->is_done) { + merge_and_cleanup(curl_conf); + } + mtx_destroy(&mtx); + cnd_destroy(&cnd); + } + free_queue(&dl_queue); + curl_url_cleanup(h); + curl_global_cleanup(); +} + +void poll_status(status_t *stat) { + if (!is_empty_queue(&dl_queue) && stat->is_done) { + /* extract_done is a flag used to signal that + * the extractor process is done. */ + curl_conf = (curl_conf_t *)dequeue(&dl_queue); + if (download(curl_conf)) { + // Something went wrong when creating download task + DEBUG_PRINT("Creating download task failed.\n"); + }; + stat->is_done = false; + } + if (curl_conf) { + curl_conf->dlnow = 0L; + for (unsigned char i = 0; i < curl_conf->total_thrd; i++) { + curl_conf->dlnow += curl_conf->dlnow_per_thrd[i]; + } + stat->cur = curl_conf->dlnow; + stat->total = curl_conf->dltotal; + DEBUG_PRINT("success_thrd: %hhu, total_thrd: %hhu, is_done: %s\n", + curl_conf->success_thrd, curl_conf->total_thrd, + stat->is_done ? "yes" : "no"); + mtx_lock(&mtx); + if (curl_conf->success_thrd == curl_conf->total_thrd && + (curl_conf->total_thrd && !stat->is_done)) { + stat->is_done = true; + for (unsigned short i = 0; i < curl_conf->total_thrd; i++) { + int r; + thrd_join(tid[i], &r); + } + merge_and_cleanup(curl_conf); + append_log("Download %s finished.\n", curl_conf->outfn); + curl_conf = NULL; + } + mtx_unlock(&mtx); + } +} + +int get(const char *URL, char **pdstr) { + CURL *curl = curl_easy_init(); + str_data_t pagedata = {0}; + pagedata.string = malloc(1); + pagedata.string[0] = '\0'; + curl_easy_setopt(curl, CURLOPT_URL, URL); + curl_easy_setcommonopts(curl); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write2str); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&pagedata); + CURLcode res = logerr(curl_easy_perform(curl)); + *pdstr = malloc(pagedata.len + 1); + strcpy(*pdstr, pagedata.string); + curl_easy_cleanup(curl); + return res; +} + +/* Add an URL to dl_queue. + * - If outdir is NULL or a empty string, reuse the cached outdir_g + * - If fn is NULL or a empty string, infer the filename from URL (otherwise + * fail and quit') + * - If referer is NULL or a empty string, uses NULL */ +void add_url(const char *URL, const char *outdir, const char *fn, + const char *referer) { + if (outdir && outdir[0] != '\0') { + outdir_g = outdir; + } + referer_g = referer; + if (referer && referer[0] == '\0') { + referer_g = NULL; + } + DEBUG_PRINT("referer_g: %s\n", referer_g); + + char *filename; + if (fn == NULL || fn[0] == '\0') { + filename = NULL; + } else { + filename = malloc(strlen(fn) + 1); + strcpy(filename, fn); + } + + // Pass our cache (outdir_g) to parse_url() + if (parse_url(URL, outdir_g, filename)) { + DEBUG_PRINT("parse_url() failed with error.\n"); + return; // Parse failed, quit the task directly + }; +} diff --git a/src/process_url.h b/src/process_url.h new file mode 100644 index 0000000..03023b5 --- /dev/null +++ b/src/process_url.h @@ -0,0 +1,59 @@ +#ifndef PROCESS_URL_H_ +#define PROCESS_URL_H_ + +#include <curl/curl.h> +#include <limits.h> +#include <stdbool.h> + +#include "utils.h" + +#define MAX_THREAD 6 +#define MAX_THREAD_SIZE 10485760 + +#define ERRTOSTRING(err) curl_easy_strerror(err) +#define logerr(X) \ + _Generic((X), CURLcode \ + : logerr_b, CURLHcode \ + : logerr_h, CURLUcode \ + : logerr_u)(X) + +typedef struct curl_conf { + curl_off_t dlnow_per_thrd[MAX_THREAD]; + curl_off_t dlnow; + curl_off_t dltotal; + unsigned char success_thrd; + unsigned char total_thrd; + char *URL; + char outfn[USHRT_MAX]; + str_array_t partfn; + FILE *fplist[MAX_THREAD]; +} curl_conf_t; + +typedef struct thrd_info { + unsigned char no; + curl_conf_t *curl_c; + char range[UCHAR_MAX]; +} thrd_info_t; + +typedef struct status { + curl_off_t cur; + curl_off_t total; + bool is_done; +} status_t; + +typedef struct str_data { + char *string; + size_t len; +} str_data_t; + +void curl_init(); + +void curl_cleanup(status_t *); + +void poll_status(status_t *); + +int get(const char *, char **); + +void add_url(const char *, const char *, const char *, const char *); + +#endif diff --git a/src/style.h b/src/style.h new file mode 100644 index 0000000..cb7387b --- /dev/null +++ b/src/style.h @@ -0,0 +1,133 @@ +/* + https://github.com/Immediate-Mode-UI/Nuklear/blob/b4b94b0486c0c43045d0176d03cce016190fe3ff/demo/common/style.c +*/ +enum theme {THEME_BLACK, THEME_WHITE, THEME_RED, THEME_BLUE, THEME_DARK}; + +static void +set_style(struct nk_context *ctx, enum theme theme) +{ + struct nk_color table[NK_COLOR_COUNT]; + if (theme == THEME_WHITE) { + table[NK_COLOR_TEXT] = nk_rgba(70, 70, 70, 255); + table[NK_COLOR_WINDOW] = nk_rgba(175, 175, 175, 255); + table[NK_COLOR_HEADER] = nk_rgba(175, 175, 175, 255); + table[NK_COLOR_BORDER] = nk_rgba(0, 0, 0, 255); + table[NK_COLOR_BUTTON] = nk_rgba(185, 185, 185, 255); + table[NK_COLOR_BUTTON_HOVER] = nk_rgba(170, 170, 170, 255); + table[NK_COLOR_BUTTON_ACTIVE] = nk_rgba(160, 160, 160, 255); + table[NK_COLOR_TOGGLE] = nk_rgba(150, 150, 150, 255); + table[NK_COLOR_TOGGLE_HOVER] = nk_rgba(120, 120, 120, 255); + table[NK_COLOR_TOGGLE_CURSOR] = nk_rgba(175, 175, 175, 255); + table[NK_COLOR_SELECT] = nk_rgba(190, 190, 190, 255); + table[NK_COLOR_SELECT_ACTIVE] = nk_rgba(175, 175, 175, 255); + table[NK_COLOR_SLIDER] = nk_rgba(190, 190, 190, 255); + table[NK_COLOR_SLIDER_CURSOR] = nk_rgba(80, 80, 80, 255); + table[NK_COLOR_SLIDER_CURSOR_HOVER] = nk_rgba(70, 70, 70, 255); + table[NK_COLOR_SLIDER_CURSOR_ACTIVE] = nk_rgba(60, 60, 60, 255); + table[NK_COLOR_PROPERTY] = nk_rgba(175, 175, 175, 255); + table[NK_COLOR_EDIT] = nk_rgba(150, 150, 150, 255); + table[NK_COLOR_EDIT_CURSOR] = nk_rgba(0, 0, 0, 255); + table[NK_COLOR_COMBO] = nk_rgba(175, 175, 175, 255); + table[NK_COLOR_CHART] = nk_rgba(160, 160, 160, 255); + table[NK_COLOR_CHART_COLOR] = nk_rgba(45, 45, 45, 255); + table[NK_COLOR_CHART_COLOR_HIGHLIGHT] = nk_rgba( 255, 0, 0, 255); + table[NK_COLOR_SCROLLBAR] = nk_rgba(180, 180, 180, 255); + table[NK_COLOR_SCROLLBAR_CURSOR] = nk_rgba(140, 140, 140, 255); + table[NK_COLOR_SCROLLBAR_CURSOR_HOVER] = nk_rgba(150, 150, 150, 255); + table[NK_COLOR_SCROLLBAR_CURSOR_ACTIVE] = nk_rgba(160, 160, 160, 255); + table[NK_COLOR_TAB_HEADER] = nk_rgba(180, 180, 180, 255); + nk_style_from_table(ctx, table); + } else if (theme == THEME_RED) { + table[NK_COLOR_TEXT] = nk_rgba(190, 190, 190, 255); + table[NK_COLOR_WINDOW] = nk_rgba(30, 33, 40, 215); + table[NK_COLOR_HEADER] = nk_rgba(181, 45, 69, 220); + table[NK_COLOR_BORDER] = nk_rgba(51, 55, 67, 255); + table[NK_COLOR_BUTTON] = nk_rgba(181, 45, 69, 255); + table[NK_COLOR_BUTTON_HOVER] = nk_rgba(190, 50, 70, 255); + table[NK_COLOR_BUTTON_ACTIVE] = nk_rgba(195, 55, 75, 255); + table[NK_COLOR_TOGGLE] = nk_rgba(51, 55, 67, 255); + table[NK_COLOR_TOGGLE_HOVER] = nk_rgba(45, 60, 60, 255); + table[NK_COLOR_TOGGLE_CURSOR] = nk_rgba(181, 45, 69, 255); + table[NK_COLOR_SELECT] = nk_rgba(51, 55, 67, 255); + table[NK_COLOR_SELECT_ACTIVE] = nk_rgba(181, 45, 69, 255); + table[NK_COLOR_SLIDER] = nk_rgba(51, 55, 67, 255); + table[NK_COLOR_SLIDER_CURSOR] = nk_rgba(181, 45, 69, 255); + table[NK_COLOR_SLIDER_CURSOR_HOVER] = nk_rgba(186, 50, 74, 255); + table[NK_COLOR_SLIDER_CURSOR_ACTIVE] = nk_rgba(191, 55, 79, 255); + table[NK_COLOR_PROPERTY] = nk_rgba(51, 55, 67, 255); + table[NK_COLOR_EDIT] = nk_rgba(51, 55, 67, 225); + table[NK_COLOR_EDIT_CURSOR] = nk_rgba(190, 190, 190, 255); + table[NK_COLOR_COMBO] = nk_rgba(51, 55, 67, 255); + table[NK_COLOR_CHART] = nk_rgba(51, 55, 67, 255); + table[NK_COLOR_CHART_COLOR] = nk_rgba(170, 40, 60, 255); + table[NK_COLOR_CHART_COLOR_HIGHLIGHT] = nk_rgba( 255, 0, 0, 255); + table[NK_COLOR_SCROLLBAR] = nk_rgba(30, 33, 40, 255); + table[NK_COLOR_SCROLLBAR_CURSOR] = nk_rgba(64, 84, 95, 255); + table[NK_COLOR_SCROLLBAR_CURSOR_HOVER] = nk_rgba(70, 90, 100, 255); + table[NK_COLOR_SCROLLBAR_CURSOR_ACTIVE] = nk_rgba(75, 95, 105, 255); + table[NK_COLOR_TAB_HEADER] = nk_rgba(181, 45, 69, 220); + nk_style_from_table(ctx, table); + } else if (theme == THEME_BLUE) { + table[NK_COLOR_TEXT] = nk_rgba(20, 20, 20, 255); + table[NK_COLOR_WINDOW] = nk_rgba(202, 212, 214, 215); + table[NK_COLOR_HEADER] = nk_rgba(137, 182, 224, 220); + table[NK_COLOR_BORDER] = nk_rgba(140, 159, 173, 255); + table[NK_COLOR_BUTTON] = nk_rgba(137, 182, 224, 255); + table[NK_COLOR_BUTTON_HOVER] = nk_rgba(142, 187, 229, 255); + table[NK_COLOR_BUTTON_ACTIVE] = nk_rgba(147, 192, 234, 255); + table[NK_COLOR_TOGGLE] = nk_rgba(177, 210, 210, 255); + table[NK_COLOR_TOGGLE_HOVER] = nk_rgba(182, 215, 215, 255); + table[NK_COLOR_TOGGLE_CURSOR] = nk_rgba(137, 182, 224, 255); + table[NK_COLOR_SELECT] = nk_rgba(177, 210, 210, 255); + table[NK_COLOR_SELECT_ACTIVE] = nk_rgba(137, 182, 224, 255); + table[NK_COLOR_SLIDER] = nk_rgba(177, 210, 210, 255); + table[NK_COLOR_SLIDER_CURSOR] = nk_rgba(137, 182, 224, 245); + table[NK_COLOR_SLIDER_CURSOR_HOVER] = nk_rgba(142, 188, 229, 255); + table[NK_COLOR_SLIDER_CURSOR_ACTIVE] = nk_rgba(147, 193, 234, 255); + table[NK_COLOR_PROPERTY] = nk_rgba(210, 210, 210, 255); + table[NK_COLOR_EDIT] = nk_rgba(210, 210, 210, 225); + table[NK_COLOR_EDIT_CURSOR] = nk_rgba(20, 20, 20, 255); + table[NK_COLOR_COMBO] = nk_rgba(210, 210, 210, 255); + table[NK_COLOR_CHART] = nk_rgba(210, 210, 210, 255); + table[NK_COLOR_CHART_COLOR] = nk_rgba(137, 182, 224, 255); + table[NK_COLOR_CHART_COLOR_HIGHLIGHT] = nk_rgba( 255, 0, 0, 255); + table[NK_COLOR_SCROLLBAR] = nk_rgba(190, 200, 200, 255); + table[NK_COLOR_SCROLLBAR_CURSOR] = nk_rgba(64, 84, 95, 255); + table[NK_COLOR_SCROLLBAR_CURSOR_HOVER] = nk_rgba(70, 90, 100, 255); + table[NK_COLOR_SCROLLBAR_CURSOR_ACTIVE] = nk_rgba(75, 95, 105, 255); + table[NK_COLOR_TAB_HEADER] = nk_rgba(156, 193, 220, 255); + nk_style_from_table(ctx, table); + } else if (theme == THEME_DARK) { + table[NK_COLOR_TEXT] = nk_rgba(210, 210, 210, 255); + table[NK_COLOR_WINDOW] = nk_rgba(57, 67, 71, 215); + table[NK_COLOR_HEADER] = nk_rgba(51, 51, 56, 220); + table[NK_COLOR_BORDER] = nk_rgba(46, 46, 46, 255); + table[NK_COLOR_BUTTON] = nk_rgba(48, 83, 111, 255); + table[NK_COLOR_BUTTON_HOVER] = nk_rgba(58, 93, 121, 255); + table[NK_COLOR_BUTTON_ACTIVE] = nk_rgba(63, 98, 126, 255); + table[NK_COLOR_TOGGLE] = nk_rgba(50, 58, 61, 255); + table[NK_COLOR_TOGGLE_HOVER] = nk_rgba(45, 53, 56, 255); + table[NK_COLOR_TOGGLE_CURSOR] = nk_rgba(48, 83, 111, 255); + table[NK_COLOR_SELECT] = nk_rgba(57, 67, 61, 255); + table[NK_COLOR_SELECT_ACTIVE] = nk_rgba(48, 83, 111, 255); + table[NK_COLOR_SLIDER] = nk_rgba(50, 58, 61, 255); + table[NK_COLOR_SLIDER_CURSOR] = nk_rgba(48, 83, 111, 245); + table[NK_COLOR_SLIDER_CURSOR_HOVER] = nk_rgba(53, 88, 116, 255); + table[NK_COLOR_SLIDER_CURSOR_ACTIVE] = nk_rgba(58, 93, 121, 255); + table[NK_COLOR_PROPERTY] = nk_rgba(50, 58, 61, 255); + table[NK_COLOR_EDIT] = nk_rgba(50, 58, 61, 225); + table[NK_COLOR_EDIT_CURSOR] = nk_rgba(210, 210, 210, 255); + table[NK_COLOR_COMBO] = nk_rgba(50, 58, 61, 255); + table[NK_COLOR_CHART] = nk_rgba(50, 58, 61, 255); + table[NK_COLOR_CHART_COLOR] = nk_rgba(48, 83, 111, 255); + table[NK_COLOR_CHART_COLOR_HIGHLIGHT] = nk_rgba(255, 0, 0, 255); + table[NK_COLOR_SCROLLBAR] = nk_rgba(50, 58, 61, 255); + table[NK_COLOR_SCROLLBAR_CURSOR] = nk_rgba(48, 83, 111, 255); + table[NK_COLOR_SCROLLBAR_CURSOR_HOVER] = nk_rgba(53, 88, 116, 255); + table[NK_COLOR_SCROLLBAR_CURSOR_ACTIVE] = nk_rgba(58, 93, 121, 255); + table[NK_COLOR_TAB_HEADER] = nk_rgba(48, 83, 111, 255); + nk_style_from_table(ctx, table); + } else { + nk_style_default(ctx); + } +} diff --git a/src/ui.c b/src/ui.c new file mode 100644 index 0000000..7f1dc52 --- /dev/null +++ b/src/ui.c @@ -0,0 +1,98 @@ +#include <limits.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "nuklear.h" +#include <nfd.h> + +#include "constants.h" +#include "logger.h" +#include "process_url.h" +#include "ui.h" + +static nk_size pct; +static nfdchar_t *outPath; + +void load_ui(struct ui_struct *ui) { + static char text[USHRT_MAX], box_buffer[UINT16_MAX], status_string[UCHAR_MAX]; + float logGroup_height = + nk_window_get_height(ui->ctx) - 80 - 45 - 50 - 35 - 80; + ui->logger->extend_box = + (logGroup_height - 15) < + ((ui->logger->box_lines + 1) * (ui->logger->font_height + 2)); + + poll_status(ui->stat); + if (ui->stat->total) { + pct = (ui->stat->cur) * 100 / (ui->stat->total); + sprintf(status_string, "%lu/%lu, %lu%%", ui->stat->cur, ui->stat->total, + pct); + } + if (ui->stat->is_done) { + (ui->stat->total) = 0; // To prevent nuklear further updating status_string + } + + nk_layout_row_dynamic(ui->ctx, 80, 1); + nk_label(ui->ctx, "Hinata", NK_TEXT_CENTERED); + + nk_layout_row_template_begin(ui->ctx, 45); + nk_layout_row_template_push_static(ui->ctx, 5); + nk_layout_row_template_push_dynamic(ui->ctx); + nk_layout_row_template_push_static(ui->ctx, 5); + nk_layout_row_template_push_static(ui->ctx, 100); + nk_layout_row_template_push_static(ui->ctx, 5); + nk_layout_row_template_end(ui->ctx); + + SPACER; + nk_edit_string_zero_terminated(ui->ctx, NK_EDIT_FIELD | NK_EDIT_AUTO_SELECT, + text, USHRT_MAX - 1, nk_filter_ascii); + SPACER; + if (nk_button_label(ui->ctx, "下载")) { + // Clear logger text + clear_log(); + + nfdresult_t result = NFD_PickFolder(&outPath, ""); + if (result == NFD_OKAY) { + DEBUG_PRINT("[NFD] outPath: %s\n", outPath); + } else if (result == NFD_ERROR) { + LOG("NFD", "Error: %s\n", NFD_GetError()); + } + + if (outPath) { + append_log("Got URL: %s\n", text); + add_url(text, outPath, NULL, NULL); + } else { + LOG("NFD", "Please specify a valid file PATH to write to!\n"); + } + } + SPACER; + + nk_layout_row_dynamic(ui->ctx, 50, 1); + nk_label(ui->ctx, status_string, NK_TEXT_CENTERED); + + nk_layout_row_template_begin(ui->ctx, 35); + nk_layout_row_template_push_static(ui->ctx, 5); + nk_layout_row_template_push_dynamic(ui->ctx); + nk_layout_row_template_push_static(ui->ctx, 5); + nk_layout_row_template_end(ui->ctx); + SPACER; + if (nk_progress(ui->ctx, &pct, MAX_VALUE, !NK_MODIFIABLE)) { + // the value of the progress bar changed, the new value is stored in + // currentValue + } + SPACER; + + nk_layout_row_dynamic(ui->ctx, logGroup_height, 1); + ui->ctx->style.window.group_padding = nk_vec2(0, 0); + if (nk_group_scrolled_begin(ui->ctx, ui->logger->scrollbar, "LogGroup", 0)) { + nk_layout_row_dynamic( + ui->ctx, + MAX(logGroup_height - 15, + (ui->logger->box_lines + 1) * (ui->logger->font_height + 2)), + 1); + nk_edit_buffer(ui->ctx, NK_EDIT_BOX | NK_EDIT_READ_ONLY | NK_EDIT_MULTILINE, + ui->logger->text_edit, nk_filter_default); + nk_group_scrolled_end(ui->ctx); + } +} diff --git a/src/ui.h b/src/ui.h new file mode 100644 index 0000000..300e950 --- /dev/null +++ b/src/ui.h @@ -0,0 +1,16 @@ +#ifndef UI_H_ +#define UI_H_ + +#define SPACER nk_spacer(ui->ctx) + +#include "process_url.h" + +struct ui_struct { + struct nk_context *ctx; + struct logger *logger; + status_t *stat; +}; + +void load_ui(struct ui_struct *); + +#endif diff --git a/src/utils.c b/src/utils.c new file mode 100644 index 0000000..1132a94 --- /dev/null +++ b/src/utils.c @@ -0,0 +1,204 @@ +#include <pcre2.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "logger.h" +#include "utils.h" + +int regex_match(const char *subject, str_array_t patterns, + str_array_t *results) { + pcre2_code *re; + + int errornumber; + int rc = PCRE2_ERROR_NOMATCH; + + PCRE2_SIZE subject_len = strlen(subject); + PCRE2_SIZE offset = 0, erroroffset; + PCRE2_SIZE *ovector; + + pcre2_match_data *match_data; + for (unsigned short i = 0; i < patterns.n; i++) { + DEBUG_PRINT("Gets pattern: %s\n", patterns.str[i]); + re = pcre2_compile((PCRE2_SPTR)patterns.str[i], PCRE2_ZERO_TERMINATED, 0, + &errornumber, &erroroffset, NULL); + + if (re == NULL) { + PCRE2_UCHAR buffer[256]; + pcre2_get_error_message(errornumber, buffer, sizeof(buffer)); + LOG("PCRE2", "compilation failed at offset %d: %s\n", (int)erroroffset, + buffer); + return 1; + } + + match_data = pcre2_match_data_create_from_pattern(re, NULL); + + unsigned char i = 0; + while (offset < subject_len && + (rc = pcre2_match(re, (PCRE2_SPTR)subject, (PCRE2_SIZE)subject_len, + offset, 0, match_data, NULL)) > 0) { + + // results->str = realloc(results->str, sizeof(char *) * (rc + + // results->n)); + resize_str_array(results, rc + i); + + ovector = pcre2_get_ovector_pointer(match_data); + DEBUG_PRINT("Get %d captures.\n", rc - 1); + DEBUG_PRINT("Match succeeded at offset %d.\n", (int)ovector[0]); + + for (unsigned short j = 1; j < rc; j++) { + PCRE2_SIZE substring_length = ovector[2 * j + 1] - ovector[2 * j]; + PCRE2_SPTR substring = (PCRE2_SPTR)subject + ovector[2 * j]; + /* Here we need to manually control the str array, + * as PCRE2_SPTR == const unsigned char + * (which cannot be directly casted) */ + results->str[j] = malloc(substring_length + 1); + sprintf(results->str[j], "%.*s", (int)substring_length, substring); + DEBUG_PRINT("index: %2d, substring_length: %d\n", j, + (int)substring_length); + } + offset = ovector[1]; + DEBUG_PRINT("offset: %zu, subject_len: %zu\n", offset, subject_len); + i++; + } + pcre2_match_data_free(match_data); + pcre2_code_free(re); + if (rc <= 0) { + + switch (rc) { + case PCRE2_ERROR_NOMATCH: + DEBUG_PRINT("No match found.\n"); + return 0; + break; + case 0: + LOG("PCRE2", + "ovector was not big enough for all the captured substrings\n"); + break; + default: + LOG("PCRE2", "Matching error %d\n", rc); + return 1; + } + } + } + return 0; +} + +generic_array_t create_array(size_t elem_size, size_t n) { + generic_array_t array; + array.data = n ? malloc(elem_size * n) : NULL; + array.elem_size = elem_size; + array.n = n; + return array; +} + +void free_array(generic_array_t *array) { + free_and_nullify(array->data); + array->n = 0; +} + +void resize_array(generic_array_t *array, size_t new_size) { + array->data = realloc(array->data, array->elem_size * new_size); + array->n = new_size; +} + +void *get_element(generic_array_t *array, size_t index) { + if (index >= array->n) { + return NULL; // Out of bounds + } + return (char *)array->data + index * array->elem_size; +} + +/* A more specific impl, specially for string (char *) */ + +str_array_t create_str_array(size_t n) { + str_array_t array; + array.str = n ? malloc(n * sizeof(char *)) : NULL; + array.n = n; + for (size_t i = 0; i < n; i++) { + array.str[i] = NULL; + } + return array; +} + +void free_str_array(str_array_t *array) { + for (size_t i = 0; i < array->n; i++) { + free(array->str[i]); + } + free_and_nullify(array->str); + array->n = 0; +} + +void resize_str_array(str_array_t *array, size_t new_size) { + array->str = realloc(array->str, sizeof(char *) * new_size); + for (size_t i = array->n; i < new_size; i++) { + array->str[i] = NULL; + } + array->n = new_size; +} + +int set_str_element(str_array_t *array, size_t index, const char *value) { + if (index >= array->n) { + return 1; // Out of bounds + } + array->str[index] = malloc(strlen(value) + 1); + strcpy(array->str[index], value); + return 0; +} + +const char *get_str_element(str_array_t *array, size_t index) { + if (index >= array->n) { + return NULL; // Out of bounds + } + return array->str[index]; +} + +queue_t create_queue(void) { + queue_t queue; + queue.front = queue.rear = NULL; + return queue; +} + +int is_empty_queue(queue_t *queue) { return queue->front == NULL; } + +void enqueue(queue_t *queue, data_t data) { + node_t *node = malloc(sizeof(node_t)); + node->data = data; + node->next = NULL; + if (queue->rear == NULL) { + queue->rear = queue->front = node; + } else { + queue->rear->next = node; + queue->rear = node; + } +} + +data_t dequeue(queue_t *queue) { + if (is_empty_queue(queue)) { + DEBUG_PRINT("Queue is empty.\n"); + return NULL; + } + + node_t *temp = queue->front; + data_t data = temp->data; + queue->front = temp->next; + free_and_nullify(temp); + + if (queue->front == NULL) { + queue->rear = NULL; + } + + return data; +} + +void free_queue(queue_t *queue) { + while (!is_empty_queue(queue)) { + dequeue(queue); + } +} + +void free_and_nullify(void *p) { + if (p) { + free(p); + p = NULL; + } +} diff --git a/src/utils.h b/src/utils.h new file mode 100644 index 0000000..e4140a6 --- /dev/null +++ b/src/utils.h @@ -0,0 +1,61 @@ +#ifndef UTILS_H_ +#define UTILS_H_ + +#include <stddef.h> + +typedef void *data_t; + +typedef struct str_array { + char **str; + size_t n; +} str_array_t; + +typedef struct generic_array { + void *data; + size_t elem_size; + size_t n; +} generic_array_t; + +typedef struct node { + data_t data; + struct node *next; +} node_t; + +typedef struct queue { + node_t *front; + node_t *rear; +} queue_t; + +int regex_match(const char *, str_array_t, str_array_t *); + +generic_array_t create_array(size_t elem_size, size_t n); + +void free_array(generic_array_t *array); + +void resize_array(generic_array_t *array, size_t new_size); + +void *get_element(generic_array_t *array, size_t index); + +void free_and_nullify(void *p); + +str_array_t create_str_array(size_t n); + +void free_str_array(str_array_t *array); + +void resize_str_array(str_array_t *array, size_t new_size); + +int set_str_element(str_array_t *array, size_t index, const char *value); + +const char *get_str_element(str_array_t *array, size_t index); + +queue_t create_queue(void); + +int is_empty_queue(queue_t *queue); + +void enqueue(queue_t *queue, data_t data); + +data_t dequeue(queue_t *queue); + +void free_queue(queue_t *queue); + +#endif |