Path: blob/master/dep/rcheevos/src/rapi/rc_api_common.c
4253 views
#include "rc_api_common.h"1#include "rc_api_request.h"2#include "rc_api_runtime.h"34#include "../rc_compat.h"56#include <ctype.h>7#include <stdio.h>8#include <stdlib.h>9#include <string.h>1011#define RETROACHIEVEMENTS_HOST "https://retroachievements.org"12#define RETROACHIEVEMENTS_IMAGE_HOST "https://media.retroachievements.org"13#define RETROACHIEVEMENTS_HOST_NONSSL "http://retroachievements.org"14#define RETROACHIEVEMENTS_IMAGE_HOST_NONSSL "http://media.retroachievements.org"15rc_api_host_t g_host = { NULL, NULL };1617/* --- rc_json --- */1819static int rc_json_parse_object(rc_json_iterator_t* iterator, rc_json_field_t* fields, size_t field_count, uint32_t* fields_seen);20static int rc_json_parse_array(rc_json_iterator_t* iterator, rc_json_field_t* field);2122static int rc_json_match_char(rc_json_iterator_t* iterator, char c)23{24if (iterator->json < iterator->end && *iterator->json == c) {25++iterator->json;26return 1;27}2829return 0;30}3132static void rc_json_skip_whitespace(rc_json_iterator_t* iterator)33{34while (iterator->json < iterator->end && isspace((unsigned char)*iterator->json))35++iterator->json;36}3738static int rc_json_find_substring(rc_json_iterator_t* iterator, const char* substring)39{40const char first = *substring;41const size_t substring_len = strlen(substring);42const char* end = iterator->end - substring_len;4344while (iterator->json <= end) {45if (*iterator->json == first) {46if (memcmp(iterator->json, substring, substring_len) == 0)47return 1;48}4950++iterator->json;51}5253return 0;54}5556static int rc_json_find_closing_quote(rc_json_iterator_t* iterator)57{58while (iterator->json < iterator->end) {59if (*iterator->json == '"')60return 1;6162if (*iterator->json == '\\') {63++iterator->json;64if (iterator->json == iterator->end)65return 0;66}6768if (*iterator->json == '\0')69return 0;7071++iterator->json;72}7374return 0;75}7677static int rc_json_parse_field(rc_json_iterator_t* iterator, rc_json_field_t* field) {78int result;7980if (iterator->json >= iterator->end)81return RC_INVALID_JSON;8283field->value_start = iterator->json;8485switch (*iterator->json)86{87case '"': /* quoted string */88++iterator->json;89if (!rc_json_find_closing_quote(iterator))90return RC_INVALID_JSON;91++iterator->json;92break;9394case '-':95case '+': /* signed number */96++iterator->json;97/* fallthrough to number */98case '0': case '1': case '2': case '3': case '4':99case '5': case '6': case '7': case '8': case '9': /* number */100while (iterator->json < iterator->end && *iterator->json >= '0' && *iterator->json <= '9')101++iterator->json;102103if (rc_json_match_char(iterator, '.')) {104while (iterator->json < iterator->end && *iterator->json >= '0' && *iterator->json <= '9')105++iterator->json;106}107break;108109case '[': /* array */110result = rc_json_parse_array(iterator, field);111if (result != RC_OK)112return result;113114break;115116case '{': /* object */117result = rc_json_parse_object(iterator, NULL, 0, &field->array_size);118if (result != RC_OK)119return result;120121break;122123default: /* non-quoted text [true,false,null] */124if (!isalpha((unsigned char)*iterator->json))125return RC_INVALID_JSON;126127while (iterator->json < iterator->end && isalnum((unsigned char)*iterator->json))128++iterator->json;129break;130}131132field->value_end = iterator->json;133return RC_OK;134}135136static int rc_json_parse_array(rc_json_iterator_t* iterator, rc_json_field_t* field) {137rc_json_field_t unused_field;138int result;139140if (!rc_json_match_char(iterator, '['))141return RC_INVALID_JSON;142143field->array_size = 0;144145if (rc_json_match_char(iterator, ']')) /* empty array */146return RC_OK;147148do149{150rc_json_skip_whitespace(iterator);151152result = rc_json_parse_field(iterator, &unused_field);153if (result != RC_OK)154return result;155156++field->array_size;157158rc_json_skip_whitespace(iterator);159} while (rc_json_match_char(iterator, ','));160161if (!rc_json_match_char(iterator, ']'))162return RC_INVALID_JSON;163164return RC_OK;165}166167static int rc_json_get_next_field(rc_json_iterator_t* iterator, rc_json_field_t* field) {168rc_json_skip_whitespace(iterator);169170if (!rc_json_match_char(iterator, '"'))171return RC_INVALID_JSON;172173field->name = iterator->json;174while (iterator->json < iterator->end && *iterator->json != '"') {175if (!*iterator->json)176return RC_INVALID_JSON;177++iterator->json;178}179180if (iterator->json == iterator->end)181return RC_INVALID_JSON;182183field->name_len = iterator->json - field->name;184++iterator->json;185186rc_json_skip_whitespace(iterator);187188if (!rc_json_match_char(iterator, ':'))189return RC_INVALID_JSON;190191rc_json_skip_whitespace(iterator);192193if (rc_json_parse_field(iterator, field) < 0)194return RC_INVALID_JSON;195196rc_json_skip_whitespace(iterator);197198return RC_OK;199}200201static int rc_json_parse_object(rc_json_iterator_t* iterator, rc_json_field_t* fields, size_t field_count, uint32_t* fields_seen) {202size_t i;203uint32_t num_fields = 0;204rc_json_field_t field;205int result;206207if (fields_seen)208*fields_seen = 0;209210for (i = 0; i < field_count; ++i)211fields[i].value_start = fields[i].value_end = NULL;212213if (!rc_json_match_char(iterator, '{'))214return RC_INVALID_JSON;215216if (rc_json_match_char(iterator, '}')) /* empty object */217return RC_OK;218219do220{221result = rc_json_get_next_field(iterator, &field);222if (result != RC_OK)223return result;224225for (i = 0; i < field_count; ++i) {226if (!fields[i].value_start && fields[i].name_len == field.name_len &&227memcmp(fields[i].name, field.name, field.name_len) == 0) {228fields[i].value_start = field.value_start;229fields[i].value_end = field.value_end;230fields[i].array_size = field.array_size;231break;232}233}234235++num_fields;236237} while (rc_json_match_char(iterator, ','));238239if (!rc_json_match_char(iterator, '}'))240return RC_INVALID_JSON;241242if (fields_seen)243*fields_seen = num_fields;244245return RC_OK;246}247248int rc_json_get_next_object_field(rc_json_iterator_t* iterator, rc_json_field_t* field) {249if (!rc_json_match_char(iterator, ',') && !rc_json_match_char(iterator, '{'))250return 0;251252return (rc_json_get_next_field(iterator, field) == RC_OK);253}254255int rc_json_get_object_string_length(const char* json) {256rc_json_iterator_t iterator;257memset(&iterator, 0, sizeof(iterator));258iterator.json = json;259iterator.end = json + (1024 * 1024 * 1024); /* arbitrary 1GB limit on JSON response */260261rc_json_parse_object(&iterator, NULL, 0, NULL);262263if (iterator.json == json) /* not JSON */264return (int)strlen(json);265266return (int)(iterator.json - json);267}268269static int rc_json_extract_html_error(rc_api_response_t* response, const rc_api_server_response_t* server_response) {270rc_json_iterator_t iterator;271memset(&iterator, 0, sizeof(iterator));272iterator.json = server_response->body;273iterator.end = server_response->body + server_response->body_length;274275/* if the title contains an HTTP status code(i.e "404 Not Found"), return the title */276if (rc_json_find_substring(&iterator, "<title>")) {277const char* title_start = iterator.json + 7;278if (isdigit((int)*title_start) && rc_json_find_substring(&iterator, "</title>")) {279response->error_message = rc_buffer_strncpy(&response->buffer, title_start, iterator.json - title_start);280response->succeeded = 0;281return RC_INVALID_JSON;282}283}284285/* title not found, or did not start with an error code, return the first line of the response */286iterator.json = server_response->body;287288while (iterator.json < iterator.end && *iterator.json != '\n' &&289iterator.json - server_response->body < 200) {290++iterator.json;291}292293if (iterator.json > server_response->body && iterator.json[-1] == '\r')294--iterator.json;295296if (iterator.json > server_response->body)297response->error_message = rc_buffer_strncpy(&response->buffer, server_response->body, iterator.json - server_response->body);298299response->succeeded = 0;300return RC_INVALID_JSON;301}302303static int rc_json_convert_error_code(const char* server_error_code)304{305switch (server_error_code[0]) {306case 'a':307if (strcmp(server_error_code, "access_denied") == 0)308return RC_ACCESS_DENIED;309break;310311case 'e':312if (strcmp(server_error_code, "expired_token") == 0)313return RC_EXPIRED_TOKEN;314break;315316case 'i':317if (strcmp(server_error_code, "invalid_credentials") == 0)318return RC_INVALID_CREDENTIALS;319break;320321case 'n':322if (strcmp(server_error_code, "not_found") == 0)323return RC_NOT_FOUND;324break;325326default:327break;328}329330return RC_API_FAILURE;331}332333int rc_json_parse_server_response(rc_api_response_t* response, const rc_api_server_response_t* server_response, rc_json_field_t* fields, size_t field_count) {334int result;335336#ifndef NDEBUG337if (field_count < 2)338return RC_INVALID_STATE;339if (strcmp(fields[0].name, "Success") != 0)340return RC_INVALID_STATE;341if (strcmp(fields[1].name, "Error") != 0)342return RC_INVALID_STATE;343#endif344345response->error_message = NULL;346347if (!server_response) {348response->succeeded = 0;349return RC_NO_RESPONSE;350}351352if (server_response->http_status_code == RC_API_SERVER_RESPONSE_CLIENT_ERROR ||353server_response->http_status_code == RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR) {354/* client provided error message is passed as the response body */355response->error_message = server_response->body;356response->succeeded = 0;357return RC_NO_RESPONSE;358}359360if (!server_response->body || !*server_response->body) {361/* expect valid HTTP status codes to have bodies that we can extract the message from,362* but provide some default messages in case they don't. */363switch (server_response->http_status_code) {364case 504: /* 504 Gateway Timeout */365case 522: /* 522 Connection Timed Out */366case 524: /* 524 A Timeout Occurred */367response->error_message = "Request has timed out.";368break;369370case 521: /* 521 Web Server is Down */371case 523: /* 523 Origin is Unreachable */372response->error_message = "Could not connect to server.";373break;374375default:376break;377}378379response->succeeded = 0;380return RC_NO_RESPONSE;381}382383if (*server_response->body != '{') {384result = rc_json_extract_html_error(response, server_response);385}386else {387rc_json_iterator_t iterator;388memset(&iterator, 0, sizeof(iterator));389iterator.json = server_response->body;390iterator.end = server_response->body + server_response->body_length;391result = rc_json_parse_object(&iterator, fields, field_count, NULL);392393rc_json_get_optional_string(&response->error_message, response, &fields[1], "Error", NULL);394rc_json_get_optional_bool(&response->succeeded, &fields[0], "Success", 1);395396/* Code will be the third field in the fields array, but may not always be present */397if (field_count > 2 && strcmp(fields[2].name, "Code") == 0) {398rc_json_get_optional_string(&response->error_code, response, &fields[2], "Code", NULL);399if (response->error_code != NULL)400result = rc_json_convert_error_code(response->error_code);401}402}403404return result;405}406407static int rc_json_missing_field(rc_api_response_t* response, const rc_json_field_t* field) {408const char* not_found = " not found in response";409const size_t not_found_len = strlen(not_found);410const size_t field_len = strlen(field->name);411412uint8_t* write = rc_buffer_reserve(&response->buffer, field_len + not_found_len + 1);413if (write) {414response->error_message = (char*)write;415memcpy(write, field->name, field_len);416write += field_len;417memcpy(write, not_found, not_found_len + 1);418write += not_found_len + 1;419rc_buffer_consume(&response->buffer, (uint8_t*)response->error_message, write);420}421422response->succeeded = 0;423return 0;424}425426int rc_json_get_required_object(rc_json_field_t* fields, size_t field_count, rc_api_response_t* response, rc_json_field_t* field, const char* field_name) {427rc_json_iterator_t iterator;428429#ifndef NDEBUG430if (strcmp(field->name, field_name) != 0)431return 0;432#else433(void)field_name;434#endif435436if (!field->value_start)437return rc_json_missing_field(response, field);438439memset(&iterator, 0, sizeof(iterator));440iterator.json = field->value_start;441iterator.end = field->value_end;442return (rc_json_parse_object(&iterator, fields, field_count, &field->array_size) == RC_OK);443}444445static int rc_json_get_array_entry_value(rc_json_field_t* field, rc_json_iterator_t* iterator) {446rc_json_skip_whitespace(iterator);447448if (iterator->json >= iterator->end)449return 0;450451if (rc_json_parse_field(iterator, field) != RC_OK)452return 0;453454rc_json_skip_whitespace(iterator);455456if (!rc_json_match_char(iterator, ','))457rc_json_match_char(iterator, ']');458459return 1;460}461462int rc_json_get_required_unum_array(uint32_t** entries, uint32_t* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) {463rc_json_iterator_t iterator;464rc_json_field_t array;465rc_json_field_t value;466uint32_t* entry;467468memset(&array, 0, sizeof(array));469if (!rc_json_get_required_array(num_entries, &array, response, field, field_name))470return RC_MISSING_VALUE;471472if (*num_entries) {473*entries = (uint32_t*)rc_buffer_alloc(&response->buffer, *num_entries * sizeof(uint32_t));474if (!*entries)475return RC_OUT_OF_MEMORY;476477value.name = field_name;478479memset(&iterator, 0, sizeof(iterator));480iterator.json = array.value_start;481iterator.end = array.value_end;482483entry = *entries;484while (rc_json_get_array_entry_value(&value, &iterator)) {485if (!rc_json_get_unum(entry, &value, field_name))486return RC_MISSING_VALUE;487488++entry;489}490}491else {492*entries = NULL;493}494495return RC_OK;496}497498int rc_json_get_required_array(uint32_t* num_entries, rc_json_field_t* array_field, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) {499#ifndef NDEBUG500if (strcmp(field->name, field_name) != 0)501return 0;502#endif503504if (!rc_json_get_optional_array(num_entries, array_field, field, field_name))505return rc_json_missing_field(response, field);506507return 1;508}509510int rc_json_get_optional_array(uint32_t* num_entries, rc_json_field_t* array_field, const rc_json_field_t* field, const char* field_name) {511#ifndef NDEBUG512if (strcmp(field->name, field_name) != 0)513return 0;514#else515(void)field_name;516#endif517518if (!field->value_start || *field->value_start != '[') {519*num_entries = 0;520return 0;521}522523memcpy(array_field, field, sizeof(*array_field));524++array_field->value_start; /* skip [ */525526*num_entries = field->array_size;527return 1;528}529530int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_iterator_t* iterator) {531rc_json_skip_whitespace(iterator);532533if (iterator->json >= iterator->end)534return 0;535536if (rc_json_parse_object(iterator, fields, field_count, NULL) != RC_OK)537return 0;538539rc_json_skip_whitespace(iterator);540541if (!rc_json_match_char(iterator, ','))542rc_json_match_char(iterator, ']');543544return 1;545}546547static uint32_t rc_json_decode_hex4(const char* input) {548char hex[5];549550memcpy(hex, input, 4);551hex[4] = '\0';552553return (uint32_t)strtoul(hex, NULL, 16);554}555556static int rc_json_ucs32_to_utf8(uint8_t* dst, uint32_t ucs32_char) {557if (ucs32_char < 0x80) {558dst[0] = (ucs32_char & 0x7F);559return 1;560}561562if (ucs32_char < 0x0800) {563dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;564dst[0] = 0xC0 | (ucs32_char & 0x1F);565return 2;566}567568if (ucs32_char < 0x010000) {569dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;570dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;571dst[0] = 0xE0 | (ucs32_char & 0x0F);572return 3;573}574575if (ucs32_char < 0x200000) {576dst[3] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;577dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;578dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;579dst[0] = 0xF0 | (ucs32_char & 0x07);580return 4;581}582583if (ucs32_char < 0x04000000) {584dst[4] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;585dst[3] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;586dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;587dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;588dst[0] = 0xF8 | (ucs32_char & 0x03);589return 5;590}591592dst[5] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;593dst[4] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;594dst[3] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;595dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;596dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;597dst[0] = 0xFC | (ucs32_char & 0x01);598return 6;599}600601int rc_json_get_string(const char** out, rc_buffer_t* buffer, const rc_json_field_t* field, const char* field_name) {602const char* src = field->value_start;603size_t len = field->value_end - field->value_start;604char* dst;605606#ifndef NDEBUG607if (strcmp(field->name, field_name) != 0)608return 0;609#else610(void)field_name;611#endif612613if (!src) {614*out = NULL;615return 0;616}617618if (len == 4 && memcmp(field->value_start, "null", 4) == 0) {619*out = NULL;620return 1;621}622623if (*src == '\"') {624++src;625626if (*src == '\"') {627/* simple optimization for empty string - don't allocate space */628*out = "";629return 1;630}631632*out = dst = (char*)rc_buffer_reserve(buffer, len - 1); /* -2 for quotes, +1 for null terminator */633634do {635if (*src == '\\') {636++src;637if (*src == 'n') {638/* newline */639++src;640*dst++ = '\n';641continue;642}643644if (*src == 'r') {645/* carriage return */646++src;647*dst++ = '\r';648continue;649}650651if (*src == 'u') {652/* unicode character */653uint32_t ucs32_char = rc_json_decode_hex4(src + 1);654src += 5;655656if (ucs32_char >= 0xD800 && ucs32_char < 0xE000) {657/* surrogate lead - look for surrogate tail */658if (ucs32_char < 0xDC00 && src[0] == '\\' && src[1] == 'u') {659const uint32_t surrogate = rc_json_decode_hex4(src + 2);660src += 6;661662if (surrogate >= 0xDC00 && surrogate < 0xE000) {663/* found a surrogate tail, merge them */664ucs32_char = (((ucs32_char - 0xD800) << 10) | (surrogate - 0xDC00)) + 0x10000;665}666}667668if (!(ucs32_char & 0xFFFF0000)) {669/* invalid surrogate pair, fallback to replacement char */670ucs32_char = 0xFFFD;671}672}673674dst += rc_json_ucs32_to_utf8((unsigned char*)dst, ucs32_char);675continue;676}677678if (*src == 't') {679/* tab */680++src;681*dst++ = '\t';682continue;683}684685/* just an escaped character, fallthrough to normal copy */686}687688*dst++ = *src++;689} while (*src != '\"');690691} else {692*out = dst = (char*)rc_buffer_reserve(buffer, len + 1); /* +1 for null terminator */693memcpy(dst, src, len);694dst += len;695}696697*dst++ = '\0';698rc_buffer_consume(buffer, (uint8_t*)(*out), (uint8_t*)dst);699return 1;700}701702void rc_json_get_optional_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name, const char* default_value) {703if (!rc_json_get_string(out, &response->buffer, field, field_name))704*out = default_value;705}706707int rc_json_get_required_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) {708if (rc_json_get_string(out, &response->buffer, field, field_name))709return 1;710711return rc_json_missing_field(response, field);712}713714int rc_json_get_num(int32_t* out, const rc_json_field_t* field, const char* field_name) {715const char* src = field->value_start;716int32_t value = 0;717int negative = 0;718719#ifndef NDEBUG720if (strcmp(field->name, field_name) != 0)721return 0;722#else723(void)field_name;724#endif725726if (!src) {727*out = 0;728return 0;729}730731/* assert: string contains only numerals and an optional sign per rc_json_parse_field */732if (*src == '-') {733negative = 1;734++src;735} else if (*src == '+') {736++src;737} else if (*src < '0' || *src > '9') {738*out = 0;739return 0;740}741742while (src < field->value_end && *src != '.') {743value *= 10;744value += *src - '0';745++src;746}747748if (negative)749*out = -value;750else751*out = value;752753return 1;754}755756void rc_json_get_optional_num(int32_t* out, const rc_json_field_t* field, const char* field_name, int default_value) {757if (!rc_json_get_num(out, field, field_name))758*out = default_value;759}760761int rc_json_get_required_num(int32_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) {762if (rc_json_get_num(out, field, field_name))763return 1;764765return rc_json_missing_field(response, field);766}767768int rc_json_get_unum(uint32_t* out, const rc_json_field_t* field, const char* field_name) {769const char* src = field->value_start;770uint32_t value = 0;771772#ifndef NDEBUG773if (strcmp(field->name, field_name) != 0)774return 0;775#else776(void)field_name;777#endif778779if (!src) {780*out = 0;781return 0;782}783784if (*src < '0' || *src > '9') {785*out = 0;786return 0;787}788789/* assert: string contains only numerals per rc_json_parse_field */790while (src < field->value_end && *src != '.') {791value *= 10;792value += *src - '0';793++src;794}795796*out = value;797return 1;798}799800void rc_json_get_optional_unum(uint32_t* out, const rc_json_field_t* field, const char* field_name, uint32_t default_value) {801if (!rc_json_get_unum(out, field, field_name))802*out = default_value;803}804805int rc_json_get_required_unum(uint32_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) {806if (rc_json_get_unum(out, field, field_name))807return 1;808809return rc_json_missing_field(response, field);810}811812int rc_json_get_float(float* out, const rc_json_field_t* field, const char* field_name) {813int32_t whole, fraction, fraction_denominator;814const char* decimal = field->value_start;815816if (!decimal) {817*out = 0.0f;818return 0;819}820821if (!rc_json_get_num(&whole, field, field_name))822return 0;823824while (decimal < field->value_end && *decimal != '.')825++decimal;826827fraction = 0;828fraction_denominator = 1;829if (decimal) {830++decimal;831while (decimal < field->value_end && *decimal >= '0' && *decimal <= '9') {832fraction *= 10;833fraction += *decimal - '0';834fraction_denominator *= 10;835++decimal;836}837}838839if (whole < 0)840fraction = -fraction;841842*out = (float)whole + ((float)fraction / (float)fraction_denominator);843return 1;844}845846void rc_json_get_optional_float(float* out, const rc_json_field_t* field, const char* field_name, float default_value) {847if (!rc_json_get_float(out, field, field_name))848*out = default_value;849}850851int rc_json_get_required_float(float* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) {852if (rc_json_get_float(out, field, field_name))853return 1;854855return rc_json_missing_field(response, field);856}857858int rc_json_get_datetime(time_t* out, const rc_json_field_t* field, const char* field_name) {859struct tm tm;860861#ifndef NDEBUG862if (strcmp(field->name, field_name) != 0)863return 0;864#else865(void)field_name;866#endif867868if (*field->value_start == '\"') {869memset(&tm, 0, sizeof(tm));870if (sscanf_s(field->value_start + 1, "%d-%d-%d %d:%d:%d", /* DB format "2013-10-20 22:12:21" */871&tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6 ||872/* NOTE: relies on sscanf stopping when it sees a non-digit after the seconds. could be 'Z', '.', '+', or '-' */873sscanf_s(field->value_start + 1, "%d-%d-%dT%d:%d:%d", /* ISO format "2013-10-20T22:12:21.000000Z */874&tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6) {875tm.tm_mon--; /* 0-based */876tm.tm_year -= 1900; /* 1900 based */877878/* mktime converts a struct tm to a time_t using the local timezone.879* the input string is UTC. since timegm is not universally cross-platform,880* figure out the offset between UTC and local time by applying the881* timezone conversion twice and manually removing the difference */882{883time_t local_timet = mktime(&tm);884time_t skewed_timet, tz_offset;885struct tm gmt_tm;886gmtime_s(&gmt_tm, &local_timet);887skewed_timet = mktime(&gmt_tm); /* applies local time adjustment second time */888tz_offset = skewed_timet - local_timet;889*out = local_timet - tz_offset;890}891892return 1;893}894}895896*out = 0;897return 0;898}899900int rc_json_get_required_datetime(time_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) {901if (rc_json_get_datetime(out, field, field_name))902return 1;903904return rc_json_missing_field(response, field);905}906907int rc_json_get_bool(int* out, const rc_json_field_t* field, const char* field_name) {908const char* src = field->value_start;909910#ifndef NDEBUG911if (strcmp(field->name, field_name) != 0)912return 0;913#else914(void)field_name;915#endif916917if (src) {918const size_t len = field->value_end - field->value_start;919if (len == 4 && strncasecmp(src, "true", 4) == 0) {920*out = 1;921return 1;922} else if (len == 5 && strncasecmp(src, "false", 5) == 0) {923*out = 0;924return 1;925} else if (len == 1) {926*out = (*src != '0');927return 1;928}929}930931*out = 0;932return 0;933}934935void rc_json_get_optional_bool(int* out, const rc_json_field_t* field, const char* field_name, int default_value) {936if (!rc_json_get_bool(out, field, field_name))937*out = default_value;938}939940int rc_json_get_required_bool(int* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) {941if (rc_json_get_bool(out, field, field_name))942return 1;943944return rc_json_missing_field(response, field);945}946947void rc_json_extract_filename(rc_json_field_t* field) {948if (field->value_end) {949const char* str = field->value_end;950951/* remove the extension */952while (str > field->value_start && str[-1] != '/') {953--str;954if (*str == '.') {955field->value_end = str;956break;957}958}959960/* find the path separator */961while (str > field->value_start && str[-1] != '/')962--str;963964field->value_start = str;965}966}967968/* --- rc_api_request --- */969970void rc_api_destroy_request(rc_api_request_t* request)971{972rc_buffer_destroy(&request->buffer);973}974975/* --- rc_url_builder --- */976977void rc_url_builder_init(rc_api_url_builder_t* builder, rc_buffer_t* buffer, size_t estimated_size) {978rc_buffer_chunk_t* used_buffer;979980memset(builder, 0, sizeof(*builder));981builder->buffer = buffer;982builder->write = builder->start = (char*)rc_buffer_reserve(buffer, estimated_size);983984used_buffer = &buffer->chunk;985while (used_buffer && used_buffer->write != (uint8_t*)builder->write)986used_buffer = used_buffer->next;987988builder->end = (used_buffer) ? (char*)used_buffer->end : builder->start + estimated_size;989}990991const char* rc_url_builder_finalize(rc_api_url_builder_t* builder) {992rc_url_builder_append(builder, "", 1);993994if (builder->result != RC_OK)995return NULL;996997rc_buffer_consume(builder->buffer, (uint8_t*)builder->start, (uint8_t*)builder->write);998return builder->start;999}10001001static int rc_url_builder_reserve(rc_api_url_builder_t* builder, size_t amount) {1002if (builder->result == RC_OK) {1003size_t remaining = builder->end - builder->write;1004if (remaining < amount) {1005const size_t used = builder->write - builder->start;1006const size_t current_size = builder->end - builder->start;1007const size_t buffer_prefix_size = sizeof(rc_buffer_chunk_t);1008char* new_start;1009size_t new_size = (current_size < 256) ? 256 : current_size * 2;1010do {1011remaining = new_size - used;1012if (remaining >= amount)1013break;10141015new_size *= 2;1016} while (1);10171018/* rc_buffer_reserve will align to 256 bytes after including the buffer prefix. attempt to account for that */1019if ((remaining - amount) > buffer_prefix_size)1020new_size -= buffer_prefix_size;10211022new_start = (char*)rc_buffer_reserve(builder->buffer, new_size);1023if (!new_start) {1024builder->result = RC_OUT_OF_MEMORY;1025return RC_OUT_OF_MEMORY;1026}10271028if (new_start != builder->start) {1029memcpy(new_start, builder->start, used);1030builder->start = new_start;1031builder->write = new_start + used;1032}10331034builder->end = builder->start + new_size;1035}1036}10371038return builder->result;1039}10401041void rc_url_builder_append_encoded_str(rc_api_url_builder_t* builder, const char* str) {1042static const char hex[] = "0123456789abcdef";1043const char* start = str;1044size_t len = 0;1045for (;;) {1046const char c = *str++;1047switch (c) {1048case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j':1049case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't':1050case 'u': case 'v': case 'w': case 'x': case 'y': case 'z':1051case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': case 'H': case 'I': case 'J':1052case 'K': case 'L': case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T':1053case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z':1054case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9':1055case '-': case '_': case '.': case '~':1056len++;1057continue;10581059case '\0':1060if (len)1061rc_url_builder_append(builder, start, len);10621063return;10641065default:1066if (rc_url_builder_reserve(builder, len + 3) != RC_OK)1067return;10681069if (len) {1070memcpy(builder->write, start, len);1071builder->write += len;1072}10731074if (c == ' ') {1075*builder->write++ = '+';1076} else {1077*builder->write++ = '%';1078*builder->write++ = hex[((unsigned char)c) >> 4];1079*builder->write++ = hex[c & 0x0F];1080}1081break;1082}10831084start = str;1085len = 0;1086}1087}10881089void rc_url_builder_append(rc_api_url_builder_t* builder, const char* data, size_t len) {1090if (rc_url_builder_reserve(builder, len) == RC_OK) {1091memcpy(builder->write, data, len);1092builder->write += len;1093}1094}10951096static int rc_url_builder_append_param_equals(rc_api_url_builder_t* builder, const char* param) {1097size_t param_len = strlen(param);10981099if (rc_url_builder_reserve(builder, param_len + 2) == RC_OK) {1100if (builder->write > builder->start) {1101if (builder->write[-1] != '?')1102*builder->write++ = '&';1103}11041105memcpy(builder->write, param, param_len);1106builder->write += param_len;1107*builder->write++ = '=';1108}11091110return builder->result;1111}11121113void rc_url_builder_append_unum_param(rc_api_url_builder_t* builder, const char* param, uint32_t value) {1114if (rc_url_builder_append_param_equals(builder, param) == RC_OK) {1115char num[16];1116int chars = snprintf(num, sizeof(num), "%u", value);1117rc_url_builder_append(builder, num, chars);1118}1119}11201121void rc_url_builder_append_num_param(rc_api_url_builder_t* builder, const char* param, int32_t value) {1122if (rc_url_builder_append_param_equals(builder, param) == RC_OK) {1123char num[16];1124int chars = snprintf(num, sizeof(num), "%d", value);1125rc_url_builder_append(builder, num, chars);1126}1127}11281129void rc_url_builder_append_str_param(rc_api_url_builder_t* builder, const char* param, const char* value) {1130rc_url_builder_append_param_equals(builder, param);1131rc_url_builder_append_encoded_str(builder, value);1132}11331134void rc_api_url_build_dorequest_url(rc_api_request_t* request, const rc_api_host_t* host) {1135#define DOREQUEST_ENDPOINT "/dorequest.php"1136rc_buffer_init(&request->buffer);11371138if (!host || !host->host) {1139request->url = RETROACHIEVEMENTS_HOST DOREQUEST_ENDPOINT;1140}1141else {1142const size_t endpoint_len = sizeof(DOREQUEST_ENDPOINT);1143const size_t host_len = strlen(host->host);1144const size_t protocol_len = (strstr(host->host, "://")) ? 0 : 7;1145const size_t url_len = protocol_len + host_len + endpoint_len;1146uint8_t* url = rc_buffer_reserve(&request->buffer, url_len);11471148if (protocol_len)1149memcpy(url, "http://", protocol_len);11501151memcpy(url + protocol_len, host->host, host_len);1152memcpy(url + protocol_len + host_len, DOREQUEST_ENDPOINT, endpoint_len);1153rc_buffer_consume(&request->buffer, url, url + url_len);11541155request->url = (char*)url;1156}1157#undef DOREQUEST_ENDPOINT1158}11591160int rc_api_url_build_dorequest(rc_api_url_builder_t* builder, const char* api, const char* username, const char* api_token) {1161if (!username || !*username || !api_token || !*api_token) {1162builder->result = RC_INVALID_STATE;1163return 0;1164}11651166rc_url_builder_append_str_param(builder, "r", api);1167rc_url_builder_append_str_param(builder, "u", username);1168rc_url_builder_append_str_param(builder, "t", api_token);11691170return (builder->result == RC_OK);1171}11721173/* --- Set Host --- */11741175static void rc_api_update_host(const char** host, const char* hostname) {1176if (*host != NULL)1177free((void*)*host);11781179if (hostname != NULL) {1180if (strstr(hostname, "://")) {1181*host = strdup(hostname);1182}1183else {1184const size_t hostname_len = strlen(hostname);1185if (hostname_len == 0) {1186*host = NULL;1187}1188else {1189char* newhost = (char*)malloc(hostname_len + 7 + 1);1190if (newhost) {1191memcpy(newhost, "http://", 7);1192memcpy(&newhost[7], hostname, hostname_len + 1);1193*host = newhost;1194}1195else {1196*host = NULL;1197}1198}1199}1200}1201else {1202*host = NULL;1203}1204}12051206const char* rc_api_default_host(void) {1207return RETROACHIEVEMENTS_HOST;1208}12091210void rc_api_set_host(const char* hostname) {1211if (hostname && strcmp(hostname, RETROACHIEVEMENTS_HOST) == 0)1212hostname = NULL;12131214rc_api_update_host(&g_host.host, hostname);12151216if (!hostname) {1217/* also clear out the image hostname */1218rc_api_set_image_host(NULL);1219}1220else if (strcmp(hostname, RETROACHIEVEMENTS_HOST_NONSSL) == 0) {1221/* if just pointing at the non-HTTPS host, explicitly use the default image host1222* so it doesn't try to use the web host directly */1223rc_api_set_image_host(RETROACHIEVEMENTS_IMAGE_HOST_NONSSL);1224}1225}12261227void rc_api_set_image_host(const char* hostname) {1228rc_api_update_host(&g_host.media_host, hostname);1229}12301231/* --- Fetch Image --- */12321233int rc_api_init_fetch_image_request(rc_api_request_t* request, const rc_api_fetch_image_request_t* api_params) {1234return rc_api_init_fetch_image_request_hosted(request, api_params, &g_host);1235}12361237int rc_api_init_fetch_image_request_hosted(rc_api_request_t* request, const rc_api_fetch_image_request_t* api_params, const rc_api_host_t* host) {1238rc_api_url_builder_t builder;12391240rc_buffer_init(&request->buffer);1241rc_url_builder_init(&builder, &request->buffer, 64);12421243if (host && host->media_host) {1244/* custom media host provided */1245if (!strstr(host->host, "://"))1246rc_url_builder_append(&builder, "http://", 7);1247rc_url_builder_append(&builder, host->media_host, strlen(host->media_host));1248}1249else if (host && host->host) {1250if (strcmp(host->host, RETROACHIEVEMENTS_HOST_NONSSL) == 0) {1251/* if host specifically set to non-ssl host, and no media host provided, use non-ssl media host */1252rc_url_builder_append(&builder, RETROACHIEVEMENTS_IMAGE_HOST_NONSSL, sizeof(RETROACHIEVEMENTS_IMAGE_HOST_NONSSL) - 1);1253}1254else if (strcmp(host->host, RETROACHIEVEMENTS_HOST) == 0) {1255/* if host specifically set to ssl host, and no media host provided, use media host */1256rc_url_builder_append(&builder, RETROACHIEVEMENTS_IMAGE_HOST, sizeof(RETROACHIEVEMENTS_IMAGE_HOST) - 1);1257}1258else {1259/* custom host and no media host provided. assume custom host is also media host */1260if (!strstr(host->host, "://"))1261rc_url_builder_append(&builder, "http://", 7);1262rc_url_builder_append(&builder, host->host, strlen(host->host));1263}1264}1265else {1266/* no custom host provided */1267rc_url_builder_append(&builder, RETROACHIEVEMENTS_IMAGE_HOST, sizeof(RETROACHIEVEMENTS_IMAGE_HOST) - 1);1268}12691270switch (api_params->image_type)1271{1272case RC_IMAGE_TYPE_GAME:1273rc_url_builder_append(&builder, "/Images/", 8);1274rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name));1275rc_url_builder_append(&builder, ".png", 4);1276break;12771278case RC_IMAGE_TYPE_ACHIEVEMENT:1279rc_url_builder_append(&builder, "/Badge/", 7);1280rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name));1281rc_url_builder_append(&builder, ".png", 4);1282break;12831284case RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED:1285rc_url_builder_append(&builder, "/Badge/", 7);1286rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name));1287rc_url_builder_append(&builder, "_lock.png", 9);1288break;12891290case RC_IMAGE_TYPE_USER:1291rc_url_builder_append(&builder, "/UserPic/", 9);1292rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name));1293rc_url_builder_append(&builder, ".png", 4);1294break;12951296default:1297return RC_INVALID_STATE;1298}12991300request->url = rc_url_builder_finalize(&builder);1301request->post_data = NULL;13021303return builder.result;1304}13051306const char* rc_api_build_avatar_url(rc_buffer_t* buffer, uint32_t image_type, const char* image_name) {1307rc_api_fetch_image_request_t image_request;1308rc_api_request_t request;1309int result;13101311memset(&image_request, 0, sizeof(image_request));1312image_request.image_type = image_type;1313image_request.image_name = image_name;13141315result = rc_api_init_fetch_image_request(&request, &image_request);1316if (result == RC_OK)1317return rc_buffer_strcpy(buffer, request.url);13181319return NULL;1320}132113221323