windows stuff
This commit is contained in:
parent
7e7c371b9e
commit
ae3536529b
3 changed files with 587 additions and 106 deletions
426
colla.c
426
colla.c
|
|
@ -5,6 +5,7 @@
|
|||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#if COLLA_TCC
|
||||
#define COLLA_NO_CONDITION_VARIABLE 1
|
||||
|
|
@ -34,6 +35,13 @@
|
|||
|
||||
colla_modules_e colla__initialised_modules = 0;
|
||||
|
||||
extern void os_init(void);
|
||||
extern void os_cleanup(void);
|
||||
#if !COLLA_NO_NET
|
||||
extern void net_init(void);
|
||||
extern void net_cleanup(void);
|
||||
#endif
|
||||
|
||||
static char *colla_fmt__stb_callback(const char *buf, void *ud, int len) {
|
||||
// TODO maybe use os_write?
|
||||
fflush(stdout);
|
||||
|
|
@ -284,6 +292,18 @@ int strv_compare(strview_t a, strview_t b) {
|
|||
(int)(a.len - b.len);
|
||||
}
|
||||
|
||||
usize strv_get_utf8_len(strview_t v) {
|
||||
usize len = 0;
|
||||
|
||||
for (usize i = 0; i < v.len; ++i) {
|
||||
if ((v.buf[i] & 0xC0) != 0x80) {
|
||||
len++;
|
||||
}
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
char strv_front(strview_t ctx) {
|
||||
return ctx.len > 0 ? ctx.buf[0] : '\0';
|
||||
}
|
||||
|
|
@ -662,7 +682,7 @@ bool istr_get_i32(instream_t *ctx, i32 *val) {
|
|||
bool istr_get_i64(instream_t *ctx, i64 *val) {
|
||||
if (!ctx || !ctx->cur || !val) return false;
|
||||
char *end = NULL;
|
||||
*val = strtoll(ctx->cur, &end, 0);
|
||||
i64 out = strtoll(ctx->cur, &end, 0);
|
||||
|
||||
if (ctx->cur == end) {
|
||||
return false;
|
||||
|
|
@ -672,6 +692,7 @@ bool istr_get_i64(instream_t *ctx, i64 *val) {
|
|||
}
|
||||
|
||||
ctx->cur = end;
|
||||
*val = out;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -897,6 +918,91 @@ bool ibstr_get_i64(ibstream_t *ib, i64 *out) {
|
|||
return ibstr_read(ib, out, sizeof(*out)) == sizeof(*out);
|
||||
}
|
||||
|
||||
// == REGEX ========================================================
|
||||
|
||||
bool rg__match_impl(rg_match_t *ctx, instream_t *r, instream_t *t) {
|
||||
bool match_any = false;
|
||||
|
||||
usize beg = STR_END;
|
||||
usize end = STR_END;
|
||||
|
||||
while (!istr_is_finished(r) && !istr_is_finished(t)) {
|
||||
char rc = istr_peek(r);
|
||||
char tc = istr_peek(t);
|
||||
|
||||
if (rc == '\\') {
|
||||
if (istr_peek_next(r) != '*' && istr_peek_next(r) != '\\') {
|
||||
warn("expected * or \\ after escape character");
|
||||
return false;
|
||||
}
|
||||
istr_skip(r, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (istr_peek_next(r) == '*' && rc == tc) {
|
||||
match_any = true;
|
||||
istr_skip(r, 2);
|
||||
istr_skip(t, 1);
|
||||
beg = istr_tell(t);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rc == '*') {
|
||||
match_any = true;
|
||||
istr_skip(r, 1);
|
||||
istr_skip(t, 1);
|
||||
beg = istr_tell(t);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rc == tc) {
|
||||
if (match_any && istr_peek_next(r) == istr_peek_next(t)) {
|
||||
end = istr_tell(t);
|
||||
ctx->text[ctx->count++] = strv(t->beg + beg, end - beg);
|
||||
beg = STR_END;
|
||||
match_any = false;
|
||||
}
|
||||
istr_skip(r, 1);
|
||||
istr_skip(t, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (match_any) {
|
||||
istr_skip(t, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (match_any && istr_is_finished(r)) {
|
||||
end = t->len;
|
||||
ctx->text[ctx->count++] = strv(t->beg + beg, end - beg);
|
||||
return true;
|
||||
}
|
||||
|
||||
return ctx->count > 0 && istr_is_finished(r) && istr_is_finished(t);
|
||||
}
|
||||
|
||||
rg_match_t rg_match(strview_t rg, strview_t text) {
|
||||
rg_match_t out = {0};
|
||||
if (strv_contains(rg, '*')) {
|
||||
instream_t r = istr_init(rg);
|
||||
instream_t t = istr_init(text);
|
||||
out.matches = rg__match_impl(&out, &r, &t);
|
||||
}
|
||||
else {
|
||||
out.matches = strv_equals(rg, text);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool rg_match_easy(strview_t rg, strview_t text) {
|
||||
return rg_match(rg, text).matches;
|
||||
}
|
||||
|
||||
// == ARENA ========================================================
|
||||
|
||||
static uptr arena__align(uptr ptr, usize align) {
|
||||
return (ptr + (align - 1)) & ~(align - 1);
|
||||
}
|
||||
|
|
@ -1036,6 +1142,7 @@ static void arena__free_virtual(arena_t *arena) {
|
|||
|
||||
os_release(arena->beg, arena_capacity(arena));
|
||||
}
|
||||
|
||||
// == MALLOC ARENA =====================================================================================================
|
||||
|
||||
static arena_t arena__make_malloc(usize size) {
|
||||
|
|
@ -1134,47 +1241,138 @@ bool os_handle_valid(oshandle_t handle) {
|
|||
|
||||
// == LOGGING ===================================
|
||||
|
||||
os_log_colour_e log__level_to_colour(os_log_level_e level) {
|
||||
os_log_colour_e colour = LOG_COL_RESET;
|
||||
switch (level) {
|
||||
case LOG_DEBUG: colour = LOG_COL_BLUE; break;
|
||||
case LOG_INFO: colour = LOG_COL_GREEN; break;
|
||||
case LOG_WARN: colour = LOG_COL_YELLOW; break;
|
||||
case LOG_ERR: colour = LOG_COL_MAGENTA; break;
|
||||
case LOG_FATAL: colour = LOG_COL_RED; break;
|
||||
default: break;
|
||||
const char* os_log_level_strings[LOG_COL__COUNT] = {
|
||||
[LOG_DEBUG] = "[DEBUG]:",
|
||||
[LOG_INFO] = "[INFO]:",
|
||||
[LOG_WARN] = "[WARN]:",
|
||||
[LOG_ERR] = "[ERR]:",
|
||||
[LOG_FATAL] = "[FATAL]:",
|
||||
};
|
||||
|
||||
os_log_colour_e os_log_level_colours[LOG_COL__COUNT] = {
|
||||
[LOG_DEBUG] = LOG_COL_BLUE,
|
||||
[LOG_INFO] = LOG_COL_GREEN,
|
||||
[LOG_WARN] = LOG_COL_YELLOW,
|
||||
[LOG_ERR] = LOG_COL_MAGENTA,
|
||||
[LOG_FATAL] = LOG_COL_RED,
|
||||
};
|
||||
|
||||
os_log_options_e log_opts = OS_LOG_SIMPLE;
|
||||
log_callback_t log_cbs[COLLA_LOG_MAX_CALLBACKS] = {0};
|
||||
int log_cbs_count = 0;
|
||||
|
||||
void os_log__stdout(log_event_t *ev) {
|
||||
bool notime = log_opts & OS_LOG_NOTIME;
|
||||
bool nofile = log_opts & OS_LOG_NOFILE;
|
||||
|
||||
if (!notime) {
|
||||
os_log_set_colour(LOG_COL_DARK_GREY);
|
||||
fmt_print(
|
||||
"%02d:%02d:%02d ",
|
||||
ev->time->tm_hour,
|
||||
ev->time->tm_min,
|
||||
ev->time->tm_sec
|
||||
);
|
||||
}
|
||||
return colour;
|
||||
|
||||
os_log_set_colour(os_log_level_colours[ev->level]);
|
||||
if (ev->level != LOG_BASIC) {
|
||||
fmt_print("%-8s ", os_log_level_strings[ev->level]);
|
||||
}
|
||||
|
||||
if (!nofile) {
|
||||
os_log_set_colour(LOG_COL_DARK_GREY);
|
||||
fmt_print("%s:%d ", ev->file, ev->line);
|
||||
}
|
||||
|
||||
os_log_set_colour(LOG_COL_RESET);
|
||||
|
||||
fmt_printv(ev->fmt, ev->args);
|
||||
fmt_print("\n");
|
||||
}
|
||||
|
||||
void os_log_print(os_log_level_e level, const char *fmt, ...) {
|
||||
void os_log__fp(log_event_t *ev) {
|
||||
u8 tmpbuf[KB(1)] = {0};
|
||||
arena_t scratch = arena_make(ARENA_STATIC, sizeof(tmpbuf), tmpbuf);
|
||||
|
||||
oshandle_t fp = {0};
|
||||
fp.data = (uptr)ev->udata;
|
||||
|
||||
bool notime = log_opts & OS_LOG_NOTIME;
|
||||
bool nofile = log_opts & OS_LOG_NOFILE;
|
||||
|
||||
if (!notime) {
|
||||
os_file_print(
|
||||
scratch,
|
||||
fp,
|
||||
"%02d:%02d:%02d ",
|
||||
ev->time->tm_hour,
|
||||
ev->time->tm_min,
|
||||
ev->time->tm_sec
|
||||
);
|
||||
}
|
||||
|
||||
if (ev->level != LOG_BASIC) {
|
||||
os_file_print(scratch, fp, "%-8s ", os_log_level_strings[ev->level]);
|
||||
}
|
||||
|
||||
if (!nofile) {
|
||||
os_file_print(scratch, fp, "%s:%d ", ev->file, ev->line);
|
||||
}
|
||||
|
||||
os_file_printv(scratch, fp, ev->fmt, ev->args);
|
||||
os_file_putc(fp, '\n');
|
||||
}
|
||||
|
||||
void os_log_add_callback(log_callback_t cb) {
|
||||
colla_assert(log_cbs_count < arrlen(log_cbs));
|
||||
log_cbs[log_cbs_count++] = cb;
|
||||
}
|
||||
|
||||
void os_log_add_fp(oshandle_t fp, os_log_level_e level) {
|
||||
os_log_add_callback((log_callback_t){
|
||||
.fn = os_log__fp,
|
||||
.udata = (void*)fp.data,
|
||||
.level = level
|
||||
});
|
||||
}
|
||||
|
||||
void os__log_init(void) {
|
||||
os_log_add_callback((log_callback_t){ .fn = os_log__stdout });
|
||||
}
|
||||
|
||||
void os_log_print(const char *file, int line, os_log_level_e level, const char *fmt, ...) {
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
os_log_printv(level, fmt, args);
|
||||
os_log_printv(file, line, level, fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void os_log_printv(os_log_level_e level, const char *fmt, va_list args) {
|
||||
const char *level_str = "";
|
||||
switch (level) {
|
||||
case LOG_DEBUG: level_str = "DEBUG"; break;
|
||||
case LOG_INFO: level_str = "INFO"; break;
|
||||
case LOG_WARN: level_str = "WARN"; break;
|
||||
case LOG_ERR: level_str = "ERR"; break;
|
||||
case LOG_FATAL: level_str = "FATAL"; break;
|
||||
default: break;
|
||||
void os_log_printv(const char *file, int line, os_log_level_e level, const char *fmt, va_list args) {
|
||||
time_t t = time(NULL);
|
||||
struct tm log_time = { 0 };
|
||||
#define gettime(src, dst) localtime_s(&dst, &src)
|
||||
log_event_t ev = {
|
||||
.level = level,
|
||||
.line = line,
|
||||
.args = args,
|
||||
.fmt = fmt,
|
||||
.file = file,
|
||||
.time = &log_time,
|
||||
};
|
||||
|
||||
localtime_s(&log_time, &t);
|
||||
for (int i = 0; i < log_cbs_count; ++i) {
|
||||
if (log_cbs[i].level > level) {
|
||||
continue;
|
||||
}
|
||||
ev.udata = log_cbs[i].udata;
|
||||
log_cbs[i].fn(&ev);
|
||||
}
|
||||
|
||||
os_log_set_colour(log__level_to_colour(level));
|
||||
if (level != LOG_BASIC) {
|
||||
fmt_print("[%s]: ", level_str);
|
||||
}
|
||||
os_log_set_colour(LOG_COL_RESET);
|
||||
bool nocrash = log_opts & OS_LOG_NOCRASH;
|
||||
|
||||
fmt_printv(fmt, args);
|
||||
fmt_print("\n");
|
||||
|
||||
if (level == LOG_FATAL) {
|
||||
if (!nocrash && level == LOG_FATAL) {
|
||||
os_abort(1);
|
||||
}
|
||||
}
|
||||
|
|
@ -1346,6 +1544,103 @@ usize os_pad_to_page(usize byte_count) {
|
|||
return byte_count + padding;
|
||||
}
|
||||
|
||||
#if !COLLA_NO_CONDITION_VARIABLE
|
||||
|
||||
// == JOB QUEUE =================================
|
||||
|
||||
int jq__worker_function(u64 id, void *udata) {
|
||||
job_queue_t *q = udata;
|
||||
|
||||
// TODO: is this safe to not have an atomic variable?
|
||||
while (!q->should_stop) {
|
||||
job_t *job = jq_pop_job(q);
|
||||
if (!job) {
|
||||
if (q->stop_when_finished) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
job->func(job->userdata);
|
||||
os_mutex_lock(q->mutex);
|
||||
list_push(q->freelist, job);
|
||||
os_mutex_unlock(q->mutex);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
job_queue_t *jq_init(arena_t *arena, int worker_count) {
|
||||
job_queue_t *q = alloc(arena, job_queue_t);
|
||||
q->mutex = os_mutex_create();
|
||||
q->condvar = os_cond_create();
|
||||
q->thread_count = worker_count ? worker_count : os_get_system_info().processor_count;
|
||||
q->threads = alloc(arena, oshandle_t, q->thread_count);
|
||||
|
||||
for (int i = 0; i < q->thread_count; ++i) {
|
||||
q->threads[i] = os_thread_launch(jq__worker_function, q);
|
||||
}
|
||||
|
||||
return q;
|
||||
}
|
||||
|
||||
void jq_stop(job_queue_t *queue) {
|
||||
os_mutex_lock(queue->mutex);
|
||||
job_t *remaining = queue->jobs;
|
||||
list_push(queue->freelist, remaining);
|
||||
queue->jobs = NULL;
|
||||
queue->should_stop = true;
|
||||
os_cond_broadcast(queue->condvar);
|
||||
os_mutex_unlock(queue->mutex);
|
||||
|
||||
for (int i = 0; i < queue->thread_count; ++i) {
|
||||
os_thread_join(queue->threads[i], NULL);
|
||||
}
|
||||
|
||||
os_mutex_free(queue->mutex);
|
||||
os_cond_free(queue->condvar);
|
||||
}
|
||||
|
||||
void jq_cleanup(job_queue_t *queue) {
|
||||
os_mutex_lock(queue->mutex);
|
||||
queue->stop_when_finished = true;
|
||||
os_cond_broadcast(queue->condvar);
|
||||
os_mutex_unlock(queue->mutex);
|
||||
|
||||
for (int i = 0; i < queue->thread_count; ++i) {
|
||||
os_thread_join(queue->threads[i], NULL);
|
||||
}
|
||||
|
||||
os_mutex_free(queue->mutex);
|
||||
os_cond_free(queue->condvar);
|
||||
}
|
||||
|
||||
void jq_push(arena_t *arena, job_queue_t *queue, job_func_f *func, void *userdata) {
|
||||
os_mutex_lock(queue->mutex);
|
||||
job_t *job = queue->freelist;
|
||||
if (!job) {
|
||||
job = alloc(arena, job_t);
|
||||
}
|
||||
job->func = func;
|
||||
job->userdata = userdata;
|
||||
list_push(queue->jobs, job);
|
||||
os_cond_broadcast(queue->condvar);
|
||||
os_mutex_unlock(queue->mutex);
|
||||
}
|
||||
|
||||
job_t *jq_pop_job(job_queue_t *queue) {
|
||||
job_t *job = NULL;
|
||||
os_mutex_lock(queue->mutex);
|
||||
while (!queue->jobs && !queue->should_stop && !queue->stop_when_finished) {
|
||||
os_cond_wait(queue->condvar, queue->mutex, OS_WAIT_INFINITE);
|
||||
}
|
||||
job = queue->jobs;
|
||||
list_pop(queue->jobs);
|
||||
os_mutex_unlock(queue->mutex);
|
||||
return job;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// == INI ============================================
|
||||
|
||||
void ini__parse(arena_t *arena, ini_t *ini, const iniopt_t *options);
|
||||
|
|
@ -1538,7 +1833,6 @@ iniopt_t ini__get_options(const iniopt_t *options) {
|
|||
return out;
|
||||
}
|
||||
|
||||
|
||||
void ini__add_value(arena_t *arena, initable_t *table, instream_t *in, iniopt_t *opts) {
|
||||
colla_assert(table);
|
||||
|
||||
|
|
@ -1660,6 +1954,7 @@ void ini__parse(arena_t *arena, ini_t *ini, const iniopt_t *options) {
|
|||
// == JSON ===========================================
|
||||
|
||||
bool json__parse_obj(arena_t *arena, instream_t *in, jsonflags_e flags, json_t **out);
|
||||
bool json__parse_value(arena_t *arena, instream_t *in, jsonflags_e flags, json_t **out);
|
||||
|
||||
json_t *json_parse(arena_t *arena, strview_t filename, jsonflags_e flags) {
|
||||
str_t data = os_file_read_all_str(arena, filename);
|
||||
|
|
@ -1674,12 +1969,20 @@ json_t *json_parse_str(arena_t *arena, strview_t str, jsonflags_e flags) {
|
|||
|
||||
instream_t in = istr_init(str);
|
||||
|
||||
if (!json__parse_obj(arena, &in, flags, &root->object)) {
|
||||
// reset arena
|
||||
*arena = before;
|
||||
return NULL;
|
||||
if (flags & JSON_ONLY_OBJECT_START) {
|
||||
if (!json__parse_obj(arena, &in, flags, &root->object)) {
|
||||
// reset arena
|
||||
*arena = before;
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
if (!json__parse_value(arena, &in, flags, &root)) {
|
||||
*arena = before;
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
|
|
@ -1733,8 +2036,6 @@ void json_pretty_print(json_t *root, const json_pretty_opts_t *options) {
|
|||
|
||||
#define json__ensure(c) json__check_char(in, c)
|
||||
|
||||
bool json__parse_value(arena_t *arena, instream_t *in, jsonflags_e flags, json_t **out);
|
||||
|
||||
bool json__check_char(instream_t *in, char c) {
|
||||
if (istr_get(in) == c) {
|
||||
return true;
|
||||
|
|
@ -2044,10 +2345,14 @@ void json__pretty_print_value(json_t *value, int indent, const json_pretty_opts_
|
|||
os_log_set_colour(options->colours[JSON_PRETTY_COLOUR_NUM]);
|
||||
u8 scratchbuf[256];
|
||||
arena_t scratch = arena_make(ARENA_STATIC, sizeof(scratchbuf), scratchbuf);
|
||||
const char *fmt = "%g";
|
||||
if (round(value->number) == value->number) {
|
||||
fmt = "%.0f";
|
||||
}
|
||||
os_file_print(
|
||||
scratch,
|
||||
options->custom_target,
|
||||
"%g",
|
||||
fmt,
|
||||
value->number
|
||||
);
|
||||
os_log_set_colour(LOG_COL_RESET);
|
||||
|
|
@ -2631,6 +2936,14 @@ str_t http_res_to_str(arena_t *arena, http_res_t *res) {
|
|||
return ostr_to_str(&out);
|
||||
}
|
||||
|
||||
http_header_t *http_add_header(arena_t *arena, http_header_t *headers, strview_t key, strview_t value) {
|
||||
http_header_t *h = alloc(arena, http_header_t);
|
||||
h->key = key;
|
||||
h->value = value;
|
||||
list_push(headers, h);
|
||||
return headers;
|
||||
}
|
||||
|
||||
bool http_has_header(http_header_t *headers, strview_t key) {
|
||||
for_each(h, headers) {
|
||||
if (strv_equals(h->key, key)) {
|
||||
|
|
@ -2752,13 +3065,6 @@ http_url_t http_split_url(strview_t url) {
|
|||
}
|
||||
|
||||
#if !COLLA_NO_NET
|
||||
|
||||
// HTTP /////////////////////////////
|
||||
|
||||
http_res_t http_request(http_request_desc_t *req) {
|
||||
return http_request_cb(req, NULL, NULL);
|
||||
}
|
||||
|
||||
// WEBSOCKETS ///////////////////////
|
||||
|
||||
#define WEBSOCKET_MAGIC "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||
|
|
@ -3208,3 +3514,29 @@ str_t pretty_print_get_stringv(arena_t *arena, const char *fmt, va_list args) {
|
|||
|
||||
return ostr_to_str(&out);
|
||||
}
|
||||
|
||||
usize pretty_print_get_length(strview_t view) {
|
||||
usize size = 0;
|
||||
|
||||
instream_t in = istr_init(view);
|
||||
while (!istr_is_finished(&in)) {
|
||||
strview_t part = istr_get_view(&in, '<');
|
||||
bool has_escape = strv_ends_with(part, '\\');
|
||||
|
||||
if (has_escape) {
|
||||
part.len -= 1;
|
||||
}
|
||||
|
||||
size += strv_get_utf8_len(part);
|
||||
istr_skip(&in, 1);
|
||||
|
||||
if (has_escape) {
|
||||
size++;
|
||||
continue;
|
||||
}
|
||||
|
||||
istr_ignore_and_skip(&in, '>');
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
|
|
|||
128
colla.h
128
colla.h
|
|
@ -1,8 +1,6 @@
|
|||
#ifndef COLLA_HEADER
|
||||
#define COLLA_HEADER
|
||||
|
||||
#define _FILE_OFFSET_BITS 1
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
|
|
@ -16,7 +14,13 @@ extern void *memmove(void *dst, const void *src, size_t size);
|
|||
|
||||
#define static_assert(cond, ...) _Static_assert(cond, "" __VA_ARGS__)
|
||||
|
||||
/////////////////////////////////////////////////
|
||||
typedef enum {
|
||||
COLLA_DARRAY_BLOCK_SIZE = 64,
|
||||
COLLA_RG_MAX_MATCHES = 16,
|
||||
COLLA_OS_ARENA_SIZE = 1 << 20, // MB(1)
|
||||
COLLA_OS_MAX_WAITABLE_HANDLES = 256,
|
||||
COLLA_LOG_MAX_CALLBACKS = 22,
|
||||
} colla_constants_e;
|
||||
|
||||
// CORE MODULES /////////////////////////////////
|
||||
|
||||
|
|
@ -276,8 +280,6 @@ for_each (chunk, arr) {
|
|||
}
|
||||
*/
|
||||
|
||||
#define DARRAY_DEFAULT_BLOCK_SIZE (64)
|
||||
|
||||
#define darr_define(struct_name, item_type) typedef struct struct_name struct_name; \
|
||||
struct struct_name { \
|
||||
item_type *items; \
|
||||
|
|
@ -290,7 +292,7 @@ for_each (chunk, arr) {
|
|||
#define darr__alloc_first(arena, arr) do { \
|
||||
(arr) = (arr) ? (arr) : alloc(arena, typeof(*arr)); \
|
||||
(arr)->head = (arr)->head ? (arr)->head : (arr); \
|
||||
(arr)->block_size = (arr)->block_size ? (arr)->block_size : DARRAY_DEFAULT_BLOCK_SIZE; \
|
||||
(arr)->block_size = (arr)->block_size ? (arr)->block_size : COLLA_DARRAY_BLOCK_SIZE; \
|
||||
(arr)->items = alloc(arena, typeof(*arr->items), arr->block_size); \
|
||||
colla_assert((arr)->count == 0); \
|
||||
} while (0)
|
||||
|
|
@ -431,6 +433,7 @@ strview_t strv_init_str(str_t str);
|
|||
bool strv_is_empty(strview_t ctx);
|
||||
bool strv_equals(strview_t a, strview_t b);
|
||||
int strv_compare(strview_t a, strview_t b);
|
||||
usize strv_get_utf8_len(strview_t v);
|
||||
|
||||
char strv_front(strview_t ctx);
|
||||
char strv_back(strview_t ctx);
|
||||
|
|
@ -585,6 +588,26 @@ bool ibstr_get_i16(ibstream_t *ib, i16 *out);
|
|||
bool ibstr_get_i32(ibstream_t *ib, i32 *out);
|
||||
bool ibstr_get_i64(ibstream_t *ib, i64 *out);
|
||||
|
||||
// SIMPLE REGEX /////////////////////////////////
|
||||
|
||||
// only supports *, every star matches until the following character
|
||||
// is found, e.g.
|
||||
// ab*e
|
||||
// abcde
|
||||
// matches (cd)
|
||||
|
||||
typedef struct rg_match_t rg_match_t;
|
||||
struct rg_match_t {
|
||||
strview_t text[COLLA_RG_MAX_MATCHES];
|
||||
int count;
|
||||
bool matches;
|
||||
};
|
||||
|
||||
rg_match_t rg_match(strview_t rg, strview_t text);
|
||||
bool rg_match_easy(strview_t rg, strview_t text);
|
||||
|
||||
/////////////////////////////////////////////////
|
||||
|
||||
// ARENA ////////////////////////////////////////
|
||||
|
||||
#if COLLA_WIN && !COLLA_TCC
|
||||
|
|
@ -656,7 +679,6 @@ void arena_pop(arena_t *arena, usize amount);
|
|||
|
||||
// OS LAYER /////////////////////////////////////
|
||||
|
||||
#define OS_ARENA_SIZE (MB(1))
|
||||
#define OS_WAIT_INFINITE (0xFFFFFFFF)
|
||||
|
||||
typedef struct oshandle_t oshandle_t;
|
||||
|
|
@ -664,11 +686,20 @@ struct oshandle_t {
|
|||
uptr data;
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
OS_ARCH_X86,
|
||||
OS_ARCH_ARM,
|
||||
OS_ARCH_IA64,
|
||||
OS_ARCH_AMD64,
|
||||
OS_ARCH_ARM64,
|
||||
} os_arch_e;
|
||||
|
||||
typedef struct os_system_info_t os_system_info_t;
|
||||
struct os_system_info_t {
|
||||
u32 processor_count;
|
||||
u64 page_size;
|
||||
str_t machine_name;
|
||||
os_arch_e architecture;
|
||||
};
|
||||
|
||||
void os_init(void);
|
||||
|
|
@ -686,8 +717,6 @@ oshandle_t os_handle_zero(void);
|
|||
bool os_handle_match(oshandle_t a, oshandle_t b);
|
||||
bool os_handle_valid(oshandle_t handle);
|
||||
|
||||
#define OS_MAX_WAITABLE_HANDLES 256
|
||||
|
||||
typedef enum {
|
||||
OS_WAIT_FINISHED,
|
||||
OS_WAIT_ABANDONED,
|
||||
|
|
@ -739,8 +768,36 @@ typedef enum os_log_colour_e {
|
|||
LOG_COL__COUNT,
|
||||
} os_log_colour_e;
|
||||
|
||||
void os_log_print(os_log_level_e level, const char *fmt, ...);
|
||||
void os_log_printv(os_log_level_e level, const char *fmt, va_list args);
|
||||
typedef struct log_event_t log_event_t;
|
||||
struct log_event_t {
|
||||
va_list args;
|
||||
const char *fmt;
|
||||
const char *file;
|
||||
int line;
|
||||
struct tm *time;
|
||||
os_log_level_e level;
|
||||
void *udata;
|
||||
};
|
||||
|
||||
typedef struct log_callback_t log_callback_t;
|
||||
struct log_callback_t {
|
||||
void (*fn)(log_event_t *ev);
|
||||
void *udata;
|
||||
os_log_level_e level;
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
OS_LOG_DEFAULT = 0,
|
||||
OS_LOG_NOTIME = 1 << 0,
|
||||
OS_LOG_NOFILE = 1 << 1,
|
||||
OS_LOG_NOCRASH = 1 << 2,
|
||||
OS_LOG_SIMPLE = OS_LOG_NOTIME | OS_LOG_NOFILE,
|
||||
} os_log_options_e;
|
||||
|
||||
void os_log_add_callback(log_callback_t cb);
|
||||
void os_log_add_fp(oshandle_t fp, os_log_level_e level);
|
||||
void os_log_print(const char *file, int line, os_log_level_e level, const char *fmt, ...);
|
||||
void os_log_printv(const char *file, int line, os_log_level_e level, const char *fmt, va_list args);
|
||||
void os_log_set_colour(os_log_colour_e colour);
|
||||
void os_log_set_colour_bg(os_log_colour_e foreground, os_log_colour_e background);
|
||||
|
||||
|
|
@ -748,12 +805,12 @@ oshandle_t os_stdout(void);
|
|||
oshandle_t os_stdin(void);
|
||||
|
||||
#define print(...) fmt_print(__VA_ARGS__)
|
||||
#define println(...) os_log_print(LOG_BASIC, __VA_ARGS__)
|
||||
#define debug(...) os_log_print(LOG_DEBUG, __VA_ARGS__)
|
||||
#define info(...) os_log_print(LOG_INFO, __VA_ARGS__)
|
||||
#define warn(...) os_log_print(LOG_WARN, __VA_ARGS__)
|
||||
#define err(...) os_log_print(LOG_ERR, __VA_ARGS__)
|
||||
#define fatal(...) os_log_print(LOG_FATAL, __VA_ARGS__)
|
||||
#define println(...) os_log_print(__FILE__, __LINE__, LOG_BASIC, __VA_ARGS__)
|
||||
#define debug(...) os_log_print(__FILE__, __LINE__, LOG_DEBUG, __VA_ARGS__)
|
||||
#define info(...) os_log_print(__FILE__, __LINE__, LOG_INFO, __VA_ARGS__)
|
||||
#define warn(...) os_log_print(__FILE__, __LINE__, LOG_WARN, __VA_ARGS__)
|
||||
#define err(...) os_log_print(__FILE__, __LINE__, LOG_ERR, __VA_ARGS__)
|
||||
#define fatal(...) os_log_print(__FILE__, __LINE__, LOG_FATAL, __VA_ARGS__)
|
||||
|
||||
// == FILE ======================================
|
||||
|
||||
|
|
@ -893,6 +950,39 @@ void os_cond_broadcast(oshandle_t cond);
|
|||
|
||||
void os_cond_wait(oshandle_t cond, oshandle_t mutex, int milliseconds);
|
||||
|
||||
// == JOB QUEUE =================================
|
||||
|
||||
typedef void (job_func_f)(void *userdata);
|
||||
|
||||
typedef struct job_t job_t;
|
||||
struct job_t {
|
||||
job_t *next;
|
||||
job_func_f *func;
|
||||
void *userdata;
|
||||
};
|
||||
|
||||
typedef struct job_queue_t job_queue_t;
|
||||
struct job_queue_t {
|
||||
job_t *jobs;
|
||||
job_t *freelist;
|
||||
bool should_stop;
|
||||
bool stop_when_finished;
|
||||
int reader;
|
||||
int writer;
|
||||
oshandle_t mutex;
|
||||
oshandle_t condvar;
|
||||
oshandle_t *threads;
|
||||
int thread_count;
|
||||
};
|
||||
|
||||
// pass 0 to worker count to use max workers (os_get_system_info().processor_count)
|
||||
job_queue_t *jq_init(arena_t *arena, int worker_count);
|
||||
void jq_stop(job_queue_t *queue);
|
||||
// no need to call this if you call jq_stop
|
||||
void jq_cleanup(job_queue_t *queue);
|
||||
void jq_push(arena_t *arena, job_queue_t *queue, job_func_f *func, void *userdata);
|
||||
job_t *jq_pop_job(job_queue_t *queue);
|
||||
|
||||
#endif
|
||||
|
||||
// PARSERS //////////////////////////////////////
|
||||
|
|
@ -969,7 +1059,6 @@ struct ini_pretty_opts_t {
|
|||
|
||||
void ini_pretty_print(ini_t *ini, const ini_pretty_opts_t *options);
|
||||
|
||||
|
||||
// == JSON ===========================================
|
||||
|
||||
typedef enum jsontype_e {
|
||||
|
|
@ -985,6 +1074,7 @@ typedef enum jsonflags_e {
|
|||
JSON_DEFAULT = 0,
|
||||
JSON_NO_TRAILING_COMMAS = 1 << 0,
|
||||
JSON_NO_COMMENTS = 1 << 1,
|
||||
JSON_ONLY_OBJECT_START = 1 << 2,
|
||||
} jsonflags_e;
|
||||
|
||||
typedef struct json_t json_t;
|
||||
|
|
@ -1144,6 +1234,7 @@ http_res_t http_parse_res(arena_t *arena, strview_t response);
|
|||
str_t http_req_to_str(arena_t *arena, http_req_t *req);
|
||||
str_t http_res_to_str(arena_t *arena, http_res_t *res);
|
||||
|
||||
http_header_t *http_add_header(arena_t *arena, http_header_t *headers, strview_t key, strview_t value);
|
||||
bool http_has_header(http_header_t *headers, strview_t key);
|
||||
void http_set_header(http_header_t *headers, strview_t key, strview_t value);
|
||||
strview_t http_get_header(http_header_t *headers, strview_t key);
|
||||
|
|
@ -1285,5 +1376,6 @@ void pretty_printv(arena_t scratch, const char *fmt, va_list args);
|
|||
|
||||
str_t pretty_print_get_string(arena_t *arena, const char *fmt, ...);
|
||||
str_t pretty_print_get_stringv(arena_t *arena, const char *fmt, va_list args);
|
||||
usize pretty_print_get_length(strview_t view);
|
||||
|
||||
#endif
|
||||
|
|
|
|||
139
colla_win32.c
139
colla_win32.c
|
|
@ -15,6 +15,50 @@
|
|||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef PROCESSOR_ARCHITECTURE_ARM64
|
||||
#define PROCESSOR_ARCHITECTURE_ARM64 12
|
||||
#endif
|
||||
|
||||
const char win32__fg_colours[LOG_COL__COUNT][6] = {
|
||||
[LOG_COL_RESET] = "\x1b[39m",
|
||||
[LOG_COL_BLACK] = "\x1b[30m",
|
||||
[LOG_COL_RED] = "\x1b[31m",
|
||||
[LOG_COL_GREEN] = "\x1b[32m",
|
||||
[LOG_COL_YELLOW] = "\x1b[33m",
|
||||
[LOG_COL_BLUE] = "\x1b[34m",
|
||||
[LOG_COL_MAGENTA] = "\x1b[35m",
|
||||
[LOG_COL_CYAN] = "\x1b[36m",
|
||||
[LOG_COL_WHITE] = "\x1b[37m",
|
||||
[LOG_COL_DARK_GREY] = "\x1b[90m",
|
||||
[LOG_COL_LIGHT_RED] = "\x1b[91m",
|
||||
[LOG_COL_LIGHT_GREEN] = "\x1b[92m",
|
||||
[LOG_COL_LIGHT_YELLOW] = "\x1b[93m",
|
||||
[LOG_COL_LIGHT_BLUE] = "\x1b[94m",
|
||||
[LOG_COL_LIGHT_MAGENTA] = "\x1b[95m",
|
||||
[LOG_COL_LIGHT_CYAN] = "\x1b[96m",
|
||||
};
|
||||
|
||||
const char win32__bg_colours[LOG_COL__COUNT][7] = {
|
||||
[LOG_COL_RESET] = "\x1b[49m",
|
||||
[LOG_COL_BLACK] = "\x1b[40m",
|
||||
[LOG_COL_RED] = "\x1b[41m",
|
||||
[LOG_COL_GREEN] = "\x1b[42m",
|
||||
[LOG_COL_YELLOW] = "\x1b[43m",
|
||||
[LOG_COL_BLUE] = "\x1b[44m",
|
||||
[LOG_COL_MAGENTA] = "\x1b[45m",
|
||||
[LOG_COL_CYAN] = "\x1b[46m",
|
||||
[LOG_COL_WHITE] = "\x1b[47m",
|
||||
[LOG_COL_DARK_GREY] = "\x1b[100m",
|
||||
[LOG_COL_LIGHT_RED] = "\x1b[101m",
|
||||
[LOG_COL_LIGHT_GREEN] = "\x1b[102m",
|
||||
[LOG_COL_LIGHT_YELLOW] = "\x1b[103m",
|
||||
[LOG_COL_LIGHT_BLUE] = "\x1b[104m",
|
||||
[LOG_COL_LIGHT_MAGENTA] = "\x1b[105m",
|
||||
[LOG_COL_LIGHT_CYAN] = "\x1b[106m",
|
||||
};
|
||||
|
||||
void os__log_init(void);
|
||||
|
||||
str_t str_os_from_str16(arena_t *arena, str16_t src) {
|
||||
str_t out = {0};
|
||||
|
||||
|
|
@ -144,11 +188,20 @@ void os_init(void) {
|
|||
SYSTEM_INFO sysinfo = {0};
|
||||
GetSystemInfo(&sysinfo);
|
||||
|
||||
os_arch_e architectures[] = {
|
||||
[PROCESSOR_ARCHITECTURE_INTEL] = OS_ARCH_X86,
|
||||
[PROCESSOR_ARCHITECTURE_ARM] = OS_ARCH_ARM,
|
||||
[PROCESSOR_ARCHITECTURE_IA64] = OS_ARCH_IA64,
|
||||
[PROCESSOR_ARCHITECTURE_AMD64] = OS_ARCH_AMD64,
|
||||
[PROCESSOR_ARCHITECTURE_ARM64] = OS_ARCH_ARM64,
|
||||
};
|
||||
|
||||
os_system_info_t *info = &w32_data.info;
|
||||
info->processor_count = (u64)sysinfo.dwNumberOfProcessors;
|
||||
info->page_size = sysinfo.dwPageSize;
|
||||
info->architecture = architectures[sysinfo.wProcessorArchitecture];
|
||||
|
||||
w32_data.arena = arena_make(ARENA_VIRTUAL, OS_ARENA_SIZE);
|
||||
w32_data.arena = arena_make(ARENA_VIRTUAL, COLLA_OS_ARENA_SIZE);
|
||||
|
||||
TCHAR namebuf[MAX_COMPUTERNAME_LENGTH + 1];
|
||||
DWORD namebuflen = sizeof(namebuf);
|
||||
|
|
@ -176,6 +229,7 @@ void os_init(void) {
|
|||
else {
|
||||
err("couldn't get console screen buffer info: %v", os_get_error_string(os_get_last_error()));
|
||||
}
|
||||
os__log_init();
|
||||
}
|
||||
|
||||
void os_cleanup(void) {
|
||||
|
|
@ -187,7 +241,9 @@ void os_cleanup(void) {
|
|||
|
||||
void os_abort(int code) {
|
||||
#if COLLA_DEBUG
|
||||
if (code != 0) abort();
|
||||
if (code != 0) {
|
||||
__debugbreak();
|
||||
}
|
||||
#endif
|
||||
ExitProcess(code);
|
||||
}
|
||||
|
|
@ -218,8 +274,8 @@ str_t os_get_error_string(iptr error) {
|
|||
}
|
||||
|
||||
os_wait_t os_wait_on_handles(oshandle_t *handles, int count, bool wait_all, u32 milliseconds) {
|
||||
HANDLE win_handles[OS_MAX_WAITABLE_HANDLES] = {0};
|
||||
colla_assert(count < MAXIMUM_WAIT_OBJECTS);
|
||||
HANDLE win_handles[COLLA_OS_MAX_WAITABLE_HANDLES] = {0};
|
||||
colla_assert(count < COLLA_OS_MAX_WAITABLE_HANDLES);
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
win_handles[i] = (HANDLE)(handles[i].data);
|
||||
|
|
@ -252,16 +308,12 @@ os_system_info_t os_get_system_info(void) {
|
|||
}
|
||||
|
||||
void os_log_set_colour(os_log_colour_e colour) {
|
||||
WORD attribute = colour == LOG_COL_RESET ? w32_data.default_fg : (WORD)colour;
|
||||
SetConsoleTextAttribute((HANDLE)w32_data.hstdout.data, attribute);
|
||||
WriteFile((HANDLE)w32_data.hstdout.data, win32__fg_colours[colour], arrlen(win32__fg_colours[colour]), NULL, NULL);
|
||||
}
|
||||
|
||||
void os_log_set_colour_bg(os_log_colour_e foreground, os_log_colour_e background) {
|
||||
WORD fg_attr = foreground == LOG_COL_RESET ? w32_data.default_fg : (WORD)foreground;
|
||||
WORD bg_attr = (background == LOG_COL_RESET ? w32_data.default_bg : (WORD)background) << 4;
|
||||
|
||||
WORD attribute = fg_attr | bg_attr;
|
||||
SetConsoleTextAttribute((HANDLE)w32_data.hstdout.data, attribute);
|
||||
WriteFile((HANDLE)w32_data.hstdout.data, win32__fg_colours[foreground], arrlen(win32__fg_colours[foreground]), NULL, NULL);
|
||||
WriteFile((HANDLE)w32_data.hstdout.data, win32__bg_colours[background], arrlen(win32__bg_colours[background]), NULL, NULL);
|
||||
}
|
||||
|
||||
oshandle_t os_stdout(void) {
|
||||
|
|
@ -456,16 +508,20 @@ u64 os_file_time_fp(oshandle_t handle) {
|
|||
// == DIR WALKER ================================
|
||||
|
||||
typedef struct dir_t {
|
||||
WIN32_FIND_DATA find_data;
|
||||
WIN32_FIND_DATAW find_data;
|
||||
HANDLE handle;
|
||||
dir_entry_t cur_entry;
|
||||
dir_entry_t next_entry;
|
||||
} dir_t;
|
||||
|
||||
dir_entry_t os__dir_entry_from_find_data(arena_t *arena, WIN32_FIND_DATA *fd) {
|
||||
dir_entry_t os__dir_entry_from_find_data(arena_t *arena, WIN32_FIND_DATAW *fd) {
|
||||
dir_entry_t out = {0};
|
||||
|
||||
out.name = str_from_tstr(arena, tstr_init(fd->cFileName, 0));
|
||||
out.name = str_from_str16(arena, str16_init(fd->cFileName, 0));
|
||||
|
||||
if (strv_equals(strv(out.name), strv("cygwin"))) {
|
||||
out.type = 0;
|
||||
}
|
||||
|
||||
if (fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
|
||||
out.type = DIRTYPE_DIR;
|
||||
|
|
@ -482,33 +538,30 @@ dir_entry_t os__dir_entry_from_find_data(arena_t *arena, WIN32_FIND_DATA *fd) {
|
|||
}
|
||||
|
||||
dir_t *os_dir_open(arena_t *arena, strview_t path) {
|
||||
usize prev = arena_tell(arena);
|
||||
dir_t* ctx = alloc(arena, dir_t);
|
||||
|
||||
arena_t scratch = *arena;
|
||||
#if 0
|
||||
u8 tmpbuf[KB(1)] = {0};
|
||||
arena_t scratch = arena_make(ARENA_STATIC, sizeof(tmpbuf), tmpbuf);
|
||||
#endif
|
||||
str16_t winpath = strv_to_str16(&scratch, path);
|
||||
DWORD pathlen = GetFullPathNameW(winpath.buf, 0, NULL, NULL);
|
||||
|
||||
WCHAR *fullpath = alloc(&scratch, WCHAR, pathlen + 10);
|
||||
|
||||
tstr_t winpath = strv_to_tstr(&scratch, path);
|
||||
// get a little extra leeway
|
||||
TCHAR fullpath[MAX_PATH + 16] = {0};
|
||||
DWORD pathlen = GetFullPathName(winpath.buf, MAX_PATH, fullpath, NULL);
|
||||
// add asterisk at the end of the path
|
||||
if (fullpath[pathlen] != '\\' && fullpath[pathlen] != '/') {
|
||||
fullpath[pathlen++] = '\\';
|
||||
pathlen = GetFullPathNameW(winpath.buf, pathlen + 1, fullpath, NULL);
|
||||
|
||||
if (fullpath[pathlen] != L'\\' && fullpath[pathlen] != L'/') {
|
||||
fullpath[pathlen++] = L'\\';
|
||||
}
|
||||
fullpath[pathlen++] = '*';
|
||||
fullpath[pathlen++] = '\0';
|
||||
fullpath[pathlen++] = L'*';
|
||||
|
||||
ctx->handle = FindFirstFile(fullpath, &ctx->find_data);
|
||||
WIN32_FIND_DATAW first = {0};
|
||||
HANDLE handle = FindFirstFileW(fullpath, &first);
|
||||
|
||||
if (ctx->handle == INVALID_HANDLE_VALUE) {
|
||||
arena_rewind(arena, prev);
|
||||
if (handle == INVALID_HANDLE_VALUE) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
dir_t *ctx = alloc(arena, dir_t);
|
||||
ctx->handle = handle;
|
||||
ctx->find_data = first;
|
||||
|
||||
ctx->next_entry = os__dir_entry_from_find_data(arena, &ctx->find_data);
|
||||
|
||||
return ctx;
|
||||
|
|
@ -530,13 +583,18 @@ dir_entry_t *os_dir_next(arena_t *arena, dir_t *dir) {
|
|||
|
||||
dir->cur_entry = dir->next_entry;
|
||||
|
||||
dir->next_entry = (dir_entry_t){0};
|
||||
|
||||
if (FindNextFile(dir->handle, &dir->find_data)) {
|
||||
while (true) {
|
||||
dir->next_entry = (dir_entry_t){0};
|
||||
if (!FindNextFileW(dir->handle, &dir->find_data)) {
|
||||
os_dir_close(dir);
|
||||
break;
|
||||
}
|
||||
// HACK: skip system files/directories
|
||||
if (dir->find_data.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM) {
|
||||
continue;
|
||||
}
|
||||
dir->next_entry = os__dir_entry_from_find_data(arena, &dir->find_data);
|
||||
}
|
||||
else {
|
||||
os_dir_close(dir);
|
||||
break;
|
||||
}
|
||||
|
||||
return &dir->cur_entry;
|
||||
|
|
@ -956,7 +1014,7 @@ http_res_t http_request_cb(http_request_desc_t *req, http_request_callback_fn ca
|
|||
(DWORD_PTR)NULL // userdata
|
||||
);
|
||||
if (!connection) {
|
||||
err("call to InternetConnect failed: %u", os_get_error_string(os_get_last_error()));
|
||||
err("call to InternetConnect failed: %u", os_get_last_error());
|
||||
goto failed;
|
||||
}
|
||||
|
||||
|
|
@ -1014,7 +1072,6 @@ http_res_t http_request_cb(http_request_desc_t *req, http_request_callback_fn ca
|
|||
|
||||
// buffer is not big enough, allocate one with the arena instead
|
||||
if (!result && GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
|
||||
info("buffer is too small");
|
||||
buffer = alloc(req->arena, u8, bufsize + 1);
|
||||
result = HttpQueryInfo(request, HTTP_QUERY_RAW_HEADERS_CRLF, buffer, &bufsize, NULL);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue