@@ -0,0 +1,3 @@ | |||
*.o | |||
caniadd | |||
TODO |
@@ -0,0 +1,10 @@ | |||
[submodule "subm/tiny-AES-c"] | |||
path = subm/tiny-AES-c | |||
url = https://github.com/kokke/tiny-AES-c | |||
[submodule "subm/md5-c"] | |||
path = subm/md5-c | |||
url = https://github.com/Zunawe/md5-c | |||
[submodule "subm/MD4"] | |||
path = subm/MD4 | |||
url = https://github.com/moex3/MD4 | |||
branch = ptr_addition_fix |
@@ -0,0 +1,36 @@ | |||
InstallPrefix := /usr/local/bin | |||
PROGNAME := caniadd | |||
VERSION := 1 | |||
CFLAGS := -Wall -std=gnu11 #-march=native #-Werror | |||
CPPFLAGS := -DCBC=0 -DCTR=0 -DECB=1 -Isubm/tiny-AES-c/ -Isubm/md5-c/ -Isubm/MD4/ -DPROG_VERSION='"$(VERSION)"' | |||
LDFLAGS := -lpthread -lsqlite3 | |||
SOURCES := $(wildcard src/*.c) subm/tiny-AES-c/aes.c subm/md5-c/md5.c subm/MD4/md4.c #$(TOML_SRC) $(BENCODE_SRC) | |||
OBJS := $(SOURCES:.c=.o) | |||
all: CFLAGS += -O3 -flto | |||
all: CPPFLAGS += #-DNDEBUG Just to be safe | |||
all: $(PROGNAME) | |||
# no-pie cus it crashes on my 2nd pc for some reason | |||
dev: CFLAGS += -Og -ggdb -fsanitize=address -fsanitize=leak -fstack-protector-all -no-pie | |||
dev: $(PROGNAME) | |||
t: | |||
echo $(SOURCES) | |||
install: $(PROGNAME) | |||
install -s -- $< $(InstallPrefix)/$(PROGNAME) | |||
uninstall: | |||
rm -f -- $(InstallPrefix)/$(PROGNAME) | |||
$(PROGNAME): $(OBJS) | |||
$(CC) -o $@ $+ $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) | |||
clean: | |||
-rm -- $(OBJS) $(PROGNAME) | |||
re: clean all | |||
red: clean dev |
@@ -0,0 +1,27 @@ | |||
# Caniadd will add files to an AniDB list. | |||
None of the already existing clients did exactly what I wanted, so here I am. | |||
Caniadd is still in developement, but is is already usable. | |||
That is, it implements logging in, encryption, ed2k hashing and adding files, basic ratelimit, keepalive, and file caching. | |||
In the future I want to write an mpv plugin that will use the cached database from caniadd to automatically mark an episode watched on AniDB. | |||
That will be the peak *comfy* animu list management experience. | |||
## Things to do: | |||
- NAT handling | |||
- Multi thread hashing | |||
- Read/write timeout in net | |||
- Api ratelimit (the other part) | |||
- Decode escaping from server | |||
- Use a config file | |||
- Add newline escape to server | |||
- Better field parsing, remove the horrors at code 310 | |||
- Add myliststats cmd as --stats arg | |||
- Add support for compression | |||
- Make deleting from mylist possible, with | |||
- Name regexes, | |||
- If file is not found at a scan | |||
- Use api\_cmd style in api\_encrypt\_init | |||
- Buffer up mylistadd api cmds when waiting for ratelimit | |||
- Handle C-c gracefully at any time | |||
- Write -h page, and maybe a man page too |
@@ -0,0 +1,802 @@ | |||
#include <stdbool.h> | |||
#include <string.h> | |||
#include <assert.h> | |||
#include <printf.h> | |||
#include <time.h> | |||
#include <pthread.h> | |||
#include <errno.h> | |||
#include <md5.h> | |||
#include <aes.h> | |||
#include "api.h" | |||
#include "net.h" | |||
#include "uio.h" | |||
#include "config.h" | |||
#include "ed2k.h" | |||
#include "util.h" | |||
/* Needed, bcuz of custom %B format */ | |||
#pragma GCC diagnostic push | |||
#pragma GCC diagnostic ignored "-Wformat" | |||
#pragma GCC diagnostic ignored "-Wformat-extra-args" | |||
#ifdef CLOCK_MONOTONIC_COARSE | |||
#define API_CLOCK CLOCK_MONOTONIC_COARSE | |||
#elif defined(CLOCK_MONOTONIC) | |||
#warn "No coarse monotonic clock" | |||
#define API_CLOCK CLOCK_MONOTONIC | |||
#else | |||
#error "No monotonic clock" | |||
#endif | |||
#define MS_TO_TIMESPEC(ts, ms) { \ | |||
ts->tv_sec = ms / 1000; \ | |||
ts->tv_nsec = (ms % 1000) * 1000000; \ | |||
} | |||
#define MS_TO_TIMESPEC_L(ts, ms) { \ | |||
ts.tv_sec = ms / 1000; \ | |||
ts.tv_nsec = (ms % 1000) * 1000000; \ | |||
} | |||
static enum error api_cmd_logout(struct api_result *res); | |||
static enum error api_cmd_auth(const char *uname, const char *pass, | |||
struct api_result *res); | |||
static bool api_authed = false; | |||
static char api_session[API_SMAXSIZE] = {0}; /* No escaping is needed */ | |||
static uint8_t e_key[16] = {0}; | |||
static bool api_encryption = false; | |||
static pthread_t api_ka_thread = 0; | |||
static pthread_mutex_t api_work_mx; | |||
static bool api_ka_now = false; /* Are we doing keepalive now? */ | |||
static struct timespec api_last_packet = {0}; /* Last packet time */ | |||
static int32_t api_packet_count = 0; /* Only increment */ | |||
static int32_t api_fast_packet_count = 0; /* Incremented or decrement */ | |||
static int api_escaped_string(FILE *io, const struct printf_info *info, | |||
const void *const *args) | |||
{ | |||
/* Ignore newline escapes for now */ | |||
char *str = *(char**)args[0]; | |||
char *and_pos = strchr(str, '&'); | |||
size_t w_chars = 0; | |||
if (and_pos == NULL) | |||
return fprintf(io, "%s", str); | |||
while (and_pos) { | |||
w_chars += fprintf(io, "%.*s", (int)(and_pos - str), str); | |||
w_chars += fprintf(io, "&"); | |||
str = and_pos + 1; | |||
and_pos = strchr(str, '&'); | |||
} | |||
if (*str) | |||
w_chars += fprintf(io, "%s", str); | |||
return w_chars; | |||
} | |||
static int api_escaped_sring_info(const struct printf_info *info, size_t n, | |||
int *argtypes, int *size) | |||
{ | |||
if (n > 0) { | |||
argtypes[0] = PA_STRING; | |||
size[0] = sizeof(const char*); | |||
} | |||
return 1; | |||
} | |||
static enum error api_init_encrypt(const char *api_key, const char *uname) | |||
{ | |||
char buffer[API_BUFSIZE]; | |||
MD5Context md5_ctx; | |||
char *salt_start = buffer + 4 /* 209 [salt here] ... */, *salt_end; | |||
ssize_t r_len, salt_len; | |||
if (net_send(buffer, snprintf(buffer, sizeof(buffer), | |||
"ENCRYPT user=%s&type=1", uname)) == -1) { | |||
return ERR_API_COMMFAIL; | |||
} | |||
r_len = net_read(buffer, sizeof(buffer)); | |||
if (strncmp(buffer, "209", 3) != 0) { | |||
uio_error("We expected 209 response, but got: %.*s", | |||
(int)r_len, buffer); | |||
return ERR_API_ENCRYPTFAIL; | |||
} | |||
salt_end = strchr(salt_start, ' '); | |||
if (!salt_end) { | |||
uio_error("Cannot find space after salt in response"); | |||
return ERR_API_ENCRYPTFAIL; | |||
} | |||
salt_len = salt_end - salt_start; | |||
md5Init(&md5_ctx); | |||
md5Update(&md5_ctx, (uint8_t*)api_key, strlen(api_key)); | |||
md5Update(&md5_ctx, (uint8_t*)salt_start, salt_len); | |||
md5Finalize(&md5_ctx); | |||
memcpy(e_key, md5_ctx.digest, sizeof(e_key)); | |||
#if 1 | |||
char *buffpos = buffer; | |||
for (int i = 0; i < 16; i++) | |||
buffpos += sprintf(buffpos, "%02x", e_key[i]); | |||
uio_debug("Encryption key is: '%s'", buffer); | |||
#endif | |||
api_encryption = true; | |||
return NOERR; | |||
} | |||
static size_t api_encrypt(char *buffer, size_t data_len) | |||
{ | |||
struct AES_ctx actx; | |||
size_t rem_data_len = data_len, ret_len = data_len; | |||
char pad_value; | |||
AES_init_ctx(&actx, e_key); | |||
while (rem_data_len >= AES_BLOCKLEN) { | |||
AES_ECB_encrypt(&actx, (uint8_t*)buffer); | |||
buffer += AES_BLOCKLEN; | |||
rem_data_len -= AES_BLOCKLEN; | |||
} | |||
/* Possible BOF here? maybe? certanly. */ | |||
pad_value = AES_BLOCKLEN - rem_data_len; | |||
ret_len += pad_value; | |||
memset(buffer + rem_data_len, pad_value, pad_value); | |||
AES_ECB_encrypt(&actx, (uint8_t*)buffer); | |||
assert(ret_len % AES_BLOCKLEN == 0); | |||
return ret_len; | |||
} | |||
static size_t api_decrypt(char *buffer, size_t data_len) | |||
{ | |||
assert(data_len % AES_BLOCKLEN == 0); | |||
struct AES_ctx actx; | |||
size_t ret_len = data_len; | |||
char pad_value; | |||
AES_init_ctx(&actx, e_key); | |||
while (data_len) { | |||
AES_ECB_decrypt(&actx, (uint8_t*)buffer); | |||
buffer += AES_BLOCKLEN; | |||
data_len -= AES_BLOCKLEN; | |||
} | |||
pad_value = buffer[data_len - 1]; | |||
ret_len -= pad_value; | |||
return ret_len; | |||
} | |||
static enum error api_auth(const char* uname, const char *passw) | |||
{ | |||
struct api_result res; | |||
enum error err = NOERR; | |||
if (!api_encryption) | |||
uio_warning("Logging in without encryption!"); | |||
if (api_cmd_auth(uname, passw, &res) != NOERR) { | |||
return ERR_API_AUTH_FAIL; | |||
} | |||
switch (res.code) { | |||
case 201: | |||
uio_warning("A new client version is available!"); | |||
case 200: | |||
memcpy(api_session, res.auth.session_key, sizeof(api_session)); | |||
api_authed = true; | |||
uio_debug("Succesfully logged in. Session key: '%s'", api_session); | |||
break; | |||
default: | |||
err = ERR_API_AUTH_FAIL; | |||
switch (res.code) { | |||
case 500: | |||
uio_error("Login failed. Please check your credentials again"); | |||
break; | |||
case 503: | |||
uio_error("Client is outdated. You're probably out of luck here."); | |||
break; | |||
case 504: | |||
uio_error("Client is banned :( Reason: %s", res.auth.banned_reason); | |||
free(res.auth.banned_reason); | |||
break; | |||
case 505: | |||
uio_error("Illegal input or access denied"); | |||
break; | |||
case 601: | |||
uio_error("AniDB out of service"); | |||
break; | |||
default: | |||
uio_error("Unknown error: %hu", res.code); | |||
break; | |||
} | |||
} | |||
return err; | |||
} | |||
enum error api_logout() | |||
{ | |||
struct api_result res; | |||
enum error err = NOERR; | |||
if (api_cmd_logout(&res) != NOERR) { | |||
return ERR_API_AUTH_FAIL; | |||
} | |||
switch (res.code) { | |||
case 203: | |||
uio_debug("Succesfully logged out"); | |||
api_authed = false; | |||
break; | |||
case 403: | |||
uio_error("Cannot log out, because we aren't logged in"); | |||
api_authed = false; | |||
break; | |||
default: | |||
err = ERR_API_LOGOUT; | |||
uio_error("Unknown error: %hu", res.code); | |||
break; | |||
} | |||
return err; | |||
} | |||
static void api_keepalive(struct timespec *out_next) | |||
{ | |||
struct timespec ts = {0}; | |||
uint64_t msdiff; | |||
clock_gettime(API_CLOCK, &ts); | |||
msdiff = util_timespec_diff(&api_last_packet, &ts); | |||
if (msdiff >= API_TIMEOUT) { | |||
struct api_result r; | |||
MS_TO_TIMESPEC(out_next, API_TIMEOUT); | |||
uio_debug("Sending uptime command for keep alive"); | |||
// TODO what if another action is already in progress? | |||
api_cmd_uptime(&r); | |||
} else { | |||
uint64_t msnext = API_TIMEOUT - msdiff; | |||
uio_debug("Got keepalive request, but time is not up yet"); | |||
MS_TO_TIMESPEC(out_next, msnext); | |||
} | |||
} | |||
void *api_keepalive_main(void *arg) | |||
{ | |||
struct timespec ka_time; | |||
MS_TO_TIMESPEC_L(ka_time, API_TIMEOUT); | |||
uio_debug("Hi from keepalie thread"); | |||
for (;;) { | |||
if (nanosleep(&ka_time, NULL) != 0) { | |||
int e = errno; | |||
uio_error("Nanosleep failed: %s", strerror(e)); | |||
} | |||
/* Needed, because the thread could be canceled while in recv or send | |||
* and in that case, the mutex will remain locked | |||
* Could be replaced with a pthread_cleanup_push ? */ | |||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); | |||
pthread_mutex_lock(&api_work_mx); | |||
api_ka_now = true; | |||
uio_debug("G'moooooning! Is it time to keep our special connection alive?"); | |||
api_keepalive(&ka_time); | |||
uio_debug("Next wakey-wakey in %ld seconds", ka_time.tv_sec); | |||
api_ka_now = false; | |||
pthread_mutex_unlock(&api_work_mx); | |||
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); | |||
} | |||
return NULL; | |||
} | |||
enum error api_clock_init() | |||
{ | |||
struct timespec ts; | |||
memset(&api_last_packet, 0, sizeof(api_last_packet)); | |||
api_packet_count = 0; | |||
api_fast_packet_count = 0; | |||
if (clock_getres(API_CLOCK, &ts) != 0) { | |||
uio_error("Cannot get clock resolution: %s", strerror(errno)); | |||
return ERR_API_CLOCK; | |||
} | |||
uio_debug("Clock resolution: %f ms", | |||
(ts.tv_sec * 1000) + (ts.tv_nsec / 1000000.0)); | |||
return NOERR; | |||
} | |||
enum error api_init(bool auth) | |||
{ | |||
enum error err = NOERR; | |||
const char **api_key, **uname, **passwd; | |||
err = api_clock_init(); | |||
if (err != NOERR) | |||
return err; | |||
err = net_init(); | |||
if (err != NOERR) | |||
return err; | |||
if (config_get("api-key", (void**)&api_key) == NOERR) { | |||
if (config_get("username", (void**)&uname) != NOERR) { | |||
uio_error("Api key is specified, but that also requires " | |||
"the username!"); | |||
err = ERR_OPT_REQUIRED; | |||
goto fail; | |||
} | |||
err = api_init_encrypt(*api_key, *uname); | |||
if (err != NOERR) { | |||
uio_error("Cannot init api encryption"); | |||
goto fail; | |||
} | |||
} | |||
/* Define an escaped string printf type */ | |||
if (register_printf_specifier('B', api_escaped_string, | |||
api_escaped_sring_info) != 0) { | |||
uio_error("Failed to register escaped printf string function"); | |||
err = ERR_API_PRINTFFUNC; | |||
goto fail; | |||
} | |||
if (auth) { | |||
if (config_get("username", (void**)&uname) != NOERR) { | |||
uio_error("Username is not specified, but it is required!"); | |||
err = ERR_OPT_REQUIRED; | |||
goto fail; | |||
} | |||
if (config_get("password", (void**)&passwd) != NOERR) { | |||
uio_error("Password is not specified, but it is required!"); | |||
err = ERR_OPT_REQUIRED; | |||
goto fail; | |||
} | |||
err = api_auth(*uname, *passwd); | |||
if (err != NOERR) | |||
goto fail; | |||
/* Only do keep alive if we have a session */ | |||
if (pthread_mutex_init(&api_work_mx, NULL) != 0) { | |||
uio_error("Cannot create mutex"); | |||
err = ERR_THRD; | |||
goto fail; | |||
} | |||
if (pthread_create(&api_ka_thread, NULL, api_keepalive_main, NULL) != 0) { | |||
uio_error("Cannot create api keepalive thread"); | |||
err = ERR_THRD; | |||
goto fail; | |||
} | |||
} | |||
#if 0 | |||
printf("Testings: %B\n", "oi&ha=hi&wooooowz&"); | |||
printf("Testings: %B\n", "oi&ha=hi&wooooowz"); | |||
printf("Testings: %B\n", "&oi&ha=hi&wooooowz"); | |||
printf("Testings: %B\n", "oooooooooiiiiii"); | |||
#endif | |||
return err; | |||
fail: | |||
api_free(); | |||
return err; | |||
} | |||
void api_free() | |||
{ | |||
if (api_authed) { | |||
if (pthread_cancel(api_ka_thread) != 0) { | |||
uio_error("Cannot cancel api keepalive thread"); | |||
} else { | |||
int je = pthread_join(api_ka_thread, NULL); | |||
if (je != 0) { | |||
uio_error("Cannot join api keepalive thread: %s", | |||
strerror(je)); | |||
} | |||
if (pthread_mutex_destroy(&api_work_mx) != 0) | |||
uio_error("Cannot destroy api work mutex"); | |||
} | |||
api_logout(); | |||
memset(api_session, 0, sizeof(api_session)); | |||
api_authed = false; /* duplicate */ | |||
} | |||
if (api_encryption) { | |||
api_encryption = false; | |||
memset(e_key, 0, sizeof(e_key)); | |||
} | |||
register_printf_specifier('B', NULL, NULL); | |||
net_free(); | |||
} | |||
/* | |||
* We just sent a packet, so update the last packet time here | |||
*/ | |||
static void api_ratelimit_sent() | |||
{ | |||
clock_gettime(API_CLOCK, &api_last_packet); | |||
} | |||
static void api_ratelimit() | |||
{ | |||
struct timespec ts = {0}; | |||
uint64_t msdiff, mswait; | |||
clock_gettime(API_CLOCK, &ts); | |||
msdiff = util_timespec_diff(&api_last_packet, &ts); | |||
uio_debug("Time since last packet: %ld ms", msdiff); | |||
if (msdiff >= API_SENDWAIT) | |||
return; /* No ratelimiting is needed */ | |||
/* Need ratelimit, so do it here for now */ | |||
mswait = API_SENDWAIT - msdiff; | |||
uio_debug("Ratelimit is needed, sleeping for %ld ms", mswait); | |||
MS_TO_TIMESPEC_L(ts, mswait); | |||
if (nanosleep(&ts, NULL) == -1) { | |||
if (errno == EINTR) | |||
uio_error("Nanosleep got interrupted"); | |||
else | |||
uio_error("Nanosleep failed"); | |||
} | |||
} | |||
static ssize_t api_send(char *buffer, size_t data_len, size_t buf_size) | |||
{ | |||
ssize_t read_len; | |||
api_ratelimit(); | |||
uio_debug("{Api}: Sending: %.*s", (int)data_len, buffer); | |||
if (api_encryption) | |||
data_len = api_encrypt(buffer, data_len); | |||
if (net_send(buffer, data_len) == -1) { | |||
uio_error("Cannot send data: %s", strerror(errno)); | |||
return -1; | |||
} | |||
read_len = net_read(buffer, buf_size); | |||
api_ratelimit_sent(); | |||
if (api_encryption) | |||
read_len = api_decrypt(buffer, read_len); | |||
uio_debug("{Api}: Reading: %.*s", (int)read_len, buffer); | |||
return read_len; | |||
} | |||
long api_res_code(const char *buffer) | |||
{ | |||
char *end; | |||
long res = strtol(buffer, &end, 10); | |||
if (res == 0 && buffer == end) { | |||
uio_error("No error codes in the response"); | |||
return -1; | |||
} | |||
assert(*end == ' '); | |||
return res; | |||
} | |||
static bool api_get_fl(const char *buffer, int32_t index, const char *delim, | |||
char **const out_start, size_t *const out_len) | |||
{ | |||
assert(index > 0); | |||
size_t len = strcspn(buffer, delim); | |||
while (--index > 0) { | |||
buffer += len + 1; | |||
len = strcspn(buffer, delim); | |||
} | |||
*out_start = (char*)buffer; | |||
*out_len = len; | |||
return true; | |||
} | |||
static bool api_get_line(const char *buffer, int32_t line_num, | |||
char **const out_line_start, size_t *const out_line_len) | |||
{ | |||
return api_get_fl(buffer, line_num, "\n", out_line_start, out_line_len); | |||
} | |||
static bool api_get_field(const char *buffer, int32_t field_num, | |||
char **const out_field_start, size_t *const out_field_len) | |||
{ | |||
return api_get_fl(buffer, field_num, " |\n", out_field_start, out_field_len); | |||
} | |||
#if 0 | |||
static char *api_get_field_mod(char *buffer, int32_t field_num) | |||
{ | |||
char *sptr = NULL; | |||
char *f_start; | |||
f_start = strtok_r(buffer, " ", &sptr); | |||
if (!f_start) | |||
return NULL; | |||
while (field_num --> 0) { | |||
f_start = strtok_r(NULL, " ", &sptr); | |||
if (!f_start) | |||
return NULL; | |||
} | |||
return f_start; | |||
} | |||
#endif | |||
enum error api_cmd_version(struct api_result *res) | |||
{ | |||
char buffer[API_BUFSIZE] = "VERSION"; | |||
size_t res_len = api_send(buffer, strlen(buffer), sizeof(buffer)); | |||
long code; | |||
enum error err = NOERR; | |||
pthread_mutex_lock(&api_work_mx); | |||
if (res_len == -1) { | |||
err = ERR_API_COMMFAIL; | |||
goto end; | |||
} | |||
code = api_res_code(buffer); | |||
if (code == -1) { | |||
err = ERR_API_RESP_INVALID; | |||
goto end; | |||
} | |||
if (code == 998) { | |||
char *ver_start; | |||
size_t ver_len; | |||
bool glr = api_get_line(buffer, 2, &ver_start, &ver_len); | |||
assert(glr); | |||
(void)glr; | |||
assert(ver_len < sizeof(res->version.version_str)); | |||
memcpy(res->version.version_str, ver_start, ver_len); | |||
res->version.version_str[ver_len] = '\0'; | |||
} | |||
res->code = (uint16_t)code; | |||
end: | |||
pthread_mutex_unlock(&api_work_mx); | |||
return err; | |||
} | |||
static enum error api_cmd_auth(const char *uname, const char *pass, | |||
struct api_result *res) | |||
{ | |||
pthread_mutex_lock(&api_work_mx); | |||
char buffer[API_BUFSIZE]; | |||
long code; | |||
size_t res_len = api_send(buffer, snprintf(buffer, sizeof(buffer), | |||
"AUTH user=%s&pass=%B&protover=3&client=caniadd&clientver=" | |||
PROG_VERSION "&enc=UTF-8", uname, pass), sizeof(buffer)); | |||
enum error err = NOERR; | |||
if (res_len == -1) { | |||
err = ERR_API_COMMFAIL; | |||
goto end; | |||
} | |||
code = api_res_code(buffer); | |||
if (code == -1) { | |||
err = ERR_API_RESP_INVALID; | |||
goto end; | |||
} | |||
if (code == 200 || code == 201) { | |||
char *sess; | |||
size_t sess_len; | |||
bool gfr = api_get_field(buffer, 2, &sess, &sess_len); | |||
assert(gfr); | |||
(void)gfr; | |||
assert(sess_len < sizeof(res->auth.session_key)); | |||
memcpy(res->auth.session_key, sess, sess_len); | |||
res->auth.session_key[sess_len] = '\0'; | |||
} else if (code == 504) { | |||
char *reason; | |||
size_t reason_len; | |||
bool gfr = api_get_field(buffer, 5, &reason, &reason_len); | |||
assert(gfr); | |||
(void)gfr; | |||
res->auth.banned_reason = strndup(reason, reason_len); | |||
} | |||
res->code = (uint16_t)code; | |||
end: | |||
pthread_mutex_unlock(&api_work_mx); | |||
return err; | |||
} | |||
static enum error api_cmd_logout(struct api_result *res) | |||
{ | |||
pthread_mutex_lock(&api_work_mx); | |||
char buffer[API_BUFSIZE]; | |||
size_t res_len = api_send(buffer, snprintf(buffer, sizeof(buffer), | |||
"LOGOUT s=%s", api_session), sizeof(buffer)); | |||
long code; | |||
enum error err = NOERR; | |||
if (res_len == -1) { | |||
err = ERR_API_COMMFAIL; | |||
goto end; | |||
} | |||
code = api_res_code(buffer); | |||
if (code == -1) { | |||
err = ERR_API_RESP_INVALID; | |||
goto end; | |||
} | |||
res->code = (uint16_t)code; | |||
end: | |||
pthread_mutex_unlock(&api_work_mx); | |||
return err; | |||
} | |||
enum error api_cmd_uptime(struct api_result *res) | |||
{ | |||
/* If mutex is not already locked from the keepalive thread */ | |||
/* Or we could use a recursive mutex? */ | |||
if (!api_ka_now) | |||
pthread_mutex_lock(&api_work_mx); | |||
char buffer[API_BUFSIZE]; | |||
size_t res_len = api_send(buffer, snprintf(buffer, sizeof(buffer), | |||
"UPTIME s=%s", api_session), sizeof(buffer)); | |||
long code; | |||
enum error err = NOERR; | |||
if (res_len == -1) { | |||
err = ERR_API_COMMFAIL; | |||
goto end; | |||
} | |||
code = api_res_code(buffer); | |||
if (code == -1) { | |||
err = ERR_API_RESP_INVALID; | |||
goto end; | |||
} | |||
if (code == 208) { | |||
char *ls; | |||
size_t ll; | |||
bool glf = api_get_line(buffer, 2, &ls, &ll); | |||
assert(glf); | |||
(void)glf; | |||
res->uptime.ms = strtol(ls, NULL, 10); | |||
} | |||
res->code = (uint16_t)code; | |||
end: | |||
if (!api_ka_now) | |||
pthread_mutex_unlock(&api_work_mx); | |||
return err; | |||
} | |||
enum error api_cmd_mylistadd(int64_t size, const uint8_t *hash, | |||
enum mylist_state ml_state, bool watched, struct api_result *res) | |||
{ | |||
char buffer[API_BUFSIZE]; | |||
char hash_str[ED2K_HASH_SIZE * 2 + 1]; | |||
size_t res_len; | |||
enum error err = NOERR; | |||
long code; | |||
pthread_mutex_lock(&api_work_mx); | |||
util_byte2hex(hash, ED2K_HASH_SIZE, false, hash_str); | |||
/* Wiki says file size is 4 bytes, but no way that's true lol */ | |||
res_len = api_send(buffer, snprintf(buffer, sizeof(buffer), | |||
"MYLISTADD s=%s&size=%ld&ed2k=%s&state=%hu&viewed=%d", | |||
api_session, size, hash_str, ml_state, watched), | |||
sizeof(buffer)); | |||
if (res_len == -1) { | |||
err = ERR_API_COMMFAIL; | |||
goto end; | |||
} | |||
code = api_res_code(buffer); | |||
if (code == -1) { | |||
err = ERR_API_RESP_INVALID; | |||
goto end; | |||
} | |||
if (code == 210) { | |||
char *ls, id_str[12]; | |||
size_t ll; | |||
bool glr = api_get_line(buffer, 2, &ls, &ll); | |||
assert(glr); | |||
(void)glr; | |||
assert(sizeof(id_str) > ll); | |||
memcpy(id_str, ls, ll); | |||
id_str[ll] = '\0'; | |||
res->mylistadd.new_id = strtoll(id_str, NULL, 10); | |||
/* Wiki says these id's are 4 bytes, which is untrue... | |||
* that page may be a little out of date (or they just | |||
* expect us to use common sense lmao */ | |||
} else if (code == 310) { | |||
/* {int4 lid}|{int4 fid}|{int4 eid}|{int4 aid}|{int4 gid}| | |||
* {int4 date}|{int2 state}|{int4 viewdate}|{str storage}| | |||
* {str source}|{str other}|{int2 filestate} */ | |||
char *ls; | |||
size_t ll; | |||
struct api_mylistadd_result *mr = &res->mylistadd; | |||
bool glr = api_get_line(buffer, 2, &ls, &ll); | |||
assert(glr); | |||
assert(ll < API_BUFSIZE - 1); | |||
(void)glr; | |||
ls[ll] = '\0'; | |||
void *fptrs[] = { | |||
&mr->lid, &mr->fid, &mr->eid, &mr->aid, &mr->gid, &mr->date, | |||
&mr->state, &mr->viewdate, &mr->storage, &mr->source, | |||
&mr->other, &mr->filestate, | |||
}; | |||
for (int idx = 1; idx <= 12; idx++) { | |||
char *fs, *endptr; | |||
size_t fl; | |||
bool pr; | |||
uint64_t val; | |||
size_t cpy_size = sizeof(mr->lid); | |||
if (idx == 7) | |||
cpy_size = sizeof(mr->state); | |||
if (idx == 12) | |||
cpy_size = sizeof(mr->filestate); | |||
pr = api_get_field(ls, idx, &fs, &fl); | |||
assert(pr); | |||
(void)pr; | |||
if (idx == 9 || idx == 10 || idx == 11) { /* string fields */ | |||
if (fl == 0) | |||
*(char**)fptrs[idx-1] = NULL; | |||
else | |||
*(char**)fptrs[idx-1] = strndup(fs, fl); | |||
continue; | |||
} | |||
val = strtoull(fs, &endptr, 10); | |||
assert(!(val == 0 && fs == endptr)); | |||
memcpy(fptrs[idx-1], &val, cpy_size); | |||
} | |||
} | |||
res->code = (uint16_t)code; | |||
end: | |||
pthread_mutex_unlock(&api_work_mx); | |||
return err; | |||
} | |||
#pragma GCC diagnostic pop |
@@ -0,0 +1,91 @@ | |||
#ifndef _API_H | |||
#define _API_H | |||
#include <stdint.h> | |||
#include <time.h> | |||
#include "error.h" | |||
/* Maximum length of one response/request */ | |||
#define API_BUFSIZE 1400 | |||
/* Session key maximum size, including '\0' */ | |||
#define API_SMAXSIZE 16 | |||
/* The session timeout in miliseconds */ | |||
#define API_TIMEOUT 30 * 60 * 1000 | |||
/* How many miliseconds to wait between sends */ | |||
#define API_SENDWAIT 2 * 1000 | |||
/* The number of packets that are exccempt from the ratelimit */ | |||
#define API_FREESEND 5 | |||
/* Long term wait between sends */ | |||
#define API_SENDWAIT_LONG 4 * 1000 | |||
/* After this many packets has been sent, use the longterm ratelimit */ | |||
#define API_LONGTERM_PACKETS 100 | |||
enum mylist_state { | |||
MYLIST_STATE_UNKNOWN = 0, | |||
MYLIST_STATE_INTERNAL, | |||
MYLIST_STATE_EXTERNAL, | |||
MYLIST_STATE_DELETED, | |||
MYLIST_STATE_REMOTE, | |||
}; | |||
enum file_state { | |||
FILE_STATE_NORMAL = 0, | |||
FILE_STATE_CORRUPT, | |||
FILE_STATE_SELF_EDIT, | |||
FILE_STATE_SELF_RIP = 10, | |||
FILE_STATE_ON_DVD, | |||
FILE_STATE_ON_VHS, | |||
FILE_STATE_ON_TV, | |||
FILE_STATE_IN_THEATERS, | |||
FILE_STATE_STREAMED, | |||
FILE_STATE_OTHER = 100, | |||
}; | |||
struct api_version_result { | |||
char version_str[40]; | |||
}; | |||
struct api_auth_result { | |||
union { | |||
char session_key[API_SMAXSIZE]; | |||
/* free() */ | |||
char *banned_reason; | |||
}; | |||
}; | |||
struct api_uptime_result { | |||
int32_t ms; | |||
}; | |||
struct api_mylistadd_result { | |||
union { | |||
uint64_t new_id; | |||
struct { | |||
uint64_t lid, fid, eid, aid, gid, date, viewdate; | |||
/* free() if != NULL ofc */ | |||
char *storage, *source, *other; | |||
enum mylist_state state; | |||
enum file_state filestate; | |||
}; | |||
}; | |||
}; | |||
#define e(n) struct api_##n##_result n | |||
struct api_result { | |||
uint16_t code; | |||
union { | |||
struct api_version_result version; | |||
struct api_auth_result auth; | |||
struct api_uptime_result uptime; | |||
e(mylistadd); | |||
}; | |||
}; | |||
#undef e | |||
enum error api_init(bool auth); | |||
void api_free(); | |||
enum error api_cmd_version(struct api_result *res); | |||
enum error api_cmd_uptime(struct api_result *res); | |||
enum error api_cmd_mylistadd(int64_t size, const uint8_t *hash, | |||
enum mylist_state fstate, bool watched, struct api_result *res); | |||
#endif /* _API_H */ |
@@ -0,0 +1,208 @@ | |||
#include <stddef.h> | |||
#include <sqlite3.h> | |||
#include "cache.h" | |||
#include "config.h" | |||
#include "uio.h" | |||
#include "ed2k.h" | |||
#include "util.h" | |||
#define sqlite_bind_goto(smt, name, type, ...) { \ | |||
int sb_idx = sqlite3_bind_parameter_index(smt, name); \ | |||
if (sb_idx == 0) { \ | |||
uio_error("Cannot get named parameter for var: %s", name); \ | |||
err = ERR_CACHE_SQLITE; \ | |||
goto fail; \ | |||
} \ | |||
int sb_sret = sqlite3_bind_##type(smt, sb_idx, __VA_ARGS__); \ | |||
if (sb_sret != SQLITE_OK) {\ | |||
uio_error("Cannot bind to statement: %s", sqlite3_errmsg(cache_db));\ | |||
err = ERR_CACHE_SQLITE;\ | |||
goto fail;\ | |||
} \ | |||
} | |||
static sqlite3 *cache_db = NULL; | |||
static const char sql_create_table[] = "CREATE TABLE IF NOT EXISTS mylist (" | |||
"lid INTEGER NOT NULL PRIMARY KEY," | |||
"fname TEXT NOT NULL," | |||
"fsize INTEGER NOT NULL," | |||
"ed2k TEXT NOT NULL," | |||
"UNIQUE (fname, fsize) )"; | |||
static const char sql_mylist_add[] = "INSERT INTO mylist " | |||
"(lid, fname, fsize, ed2k) VALUES " | |||
//"(?, ?, ?, ?)"; | |||
"(:lid, :fname, :fsize, :ed2k)"; | |||
static const char sql_mylist_get[] = "SELECT * FROM mylist WHERE " | |||
"fsize=:fsize AND fname=:fname"; | |||
#if 0 | |||
static const char sql_has_tables[] = "SELECT 1 FROM sqlite_master " | |||
"WHERE type='table' AND tbl_name='mylist'"; | |||
/* Return 0 if false, 1 if true, and -1 if error */ | |||
static int cache_has_tables() | |||
{ | |||
sqlite3_smt smt; | |||
int sret; | |||
sret = sqlite3_prepare_v2(cache_db, sql_has_tables, | |||
sizeof(sql_has_tables), &smt, NULL); | |||
if (sret != SQLITE_OK) { | |||
uio_error("Cannot prepare statement: %s", sqlite3_errmsg(cache_db)); | |||
return -1; | |||
} | |||
sqlite3_step(&smt); | |||
// ehh fuck this, lets just use if not exists | |||
sret = sqlite3_finalize(&smt); | |||
if (sret != SQLITE_OK) | |||
uio_debug("sql3_finalize failed: %s", sqlite3_errmsg(cache_db)); | |||
} | |||
#endif | |||
/* | |||
* Create database table(s) | |||
*/ | |||
static enum error cache_init_table() | |||
{ | |||
sqlite3_stmt *smt; | |||
int sret; | |||
enum error err = NOERR; | |||
sret = sqlite3_prepare_v2(cache_db, sql_create_table, | |||
sizeof(sql_create_table), &smt, NULL); | |||
if (sret != SQLITE_OK) { | |||
uio_error("Cannot prepare statement: %s", sqlite3_errmsg(cache_db)); | |||
return ERR_CACHE_SQLITE; | |||
} | |||
sret = sqlite3_step(smt); | |||
if (sret != SQLITE_DONE) { | |||
uio_error("sql3_step is not done: %s", sqlite3_errmsg(cache_db)); | |||
err = ERR_CACHE_SQLITE; | |||
} | |||
sret = sqlite3_finalize(smt); | |||
if (sret != SQLITE_OK) | |||
uio_debug("sql3_finalize failed: %s", sqlite3_errmsg(cache_db)); | |||
return err; | |||
} | |||
enum error cache_init() | |||
{ | |||
char **db_path; | |||
enum error err; | |||
int sret; | |||
err = config_get("cachedb", (void**)&db_path); | |||
if (err != NOERR) { | |||
uio_error("Cannot get cache db path from args"); | |||
return err; | |||
} | |||
uio_debug("Opening cache db: '%s'", *db_path); | |||
sret = sqlite3_open(*db_path, &cache_db); | |||
if (sret != SQLITE_OK) { | |||
uio_error("Cannot create sqlite3 database: %s", sqlite3_errstr(sret)); | |||
sqlite3_close(cache_db); /* Even if arg is NULL, it's A'OK */ | |||
return ERR_CACHE_SQLITE; | |||
} | |||
sqlite3_extended_result_codes(cache_db, 1); | |||
err = cache_init_table(); | |||
if (err != NOERR) | |||
goto fail; | |||
return NOERR; | |||
fail: | |||
cache_free(); | |||
return err; | |||
} | |||
void cache_free() | |||
{ | |||
sqlite3_close(cache_db); | |||
uio_debug("Closed cache db"); | |||
} | |||
enum error cache_add(uint64_t lid, const char *fname, | |||
uint64_t fsize, const uint8_t *ed2k) | |||
{ | |||
char ed2k_str[ED2K_HASH_SIZE * 2 + 1]; | |||
sqlite3_stmt *smt; | |||
int sret; | |||
enum error err = NOERR; | |||
sret = sqlite3_prepare_v2(cache_db, sql_mylist_add, | |||
sizeof(sql_mylist_add), &smt, NULL); | |||
if (sret != SQLITE_OK) { | |||
uio_error("Cannot prepare statement: %s", sqlite3_errmsg(cache_db)); | |||
return ERR_CACHE_SQLITE; | |||
} | |||
util_byte2hex(ed2k, ED2K_HASH_SIZE, false, ed2k_str); | |||
sqlite_bind_goto(smt, ":lid", int64, lid); | |||
sqlite_bind_goto(smt, ":fname", text, fname, -1, SQLITE_STATIC); | |||
sqlite_bind_goto(smt, ":fsize", int64, fsize); | |||
sqlite_bind_goto(smt, ":ed2k", text, ed2k_str, -1, SQLITE_STATIC); | |||
sret = sqlite3_step(smt); | |||
if (sret != SQLITE_DONE) { | |||
if (sret == SQLITE_CONSTRAINT_PRIMARYKEY) { | |||
uio_debug("Attempted to add duplicate entry!"); | |||
err = ERR_CACHE_EXISTS; | |||
} else if (sret == SQLITE_CONSTRAINT_UNIQUE) { | |||
uio_debug("An entry with the same name and size already exists!"); | |||
err = ERR_CACHE_NON_UNIQUE; | |||
} else { | |||
uio_error("error after sql3_step: %s %d", sqlite3_errmsg(cache_db), sret); | |||
err = ERR_CACHE_SQLITE; | |||
} | |||
} | |||
fail: | |||
sqlite3_finalize(smt); | |||
return err; | |||
} | |||
enum error cache_get(const char *fname, uint64_t fsize, | |||
struct cache_entry *out_ce) | |||
{ | |||
sqlite3_stmt *smt; | |||
int sret; | |||
enum error err = NOERR; | |||
sret = sqlite3_prepare_v2(cache_db, sql_mylist_get, | |||
sizeof(sql_mylist_get), &smt, NULL); | |||
if (sret != SQLITE_OK) { | |||
uio_error("Cannot prepare statement: %s", sqlite3_errmsg(cache_db)); | |||
return ERR_CACHE_SQLITE; | |||
} | |||
sqlite_bind_goto(smt, ":fname", text, fname, -1, SQLITE_STATIC); | |||
sqlite_bind_goto(smt, ":fsize", int64, fsize); | |||
sret = sqlite3_step(smt); | |||
if (sret == SQLITE_DONE) { | |||
uio_debug("Cache entry with size (%lu) and name (%s) not found", fsize, fname); | |||
err = ERR_CACHE_NO_EXISTS; | |||
} else if (sret == SQLITE_ROW) { | |||
uio_debug("Found Cache entry with size (%lu) and name (%s)", fsize, fname); | |||
} else { | |||
uio_error("sqlite_step failed: %s", sqlite3_errmsg(cache_db)); | |||
err = ERR_CACHE_SQLITE; | |||
} | |||
fail: | |||
sqlite3_finalize(smt); | |||
return err; | |||
} |
@@ -0,0 +1,40 @@ | |||
#ifndef _CACHE_H | |||
#define _CACHE_H | |||
#include <stdint.h> | |||
#include <stdbool.h> | |||
#include "error.h" | |||
#include "ed2k.h" | |||
struct cache_entry { | |||
uint64_t lid, fsize; | |||
char *fname; | |||
uint8_t ed2k[ED2K_HASH_SIZE]; | |||
}; | |||
/* | |||
* Init tha cache | |||
*/ | |||
enum error cache_init(); | |||
/* | |||
* Free tha cache | |||
*/ | |||
void cache_free(); | |||
/* | |||
* Add a new mylist entry to the cache | |||
*/ | |||
enum error cache_add(uint64_t lid, const char *fname, | |||
uint64_t fsize, const uint8_t *ed2k); | |||
/* | |||
* Get a cache entry | |||
* | |||
* out_ce can be NULL. Useful, if we only want | |||
* to check if the entry exists or not. | |||
*/ | |||
enum error cache_get(const char *fname, uint64_t size, | |||
struct cache_entry *out_ce); | |||
#endif /* _CACHE_H */ |
@@ -0,0 +1,29 @@ | |||
#include <stdio.h> | |||
#include <stdlib.h> | |||
#include <signal.h> | |||
#include "config.h" | |||
#include "error.h" | |||
#include "uio.h" | |||
#include "cmd.h" | |||
int main(int argc, char **argv) | |||
{ | |||
int exit_code = EXIT_SUCCESS; | |||
enum error err = config_parse(argc, argv); | |||
if (err == ERR_OPT_EXIT) | |||
return EXIT_SUCCESS; | |||
else if (err != NOERR) | |||
return EXIT_FAILURE; | |||
//config_dump(); | |||
err = cmd_main(); | |||
if (err != NOERR) | |||
exit_code = EXIT_FAILURE; | |||
config_free(); | |||
return exit_code; | |||
} |
@@ -0,0 +1,75 @@ | |||
#include <stdbool.h> | |||
#include "cmd.h" | |||
#include "error.h" | |||
#include "config.h" | |||
#include "api.h" | |||
#include "uio.h" | |||
#include "net.h" | |||
#include "cache.h" | |||
struct cmd_entry { | |||
bool need_api : 1; /* Does this command needs to connect to the api? */ | |||
bool need_auth : 1; /* Does this command needs auth to the api? sets need_api */ | |||
bool need_cache : 1; /* Does this cmd needs the file cache? */ | |||
const char *arg_name; /* If this argument is present, execute this cmd */ | |||
enum error (*fn)(void *data); /* The function for the command */ | |||
}; | |||
static const struct cmd_entry ents[] = { | |||
{ .arg_name = "version", .fn = cmd_prog_version, }, | |||
{ .arg_name = "server-version", .fn = cmd_server_version, .need_api = true }, | |||
{ .arg_name = "uptime", .fn = cmd_server_uptime, .need_auth = true }, | |||
{ .arg_name = "ed2k", .fn = cmd_ed2k, }, | |||
{ .arg_name = "add", .fn = cmd_add, .need_auth = true, .need_cache = true, }, | |||
}; | |||
static const int32_t ents_len = sizeof(ents)/sizeof(*ents); | |||
static enum error cmd_run_one(const struct cmd_entry *ent) | |||
{ | |||
enum error err = NOERR; | |||
if (ent->need_cache) { | |||
err = cache_init(); | |||
if (err != NOERR) | |||
goto end; | |||
} | |||
if (ent->need_api || ent->need_auth) { | |||
err = api_init(ent->need_auth); | |||
if (err != NOERR) | |||
return err; | |||
} | |||
void *data = NULL; | |||
err = ent->fn(data); | |||
end: | |||
if (ent->need_api || ent->need_auth) | |||
api_free(); | |||
if (ent->need_cache) | |||
cache_free(); | |||
return err; | |||
} | |||
enum error cmd_main() | |||
{ | |||
for (int i = 0; i < ents_len; i++) { | |||
enum error err; | |||
bool *is_set; | |||
err = config_get(ents[i].arg_name, (void**)&is_set); | |||
if (err != NOERR && err != ERR_OPT_UNSET) { | |||
uio_error("Cannot get arg '%s' (%s)", ents[i].arg_name, | |||
error_to_string(err)); | |||
continue; | |||
} | |||
if (*is_set) { | |||
err = cmd_run_one(&ents[i]); | |||
return err; | |||
} | |||
} | |||
return ERR_CMD_NONE; | |||
} |
@@ -0,0 +1,38 @@ | |||
#ifndef _CMD_H | |||
#define _CMD_H | |||
#include "error.h" | |||
#include "config.h" | |||
/* | |||
* Read commands from config and execute them | |||
*/ | |||
enum error cmd_main(); | |||
/* | |||
* Add files to the AniDB list | |||
*/ | |||
enum error cmd_add(void *); | |||
/* | |||
* Take in a file/folder and print out | |||
* the ed2k hash of it | |||
*/ | |||
enum error cmd_ed2k(void *data); | |||
/* | |||
* Get and print the server api version | |||
*/ | |||
enum error cmd_server_version(void *); | |||
/* | |||
* Print the server uptime | |||
*/ | |||
enum error cmd_server_uptime(void *); | |||
/* | |||
* Print the program version | |||
*/ | |||
enum error cmd_prog_version(void *); | |||
#endif /* _CMD_H */ |
@@ -0,0 +1,114 @@ | |||
#include <sys/stat.h> | |||
#include <stdlib.h> | |||
#include <stdio.h> | |||
#include <stdbool.h> | |||
#include "cmd.h" | |||
#include "error.h" | |||
#include "uio.h" | |||
#include "api.h" | |||
#include "config.h" | |||
#include "ed2k_util.h" | |||
#include "cache.h" | |||
#include "util.h" | |||
struct add_opts { | |||
enum mylist_state ao_state; | |||
bool ao_watched; | |||
}; | |||
enum error cmd_add_cachecheck(const char *path, const struct stat *st, | |||
void *data) | |||
{ | |||
const char *bname = util_basename(path); | |||
enum error err; | |||
err = cache_get(bname, st->st_size, NULL); | |||
if (err == NOERR) { | |||
/* We could get the entry, so it exists already */ | |||
uio_user("This file (%s) with size (%lu) already exists in cache." | |||
" Skipping", bname, st->st_size); | |||
return ED2KUTIL_DONTHASH; | |||
} else if (err != ERR_CACHE_NO_EXISTS) { | |||
uio_error("Some error when trying to get from cache: %s", | |||
error_to_string(err)); | |||
return ED2KUTIL_DONTHASH; | |||
} | |||
uio_user("Hashing %s", path); | |||
return NOERR; | |||
} | |||
enum error cmd_add_apisend(const char *path, const uint8_t *hash, | |||
const struct stat *st, void *data) | |||
{ | |||
struct api_result r; | |||
struct add_opts* ao = (struct add_opts*)data; | |||
if (api_cmd_mylistadd(st->st_size, hash, ao->ao_state, ao->ao_watched, &r) | |||
!= NOERR) | |||
return ERR_CMD_FAILED; | |||
if (r.code == 310) { | |||
struct api_mylistadd_result *x = &r.mylistadd; | |||
uio_warning("File already added! Adding it to cache"); | |||
uio_debug("File info: lid: %ld, fid: %ld, eid: %ld, aid: %ld," | |||
" gid: %ld, date: %ld, viewdate: %ld, state: %d," | |||
" filestate: %d\nstorage: %s\nsource: %s\nother: %s", | |||
x->lid, x->fid, x->eid, x->aid, x->gid, x->date, x->viewdate, | |||
x->state, x->filestate, x->storage, x->source, x->other); | |||
cache_add(x->lid, util_basename(path), st->st_size, hash); | |||
if (x->storage) | |||
free(x->storage); | |||
if (x->source) | |||
free(x->source); | |||
if (x->other) | |||
free(x->other); | |||
return NOERR; | |||
} | |||
if (r.code != 210) { | |||
uio_error("Mylistadd failure: %hu", r.code); | |||
return ERR_CMD_FAILED; | |||
} | |||
uio_user("Succesfully added!"); | |||
uio_debug("New mylist id is: %ld", r.mylistadd.new_id); | |||
cache_add(r.mylistadd.new_id, util_basename(path), st->st_size, hash); | |||
return NOERR; | |||
} | |||
enum error cmd_add(void *data) | |||
{ | |||
struct add_opts add_opts = {0}; | |||
struct ed2k_util_opts ed2k_opts = { | |||
.pre_hash_fn = cmd_add_cachecheck, | |||
.post_hash_fn = cmd_add_apisend, | |||
.data = &add_opts, | |||
}; | |||
bool *watched; | |||
enum error err = NOERR; | |||
int fcount; | |||
fcount = config_get_nonopt_count(); | |||
if (fcount == 0) { | |||
uio_error("No files specified"); | |||
return ERR_CMD_ARG; | |||
} | |||
if (config_get("watched", (void**)&watched) == NOERR) { | |||
add_opts.ao_watched = *watched; | |||
} | |||
add_opts.ao_state = MYLIST_STATE_INTERNAL; | |||
for (int i = 0; i < fcount; i++) { | |||
err = ed2k_util_iterpath(config_get_nonopt(i), &ed2k_opts); | |||
if (err != NOERR) | |||
break; | |||
} | |||
return err; | |||
} | |||
@@ -0,0 +1,62 @@ | |||
#include <sys/stat.h> | |||
#include <stdio.h> | |||
#include <stdbool.h> | |||
#include "cmd.h" | |||
#include "error.h" | |||
#include "uio.h" | |||
#include "config.h" | |||
#include "ed2k_util.h" | |||
#include "ed2k.h" | |||
#include "util.h" | |||
struct cmd_ed2k_opts { | |||
bool link; | |||
}; | |||
static enum error cmd_ed2k_output(const char *path, const uint8_t *hash, | |||
const struct stat *st, void *data) | |||
{ | |||
struct cmd_ed2k_opts *eo = data; | |||
char buff[ED2K_HASH_SIZE * 2 + 1]; | |||
bool upcase = eo->link; | |||
util_byte2hex(hash, ED2K_HASH_SIZE, upcase, buff); | |||
if (eo->link) { | |||
char *name_part = util_basename(path); | |||
printf("ed2k://|file|%s|%ld|%s|/\n", name_part, st->st_size, buff); | |||
} else { | |||
printf("%s\t%s\n", buff, path); | |||
} | |||
return NOERR; | |||
} | |||
enum error cmd_ed2k(void *data) | |||
{ | |||
struct cmd_ed2k_opts opts = {0}; | |||
struct ed2k_util_opts ed2k_opts = { | |||
.post_hash_fn = cmd_ed2k_output, | |||
.data = &opts, | |||
}; | |||
bool *link; | |||
enum error err = NOERR; | |||
int fcount; | |||
fcount = config_get_nonopt_count(); | |||
if (fcount == 0) { | |||
uio_error("No files specified"); | |||
return ERR_CMD_ARG; | |||
} | |||
if (config_get("link", (void**)&link) == NOERR) | |||
opts.link = *link; | |||
for (int i = 0; i < fcount; i++) { | |||
err = ed2k_util_iterpath(config_get_nonopt(i), &ed2k_opts); | |||
if (err != NOERR) | |||
break; | |||
} | |||
return err; | |||
} | |||
@@ -0,0 +1,18 @@ | |||
#include <stdio.h> | |||
#include "cmd.h" | |||
#include "cache.h" | |||
#include <string.h> | |||
enum error cmd_prog_version(void *data) | |||
{ | |||
enum error err = NOERR; | |||
printf("caniadd v0.1.0" | |||
#ifdef GIT_REF | |||
" (" GIT_REF ")" | |||
#endif | |||
"\n"); | |||
return err; | |||
} |
@@ -0,0 +1,31 @@ | |||
#include <stdio.h> | |||
#include <stdlib.h> | |||
#include "cmd.h" | |||
#include "uio.h" | |||
#include "api.h" | |||
enum error cmd_server_uptime(void *data) | |||
{ | |||
struct api_result r; | |||
int32_t h, m, s; | |||
div_t dt; | |||
if (api_cmd_uptime(&r) != NOERR) | |||
return ERR_CMD_FAILED; | |||
if (r.code != 208) { | |||
uio_error("VERSION cmd is unsuccesful: %hu", r.code); | |||
return ERR_CMD_FAILED; | |||
} | |||
dt = div(r.uptime.ms, 1000*60*60); | |||
h = dt.quot; | |||
dt = div(dt.rem, 1000*60); | |||
m = dt.quot; | |||
dt = div(dt.rem, 1000); | |||
s = dt.quot; | |||
printf("up %d hours, %d minutes, %d seconds\n", h, m, s); | |||
return NOERR; | |||
} |
@@ -0,0 +1,23 @@ | |||
#include <stdio.h> | |||
#include "cmd.h" | |||
#include "uio.h" | |||
#include "api.h" | |||
enum error cmd_server_version(void *data) | |||
{ | |||
struct api_result a_res; | |||
struct api_version_result *vr = &a_res.version; | |||
if (api_cmd_version(&a_res) != NOERR) | |||
return ERR_CMD_FAILED; | |||
if (a_res.code == 998) { | |||
printf("%s\n", vr->version_str); | |||
} else { | |||
uio_error("VERSION cmd is unsuccesful: %hu", a_res.code); | |||
return ERR_CMD_FAILED; | |||
} | |||
return NOERR; | |||
} |
@@ -0,0 +1,539 @@ | |||
#include <stddef.h> | |||
#include <inttypes.h> | |||
#include <getopt.h> | |||
#include <stdio.h> | |||
#include <stdlib.h> | |||
#include <string.h> | |||
#include <stdbool.h> | |||
#include <sys/types.h> | |||
#include <sys/stat.h> | |||
#include <unistd.h> | |||
#include <errno.h> | |||
#include <assert.h> | |||
//#include <toml.h> | |||
#include <limits.h> | |||
#include <time.h> | |||
#include <unistd.h> | |||
#include <ctype.h> | |||
#include "config.h" | |||
#include "error.h" | |||
#include "util.h" | |||
static int show_help(struct conf_entry *ce); | |||
static int config_parse_file(); | |||
static enum error config_required_check(); | |||
static int config_set_str(struct conf_entry *ce, char *arg); | |||
static int config_set_port(struct conf_entry *ce, char *arg); | |||
static int config_set_bool(struct conf_entry *ce, char *arg); | |||
//static int config_def_cachedb(struct conf_entry *ce); | |||
//static int config_action_write_config(struct conf_entry *ce); | |||
/* Everything not explicitly defined, is 0 */ | |||
/* If an option only has a long name, the short name also has to be | |||
* defined. For example, a number larger than UCHAR_MAX */ | |||
static struct conf_entry options[] = { | |||
{ .l_name = "help", .s_name = 'h', .has_arg = no_argument, | |||
.action_func = show_help, .in_args = true, | |||
.type = OTYPE_ACTION, .handle_order = 0 }, | |||
/* | |||
{ .l_name = "config-dir", .s_name = 'b', .has_arg = required_argument, | |||
.default_func = config_def_config_dir, .set_func = config_set_str, | |||
.in_args = true, .type = OTYPE_S, .handle_order = 0 }, | |||
{ .l_name = "default-download-dir", .s_name = 'd', .has_arg = required_argument, | |||
.default_func = config_def_default_download_dir, .set_func = config_set_str, | |||
.in_file = true, .in_args = true, .type = OTYPE_S, .handle_order = 1 }, | |||
{ .l_name = "port", .s_name = 'p', .has_arg = required_argument, | |||
.set_func = config_set_port, .value.hu = 21729, .value_is_set = true, | |||
.in_file = true, .in_args = true, .type = OTYPE_HU, .handle_order = 1 }, | |||
{ .l_name = "foreground", .s_name = 'f', .has_arg = no_argument, | |||
.set_func = config_set_bool, .value.b = false, .value_is_set = true, | |||
.in_args = true, .type = OTYPE_B, .handle_order = 1 }, | |||
{ .l_name = "write-config", .s_name = UCHAR_MAX + 1, .has_arg = no_argument, | |||
.action_func = config_action_write_config, .value_is_set = true, | |||
.in_args = true, .type = OTYPE_ACTION, .handle_order = 2 }, | |||
{ .l_name = "peer-id", .s_name = UCHAR_MAX + 2, .has_arg = required_argument, | |||
.default_func = config_def_peer_id, .type = OTYPE_S, .handle_order = 1 }, | |||
*/ | |||
{ .l_name = "username", .s_name = 'u', .has_arg = required_argument, | |||
.set_func = config_set_str, .in_args = true, .in_file = true, | |||
.type = OTYPE_S, .handle_order = 1 }, | |||
{ .l_name = "password", .s_name = 'p', .has_arg = required_argument, | |||
.set_func = config_set_str, .in_args = true, .in_file = true, | |||
.type = OTYPE_S, .handle_order = 1 }, | |||
{ .l_name = "port", .s_name = 'P', .has_arg = required_argument, | |||
.set_func = config_set_port, .in_args = true, .in_file = true, | |||
.type = OTYPE_HU, .handle_order = 1, .value.hu = 29937, | |||
.value_is_set = true }, | |||
{ .l_name = "api-server", .s_name = UCHAR_MAX + 1, .has_arg = required_argument, | |||
.set_func = config_set_str, .in_args = true, .in_file = true, | |||
.type = OTYPE_S, .handle_order = 1, .value.s = "api.anidb.net:9000", | |||
.value_is_set = true }, | |||
{ .l_name = "api-key", .s_name = 'k', .has_arg = required_argument, | |||
.set_func = config_set_str, .in_args = true, .in_file = true, | |||
.type = OTYPE_S, .handle_order = 1, }, | |||
{ .l_name = "save-session", .s_name = 's', .has_arg = no_argument, | |||
.set_func = config_set_bool, .in_args = true, .in_file = true, | |||
.type = OTYPE_B, .handle_order = 1, .value_is_set = true }, | |||
{ .l_name = "destroy-session", .s_name = 'S', .has_arg = no_argument, | |||
.set_func = config_set_bool, .in_args = true, .in_file = false, | |||
.type = OTYPE_B, .handle_order = 1, .value_is_set = true }, | |||
{ .l_name = "watched", .s_name = 'w', .has_arg = no_argument, | |||
.set_func = config_set_bool, .in_args = true, | |||
.type = OTYPE_B, .handle_order = 1, .value_is_set = true }, | |||
{ .l_name = "link", .s_name = 'l', .has_arg = no_argument, | |||
.set_func = config_set_bool, .in_args = true, | |||
.type = OTYPE_B, .handle_order = 1, .value_is_set = true }, | |||
{ .l_name = "cachedb", .s_name = 'd', .has_arg = required_argument, | |||
.set_func = config_set_str, .in_args = true, .in_file = true, | |||
.type = OTYPE_S, .handle_order = 1, /*.default_func = config_def_cachedb*/ }, | |||
{ .l_name = "debug", .s_name = 'D', .has_arg = no_argument, | |||
.set_func = config_set_bool, .in_args = true, .in_file = true, | |||
.type = OTYPE_B, .handle_order = 1, .value_is_set = true, }, | |||
/*### cmd ###*/ | |||
{ .l_name = "server-version", .s_name = UCHAR_MAX + 2, | |||
.has_arg = no_argument, .set_func = config_set_bool, .in_args = true, | |||
.type = OTYPE_B, .handle_order = 1, .value_is_set = true }, | |||
{ .l_name = "version", .s_name = 'v', | |||
.has_arg = no_argument, .set_func = config_set_bool, .in_args = true, | |||
.type = OTYPE_B, .handle_order = 1, .value_is_set = true }, | |||
{ .l_name = "uptime", .s_name = UCHAR_MAX + 3, | |||
.has_arg = no_argument, .set_func = config_set_bool, .in_args = true, | |||
.type = OTYPE_B, .handle_order = 1, .value_is_set = true }, | |||
{ .l_name = "ed2k", .s_name = 'e', | |||
.has_arg = no_argument, .set_func = config_set_bool, .in_args = true, | |||
.type = OTYPE_B, .handle_order = 1 }, | |||
{ .l_name = "add", .s_name = 'a', | |||
.has_arg = no_argument, .set_func = config_set_bool, .in_args = true, | |||
.type = OTYPE_B, .handle_order = 1 }, | |||
/*{ .l_name = "stats", .s_name = UCHAR_MAX + 4, | |||
.has_arg = no_argument, .set_func = config_set_bool, .in_args = true, | |||
.type = OTYPE_B, .handle_order = 1, .value_is_set = true },*/ | |||
}; | |||
static const size_t options_count = sizeof(options) / sizeof(options[0]); | |||
static const char **opt_argv = NULL; | |||
static int opt_argc = 0; | |||
static void config_build_getopt_args(char out_sopt[options_count * 2 + 1], | |||
struct option out_lopt[options_count + 1]) | |||
{ | |||
int i_sopt = 0, i_lopt = 0; | |||
for (int i = 0; i < options_count; i++) { | |||
/* Short options */ | |||
if (options[i].s_name && options[i].s_name <= UCHAR_MAX) { | |||
out_sopt[i_sopt++] = options[i].s_name; | |||
if (options[i].has_arg == required_argument) | |||
out_sopt[i_sopt++] = ':'; | |||
assert(options[i].has_arg == required_argument || | |||
options[i].has_arg == no_argument); | |||
} | |||
/* Long options */ | |||
if (options[i].l_name) { | |||
assert(options[i].s_name); | |||
out_lopt[i_lopt].name = options[i].l_name; | |||
out_lopt[i_lopt].has_arg = options[i].has_arg; | |||
out_lopt[i_lopt].flag = NULL; | |||
out_lopt[i_lopt].val = options[i].s_name; | |||
i_lopt++; | |||
} | |||
} | |||
out_sopt[i_sopt] = '\0'; | |||
memset(&out_lopt[i_lopt], 0, sizeof(struct option)); | |||
} | |||
static int config_read_args(int argc, char **argv, char sopt[options_count * 2 + 1], | |||
struct option lopt[options_count + 1], int level) | |||
{ | |||
int optc, err = NOERR; | |||
optind = 1; | |||
while ((optc = getopt_long(argc, argv, sopt, | |||
lopt, NULL)) >= 0) { | |||
bool handled = false; | |||
for (int i = 0; i < options_count; i++) { | |||
if (options[i].handle_order != level) { | |||
/* Lie a lil :x */ | |||
handled = true; | |||
continue; | |||
} | |||
if (optc == options[i].s_name) { | |||
if (options[i].type != OTYPE_ACTION) | |||
err = options[i].set_func(&options[i], optarg); | |||
else | |||
err = options[i].action_func(&options[i]); | |||
if (err != NOERR) | |||
goto end; | |||
options[i].value_is_set = true; | |||
handled = true; | |||
break; | |||
} | |||
} | |||
if (handled) | |||
continue; | |||
if (optc == '?') { | |||
err = ERR_OPT_FAILED; | |||
goto end; | |||
} else { | |||
fprintf(stderr, "Unhandled option? '%c'\n", optc); | |||
err = ERR_OPT_UNHANDLED; | |||
goto end; | |||
} | |||
} | |||
end: | |||
return err; | |||
} | |||
static enum error config_required_check() | |||
{ | |||
enum error err = NOERR; | |||
for (int i = 0; i < options_count; i++) { | |||
if (options[i].required && !options[i].value_is_set) { | |||
printf("Argument %s is required!\n", options[i].l_name); | |||
err = ERR_OPT_REQUIRED; | |||
} | |||
} | |||
return err; | |||
} | |||
enum error config_parse(int argc, char **argv) | |||
{ | |||
enum error err = NOERR; | |||
char sopt[options_count * 2 + 1]; | |||
struct option lopt[options_count + 1]; | |||
opt_argv = (const char**)argv; | |||
opt_argc = argc; | |||
config_build_getopt_args(sopt, lopt); | |||
err = config_read_args(argc, argv, sopt, lopt, 0); | |||
if (err != NOERR) | |||
goto end; | |||
err = config_parse_file(); | |||
if (err != NOERR) | |||
goto end; | |||
err = config_read_args(argc, argv, sopt, lopt, 1); | |||
if (err != NOERR) | |||
goto end; | |||
/* Set defaults for those, that didn't got set above */ | |||
for (int i = 0; i < options_count; i++) { | |||
if (!options[i].value_is_set && options[i].type != OTYPE_ACTION && | |||
options[i].default_func) { | |||
err = options[i].default_func(&options[i]); | |||
if (err != NOERR) | |||
goto end; | |||
options[i].value_is_set = true; | |||
} | |||
} | |||
err = config_read_args(argc, argv, sopt, lopt, 2); | |||
if (err != NOERR) | |||
goto end; | |||
err = config_required_check(); | |||
end: | |||
if (err != NOERR) | |||
config_free(); | |||
return err; | |||
} | |||
#if 0 | |||
static int config_def_config_dir(struct conf_entry *ce) | |||
{ | |||
char *dir; | |||
int len; | |||
const char *format = "%s/.config/" CONFIG_DIR_NAME; | |||
const char *home_env = getenv("HOME"); | |||
if (!home_env) { | |||
/* Fix this at a later date with getuid and getpw */ | |||
fprintf(stderr, "HOME environment variable not found!\n"); | |||
return ERR_NOTFOUND; | |||
} | |||
len = snprintf(NULL, 0, format, home_env); | |||
if (len == -1) { | |||
int err = errno; | |||
fprintf(stderr, "Failed to call funky snpintf: %s\n", strerror(err)); | |||
return err; | |||
} | |||
dir = malloc(len + 1); | |||
sprintf(dir, format, home_env); | |||
ce->value.s = dir; | |||
ce->value_is_dyn = true; | |||
return NOERR; | |||
} | |||
#endif | |||
#if 0 | |||
static int config_def_cachedb(struct conf_entry *ce) | |||
{ | |||
bool dh_free = false; | |||
const char *data_home = getenv("XDG_DATA_HOME"); | |||
if (!data_home) { | |||
const char *home = util_get_home(); | |||
if (!home) | |||
return ERR_OPT_FAILED; | |||
sprintf(NULL, "%s/.local/share", home); | |||
} | |||
return NOERR; | |||
} | |||
#endif | |||
static int config_set_str(struct conf_entry *ce, char *arg) | |||
{ | |||
// TODO use realpath(3), when necessary | |||
ce->value.s = arg; | |||
return NOERR; | |||
} | |||
static int config_set_port(struct conf_entry *ce, char *arg) | |||
{ | |||
long portval = strtol(arg, NULL, 10); | |||
/* A zero return will be invalid no matter if strtol succeeded or not */ | |||
if (portval > UINT16_MAX || portval <= 0) { | |||
fprintf(stderr, "Invalid port value '%s'\n", arg); | |||
return ERR_OPT_INVVAL; | |||
} | |||
ce->value.hu = (uint16_t)portval; | |||
return NOERR; | |||
} | |||
static int config_set_bool(struct conf_entry *ce, char *arg) | |||
{ | |||
ce->value.b = true; | |||
return NOERR; | |||
} | |||
static int show_help(struct conf_entry *ce) | |||
{ | |||
printf("Todo...\n"); | |||
return ERR_OPT_EXIT; | |||
} | |||
static int config_parse_file() | |||
{ | |||
// TODO implement this | |||
#if 0 | |||
assert(conf.config_file_path); | |||
FILE *f = fopen(conf.config_file_path, "rb"); | |||
char errbuf[200]; | |||
toml_table_t *tml = toml_parse_file(f, errbuf, sizeof(errbuf)); | |||
fclose(f); | |||
if (!tml) { | |||
fprintf(stderr, "Failed to parse config toml: %s\n", errbuf); | |||
return ERR_TOML_PARSE_ERROR; | |||
} | |||
toml_datum_t port = toml_int_in(tml, "port"); | |||
if (port.ok) | |||
conf.port = (uint16_t)port.u.i; | |||
else | |||
fprintf(stderr, "Failed to parse port from config toml: %s\n", errbuf); | |||
toml_datum_t dldir = toml_string_in(tml, "default_download_dir"); | |||
if (dldir.ok) { | |||
conf.default_download_dir = dldir.u.s; | |||
printf("%s\n", dldir.u.s); | |||
conf_dyn.default_download_dir = dldir.u.s; | |||
conf_dyn.default_download_dir = true; | |||
/* TODO is this always malloced?? if yes, remve dyn check */ | |||
} else { | |||
fprintf(stderr, "Failed to parse download dir from config toml: %s\n", errbuf); | |||
} | |||
toml_free(tml); | |||
#endif | |||
return NOERR; | |||
} | |||
int config_free() | |||
{ | |||
for (int i = 0; i < options_count; i++) { | |||
if (options[i].value_is_dyn) { | |||
free(options[i].value.s); | |||
options[i].value.s = NULL; | |||
options[i].value_is_dyn = false; | |||
options[i].value_is_set = false; | |||
} | |||
} | |||
return NOERR; | |||
} | |||
#if 0 | |||
static int config_action_write_config(struct conf_entry *ce) | |||
{ | |||
/* This is the success return here */ | |||
int err = ERR_OPT_EXIT, plen; | |||
const char *config_dir; | |||
FILE *f = NULL; | |||
config_dir = config_get("config-dir"); | |||
plen = snprintf(NULL, 0, "%s/%s", config_dir, CONFIG_FILE_NAME); | |||
char path[plen + 1]; | |||
snprintf(path, plen + 1, "%s/%s", config_dir, CONFIG_FILE_NAME); | |||
for (int i = 0; i < 2; i++) { | |||
f = fopen(path, "wb"); | |||
if (!f) { | |||
int errn = errno; | |||
if (errn == ENOENT && i == 0) { | |||
/* Try to create parent directory */ | |||
if (mkdir(config_dir, 0755) == -1) { | |||
err = errno; | |||
fprintf(stderr, "Config mkdir failed: %s\n", strerror(err)); | |||
goto end; | |||
} | |||
} else { | |||
err = errn; | |||
fprintf(stderr, "Config fopen failed: %s\n", strerror(err)); | |||
goto end; | |||
} | |||
} else { | |||
break; | |||
} | |||
} | |||
for (int i = 0; i < options_count; i++) { | |||
if (!options[i].in_file) | |||
continue; | |||
fprintf(f, "%s = ", options[i].l_name); | |||
switch (options[i].type) { | |||
case OTYPE_S: | |||
// TODO toml escaping | |||
fprintf(f, "\"%s\"\n", options[i].value.s); | |||
break; | |||
case OTYPE_HU: | |||
fprintf(f, "%hu\n", options[i].value.hu); | |||
break; | |||
case OTYPE_B: | |||
fprintf(f, "%s\n", options[i].value.b ? "true" : "false"); | |||
break; | |||
default: | |||
break; | |||
} | |||
} | |||
config_dump(); | |||
end: | |||
if (f) | |||
fclose(f); | |||
return err; | |||
} | |||
#endif | |||
enum error config_get(const char *key, void **out) | |||
{ | |||
enum error err = ERR_OPT_NOTFOUND; | |||
for (int i = 0; i < options_count; i++) { | |||
struct conf_entry *cc = &options[i]; | |||
if (strcmp(cc->l_name, key) == 0) { | |||
if (cc->value_is_set) { | |||
if (out) | |||
*out = &cc->value.s; | |||
err = NOERR; | |||
} else { | |||
err = ERR_OPT_UNSET; | |||
} | |||
break; | |||
} | |||
} | |||
return err; | |||
} | |||
const char *config_get_nonopt(int index) | |||
{ | |||
if (index >= config_get_nonopt_count()) | |||
return NULL; | |||
return opt_argv[optind + index]; | |||
} | |||
int config_get_nonopt_count() | |||
{ | |||
return opt_argc - optind; | |||
} | |||
void config_dump() | |||
{ | |||
for (int i = 0; i < options_count; i++) { | |||
if (options[i].type == OTYPE_ACTION) | |||
continue; | |||
printf("%s: ", options[i].l_name); | |||
if (!options[i].value_is_set) { | |||
printf("[UNSET (>.<)]\n"); | |||
continue; | |||
} | |||
switch (options[i].type) { | |||
case OTYPE_S: | |||
printf("%s\n", options[i].value.s); | |||
break; | |||
case OTYPE_HU: | |||
printf("%hu\n", options[i].value.hu); | |||
break; | |||
case OTYPE_B: | |||
printf("%s\n", options[i].value.b ? "True" : "False"); | |||
break; | |||
default: | |||
printf("Error :(\n"); | |||
break; | |||
} | |||
} | |||
} |
@@ -0,0 +1,94 @@ | |||
#ifndef _CONFIG_H | |||
#define _CONFIG_H | |||
#include <inttypes.h> | |||
#include <stdbool.h> | |||
#include "error.h" | |||
#ifndef CONFIG_DIR_NAME | |||
#define CONFIG_DIR_NAME "caniadd" | |||
#endif | |||
enum option_type { | |||
OTYPE_S, /* Stores a string */ | |||
OTYPE_HU, /* Stores an unsigned short */ | |||
OTYPE_B, /* Stores a boolean */ | |||
/* Does not store anything, does an action. Handled after every | |||
* other option are parsed, and defaults set */ | |||
OTYPE_ACTION, | |||
_OTYPE_COUNT | |||
}; | |||
struct conf_entry { | |||
const char *l_name; /* The long name for the option, or for the config file */ | |||
int s_name; /* Short option name */ | |||
union { /* Value of the param */ | |||
char *s; | |||
uint16_t hu; | |||
bool b; | |||
} value; | |||
/* The function to use to init a default value, if it's a complex one */ | |||
int (*default_func)(struct conf_entry *ce); | |||
union { | |||
/* The function to use to set the value of the arg from the | |||
* command line or from the loaded config file */ | |||
int (*set_func)(struct conf_entry *ce, char *arg); | |||
/* Callback for an action option */ | |||
int (*action_func)(struct conf_entry *ce); | |||
}; | |||
int has_arg : 4; /* Do we need to specify an argument for this option on the cmd line? */ | |||
/* Did we set the value? If not, we may need to call the default func */ | |||
bool value_is_set : 1; | |||
/* Is the value required? */ | |||
bool required : 1; | |||
bool value_is_dyn : 1; /* Do we need to free the value? */ | |||
bool in_file : 1; /* Is this option in the config file? */ | |||
bool in_args : 1; /* Is this option in the argument list? */ | |||
enum option_type type : 4; /* Type of the option's value */ | |||
/* | |||
* In which step do we handle this arg? | |||
* We need this, because: | |||
* 1. Read in the base dir option from the command line, if present | |||
* 2. Use the base dir to load the options from the config file | |||
* 3. Read, and override options from the command line | |||
* 4. Execute action arguments | |||
*/ | |||
int handle_order : 4; | |||
}; | |||
/* | |||
* Parse options from the command line | |||
* | |||
* Returns 0 on success. | |||
*/ | |||
enum error config_parse(int argc, char **argv); | |||
/* | |||
* Free any memory that may have been allocated | |||
* in config_parse | |||
*/ | |||
int config_free(); | |||
/* | |||
* Write out the options to stdout | |||
*/ | |||
void config_dump(); | |||
/* | |||
* Get a config option by its long name | |||
* The pointer to the options value will be stored, at the pointer pointed to | |||
* by out | |||
* If the option is unset, it returns ERR_OPT_UNSET and out is unchanged | |||
* It the options is not found, it returns ERR_OPT_NOTFOUND, and out is unchanged | |||
*/ | |||
enum error config_get(const char *key, void **out); | |||
/* | |||
* Return an cmd line that is not an option | |||
* or null if error, or out of elements | |||
*/ | |||
const char *config_get_nonopt(int index); | |||
int config_get_nonopt_count(); | |||
#endif /* _CONFIG_H */ |
@@ -0,0 +1,61 @@ | |||
#include <assert.h> | |||
#include "ed2k.h" | |||
/* https://wiki.anidb.net/Ed2k-hash */ | |||
/* This is using the red method */ | |||
#define ED2K_CHUNK_SIZE 9728000 | |||
#define MIN(a,b) (((a) < (b)) ? (a) : (b)) | |||
void ed2k_init(struct ed2k_ctx *ctx) | |||
{ | |||
md4_init(&ctx->chunk_md4_ctx); | |||
md4_init(&ctx->hash_md4_ctx); | |||
ctx->byte_count = 0; | |||
} | |||
static void ed2k_hash_chunk(struct ed2k_ctx *ctx) | |||
{ | |||
unsigned char chunk_hash[MD4_DIGEST_SIZE]; | |||
md4_final(&ctx->chunk_md4_ctx, chunk_hash); | |||
md4_update(&ctx->hash_md4_ctx, chunk_hash, sizeof(chunk_hash)); | |||
md4_init(&ctx->chunk_md4_ctx); | |||
} | |||
void ed2k_update(struct ed2k_ctx *ctx, const void *data, size_t data_len) | |||
{ | |||
const char *bytes = (const char*)data; | |||
while (data_len) { | |||
size_t hdata_size = MIN(ED2K_CHUNK_SIZE - | |||
(ctx->byte_count % ED2K_CHUNK_SIZE), data_len); | |||
md4_update(&ctx->chunk_md4_ctx, bytes, hdata_size); | |||
ctx->byte_count += hdata_size; | |||
if (ctx->byte_count % ED2K_CHUNK_SIZE == 0) | |||
ed2k_hash_chunk(ctx); | |||
data_len -= hdata_size; | |||
bytes += hdata_size; | |||
} | |||
} | |||
void ed2k_final(struct ed2k_ctx *ctx, unsigned char *out_hash) | |||
{ | |||
struct md4_ctx *md_ctx; | |||
if (ctx->byte_count < ED2K_CHUNK_SIZE) { | |||
/* File has only 1 chunk, so return the md4 hash of that chunk */ | |||
md_ctx = &ctx->chunk_md4_ctx; | |||
} else { | |||
/* Else hash the md4 hashes, and return that hash */ | |||
ed2k_hash_chunk(ctx); /* Hash the last partial chunk here */ | |||
md_ctx = &ctx->hash_md4_ctx; | |||
} | |||
md4_final(md_ctx, out_hash); | |||
} | |||
@@ -0,0 +1,18 @@ | |||
#ifndef _ED2K_H | |||
#define _ED2K_H | |||
#include <stdint.h> | |||
#include "md4.h" | |||
#define ED2K_HASH_SIZE MD4_DIGEST_SIZE | |||
struct ed2k_ctx { | |||
struct md4_ctx hash_md4_ctx, chunk_md4_ctx; | |||
uint64_t byte_count; | |||
}; | |||
void ed2k_init(struct ed2k_ctx *ctx); | |||
void ed2k_update(struct ed2k_ctx *ctx, const void *data, size_t data_len); | |||
void ed2k_final(struct ed2k_ctx *ctx, unsigned char *out_hash); | |||
#endif /* _ED2K_H */ |
@@ -0,0 +1,93 @@ | |||
#define _XOPEN_SOURCE 500 | |||
#include <sys/stat.h> | |||
#include <string.h> | |||
#include <errno.h> | |||
#include <ftw.h> | |||
#include "ed2k.h" | |||
#include "ed2k_util.h" | |||
#include "uio.h" | |||
static struct ed2k_util_opts l_opts; | |||
static enum error ed2k_util_hash(const char *file_path, blksize_t blksize, | |||
const struct stat *st) | |||
{ | |||
unsigned char buf[blksize], hash[ED2K_HASH_SIZE]; | |||
struct ed2k_ctx ed2k; | |||
FILE *f; | |||
size_t read_len; | |||
if (l_opts.pre_hash_fn) { | |||
enum error err = l_opts.pre_hash_fn(file_path, st, l_opts.data); | |||
if (err == ED2KUTIL_DONTHASH) | |||
return NOERR; | |||
else if (err != NOERR) | |||
return err; | |||
} | |||
f = fopen(file_path, "rb"); | |||
if (!f) { | |||
uio_error("Failed to open file: %s (%s)", file_path, strerror(errno)); | |||
return ERR_ED2KUTIL_FS; | |||
} | |||
ed2k_init(&ed2k); | |||
read_len = fread(buf, 1, sizeof(buf), f); | |||
while (read_len > 0) { | |||
ed2k_update(&ed2k, buf, read_len); | |||
read_len = fread(buf, 1, sizeof(buf), f); | |||
} | |||
// TODO check if eof or error | |||
ed2k_final(&ed2k, hash); | |||
fclose(f); | |||
if (l_opts.post_hash_fn) | |||
return l_opts.post_hash_fn(file_path, hash, st, l_opts.data); | |||
return NOERR; | |||
} | |||
static int ed2k_util_walk(const char *fpath, const struct stat *sb, | |||
int typeflag, struct FTW *ftwbuf) | |||
{ | |||
if (typeflag == FTW_DNR) { | |||
uio_error("Cannot read directory '%s'. Skipping", fpath); | |||
return NOERR; | |||
} | |||
if (typeflag == FTW_D) | |||
return NOERR; | |||
if (typeflag != FTW_F) { | |||
uio_error("Unhandled error '%d'", typeflag); | |||
return ERR_ED2KUTIL_UNSUP; | |||
} | |||
return ed2k_util_hash(fpath, sb->st_blksize, sb); | |||
} | |||
enum error ed2k_util_iterpath(const char *path, const struct ed2k_util_opts *opts) | |||
{ | |||
struct stat ts; | |||
if (stat(path, &ts) != 0) { | |||
uio_error("Stat failed for path: '%s' (%s)", | |||
path, strerror(errno)); | |||
return ERR_ED2KUTIL_FS; | |||
} | |||
l_opts = *opts; | |||
if (S_ISREG(ts.st_mode)) { | |||
return ed2k_util_hash(path, ts.st_blksize, &ts); | |||
} else if (S_ISDIR(ts.st_mode)) { | |||
int ftwret = nftw(path, ed2k_util_walk, 20, 0); | |||
if (ftwret == -1) { | |||
uio_error("nftw failure"); | |||
return ERR_ED2KUTIL_FS; | |||
} | |||
return ftwret; | |||
} | |||
uio_error("Unsupported file type: %d", ts.st_mode & S_IFMT); | |||
return ERR_ED2KUTIL_UNSUP; | |||
} |
@@ -0,0 +1,33 @@ | |||
#ifndef _ED2K_UTIL_H | |||
#define _ED2K_UTIL_H | |||
#include <sys/stat.h> | |||
#include <stdint.h> | |||
#include "error.h" | |||
typedef enum error (*ed2k_util_fn)(const char *path, const uint8_t *hash, | |||
const struct stat *st, void *data); | |||
/* | |||
* If this returns ED2KUTIL_DONTHASH, then skip the hashing, | |||
* and the post_hash function | |||
*/ | |||
typedef enum error (*ed2k_util_prehash_fn)(const char *path, | |||
const struct stat *st, void *data); | |||
struct ed2k_util_opts { | |||
ed2k_util_fn post_hash_fn; | |||
ed2k_util_prehash_fn pre_hash_fn; | |||
void *data; | |||
}; | |||
/* | |||
* Given a path (file or directory) calculate the ed2k | |||
* hash for the file(s), and call opts.post_hash_fn if not NULL | |||
* if opts.pre_hash_fn is not NULL, then also call that before the hashing | |||
* | |||
* If fn returns any error, the iteration will stop, and this | |||
* function will return with that error code. | |||
*/ | |||
enum error ed2k_util_iterpath(const char *path, const struct ed2k_util_opts *opts); | |||
#endif /* _ED2K_UTIL_H */ |
@@ -0,0 +1,12 @@ | |||
#include "error.h" | |||
static const char *error_string[] = { | |||
FE_ERROR(GEN_STRING) | |||
}; | |||
const char *error_to_string(enum error err) | |||
{ | |||
if (err >= _ERR_COUNT) | |||
return "ERR_UNKNOWN"; | |||
return error_string[err]; | |||
} |
@@ -0,0 +1,63 @@ | |||
#ifndef _ERROR_H | |||
#define _ERROR_H | |||
#define FE_ERROR(E) \ | |||
E(NOERR = 0) \ | |||
E(ERR_UNKNOWN) \ | |||
E(ERR_NOTFOUND) \ | |||
\ | |||
E(ERR_OPT_REQUIRED) \ | |||
E(ERR_OPT_FAILED) \ | |||
E(ERR_OPT_UNHANDLED) \ | |||
E(ERR_OPT_INVVAL) \ | |||
E(ERR_OPT_EXIT) /* We should exit in main, if config_parse returns this */ \ | |||
E(ERR_OPT_UNSET) /* In config_get, if the value isn't set */ \ | |||
E(ERR_OPT_NOTFOUND) /* In config_get, if the options is not found */ \ | |||
\ | |||
E(ERR_NET_APIADDR) /* If there are problems with the api servers address */ \ | |||
E(ERR_NET_SOCKET) /* If there are problems with the udp socket */ \ | |||
E(ERR_NET_CONNECTED) /* Socket already connected */ \ | |||
E(ERR_NET_CONNECT_FAIL) /* Connect attempt failed */ \ | |||
E(ERR_NET_NOT_CONNECTED) /* Socket wasn't connected */ \ | |||
\ | |||
E(ERR_CMD_FAILED) /* Running the command failed */ \ | |||
E(ERR_CMD_NONE) /* No command was run */ \ | |||
E(ERR_CMD_ARG) /* Some problem with the command arguments */ \ | |||
\ | |||
E(ERR_ED2KUTIL_FS) /* Some filesystem problem */ \ | |||
E(ERR_ED2KUTIL_UNSUP) /* Operation or file type is unsupported */ \ | |||
E(ED2KUTIL_DONTHASH) /* Skip the hashing part. pre_hash_fn can return this */ \ | |||
\ | |||
E(ERR_API_ENCRYPTFAIL) /* Cannot start encryption with the api */ \ | |||
E(ERR_API_COMMFAIL) /* Communication failure */ \ | |||
E(ERR_API_RESP_INVALID) /* Invalid response */ \ | |||
E(ERR_API_AUTH_FAIL) /* Auth failed */ \ | |||
E(ERR_API_LOGOUT) /* Logout failed */ \ | |||
E(ERR_API_PRINTFFUNC) /* New printf function registration failed */ \ | |||
E(ERR_API_CLOCK) /* Some error with clocks */ \ | |||
\ | |||
E(ERR_CACHE_SQLITE) /* Generic sqlite error code */ \ | |||
E(ERR_CACHE_EXISTS) /* Entry already exists, as determined by lid */ \ | |||
/* The entry to be added is not unique, (filename and size duplicate, not hash or lid) */ \ | |||
E(ERR_CACHE_NON_UNIQUE) \ | |||
E(ERR_CACHE_NO_EXISTS) /* Entry does not exists */ \ | |||
\ | |||
E(ERR_THRD) /* Generic pthread error */ \ | |||
\ | |||
E(ERR_LIBEVENT) /* There are some problem with a libevent function */ \ | |||
E(_ERR_COUNT) \ | |||
#define GEN_ENUM(ENUM) ENUM, | |||
#define GEN_STRING(STRING) #STRING, | |||
enum error { | |||
FE_ERROR(GEN_ENUM) | |||
}; | |||
/* | |||
* Convert a number (0) to the enum name (NOERR) | |||
*/ | |||
const char *error_to_string(enum error err); | |||
#endif /* _ERROR_H */ |
@@ -0,0 +1,265 @@ | |||
#include <arpa/inet.h> | |||
#include <sys/socket.h> | |||
#include <sys/time.h> | |||
#include <netdb.h> | |||
#include <stdlib.h> | |||
#include <string.h> | |||
#include <limits.h> | |||
#include <ctype.h> | |||
#include <assert.h> | |||
#include <unistd.h> | |||
#include <stdbool.h> | |||
#include <errno.h> | |||
#include "net.h" | |||
#include "config.h" | |||
#include "uio.h" | |||
static struct addrinfo net_server; | |||
static bool net_server_set = false; | |||
static bool net_connected = false; | |||
static int net_socket = -1; | |||
static bool net_parse_address(const char* srv, size_t *out_domain_len, | |||
char **out_port_start) | |||
{ | |||
char *port_iter; | |||
char *port_start = strchr(srv, ':'); | |||
if (!port_start) { | |||
*out_port_start = NULL; | |||
*out_domain_len = strlen(srv); | |||
return true; | |||
} | |||
/* Only one ':' is allowed */ | |||
if (strchr(port_start + 1, ':')) | |||
return false; | |||
*out_domain_len = port_start - srv; | |||
/*port = strtol(port_start + 1, &port_end, 10); | |||
if (port_end == port_start || *port_end != '\0' || | |||
((port == LONG_MIN || port == LONG_MAX) && errno)) | |||
return false;*/ | |||
/*if (port <= 0 || port > 65535) | |||
return false;*/ | |||
port_iter = port_start + 1; | |||
if (*port_iter == '\0') | |||
return false; | |||
while (*port_iter) { | |||
if (!isdigit(*port_iter)) | |||
return false; | |||
port_iter++; | |||
} | |||
*out_port_start = port_start + 1; | |||
return true; | |||
} | |||
static const void *net_get_sockaddr_addr(const struct sockaddr *sa) | |||
{ | |||
switch (sa->sa_family) { | |||
case AF_INET: | |||
return &((struct sockaddr_in*)sa)->sin_addr; | |||
case AF_INET6: | |||
return &((struct sockaddr_in6*)sa)->sin6_addr; | |||
default: | |||
uio_error("Sockaddr is not ipv4 or ipv6"); | |||
exit(1); | |||
} | |||
} | |||
static bool net_lookup_server(const char *domain, const char *port, | |||
struct addrinfo *out_addr) | |||
{ | |||
struct addrinfo hints = { | |||
.ai_family = AF_UNSPEC, | |||
//.ai_family = AF_INET6, | |||
.ai_socktype = SOCK_DGRAM, | |||
.ai_flags = AI_NUMERICSERV | AI_ADDRCONFIG, | |||
//.ai_flags = AI_NUMERICSERV, | |||
}, *res = NULL, *curr_res; | |||
int ret = getaddrinfo(domain, port, &hints, &res); | |||
if (ret != 0) { | |||
uio_error("Cannot get addrinfo from address: '%s:%s' (%s)", | |||
domain, port, gai_strerror(ret)); | |||
return false; | |||
} | |||
curr_res = res; | |||
while (curr_res) { | |||
char ip_buffer[INET6_ADDRSTRLEN] = {0}; | |||
if (inet_ntop(curr_res->ai_family, | |||
net_get_sockaddr_addr(curr_res->ai_addr), | |||
ip_buffer, sizeof(ip_buffer))) | |||
uio_debug("Lookup addrinfo entry: %s", ip_buffer); | |||
else | |||
uio_debug("Cannot convert binary ip to string: %s", | |||
strerror(errno)); | |||
/* For now, always choose the first one */ | |||
break; | |||
curr_res = curr_res->ai_next; | |||
} | |||
if (!curr_res) { | |||
uio_error("Cannot select a usable address."); | |||
freeaddrinfo(res); | |||
return false; | |||
} | |||
*out_addr = *curr_res; | |||
out_addr->ai_addr = malloc(sizeof(struct sockaddr)); | |||
*out_addr->ai_addr = *curr_res->ai_addr; | |||
out_addr->ai_next = NULL; | |||
out_addr->ai_canonname = NULL; | |||
freeaddrinfo(res); | |||
return true; | |||
} | |||
int net_socket_setup() | |||
{ | |||
struct sockaddr l_addr = {0}; | |||
socklen_t l_addr_len; | |||
int sock; | |||
uint16_t *port; | |||
enum error err; | |||
if ((err = config_get("port", (void**)&port)) != NOERR) { | |||
uio_error("Cannot get UDP binding port from config (%s)", | |||
error_to_string(err)); | |||
return -1; | |||
} | |||
sock = socket(net_server.ai_family, net_server.ai_socktype, | |||
net_server.ai_protocol); | |||
if (sock == -1) { | |||
uio_error("Cannot create new socket: %s", strerror(errno)); | |||
return -1; | |||
} | |||
l_addr.sa_family = net_server.ai_family; | |||
if (net_server.ai_family == AF_INET) { | |||
struct sockaddr_in *tmp = (struct sockaddr_in*)&l_addr; | |||
l_addr_len = sizeof(struct sockaddr_in); | |||
tmp->sin_port = htons(*port); | |||
tmp->sin_addr.s_addr = INADDR_ANY; | |||
} else { | |||
struct sockaddr_in6 *tmp = (struct sockaddr_in6*)&l_addr; | |||
l_addr_len = sizeof(struct sockaddr_in6); | |||
tmp->sin6_port = htons(*port); | |||
tmp->sin6_addr = in6addr_any; | |||
} | |||
if (bind(sock, &l_addr, l_addr_len) != 0) { | |||
uio_error("Cannot bind UDP socket to local port: %s", strerror(errno)); | |||
close(sock); | |||
return -1; | |||
} | |||
return sock; | |||
} | |||
static enum error net_connect(int sock, struct addrinfo *ai) | |||
{ | |||
if (net_connected) | |||
return ERR_NET_CONNECTED; | |||
if (connect(sock, ai->ai_addr, ai->ai_addrlen) != 0) { | |||
uio_error("Cannot connect to the server: %s\n", strerror(errno)); | |||
return ERR_NET_CONNECT_FAIL; | |||
} | |||
net_connected = true; | |||
return NOERR; | |||
} | |||
enum error net_init() | |||
{ | |||
enum error err; | |||
const char **srv = NULL; | |||
char *port_start = NULL; | |||
size_t domain_len; | |||
int sock; | |||
err = config_get("api-server", (void**)&srv); | |||
if (err != NOERR) { | |||
uio_error("Cannot get the api servers address (%s).", error_to_string(err)); | |||
return ERR_NET_APIADDR; | |||
} | |||
if (!net_parse_address(*srv, &domain_len, &port_start)) { | |||
uio_error("Cannot parse the api server address: '%s'.", *srv); | |||
return ERR_NET_APIADDR; | |||
} | |||
/* Port will be set to NULL, if its not in the address */ | |||
if (port_start == NULL) | |||
port_start = "9000"; | |||
char api_domain[domain_len + 1]; | |||
memcpy(api_domain, *srv, domain_len); | |||
api_domain[domain_len] = '\0'; | |||
if (!net_lookup_server(api_domain, port_start, &net_server)) { | |||
//uio_error("Cannot look up the api server address"); | |||
return ERR_NET_APIADDR; | |||
} | |||
net_server_set = true; | |||
sock = net_socket_setup(); | |||
if (sock == -1) { | |||
return ERR_NET_SOCKET; | |||
} | |||
err = net_connect(sock, &net_server); | |||
if (err != NOERR) { | |||
net_free(); | |||
return err; | |||
} | |||
net_socket = sock; | |||
return NOERR; | |||
} | |||
void net_free() | |||
{ | |||
if (net_server_set) { | |||
free(net_server.ai_addr); | |||
memset(&net_server, 0, sizeof(net_server)); | |||
net_server_set = false; | |||
} | |||
if (net_socket != -1) { | |||
if (net_connected) | |||
shutdown(net_socket, SHUT_RDWR); | |||
close(net_socket); | |||
net_socket = -1; | |||
} | |||
net_connected = false; | |||
} | |||
ssize_t net_send(const void *msg, size_t msg_len) | |||
{ | |||
ssize_t w_len = send(net_socket, msg, msg_len, 0); | |||
if (w_len == -1) { | |||
uio_error("{net} Send failed: %s", strerror(errno)); | |||
return -1; | |||
} | |||
return w_len; | |||
} | |||
ssize_t net_read(void* out_data, size_t read_size) | |||
{ | |||
ssize_t read = recv(net_socket, out_data, read_size, 0); | |||
if (read == -1) { | |||
uio_error("{net} Read failed: %s", strerror(errno)); | |||
return -1; | |||
} | |||
return read; | |||
} |
@@ -0,0 +1,24 @@ | |||
#ifndef _NET_H | |||
#define _NET_H | |||
#include <stdint.h> | |||
#include <sys/types.h> | |||
#include "error.h" | |||
/* | |||
* Initializes the net class | |||
*/ | |||
enum error net_init(); | |||
/* | |||
* Send and read data to and from the api | |||
*/ | |||
ssize_t net_send(const void *msg, size_t msg_len); | |||
ssize_t net_read(void *out_data, size_t read_size); | |||
/* | |||
* Frees the net class | |||
*/ | |||
void net_free(); | |||
#endif /* _NET_H */ |
@@ -0,0 +1,58 @@ | |||
#include <stdarg.h> | |||
#include <stdio.h> | |||
#include "uio.h" | |||
#include "config.h" | |||
void uio_user(const char *format, ...) | |||
{ | |||
va_list ap; | |||
va_start(ap, format); | |||
vprintf(format, ap); | |||
printf("\n"); | |||
va_end(ap); | |||
} | |||
void uio_error(const char *format, ...) | |||
{ | |||
va_list ap; | |||
va_start(ap, format); | |||
printf("\033[31m[ERROR]: "); | |||
vprintf(format, ap); | |||
printf("\033[0m\n"); | |||
va_end(ap); | |||
} | |||
void uio_debug(const char *format, ...) | |||
{ | |||
bool *dbg_enabled; | |||
va_list ap; | |||
config_get("debug", (void**)&dbg_enabled); | |||
if (!*dbg_enabled) | |||
return; | |||
va_start(ap, format); | |||
printf("\033[35m[DEBUG]: "); | |||
vprintf(format, ap); | |||
printf("\033[0m\n"); | |||
va_end(ap); | |||
} | |||
void uio_warning(const char *format, ...) | |||
{ | |||
va_list ap; | |||
va_start(ap, format); | |||
printf("\033[33m[WARNING]: "); | |||
vprintf(format, ap); | |||
printf("\033[0m\n"); | |||
va_end(ap); | |||
} |
@@ -0,0 +1,9 @@ | |||
#ifndef _UIO_H | |||
#define _UIO_H | |||
void uio_user(const char *format, ...) __attribute__((format (printf, 1, 2))); | |||
void uio_error(const char *format, ...) __attribute__((format (printf, 1, 2))); | |||
void uio_debug(const char *format, ...) __attribute__((format (printf, 1, 2))); | |||
void uio_warning(const char *format, ...) __attribute__((format (printf, 1, 2))); | |||
#endif /* _UIO_H */ |
@@ -0,0 +1,39 @@ | |||
#include <stdlib.h> | |||
#include <string.h> | |||
#include "util.h" | |||
void util_byte2hex(const uint8_t* bytes, size_t bytes_len, | |||
bool uppercase, char* out) | |||
{ | |||
const char* hex = (uppercase) ? "0123456789ABCDEF" : "0123456789abcdef"; | |||
for (size_t i = 0; i < bytes_len; i++) { | |||
*out++ = hex[bytes[i] >> 4]; | |||
*out++ = hex[bytes[i] & 0xF]; | |||
} | |||
*out = '\0'; | |||
} | |||
const char *util_get_home() | |||
{ | |||
const char *home_env = getenv("HOME"); | |||
return home_env; /* TODO this can be null, use other methods as fallback */ | |||
} | |||
char *util_basename(const char *fullpath) | |||
{ | |||
char *name_part = strrchr(fullpath, '/'); | |||
if (name_part) | |||
name_part++; | |||
else | |||
name_part = (char*)fullpath; | |||
return name_part; | |||
} | |||
uint64_t util_timespec_diff(const struct timespec *past, | |||
const struct timespec *future) | |||
{ | |||
int64_t sdiff = future->tv_sec - past->tv_sec; | |||
int64_t nsdiff = future->tv_nsec - past->tv_nsec; | |||
return sdiff * 1000 + (nsdiff / 1000000); | |||
} |
@@ -0,0 +1,35 @@ | |||
#ifndef _UTIL_H | |||
#define _UTIL_H | |||
#include <stdint.h> | |||
#include <stddef.h> | |||
#include <stdbool.h> | |||
/* | |||
* Convert bytes to a hex string | |||
* out needs to be at least (bytes_len * 2 + 1) bytes | |||
*/ | |||
void util_byte2hex(const uint8_t* bytes, size_t bytes_len, | |||
bool uppercase, char* out); | |||
/* | |||
* Return the user's home directory | |||
*/ | |||
const char *util_get_home(); | |||
/* | |||
* Return the filename part of the path | |||
* This will return a pointer in fullpath | |||
* !! ONLY WORKS FOR FILES !! | |||
*/ | |||
char *util_basename(const char *fullpath); | |||
/* | |||
* Calculate the difference between 2 timespec structs in miliseconds | |||
* | |||
* future cannot be more in the past than past | |||
* if that makes any sense | |||
*/ | |||
uint64_t util_timespec_diff(const struct timespec *past, | |||
const struct timespec *future); | |||
#endif /* _UTIL_H */ |
@@ -0,0 +1 @@ | |||
Subproject commit 6771029ebea612a1a53822af6867b6cf172c31f0 |
@@ -0,0 +1 @@ | |||
Subproject commit 1525b0db1fb608afed8948f3a972d55486e8cb31 |
@@ -0,0 +1 @@ | |||
Subproject commit f06ac37fc31dfdaca2e0d9bec83f90d5663c319b |