Path: blob/master/dep/rcheevos/src/rapi/rc_api_common.c
4806 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/* assume the title contains the most appropriate message to display to the user */276if (rc_json_find_substring(&iterator, "<title>")) {277const char* title_start = iterator.json + 7;278if (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, return the first line of the response (up to 200 characters) */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;319if (strcmp(server_error_code, "invalid_parameter") == 0)320return RC_INVALID_STATE;321break;322323case 'm':324if (strcmp(server_error_code, "missing_parameter") == 0)325return RC_INVALID_STATE;326break;327328case 'n':329if (strcmp(server_error_code, "not_found") == 0)330return RC_NOT_FOUND;331break;332333default:334break;335}336337return RC_API_FAILURE;338}339340int 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) {341int result;342343#ifndef NDEBUG344if (field_count < 2)345return RC_INVALID_STATE;346if (strcmp(fields[0].name, "Success") != 0)347return RC_INVALID_STATE;348if (strcmp(fields[1].name, "Error") != 0)349return RC_INVALID_STATE;350#endif351352response->error_message = NULL;353354if (!server_response) {355response->succeeded = 0;356return RC_NO_RESPONSE;357}358359if (server_response->http_status_code == RC_API_SERVER_RESPONSE_CLIENT_ERROR ||360server_response->http_status_code == RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR) {361/* client provided error message is passed as the response body */362response->error_message = server_response->body;363response->succeeded = 0;364return RC_NO_RESPONSE;365}366367if (!server_response->body || !*server_response->body) {368/* expect valid HTTP status codes to have bodies that we can extract the message from,369* but provide some default messages in case they don't. */370switch (server_response->http_status_code) {371case 504: /* 504 Gateway Timeout */372case 522: /* 522 Connection Timed Out */373case 524: /* 524 A Timeout Occurred */374response->error_message = "Request has timed out.";375break;376377case 521: /* 521 Web Server is Down */378case 523: /* 523 Origin is Unreachable */379response->error_message = "Could not connect to server.";380break;381382default:383break;384}385386response->succeeded = 0;387return RC_NO_RESPONSE;388}389390if (*server_response->body != '{') {391result = rc_json_extract_html_error(response, server_response);392}393else {394rc_json_iterator_t iterator;395memset(&iterator, 0, sizeof(iterator));396iterator.json = server_response->body;397iterator.end = server_response->body + server_response->body_length;398result = rc_json_parse_object(&iterator, fields, field_count, NULL);399400rc_json_get_optional_string(&response->error_message, response, &fields[1], "Error", NULL);401rc_json_get_optional_bool(&response->succeeded, &fields[0], "Success", 1);402403/* Code will be the third field in the fields array, but may not always be present */404if (field_count > 2 && strcmp(fields[2].name, "Code") == 0) {405rc_json_get_optional_string(&response->error_code, response, &fields[2], "Code", NULL);406if (response->error_code != NULL)407result = rc_json_convert_error_code(response->error_code);408}409}410411return result;412}413414static int rc_json_missing_field(rc_api_response_t* response, const rc_json_field_t* field) {415const char* not_found = " not found in response";416const size_t not_found_len = strlen(not_found);417const size_t field_len = strlen(field->name);418419uint8_t* write = rc_buffer_reserve(&response->buffer, field_len + not_found_len + 1);420if (write) {421response->error_message = (char*)write;422memcpy(write, field->name, field_len);423write += field_len;424memcpy(write, not_found, not_found_len + 1);425write += not_found_len + 1;426rc_buffer_consume(&response->buffer, (uint8_t*)response->error_message, write);427}428429response->succeeded = 0;430return 0;431}432433int 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) {434rc_json_iterator_t iterator;435436#ifndef NDEBUG437if (strcmp(field->name, field_name) != 0)438return 0;439#else440(void)field_name;441#endif442443if (!field->value_start)444return rc_json_missing_field(response, field);445446memset(&iterator, 0, sizeof(iterator));447iterator.json = field->value_start;448iterator.end = field->value_end;449return (rc_json_parse_object(&iterator, fields, field_count, &field->array_size) == RC_OK);450}451452static int rc_json_get_array_entry_value(rc_json_field_t* field, rc_json_iterator_t* iterator) {453rc_json_skip_whitespace(iterator);454455if (iterator->json >= iterator->end)456return 0;457458if (rc_json_parse_field(iterator, field) != RC_OK)459return 0;460461rc_json_skip_whitespace(iterator);462463if (!rc_json_match_char(iterator, ','))464rc_json_match_char(iterator, ']');465466return 1;467}468469int 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) {470rc_json_iterator_t iterator;471rc_json_field_t array;472rc_json_field_t value;473uint32_t* entry;474475memset(&array, 0, sizeof(array));476if (!rc_json_get_required_array(num_entries, &array, response, field, field_name))477return RC_MISSING_VALUE;478479if (*num_entries) {480*entries = (uint32_t*)rc_buffer_alloc(&response->buffer, *num_entries * sizeof(uint32_t));481if (!*entries)482return RC_OUT_OF_MEMORY;483484value.name = field_name;485486memset(&iterator, 0, sizeof(iterator));487iterator.json = array.value_start;488iterator.end = array.value_end;489490entry = *entries;491while (rc_json_get_array_entry_value(&value, &iterator)) {492if (!rc_json_get_unum(entry, &value, field_name))493return RC_MISSING_VALUE;494495++entry;496}497}498else {499*entries = NULL;500}501502return RC_OK;503}504505int 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) {506#ifndef NDEBUG507if (strcmp(field->name, field_name) != 0)508return 0;509#endif510511if (!rc_json_get_optional_array(num_entries, array_field, field, field_name))512return rc_json_missing_field(response, field);513514return 1;515}516517int 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) {518#ifndef NDEBUG519if (strcmp(field->name, field_name) != 0)520return 0;521#else522(void)field_name;523#endif524525if (!field->value_start || *field->value_start != '[') {526*num_entries = 0;527return 0;528}529530memcpy(array_field, field, sizeof(*array_field));531++array_field->value_start; /* skip [ */532533*num_entries = field->array_size;534return 1;535}536537int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_iterator_t* iterator) {538rc_json_skip_whitespace(iterator);539540if (iterator->json >= iterator->end)541return 0;542543if (rc_json_parse_object(iterator, fields, field_count, NULL) != RC_OK)544return 0;545546rc_json_skip_whitespace(iterator);547548if (!rc_json_match_char(iterator, ','))549rc_json_match_char(iterator, ']');550551return 1;552}553554static uint32_t rc_json_decode_hex4(const char* input) {555char hex[5];556557memcpy(hex, input, 4);558hex[4] = '\0';559560return (uint32_t)strtoul(hex, NULL, 16);561}562563static int rc_json_ucs32_to_utf8(uint8_t* dst, uint32_t ucs32_char) {564if (ucs32_char < 0x80) {565dst[0] = (ucs32_char & 0x7F);566return 1;567}568569if (ucs32_char < 0x0800) {570dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;571dst[0] = 0xC0 | (ucs32_char & 0x1F);572return 2;573}574575if (ucs32_char < 0x010000) {576dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;577dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;578dst[0] = 0xE0 | (ucs32_char & 0x0F);579return 3;580}581582if (ucs32_char < 0x200000) {583dst[3] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;584dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;585dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;586dst[0] = 0xF0 | (ucs32_char & 0x07);587return 4;588}589590if (ucs32_char < 0x04000000) {591dst[4] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;592dst[3] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;593dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;594dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;595dst[0] = 0xF8 | (ucs32_char & 0x03);596return 5;597}598599dst[5] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;600dst[4] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;601dst[3] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;602dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;603dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;604dst[0] = 0xFC | (ucs32_char & 0x01);605return 6;606}607608int rc_json_get_string(const char** out, rc_buffer_t* buffer, const rc_json_field_t* field, const char* field_name) {609const char* src = field->value_start;610size_t len = field->value_end - field->value_start;611char* dst;612613#ifndef NDEBUG614if (strcmp(field->name, field_name) != 0)615return 0;616#else617(void)field_name;618#endif619620if (!src) {621*out = NULL;622return 0;623}624625if (len == 4 && memcmp(field->value_start, "null", 4) == 0) {626*out = NULL;627return 1;628}629630if (*src == '\"') {631++src;632633if (*src == '\"') {634/* simple optimization for empty string - don't allocate space */635*out = "";636return 1;637}638639*out = dst = (char*)rc_buffer_reserve(buffer, len - 1); /* -2 for quotes, +1 for null terminator */640641do {642if (*src == '\\') {643++src;644if (*src == 'n') {645/* newline */646++src;647*dst++ = '\n';648continue;649}650651if (*src == 'r') {652/* carriage return */653++src;654*dst++ = '\r';655continue;656}657658if (*src == 'u') {659/* unicode character */660uint32_t ucs32_char = rc_json_decode_hex4(src + 1);661src += 5;662663if (ucs32_char >= 0xD800 && ucs32_char < 0xE000) {664/* surrogate lead - look for surrogate tail */665if (ucs32_char < 0xDC00 && src[0] == '\\' && src[1] == 'u') {666const uint32_t surrogate = rc_json_decode_hex4(src + 2);667src += 6;668669if (surrogate >= 0xDC00 && surrogate < 0xE000) {670/* found a surrogate tail, merge them */671ucs32_char = (((ucs32_char - 0xD800) << 10) | (surrogate - 0xDC00)) + 0x10000;672}673}674675if (!(ucs32_char & 0xFFFF0000)) {676/* invalid surrogate pair, fallback to replacement char */677ucs32_char = 0xFFFD;678}679}680681dst += rc_json_ucs32_to_utf8((unsigned char*)dst, ucs32_char);682continue;683}684685if (*src == 't') {686/* tab */687++src;688*dst++ = '\t';689continue;690}691692/* just an escaped character, fallthrough to normal copy */693}694695*dst++ = *src++;696} while (*src != '\"');697698} else {699*out = dst = (char*)rc_buffer_reserve(buffer, len + 1); /* +1 for null terminator */700memcpy(dst, src, len);701dst += len;702}703704*dst++ = '\0';705rc_buffer_consume(buffer, (uint8_t*)(*out), (uint8_t*)dst);706return 1;707}708709int rc_json_field_string_matches(const rc_json_field_t* field, const char* text) {710int is_quoted = 0;711const char* ptr = field->value_start;712if (!ptr)713return 0;714715if (*ptr == '"') {716is_quoted = 1;717++ptr;718}719720while (ptr < field->value_end) {721if (*ptr != *text) {722if (*ptr != '\\') {723if (*ptr == '"' && is_quoted && (*text == '\0')) {724is_quoted = 0;725++ptr;726continue;727}728729return 0;730}731732++ptr;733switch (*ptr) {734case 'n':735if (*text != '\n')736return 0;737break;738case 'r':739if (*text != '\r')740return 0;741break;742case 't':743if (*text != '\t')744return 0;745break;746default:747if (*text != *ptr)748return 0;749break;750}751}752753++text;754++ptr;755}756757return !is_quoted && (*text == '\0');758}759760void 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) {761if (!rc_json_get_string(out, &response->buffer, field, field_name))762*out = default_value;763}764765int rc_json_get_required_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) {766if (rc_json_get_string(out, &response->buffer, field, field_name))767return 1;768769return rc_json_missing_field(response, field);770}771772int rc_json_get_num(int32_t* out, const rc_json_field_t* field, const char* field_name) {773const char* src = field->value_start;774int32_t value = 0;775int negative = 0;776777#ifndef NDEBUG778if (strcmp(field->name, field_name) != 0)779return 0;780#else781(void)field_name;782#endif783784if (!src) {785*out = 0;786return 0;787}788789/* assert: string contains only numerals and an optional sign per rc_json_parse_field */790if (*src == '-') {791negative = 1;792++src;793} else if (*src == '+') {794++src;795} else if (*src < '0' || *src > '9') {796*out = 0;797return 0;798}799800while (src < field->value_end && *src != '.') {801value *= 10;802value += *src - '0';803++src;804}805806if (negative)807*out = -value;808else809*out = value;810811return 1;812}813814void rc_json_get_optional_num(int32_t* out, const rc_json_field_t* field, const char* field_name, int default_value) {815if (!rc_json_get_num(out, field, field_name))816*out = default_value;817}818819int rc_json_get_required_num(int32_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) {820if (rc_json_get_num(out, field, field_name))821return 1;822823return rc_json_missing_field(response, field);824}825826int rc_json_get_unum(uint32_t* out, const rc_json_field_t* field, const char* field_name) {827const char* src = field->value_start;828uint32_t value = 0;829830#ifndef NDEBUG831if (strcmp(field->name, field_name) != 0)832return 0;833#else834(void)field_name;835#endif836837if (!src) {838*out = 0;839return 0;840}841842if (*src < '0' || *src > '9') {843*out = 0;844return 0;845}846847/* assert: string contains only numerals per rc_json_parse_field */848while (src < field->value_end && *src != '.') {849value *= 10;850value += *src - '0';851++src;852}853854*out = value;855return 1;856}857858void rc_json_get_optional_unum(uint32_t* out, const rc_json_field_t* field, const char* field_name, uint32_t default_value) {859if (!rc_json_get_unum(out, field, field_name))860*out = default_value;861}862863int rc_json_get_required_unum(uint32_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) {864if (rc_json_get_unum(out, field, field_name))865return 1;866867return rc_json_missing_field(response, field);868}869870int rc_json_get_float(float* out, const rc_json_field_t* field, const char* field_name) {871int32_t whole, fraction, fraction_denominator;872const char* decimal = field->value_start;873874if (!decimal) {875*out = 0.0f;876return 0;877}878879if (!rc_json_get_num(&whole, field, field_name))880return 0;881882while (decimal < field->value_end && *decimal != '.')883++decimal;884885fraction = 0;886fraction_denominator = 1;887if (decimal) {888++decimal;889while (decimal < field->value_end && *decimal >= '0' && *decimal <= '9') {890fraction *= 10;891fraction += *decimal - '0';892fraction_denominator *= 10;893++decimal;894}895}896897if (whole < 0)898fraction = -fraction;899900*out = (float)whole + ((float)fraction / (float)fraction_denominator);901return 1;902}903904void rc_json_get_optional_float(float* out, const rc_json_field_t* field, const char* field_name, float default_value) {905if (!rc_json_get_float(out, field, field_name))906*out = default_value;907}908909int rc_json_get_required_float(float* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) {910if (rc_json_get_float(out, field, field_name))911return 1;912913return rc_json_missing_field(response, field);914}915916int rc_json_get_datetime(time_t* out, const rc_json_field_t* field, const char* field_name) {917struct tm tm;918919#ifndef NDEBUG920if (strcmp(field->name, field_name) != 0)921return 0;922#else923(void)field_name;924#endif925926if (*field->value_start == '\"') {927memset(&tm, 0, sizeof(tm));928if (sscanf_s(field->value_start + 1, "%d-%d-%d %d:%d:%d", /* DB format "2013-10-20 22:12:21" */929&tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6 ||930/* NOTE: relies on sscanf stopping when it sees a non-digit after the seconds. could be 'Z', '.', '+', or '-' */931sscanf_s(field->value_start + 1, "%d-%d-%dT%d:%d:%d", /* ISO format "2013-10-20T22:12:21.000000Z */932&tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6) {933tm.tm_mon--; /* 0-based */934tm.tm_year -= 1900; /* 1900 based */935936/* mktime converts a struct tm to a time_t using the local timezone.937* the input string is UTC. since timegm is not universally cross-platform,938* figure out the offset between UTC and local time by applying the939* timezone conversion twice and manually removing the difference */940{941time_t local_timet = mktime(&tm);942time_t skewed_timet, tz_offset;943struct tm gmt_tm;944gmtime_s(&gmt_tm, &local_timet);945skewed_timet = mktime(&gmt_tm); /* applies local time adjustment second time */946tz_offset = skewed_timet - local_timet;947*out = local_timet - tz_offset;948}949950return 1;951}952}953954*out = 0;955return 0;956}957958int rc_json_get_required_datetime(time_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) {959if (rc_json_get_datetime(out, field, field_name))960return 1;961962return rc_json_missing_field(response, field);963}964965int rc_json_get_bool(int* out, const rc_json_field_t* field, const char* field_name) {966const char* src = field->value_start;967968#ifndef NDEBUG969if (strcmp(field->name, field_name) != 0)970return 0;971#else972(void)field_name;973#endif974975if (src) {976const size_t len = field->value_end - field->value_start;977if (len == 4 && strncasecmp(src, "true", 4) == 0) {978*out = 1;979return 1;980} else if (len == 5 && strncasecmp(src, "false", 5) == 0) {981*out = 0;982return 1;983} else if (len == 1) {984*out = (*src != '0');985return 1;986}987}988989*out = 0;990return 0;991}992993void rc_json_get_optional_bool(int* out, const rc_json_field_t* field, const char* field_name, int default_value) {994if (!rc_json_get_bool(out, field, field_name))995*out = default_value;996}997998int rc_json_get_required_bool(int* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) {999if (rc_json_get_bool(out, field, field_name))1000return 1;10011002return rc_json_missing_field(response, field);1003}10041005void rc_json_extract_filename(rc_json_field_t* field) {1006if (field->value_end) {1007const char* str = field->value_end;10081009/* remove the extension */1010while (str > field->value_start && str[-1] != '/') {1011--str;1012if (*str == '.') {1013field->value_end = str;1014break;1015}1016}10171018/* find the path separator */1019while (str > field->value_start && str[-1] != '/')1020--str;10211022field->value_start = str;1023}1024}10251026/* --- rc_api_request --- */10271028void rc_api_destroy_request(rc_api_request_t* request)1029{1030rc_buffer_destroy(&request->buffer);1031}10321033/* --- rc_url_builder --- */10341035void rc_url_builder_init(rc_api_url_builder_t* builder, rc_buffer_t* buffer, size_t estimated_size) {1036rc_buffer_chunk_t* used_buffer;10371038memset(builder, 0, sizeof(*builder));1039builder->buffer = buffer;1040builder->write = builder->start = (char*)rc_buffer_reserve(buffer, estimated_size);10411042used_buffer = &buffer->chunk;1043while (used_buffer && used_buffer->write != (uint8_t*)builder->write)1044used_buffer = used_buffer->next;10451046builder->end = (used_buffer) ? (char*)used_buffer->end : builder->start + estimated_size;1047}10481049const char* rc_url_builder_finalize(rc_api_url_builder_t* builder) {1050rc_url_builder_append(builder, "", 1);10511052if (builder->result != RC_OK)1053return NULL;10541055rc_buffer_consume(builder->buffer, (uint8_t*)builder->start, (uint8_t*)builder->write);1056return builder->start;1057}10581059static int rc_url_builder_reserve(rc_api_url_builder_t* builder, size_t amount) {1060if (builder->result == RC_OK) {1061size_t remaining = builder->end - builder->write;1062if (remaining < amount) {1063const size_t used = builder->write - builder->start;1064const size_t current_size = builder->end - builder->start;1065const size_t buffer_prefix_size = sizeof(rc_buffer_chunk_t);1066char* new_start;1067size_t new_size = (current_size < 256) ? 256 : current_size * 2;1068do {1069remaining = new_size - used;1070if (remaining >= amount)1071break;10721073new_size *= 2;1074} while (1);10751076/* rc_buffer_reserve will align to 256 bytes after including the buffer prefix. attempt to account for that */1077if ((remaining - amount) > buffer_prefix_size)1078new_size -= buffer_prefix_size;10791080new_start = (char*)rc_buffer_reserve(builder->buffer, new_size);1081if (!new_start) {1082builder->result = RC_OUT_OF_MEMORY;1083return RC_OUT_OF_MEMORY;1084}10851086if (new_start != builder->start) {1087memcpy(new_start, builder->start, used);1088builder->start = new_start;1089builder->write = new_start + used;1090}10911092builder->end = builder->start + new_size;1093}1094}10951096return builder->result;1097}10981099void rc_url_builder_append_encoded_str(rc_api_url_builder_t* builder, const char* str) {1100static const char hex[] = "0123456789abcdef";1101const char* start = str;1102size_t len = 0;1103for (;;) {1104const char c = *str++;1105switch (c) {1106case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j':1107case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't':1108case 'u': case 'v': case 'w': case 'x': case 'y': case 'z':1109case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': case 'H': case 'I': case 'J':1110case 'K': case 'L': case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T':1111case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z':1112case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9':1113case '-': case '_': case '.': case '~':1114len++;1115continue;11161117case '\0':1118if (len)1119rc_url_builder_append(builder, start, len);11201121return;11221123default:1124if (rc_url_builder_reserve(builder, len + 3) != RC_OK)1125return;11261127if (len) {1128memcpy(builder->write, start, len);1129builder->write += len;1130}11311132if (c == ' ') {1133*builder->write++ = '+';1134} else {1135*builder->write++ = '%';1136*builder->write++ = hex[((unsigned char)c) >> 4];1137*builder->write++ = hex[c & 0x0F];1138}1139break;1140}11411142start = str;1143len = 0;1144}1145}11461147void rc_url_builder_append(rc_api_url_builder_t* builder, const char* data, size_t len) {1148if (rc_url_builder_reserve(builder, len) == RC_OK) {1149memcpy(builder->write, data, len);1150builder->write += len;1151}1152}11531154static int rc_url_builder_append_param_equals(rc_api_url_builder_t* builder, const char* param) {1155size_t param_len = strlen(param);11561157if (rc_url_builder_reserve(builder, param_len + 2) == RC_OK) {1158if (builder->write > builder->start) {1159if (builder->write[-1] != '?')1160*builder->write++ = '&';1161}11621163memcpy(builder->write, param, param_len);1164builder->write += param_len;1165*builder->write++ = '=';1166}11671168return builder->result;1169}11701171void rc_url_builder_append_unum_param(rc_api_url_builder_t* builder, const char* param, uint32_t value) {1172if (rc_url_builder_append_param_equals(builder, param) == RC_OK) {1173char num[16];1174int chars = snprintf(num, sizeof(num), "%u", value);1175rc_url_builder_append(builder, num, chars);1176}1177}11781179void rc_url_builder_append_num_param(rc_api_url_builder_t* builder, const char* param, int32_t value) {1180if (rc_url_builder_append_param_equals(builder, param) == RC_OK) {1181char num[16];1182int chars = snprintf(num, sizeof(num), "%d", value);1183rc_url_builder_append(builder, num, chars);1184}1185}11861187void rc_url_builder_append_str_param(rc_api_url_builder_t* builder, const char* param, const char* value) {1188rc_url_builder_append_param_equals(builder, param);1189rc_url_builder_append_encoded_str(builder, value);1190}11911192void rc_api_url_build_dorequest_url(rc_api_request_t* request, const rc_api_host_t* host) {1193#define DOREQUEST_ENDPOINT "/dorequest.php"1194rc_buffer_init(&request->buffer);11951196if (!host || !host->host) {1197request->url = RETROACHIEVEMENTS_HOST DOREQUEST_ENDPOINT;1198}1199else {1200const size_t endpoint_len = sizeof(DOREQUEST_ENDPOINT);1201const size_t host_len = strlen(host->host);1202const size_t protocol_len = (strstr(host->host, "://")) ? 0 : 7;1203const size_t url_len = protocol_len + host_len + endpoint_len;1204uint8_t* url = rc_buffer_reserve(&request->buffer, url_len);12051206if (protocol_len)1207memcpy(url, "http://", protocol_len);12081209memcpy(url + protocol_len, host->host, host_len);1210memcpy(url + protocol_len + host_len, DOREQUEST_ENDPOINT, endpoint_len);1211rc_buffer_consume(&request->buffer, url, url + url_len);12121213request->url = (char*)url;1214}1215#undef DOREQUEST_ENDPOINT1216}12171218int rc_api_url_build_dorequest(rc_api_url_builder_t* builder, const char* api, const char* username, const char* api_token) {1219if (!username || !*username || !api_token || !*api_token) {1220builder->result = RC_INVALID_STATE;1221return 0;1222}12231224rc_url_builder_append_str_param(builder, "r", api);1225rc_url_builder_append_str_param(builder, "u", username);1226rc_url_builder_append_str_param(builder, "t", api_token);12271228return (builder->result == RC_OK);1229}12301231/* --- Set Host --- */12321233static void rc_api_update_host(const char** host, const char* hostname) {1234if (*host != NULL)1235free((void*)*host);12361237if (hostname != NULL) {1238if (strstr(hostname, "://")) {1239*host = strdup(hostname);1240}1241else {1242const size_t hostname_len = strlen(hostname);1243if (hostname_len == 0) {1244*host = NULL;1245}1246else {1247char* newhost = (char*)malloc(hostname_len + 7 + 1);1248if (newhost) {1249memcpy(newhost, "http://", 7);1250memcpy(&newhost[7], hostname, hostname_len + 1);1251*host = newhost;1252}1253else {1254*host = NULL;1255}1256}1257}1258}1259else {1260*host = NULL;1261}1262}12631264const char* rc_api_default_host(void) {1265return RETROACHIEVEMENTS_HOST;1266}12671268void rc_api_set_host(const char* hostname) {1269if (hostname && strcmp(hostname, RETROACHIEVEMENTS_HOST) == 0)1270hostname = NULL;12711272rc_api_update_host(&g_host.host, hostname);12731274if (!hostname) {1275/* also clear out the image hostname */1276rc_api_set_image_host(NULL);1277}1278else if (strcmp(hostname, RETROACHIEVEMENTS_HOST_NONSSL) == 0) {1279/* if just pointing at the non-HTTPS host, explicitly use the default image host1280* so it doesn't try to use the web host directly */1281rc_api_set_image_host(RETROACHIEVEMENTS_IMAGE_HOST_NONSSL);1282}1283}12841285void rc_api_set_image_host(const char* hostname) {1286rc_api_update_host(&g_host.media_host, hostname);1287}12881289/* --- Fetch Image --- */12901291int rc_api_init_fetch_image_request(rc_api_request_t* request, const rc_api_fetch_image_request_t* api_params) {1292return rc_api_init_fetch_image_request_hosted(request, api_params, &g_host);1293}12941295int 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) {1296rc_api_url_builder_t builder;12971298rc_buffer_init(&request->buffer);1299rc_url_builder_init(&builder, &request->buffer, 64);13001301if (host && host->media_host) {1302/* custom media host provided */1303if (!strstr(host->host, "://"))1304rc_url_builder_append(&builder, "http://", 7);1305rc_url_builder_append(&builder, host->media_host, strlen(host->media_host));1306}1307else if (host && host->host) {1308if (strcmp(host->host, RETROACHIEVEMENTS_HOST_NONSSL) == 0) {1309/* if host specifically set to non-ssl host, and no media host provided, use non-ssl media host */1310rc_url_builder_append(&builder, RETROACHIEVEMENTS_IMAGE_HOST_NONSSL, sizeof(RETROACHIEVEMENTS_IMAGE_HOST_NONSSL) - 1);1311}1312else if (strcmp(host->host, RETROACHIEVEMENTS_HOST) == 0) {1313/* if host specifically set to ssl host, and no media host provided, use media host */1314rc_url_builder_append(&builder, RETROACHIEVEMENTS_IMAGE_HOST, sizeof(RETROACHIEVEMENTS_IMAGE_HOST) - 1);1315}1316else {1317/* custom host and no media host provided. assume custom host is also media host */1318if (!strstr(host->host, "://"))1319rc_url_builder_append(&builder, "http://", 7);1320rc_url_builder_append(&builder, host->host, strlen(host->host));1321}1322}1323else {1324/* no custom host provided */1325rc_url_builder_append(&builder, RETROACHIEVEMENTS_IMAGE_HOST, sizeof(RETROACHIEVEMENTS_IMAGE_HOST) - 1);1326}13271328switch (api_params->image_type)1329{1330case RC_IMAGE_TYPE_GAME:1331rc_url_builder_append(&builder, "/Images/", 8);1332rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name));1333rc_url_builder_append(&builder, ".png", 4);1334break;13351336case RC_IMAGE_TYPE_ACHIEVEMENT:1337rc_url_builder_append(&builder, "/Badge/", 7);1338rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name));1339rc_url_builder_append(&builder, ".png", 4);1340break;13411342case RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED:1343rc_url_builder_append(&builder, "/Badge/", 7);1344rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name));1345rc_url_builder_append(&builder, "_lock.png", 9);1346break;13471348case RC_IMAGE_TYPE_USER:1349rc_url_builder_append(&builder, "/UserPic/", 9);1350rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name));1351rc_url_builder_append(&builder, ".png", 4);1352break;13531354default:1355return RC_INVALID_STATE;1356}13571358request->url = rc_url_builder_finalize(&builder);1359request->post_data = NULL;13601361return builder.result;1362}13631364const char* rc_api_build_avatar_url(rc_buffer_t* buffer, uint32_t image_type, const char* image_name) {1365rc_api_fetch_image_request_t image_request;1366rc_api_request_t request;1367int result;13681369memset(&image_request, 0, sizeof(image_request));1370image_request.image_type = image_type;1371image_request.image_name = image_name;13721373result = rc_api_init_fetch_image_request(&request, &image_request);1374if (result == RC_OK)1375return rc_buffer_strcpy(buffer, request.url);13761377return NULL;1378}137913801381