From 61f517d1597f6d009e2144430658393a85a84fcb Mon Sep 17 00:00:00 2001 From: Goldenkrew3000 Date: Sat, 20 Sep 2025 18:01:59 +1000 Subject: [PATCH] Adding base source --- src/build.sh | 20 + src/config.json | 77 + src/configHandler.c | 356 ++ src/configHandler.h | 48 + src/dscrdrpc.c | 270 ++ src/dscrdrpc.h | 34 + src/external/cJSON.c | 3143 +++++++++++++++++ src/external/cJSON.h | 300 ++ src/external/cJSON_Utils.c | 1481 ++++++++ src/external/cJSON_Utils.h | 88 + src/external/libcurl_uriescape.c | 64 + src/external/libcurl_uriescape.h | 14 + src/external/md5.c | 223 ++ src/external/md5.h | 24 + src/libopensubsonic/crypto.c | 100 + src/libopensubsonic/crypto.h | 14 + src/libopensubsonic/endpoint_getAlbum.c | 174 + src/libopensubsonic/endpoint_getAlbum.h | 52 + src/libopensubsonic/endpoint_getAlbumList.c | 134 + src/libopensubsonic/endpoint_getAlbumList.h | 33 + src/libopensubsonic/endpoint_getArtist.c | 163 + src/libopensubsonic/endpoint_getArtist.h | 41 + src/libopensubsonic/endpoint_getArtists.c | 144 + src/libopensubsonic/endpoint_getArtists.h | 22 + .../endpoint_getLyricsBySongId.c | 129 + .../endpoint_getLyricsBySongId.h | 22 + src/libopensubsonic/endpoint_getPlaylist.c | 166 + src/libopensubsonic/endpoint_getPlaylist.h | 49 + src/libopensubsonic/endpoint_getPlaylists.c | 121 + src/libopensubsonic/endpoint_getPlaylists.h | 26 + src/libopensubsonic/endpoint_getSong.c | 137 + src/libopensubsonic/endpoint_getSong.h | 39 + src/libopensubsonic/endpoint_getStarred.c | 260 ++ src/libopensubsonic/endpoint_getStarred.h | 71 + src/libopensubsonic/endpoint_ping.c | 73 + src/libopensubsonic/endpoint_ping.h | 20 + src/libopensubsonic/endpoint_scrobble.c | 58 + src/libopensubsonic/endpoint_scrobble.h | 13 + src/libopensubsonic/httpclient.c | 428 +++ src/libopensubsonic/httpclient.h | 75 + src/libopensubsonic/logger.c | 74 + src/libopensubsonic/logger.h | 11 + src/libopensubsonic/scrobble_lastFm.c | 530 +++ src/libopensubsonic/scrobble_lastFm.h | 9 + src/libopensubsonic/scrobble_listenBrainz.c | 141 + src/libopensubsonic/scrobble_listenBrainz.h | 7 + src/libopensubsonic/utils.c | 106 + src/libopensubsonic/utils.h | 11 + src/main.c | 32 + src/openSubSonic.c | 259 ++ src/runTests.c | 200 ++ 51 files changed, 10086 insertions(+) create mode 100755 src/build.sh create mode 100644 src/config.json create mode 100644 src/configHandler.c create mode 100644 src/configHandler.h create mode 100644 src/dscrdrpc.c create mode 100644 src/dscrdrpc.h create mode 100644 src/external/cJSON.c create mode 100644 src/external/cJSON.h create mode 100644 src/external/cJSON_Utils.c create mode 100644 src/external/cJSON_Utils.h create mode 100644 src/external/libcurl_uriescape.c create mode 100644 src/external/libcurl_uriescape.h create mode 100644 src/external/md5.c create mode 100644 src/external/md5.h create mode 100644 src/libopensubsonic/crypto.c create mode 100644 src/libopensubsonic/crypto.h create mode 100644 src/libopensubsonic/endpoint_getAlbum.c create mode 100644 src/libopensubsonic/endpoint_getAlbum.h create mode 100644 src/libopensubsonic/endpoint_getAlbumList.c create mode 100644 src/libopensubsonic/endpoint_getAlbumList.h create mode 100644 src/libopensubsonic/endpoint_getArtist.c create mode 100644 src/libopensubsonic/endpoint_getArtist.h create mode 100644 src/libopensubsonic/endpoint_getArtists.c create mode 100644 src/libopensubsonic/endpoint_getArtists.h create mode 100644 src/libopensubsonic/endpoint_getLyricsBySongId.c create mode 100644 src/libopensubsonic/endpoint_getLyricsBySongId.h create mode 100644 src/libopensubsonic/endpoint_getPlaylist.c create mode 100644 src/libopensubsonic/endpoint_getPlaylist.h create mode 100644 src/libopensubsonic/endpoint_getPlaylists.c create mode 100644 src/libopensubsonic/endpoint_getPlaylists.h create mode 100644 src/libopensubsonic/endpoint_getSong.c create mode 100644 src/libopensubsonic/endpoint_getSong.h create mode 100644 src/libopensubsonic/endpoint_getStarred.c create mode 100644 src/libopensubsonic/endpoint_getStarred.h create mode 100644 src/libopensubsonic/endpoint_ping.c create mode 100644 src/libopensubsonic/endpoint_ping.h create mode 100644 src/libopensubsonic/endpoint_scrobble.c create mode 100644 src/libopensubsonic/endpoint_scrobble.h create mode 100644 src/libopensubsonic/httpclient.c create mode 100644 src/libopensubsonic/httpclient.h create mode 100644 src/libopensubsonic/logger.c create mode 100644 src/libopensubsonic/logger.h create mode 100644 src/libopensubsonic/scrobble_lastFm.c create mode 100644 src/libopensubsonic/scrobble_lastFm.h create mode 100644 src/libopensubsonic/scrobble_listenBrainz.c create mode 100644 src/libopensubsonic/scrobble_listenBrainz.h create mode 100644 src/libopensubsonic/utils.c create mode 100644 src/libopensubsonic/utils.h create mode 100644 src/main.c create mode 100644 src/openSubSonic.c create mode 100644 src/runTests.c diff --git a/src/build.sh b/src/build.sh new file mode 100755 index 0000000..aabca67 --- /dev/null +++ b/src/build.sh @@ -0,0 +1,20 @@ +egcc main.c \ + configHandler.c \ + libopensubsonic/httpclient.c \ + external/cJSON.c \ + external/md5.c \ + libopensubsonic/utils.c \ + libopensubsonic/logger.c \ + libopensubsonic/crypto.c \ + libopensubsonic/endpoint_ping.c \ + libopensubsonic/endpoint_getAlbum.c \ + libopensubsonic/endpoint_getSong.c \ + libopensubsonic/endpoint_getPlaylists.c \ + libopensubsonic/endpoint_getPlaylist.c \ + libopensubsonic/endpoint_getArtists.c \ + libopensubsonic/endpoint_getArtist.c \ + libopensubsonic/endpoint_getLyricsBySongId.c \ + libopensubsonic/endpoint_getAlbumList.c \ + libopensubsonic/endpoint_getStarred.c \ + libopensubsonic/endpoint_scrobble.c \ + -o main -I/usr/local/include -L/usr/local/lib -lcurl diff --git a/src/config.json b/src/config.json new file mode 100644 index 0000000..4dd9cbc --- /dev/null +++ b/src/config.json @@ -0,0 +1,77 @@ +{ + "opensubsonic_server": { + "protocol": "https", + "server": "server.com", + "username": "", + "password": "" + }, + "scrobbler": { + "listenbrainz_enable": true, + "listenbrainz_token": "", + "lastfm_enable": true, + "lastfm_username": "", + "lastfm_password": "", + "lastfm_api_key": "", + "lastfm_api_secret": "", + "lastfm_session_key": "" + }, + "audio": { + "equalizer": { + "enable": true, + "followPitch": true, + "graph": [ + { + "bandwidth": 0.5, + "frequency": 22, + "gain": 17, + "bypass": false + }, + { + "bandwidth": 0.5, + "frequency": 28, + "gain": 17, + "bypass": false + }, + { + "bandwidth": 0.5, + "frequency": 35, + "gain": 8.6, + "bypass": false + }, + { + "bandwidth": 0.5, + "frequency": 43, + "gain": 5.4, + "bypass": false + }, + { + "bandwidth": 0.5, + "frequency": 53, + "gain": 12.3, + "bypass": false + }, + { + "bandwidth": 0.5, + "frequency": 66, + "gain": 5.1, + "bypass": false + }, + { + "bandwidth": 0.5, + "frequency": 82, + "gain": 0, + "bypass": false + } + ] + }, + "pitch": { + "enable": true, + "cents": -270.0, + "rate": 1.0 + }, + "reverb": { + "enable": true, + "wetDryMix": 15.0 + } + } +} diff --git a/src/configHandler.c b/src/configHandler.c new file mode 100644 index 0000000..e578a6d --- /dev/null +++ b/src/configHandler.c @@ -0,0 +1,356 @@ +#include +#include +#include +#include +#include "configHandler.h" +#include "external/cJSON.h" +#include "libopensubsonic/logger.h" + +#if defined(__APPLE__) && defined(__MACH__) +#include "OSSP_Bridge.h" +#endif // defined(__APPLE__) && defined(__MACH__) + +static int rc = 0; + +/* + * Read a predefined config file into the configuration struct + * Returns 0 on success, 1 on failure + */ +int configHandler_Read(configHandler_config_t** configObj) { + // Allocate config object on heap + *configObj = malloc(sizeof(configHandler_config_t)); + + // Initialize struct variables + (*configObj)->opensubsonic_protocol = NULL; + (*configObj)->opensubsonic_server = NULL; + (*configObj)->opensubsonic_username = NULL; + (*configObj)->opensubsonic_password = NULL; + (*configObj)->internal_opensubsonic_version = NULL; + (*configObj)->internal_opensubsonic_clientName = NULL; + (*configObj)->internal_opensubsonic_loginSalt = NULL; + (*configObj)->internal_opensubsonic_loginToken = NULL; + (*configObj)->listenbrainz_enable = false; + (*configObj)->listenbrainz_token = NULL; + (*configObj)->lastfm_enable = false; + (*configObj)->lastfm_username = NULL; + (*configObj)->lastfm_password = NULL; + (*configObj)->lastfm_api_key = NULL; + (*configObj)->lastfm_api_secret = NULL; + (*configObj)->lastfm_api_session_key = NULL; + (*configObj)->audio_equalizer_enable = false; + (*configObj)->audio_equalizer_followPitch = false; + (*configObj)->audio_equalizer_graphCount = 0; + (*configObj)->audio_equalizer_graph = NULL; + (*configObj)->audio_pitch_enable = false; + (*configObj)->audio_pitch_cents = 0.00; + (*configObj)->audio_pitch_rate = 0.00; + (*configObj)->audio_reverb_enable = false; + (*configObj)->audio_reverb_wetDryMix = 0.00; + + // Set internal configuration values + (*configObj)->internal_opensubsonic_version = strdup("1.8.0"); + (*configObj)->internal_opensubsonic_clientName = strdup("Hojuix_OSSP"); + + // Form the path to the config JSON + char* config_path = NULL; +#if defined(__APPLE__) && defined(__MACH__) + // NOTE: This is a relatively hacky way of fetching the iOS container path without diving into the hell that is ObjC + char* root_path = getenv("HOME"); + rc = asprintf(&config_path, "%s/Documents/config.json", root_path); +#if DEBUG + printf("iOS Container Path: %s\n", config_path); +#endif // DEBUG +#else + rc = asprintf(&config_path, "config.json"); +#endif // defined(__APPLE__) && defined(__MACH__) + if (rc == -1) { + logger_log_error(__func__, "asprintf() failed (Could not generate config path)."); + free(config_path); + return 1; + } + + // Read config file + FILE* config_fd = NULL; + char* config_buf = NULL; + long config_fsize = 0; + long config_fread_rc = 0; // Needs to be separate from 'rc' as fread() returns bytes read + + // Check if the config file exists, and fetch it's size + struct stat st; + if (stat(config_path, &st) == 0) { + config_fsize = st.st_size; + } else { + logger_log_error(__func__, "stat() failed (Config file does not exist)."); + return 1; + } + + // Actually open and read in the contents of the config file + config_fd = fopen(config_path, "rb"); + if (config_fd == NULL) { + logger_log_error(__func__, "fopen() failed (Could not open config file)."); + free(config_path); + return 1; + } + free(config_path); + + config_buf = (char*)malloc(config_fsize + 1); + if (config_buf == NULL) { + logger_log_error(__func__, "malloc() failed (Could not allocate enough memory for the config file)."); + fclose(config_fd); + return 1; + } + + config_fread_rc = fread(config_buf, 1, config_fsize, config_fd); + if (config_fread_rc != config_fsize) { + logger_log_error(__func__, "fread() failed (Could not read the config file)."); + fclose(config_fd); + free(config_buf); + return 1; + } + + // Null terminate the buffer + config_buf[config_fsize] = '\0'; + fclose(config_fd); + + // Parse config JSON + cJSON* root = cJSON_Parse(config_buf); + free(config_buf); + if (root == NULL) { + logger_log_error(__func__, "cJSON_Parse() failed (Could not parse the configuration JSON)."); + return 1; + } + + // Make an object from opensubsonic_server + // TODO - Use the new OSS_P*oj functions? + cJSON* opensubsonic_server_root = cJSON_GetObjectItemCaseSensitive(root, "opensubsonic_server"); + if (opensubsonic_server_root == NULL) { + logger_log_error(__func__, "Error parsing JSON - opensubsonic_server does not exist."); + cJSON_Delete(root); + return 1; + } + + cJSON* opensubsonic_server_protocol = cJSON_GetObjectItemCaseSensitive(opensubsonic_server_root, "protocol"); + if (cJSON_IsString(opensubsonic_server_protocol) && opensubsonic_server_protocol->valuestring != NULL) { + (*configObj)->opensubsonic_protocol = strdup(opensubsonic_server_protocol->valuestring); + } + + cJSON* opensubsonic_server_server = cJSON_GetObjectItemCaseSensitive(opensubsonic_server_root, "server"); + if (cJSON_IsString(opensubsonic_server_server) && opensubsonic_server_server->valuestring != NULL) { + (*configObj)->opensubsonic_server = strdup(opensubsonic_server_server->valuestring); + } + + cJSON* opensubsonic_server_username = cJSON_GetObjectItemCaseSensitive(opensubsonic_server_root, "username"); + if (cJSON_IsString(opensubsonic_server_username) && opensubsonic_server_username->valuestring != NULL) { + (*configObj)->opensubsonic_username = strdup(opensubsonic_server_username->valuestring); + } + + cJSON* opensubsonic_server_password = cJSON_GetObjectItemCaseSensitive(opensubsonic_server_root, "password"); + if (cJSON_IsString(opensubsonic_server_password) && opensubsonic_server_password->valuestring != NULL) { + (*configObj)->opensubsonic_password = strdup(opensubsonic_server_password->valuestring); + } + + // Make an object from scrobbler + cJSON* scrobbler_root = cJSON_GetObjectItemCaseSensitive(root, "scrobbler"); + if (scrobbler_root == NULL) { + logger_log_error(__func__, "Error parsing JSON - scrobbler does not exist."); + cJSON_Delete(root); + return 1; + } + + cJSON* scrobbler_listenbrainz_enable = cJSON_GetObjectItemCaseSensitive(scrobbler_root, "listenbrainz_enable"); + if (cJSON_IsBool(scrobbler_listenbrainz_enable)) { + if (cJSON_IsTrue(scrobbler_listenbrainz_enable)) { + (*configObj)->listenbrainz_enable = true; + } + } + + cJSON* scrobbler_listenbrainz_token = cJSON_GetObjectItemCaseSensitive(scrobbler_root, "listenbrainz_token"); + if (cJSON_IsString(scrobbler_listenbrainz_token) && scrobbler_listenbrainz_token->valuestring != NULL) { + (*configObj)->listenbrainz_token = strdup(scrobbler_listenbrainz_token->valuestring); + } + + cJSON* scrobbler_lastfm_enable = cJSON_GetObjectItemCaseSensitive(scrobbler_root, "lastfm_enable"); + if (cJSON_IsBool(scrobbler_lastfm_enable)) { + if (cJSON_IsTrue(scrobbler_lastfm_enable)) { + (*configObj)->lastfm_enable = true; + } + } + + cJSON* scrobbler_lastfm_username = cJSON_GetObjectItemCaseSensitive(scrobbler_root, "lastfm_username"); + if (cJSON_IsString(scrobbler_lastfm_username) && scrobbler_lastfm_username->valuestring != NULL) { + (*configObj)->lastfm_username = strdup(scrobbler_lastfm_username->valuestring); + } + + cJSON* scrobbler_lastfm_password = cJSON_GetObjectItemCaseSensitive(scrobbler_root, "lastfm_password"); + if (cJSON_IsString(scrobbler_lastfm_password) && scrobbler_lastfm_password->valuestring != NULL) { + (*configObj)->lastfm_password = strdup(scrobbler_lastfm_password->valuestring); + } + + cJSON* scrobbler_lastfm_api_key = cJSON_GetObjectItemCaseSensitive(scrobbler_root, "lastfm_api_key"); + if (cJSON_IsString(scrobbler_lastfm_api_key) && scrobbler_lastfm_api_key->valuestring != NULL) { + (*configObj)->lastfm_api_key = strdup(scrobbler_lastfm_api_key->valuestring); + } + + cJSON* scrobbler_lastfm_api_secret = cJSON_GetObjectItemCaseSensitive(scrobbler_root, "lastfm_api_secret"); + if (cJSON_IsString(scrobbler_lastfm_api_secret) && scrobbler_lastfm_api_secret->valuestring != NULL) { + (*configObj)->lastfm_api_secret = strdup(scrobbler_lastfm_api_secret->valuestring); + } + + cJSON* scrobbler_lastfm_api_session_key = cJSON_GetObjectItemCaseSensitive(scrobbler_root, "lastfm_session_key"); + if (cJSON_IsString(scrobbler_lastfm_api_session_key) && scrobbler_lastfm_api_session_key->valuestring != NULL) { + (*configObj)->lastfm_api_session_key = strdup(scrobbler_lastfm_api_session_key->valuestring); + } + + // Make an object from audio + cJSON* audio_root = cJSON_GetObjectItemCaseSensitive(root, "audio"); + if (audio_root == NULL) { + logger_log_error(__func__, "Error parsing JSON - audio does not exist."); + cJSON_Delete(root); + return 1; + } + + // Make an object from equalizer + cJSON* equalizer_root = cJSON_GetObjectItemCaseSensitive(audio_root, "equalizer"); + if (equalizer_root == NULL) { + logger_log_error(__func__, "Error parsing JSON - equalizer does not exist."); + cJSON_Delete(root); + return 1; + } + + cJSON* audio_equalizer_enable = cJSON_GetObjectItemCaseSensitive(equalizer_root, "enable"); + if (cJSON_IsBool(audio_equalizer_enable)) { + if (cJSON_IsTrue(audio_equalizer_enable)) { + (*configObj)->audio_equalizer_enable = true; + } + } + + cJSON* audio_equalizer_followPitch = cJSON_GetObjectItemCaseSensitive(equalizer_root, "followPitch"); + if (cJSON_IsBool(audio_equalizer_followPitch)) { + if (cJSON_IsTrue(audio_equalizer_followPitch)) { + (*configObj)->audio_equalizer_followPitch = true; + } + } + + // Fetch the equalizer graph array, and allocate memory for it + cJSON* equalizer_graph_array = cJSON_GetObjectItemCaseSensitive(equalizer_root, "graph"); + if (equalizer_graph_array == NULL) { + logger_log_error(__func__, "Error parsing JSON - graph does not exist."); + cJSON_Delete(root); + return 1; + } + + (*configObj)->audio_equalizer_graphCount = cJSON_GetArraySize(equalizer_graph_array); + (*configObj)->audio_equalizer_graph = (configHandler_eqGraph_t*)malloc((*configObj)->audio_equalizer_graphCount * sizeof(configHandler_eqGraph_t)); + if ((*configObj)->audio_equalizer_graph == NULL) { + logger_log_error(__func__, "malloc() failed."); + cJSON_Delete(root); + return 1; + } + + // Initialize more struct variables + for (size_t i = 0; i < (*configObj)->audio_equalizer_graphCount; i++) { + (*configObj)->audio_equalizer_graph[i].bandwidth = 0.00; + (*configObj)->audio_equalizer_graph[i].frequency = 0; + (*configObj)->audio_equalizer_graph[i].gain = 0.00; + (*configObj)->audio_equalizer_graph[i].bypass = false; + } + + for (size_t i = 0; i < (*configObj)->audio_equalizer_graphCount; i++) { + cJSON* array_equalizer_graph_root = cJSON_GetArrayItem(equalizer_graph_array, (int)i); + if (array_equalizer_graph_root == NULL) { + logger_log_error(__func__, "Error parsing JSON - Could not fetch graph index %d.", i); + cJSON_Delete(root); + return 1; + } + + cJSON* audio_equalizer_graph_bandwidth = cJSON_GetObjectItemCaseSensitive(array_equalizer_graph_root, "bandwidth"); + if (cJSON_IsNumber(audio_equalizer_graph_bandwidth)) { + (*configObj)->audio_equalizer_graph[i].bandwidth = audio_equalizer_graph_bandwidth->valuedouble; + } + + cJSON* audio_equalizer_graph_frequency = cJSON_GetObjectItemCaseSensitive(array_equalizer_graph_root, "frequency"); + if (cJSON_IsNumber(audio_equalizer_graph_frequency)) { + (*configObj)->audio_equalizer_graph[i].frequency = audio_equalizer_graph_frequency->valueint; + } + + cJSON* audio_equalizer_graph_gain = cJSON_GetObjectItemCaseSensitive(array_equalizer_graph_root, "gain"); + if (cJSON_IsNumber(audio_equalizer_graph_gain)) { + (*configObj)->audio_equalizer_graph[i].gain = audio_equalizer_graph_gain->valuedouble; + } + + cJSON* audio_equalizer_graph_bypass = cJSON_GetObjectItemCaseSensitive(array_equalizer_graph_root, "bypass"); + if (cJSON_IsBool(audio_equalizer_graph_bypass)) { + if (cJSON_IsTrue(audio_equalizer_graph_bypass)) { + (*configObj)->audio_equalizer_graph[i].bypass = true; + } + } + } + + // Make an object from pitch + cJSON* pitch_root = cJSON_GetObjectItemCaseSensitive(audio_root, "pitch"); + if (pitch_root == NULL) { + logger_log_error(__func__, "Error parsing JSON - pitch does not exist."); + cJSON_Delete(root); + return 1; + } + + cJSON* audio_pitch_enable = cJSON_GetObjectItemCaseSensitive(pitch_root, "enable"); + if (cJSON_IsBool(audio_pitch_enable)) { + if (cJSON_IsTrue(audio_pitch_enable)) { + (*configObj)->audio_pitch_enable = true; + } + } + + cJSON* audio_pitch_cents = cJSON_GetObjectItemCaseSensitive(pitch_root, "cents"); + if (cJSON_IsNumber(audio_pitch_cents)) { + (*configObj)->audio_pitch_cents = audio_pitch_cents->valuedouble; + } + + cJSON* audio_pitch_rate = cJSON_GetObjectItemCaseSensitive(pitch_root, "rate"); + if (cJSON_IsNumber(audio_pitch_rate)) { + (*configObj)->audio_pitch_rate = audio_pitch_rate->valuedouble; + } + + // Make an object from reverb + cJSON* reverb_root = cJSON_GetObjectItemCaseSensitive(audio_root, "reverb"); + if (reverb_root == NULL) { + logger_log_error(__func__, "Error parsing JSON - reverb does not exist."); + cJSON_Delete(root); + return 1; + } + + cJSON* audio_reverb_enable = cJSON_GetObjectItemCaseSensitive(reverb_root, "enable"); + if (cJSON_IsBool(audio_reverb_enable)) { + if (cJSON_IsTrue(audio_reverb_enable)) { + (*configObj)->audio_reverb_enable = true; + } + } + + cJSON* audio_reverb_wetDryMix = cJSON_GetObjectItemCaseSensitive(reverb_root, "wetDryMix"); + if (cJSON_IsNumber(audio_reverb_wetDryMix)) { + (*configObj)->audio_reverb_wetDryMix = audio_reverb_wetDryMix->valuedouble; + } + + cJSON_Delete(root); + logger_log_general(__func__, "Successfully read configuration file."); + return 0; +} + +void configHandler_Free(configHandler_config_t** configObj) { + if ((*configObj)->opensubsonic_protocol != NULL) { free((*configObj)->opensubsonic_protocol); } + if ((*configObj)->opensubsonic_server != NULL) { free((*configObj)->opensubsonic_server); } + if ((*configObj)->opensubsonic_username != NULL) { free((*configObj)->opensubsonic_username); } + if ((*configObj)->opensubsonic_password != NULL) { free((*configObj)->opensubsonic_password); } + if ((*configObj)->internal_opensubsonic_version != NULL) { free((*configObj)->internal_opensubsonic_version); } + if ((*configObj)->internal_opensubsonic_clientName != NULL) { free((*configObj)->internal_opensubsonic_clientName); } + if ((*configObj)->internal_opensubsonic_loginSalt != NULL) { free((*configObj)->internal_opensubsonic_loginSalt); } + if ((*configObj)->internal_opensubsonic_loginToken != NULL) { free((*configObj)->internal_opensubsonic_loginToken); } + if ((*configObj)->listenbrainz_token != NULL) { free((*configObj)->listenbrainz_token); } + if ((*configObj)->lastfm_username != NULL) { free((*configObj)->lastfm_username); } + if ((*configObj)->lastfm_password != NULL) { free((*configObj)->lastfm_password); } + if ((*configObj)->lastfm_api_key != NULL) { free((*configObj)->lastfm_api_key); } + if ((*configObj)->lastfm_api_secret != NULL) { free((*configObj)->lastfm_api_secret); } + if ((*configObj)->lastfm_api_session_key != NULL) { free((*configObj)->lastfm_api_session_key); } + if ((*configObj)->audio_equalizer_graph != NULL) { free((*configObj)->audio_equalizer_graph); } + if (*configObj != NULL) { free(*configObj); } +} diff --git a/src/configHandler.h b/src/configHandler.h new file mode 100644 index 0000000..c563de4 --- /dev/null +++ b/src/configHandler.h @@ -0,0 +1,48 @@ +#ifndef _CONFIG_HANDLER_H +#define _CONFIG_HANDLER_H +#include + +typedef struct { + double bandwidth; + int frequency; // Frequency in Hz + double gain; // Gain in db + bool bypass; // Ignore entry +} configHandler_eqGraph_t; + +typedef struct { + // Opensubsonic Settings + char* opensubsonic_protocol; // http / https + char* opensubsonic_server; // address:port + char* opensubsonic_username; + char* opensubsonic_password; + char* internal_opensubsonic_version; // (Internal) Opensubsonic API Version + char* internal_opensubsonic_clientName; // (Internal) Opensubsonic Client Name + char* internal_opensubsonic_loginSalt; // (Internal) Opensubsonic Login Salt + char* internal_opensubsonic_loginToken; // (Internal) Opensubsonic Login Token + + // Scrobbler Settings + bool listenbrainz_enable; // Enable ListenBrainz Scrobbling + char* listenbrainz_token; // ListenBrainz Token + bool lastfm_enable; // Enable LastFM Scrobbling + char* lastfm_username; // LastFM Username + char* lastfm_password; // LastFM Password + char* lastfm_api_key; // LastFM API Key + char* lastfm_api_secret; // LastFM API Secret + char* lastfm_api_session_key; // LastFM API Session Key (Generated from authorization endpoint) + + // Audio Settings + bool audio_equalizer_enable; + bool audio_equalizer_followPitch; // Have equalizer align to pitch adjustment + int audio_equalizer_graphCount; + configHandler_eqGraph_t* audio_equalizer_graph; + bool audio_pitch_enable; + double audio_pitch_cents; + double audio_pitch_rate; + bool audio_reverb_enable; + double audio_reverb_wetDryMix; // Reverb Wet/Dry Mix Percent +} configHandler_config_t; + +int configHandler_Read(configHandler_config_t** config); +void configHandler_Free(configHandler_config_t** config); + +#endif diff --git a/src/dscrdrpc.c b/src/dscrdrpc.c new file mode 100644 index 0000000..cfa60a3 --- /dev/null +++ b/src/dscrdrpc.c @@ -0,0 +1,270 @@ +/* + * NOTE: The private keys in this file are PURELY for FORMATTING DEMONSTRATION. They are NOT in active use + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "external/cJSON.h" +#include "external/libcurl_uriescape.h" +#include "dscrdrpc.h" + +#include "DarwinHttpClient.h" + +void dscrdrpc_struct_init(dscrdrpc_data** dscrdrpc_struct) { + (*dscrdrpc_struct) = (dscrdrpc_data*)malloc(sizeof(dscrdrpc_data)); + (*dscrdrpc_struct)->requestType = 0; + (*dscrdrpc_struct)->trackingUuid = NULL; + (*dscrdrpc_struct)->songLength = 0; + (*dscrdrpc_struct)->songTitle = NULL; + (*dscrdrpc_struct)->songArtist = NULL; + (*dscrdrpc_struct)->coverArtUrl = NULL; + (*dscrdrpc_struct)->deviceInfo = NULL; + (*dscrdrpc_struct)->checksum = NULL; +} + +void dscrdrpc_struct_deinit(dscrdrpc_data** dscrdrpc_struct) { + if ((*dscrdrpc_struct)->trackingUuid != NULL) { free((*dscrdrpc_struct)->trackingUuid); } + if ((*dscrdrpc_struct)->songTitle != NULL) { free((*dscrdrpc_struct)->songTitle); } + if ((*dscrdrpc_struct)->songArtist != NULL) { free((*dscrdrpc_struct)->songArtist); } + if ((*dscrdrpc_struct)->coverArtUrl != NULL) { free((*dscrdrpc_struct)->coverArtUrl); } + if ((*dscrdrpc_struct)->deviceInfo != NULL) { free((*dscrdrpc_struct)->deviceInfo); } + if ((*dscrdrpc_struct)->checksum != NULL) { free((*dscrdrpc_struct)->checksum); } + if (*dscrdrpc_struct != NULL) { free(*dscrdrpc_struct); } +} + +int dscrdrpc_uuidv4(char** uuidv4) { + // Generate non-compliant UUIDv4 string + uint8_t uuidv4_bytes[16]; + static int rc = 0; + for (int i = 0; i < 16; i++) { + uuidv4_bytes[i] = arc4random() & 0xFF; + } + rc = asprintf(uuidv4, "%.2x%.2x%.2x%.2x-%.2x%.2x-%.2x%.2x-%.2x%.2x-%.2x%.2x%.2x%.2x%.2x%.2x", + uuidv4_bytes[0], uuidv4_bytes[1], uuidv4_bytes[2], uuidv4_bytes[3], uuidv4_bytes[4], + uuidv4_bytes[5], uuidv4_bytes[6], uuidv4_bytes[7], uuidv4_bytes[8], uuidv4_bytes[9], + uuidv4_bytes[10], uuidv4_bytes[11], uuidv4_bytes[12], uuidv4_bytes[13], uuidv4_bytes[14], + uuidv4_bytes[15]); + if (rc == -1) { + printf("asprintf() failed.\n"); + return 1; + } + return 0; +} + +int dscrdrpc_crc32(char* inputString, char** crc32Output) { + static int rc = 0; + uLong crc = crc32(0, (void*)inputString, (uInt)strlen(inputString)); + rc = asprintf(crc32Output, "%lx", crc); + if (rc == -1) { + printf("asprintf() failed.\n"); + return 1; + } + return 0; +} + +void dscrdrpc_form_innerJSON(dscrdrpc_data** dscrdrpc_struct, char** jsonOutput) { + cJSON* internal_root = cJSON_CreateObject(); + cJSON* internal_requestType = cJSON_CreateNumber((*dscrdrpc_struct)->requestType); + cJSON* internal_trackingUuid = cJSON_CreateString((*dscrdrpc_struct)->trackingUuid); + cJSON* internal_songLength = cJSON_CreateNumber((*dscrdrpc_struct)->songLength); + cJSON* internal_songTitle = cJSON_CreateString((*dscrdrpc_struct)->songTitle); + cJSON* internal_songArtist = cJSON_CreateString((*dscrdrpc_struct)->songArtist); + cJSON* internal_coverArtUrl = cJSON_CreateString((*dscrdrpc_struct)->coverArtUrl); + cJSON* internal_deviceInfo = cJSON_CreateString((*dscrdrpc_struct)->deviceInfo); + cJSON* internal_checksum = cJSON_CreateString((*dscrdrpc_struct)->checksum); + cJSON_AddItemToObject(internal_root, "requestType", internal_requestType); + cJSON_AddItemToObject(internal_root, "trackingUuid", internal_trackingUuid); + cJSON_AddItemToObject(internal_root, "songLength", internal_songLength); + cJSON_AddItemToObject(internal_root, "songTitle", internal_songTitle); + cJSON_AddItemToObject(internal_root, "songArtist", internal_songArtist); + cJSON_AddItemToObject(internal_root, "coverArtUrl", internal_coverArtUrl); + cJSON_AddItemToObject(internal_root, "deviceInfo", internal_deviceInfo); + cJSON_AddItemToObject(internal_root, "checksum", internal_checksum); + *jsonOutput = cJSON_PrintUnformatted(internal_root); + cJSON_Delete(internal_root); +} + +void dscrdrpc_aes_gcm_encrypt(unsigned char* key, unsigned char* iv, unsigned char* ct, unsigned char* tag, + char* plaintext, int length) { + // Initialize OpenSSL + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + + EVP_EncryptInit(ctx, EVP_aes_256_gcm(), NULL, NULL); + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, NULL); // 96 bit IV + EVP_EncryptInit_ex(ctx, NULL, NULL, key, iv); + + // Encrypt the plaintext + int len; + EVP_EncryptUpdate(ctx, ct, &len, (const unsigned char*)plaintext, length); + EVP_EncryptFinal(ctx, ct + len, &len); + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag); + + // Cleanup + EVP_CIPHER_CTX_free(ctx); +} + +EVP_PKEY* dscrdrpc_rsa_read_pubkey(char* pubkey) { + BIO* bio = BIO_new_mem_buf(pubkey, -1); // -1 causes strlen to be called + if (!bio) { + printf("BIO_new_mem_buf() failed.\n"); + return NULL; + } + EVP_PKEY* pkey = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL); + BIO_free(bio); + if (!pkey) { + printf("PEM_read_bio_RSA_PUBKEY() failed.\n"); + return NULL; + } + return pkey; +} + +int dscrdrpc_rsa_oaep_encrypt(EVP_PKEY* pkey, char* plaintext, int length, char** ct) { + // Initialize OpenSSL + EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(pkey, NULL); + EVP_PKEY_encrypt_init(ctx); + EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING); + EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha256()); + EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, EVP_sha256()); + + // Encrypt + size_t len; + EVP_PKEY_encrypt(ctx, NULL, &len, (const unsigned char*)plaintext, length); + *ct = malloc(len); + EVP_PKEY_encrypt(ctx, (unsigned char*)*ct, &len, (const unsigned char*)plaintext, length); + + // Cleanup + EVP_PKEY_CTX_free(ctx); + return (int)len; +} + +void dscrdrpc_form_outerJSON(char* iv, char* tag, char* ct, char* checksum, char** jsonOutput) { + cJSON* outer_root = cJSON_CreateObject(); + cJSON* outer_iv = cJSON_CreateString(iv); + cJSON* outer_tag = cJSON_CreateString(tag); + cJSON* outer_ct = cJSON_CreateString(ct); + cJSON* outer_checksum = cJSON_CreateString(checksum); + cJSON_AddItemToObject(outer_root, "i", outer_iv); + cJSON_AddItemToObject(outer_root, "t", outer_tag); + cJSON_AddItemToObject(outer_root, "d", outer_ct); + cJSON_AddItemToObject(outer_root, "c", outer_checksum); + *jsonOutput = cJSON_PrintUnformatted(outer_root); + cJSON_Delete(outer_root); +} + +int dscrdrpc_encrypt(dscrdrpc_data** dscrdrpc_struct) { + static int rc = 0; + + // Generate UUIDv4 (Non compliant but does not matter) + dscrdrpc_uuidv4(&(*dscrdrpc_struct)->trackingUuid); + + // Form string to make checksum + char* innerContentChecksumStr = NULL; + rc = asprintf(&innerContentChecksumStr, "%d%s%s%ld%s%s%s", + (*dscrdrpc_struct)->requestType, (*dscrdrpc_struct)->songTitle, (*dscrdrpc_struct)->songArtist, + (*dscrdrpc_struct)->songLength, (*dscrdrpc_struct)->coverArtUrl, (*dscrdrpc_struct)->deviceInfo, + (*dscrdrpc_struct)->trackingUuid); + if (rc == -1) { + printf("asprintf() failed.\n"); + return 1; + } + + // Create CRC32 checksum of contents + dscrdrpc_crc32(innerContentChecksumStr, &(*dscrdrpc_struct)->checksum); + free(innerContentChecksumStr); + + // Form inner JSON + char* innerJSON = NULL; + dscrdrpc_form_innerJSON(dscrdrpc_struct, &innerJSON); + int innerJSON_length = (int)strlen(innerJSON); + + // Encrypt inner JSON with AES GCM + unsigned char aes_key[32] = { + 0x43, 0x63, 0x50, 0x5d, 0x31, 0x23, 0x46, 0x51, + 0x74, 0x50, 0x70, 0x55, 0x4d, 0x60, 0x46, 0x69, + 0x39, 0x46, 0x52, 0x6e, 0x5f, 0x5e, 0x2f, 0x50, + 0x45, 0x27, 0x30, 0x55, 0x39, 0x68, 0x78, 0x43 + }; + unsigned char* aes_iv = calloc(12, sizeof(char)); + RAND_bytes(aes_iv, 12); // TODO replace with arc4random + unsigned char* ciphertext = calloc(innerJSON_length, sizeof(char)); + unsigned char* aes_tag = calloc(16, sizeof(char)); + dscrdrpc_aes_gcm_encrypt(aes_key, aes_iv, ciphertext, aes_tag, innerJSON, innerJSON_length); + free(innerJSON); + + printf("Key: "); for (int i = 0; i < 32; i++) { printf("%.2x", aes_key[i]); } printf("\n"); + printf("IV: "); for (int i = 0; i < 12; i++) { printf("%.2x", aes_iv[i]); } printf("\n"); + printf("Tag: "); for (int i = 0; i < 16; i++) { printf("%.2x", aes_tag[i]); } printf("\n"); + printf("Ciphertext: "); for (int i = 0; i < innerJSON_length; i++) { printf("%.2x", ciphertext[i]); } printf("\n"); + + // RSA encrypt the AES IV and Tag + char* rsa_pubkey_text = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoeZn81EnEGom5eWOpFvB\nZw3X9gDBtjzix69qhWDHfq2oh/b0tynIe2PV7G6ELr2StwQOzcVa4cx3HkQ4vmE8\nZxdQ3Ru+HN6EjGonXrfgN7J6+MUDcEE0wOglAkoIGyrDhuxlMrUFKwiTVMdPqxQy\ngOWbOvnJu0q5x/7TFbJGgkZwdRYHgFXW/1lTEzBfZZu0cCa0V7YQ0prCxtbGP5CD\nXaDND65a/rk5t4Jp+3nOQDUtd6tqZ9Rp/mOBRQm8bj4nhw03UsbRhV+vHx34rtPl\nw46JSUfEuGXiNNaluXbPxWPNAf6Lj36UJH01tFVnJrPtv6T5mQCygL22OgkbSsAN\n3QIDAQAB\n-----END PUBLIC KEY-----"; + EVP_PKEY* rsa_pubkey = dscrdrpc_rsa_read_pubkey(rsa_pubkey_text); + + char* ret_rsa_iv = NULL; + char* ret_rsa_tag = NULL; + int ret_rsa_iv_len = dscrdrpc_rsa_oaep_encrypt(rsa_pubkey, (char*)aes_iv, 12, &ret_rsa_iv); + int ret_rsa_tag_len = dscrdrpc_rsa_oaep_encrypt(rsa_pubkey, (char*)aes_tag, 16, &ret_rsa_tag); + free(aes_iv); + free(aes_tag); + + char* escaped_iv = lcue_uriescape(ret_rsa_iv, ret_rsa_iv_len); + char* escaped_tag = lcue_uriescape(ret_rsa_tag, ret_rsa_tag_len); + char* escaped_ciphertext = lcue_uriescape((char*)ciphertext, innerJSON_length); + char* escaped_aes_key = lcue_uriescape((char*)aes_key, sizeof(aes_key)); + + free(ciphertext); + free(ret_rsa_iv); + free(ret_rsa_tag); + EVP_PKEY_free(rsa_pubkey); + + // Form string to make checksum + char* outerContentChecksumStr = NULL; + rc = asprintf(&outerContentChecksumStr, "%s%s%s%s", + escaped_ciphertext, escaped_iv, escaped_tag, escaped_aes_key); + if (rc == -1) { + printf("asprintf() failed.\n"); + return 1; + } + + // Create CRC32 checksum of contents + char* outerContentChecksum = NULL; + dscrdrpc_crc32(outerContentChecksumStr, &outerContentChecksum); + free(outerContentChecksumStr); + + // Form Outer JSON + char* finalPayload = NULL; + dscrdrpc_form_outerJSON(escaped_iv, escaped_tag, escaped_ciphertext, outerContentChecksum, &finalPayload); + + free(escaped_iv); + free(escaped_tag); + free(escaped_ciphertext); + free(escaped_aes_key); + free(outerContentChecksum); + + printf("Final payload: %s\n", finalPayload); + + opensubsonic_httpClientRequest_t* httpReq; + opensubsonic_httpClient_prepareRequest(&httpReq); + httpReq->method = HTTP_METHOD_POST; + httpReq->requestUrl = strdup("http://192.168.5.140:20000/updrp"); + httpReq->isBodyRequired = true; + httpReq->requestBody = strdup(finalPayload); + opensubsonic_httpClient_request(&httpReq); + XNU_HttpRequest(&httpReq); + + printf("%s\n", httpReq->responseMsg); + + free(finalPayload); + opensubsonic_httpClient_cleanup(&httpReq); + + return 0; +} diff --git a/src/dscrdrpc.h b/src/dscrdrpc.h new file mode 100644 index 0000000..e234805 --- /dev/null +++ b/src/dscrdrpc.h @@ -0,0 +1,34 @@ +#ifndef _DSCRDRPC_H +#define _DSCRDRPC_H +#include + +#define DSCRDRPC_REQTYPE_PLAYING 1 +#define DSCRDRPC_REQTYPE_PAUSED 2 +#define DSCRDRPC_REQTYPE_UNPAUSED 3 +#define DSCRDRPC_REQTYPE_CLEAR 4 +#define DSCRDRPC_REQTYPE_KEEPALIVE 5 + +typedef struct { + int requestType; + char* trackingUuid; + long songLength; + char* songTitle; + char* songArtist; + char* coverArtUrl; + char* deviceInfo; + char* checksum; +} dscrdrpc_data; + +void dscrdrpc_struct_init(dscrdrpc_data** dscrdrpc_struct); +void dscrdrpc_struct_deinit(dscrdrpc_data** dscrdrpc_struct); +int dscrdrpc_uuidv4(char** uuidv4); +int dscrdrpc_crc32(char* inputString, char** crc32Output); +void dscrdrpc_form_innerJSON(dscrdrpc_data** dscrdrpc_struct, char** jsonOutput); +void dscrdrpc_aes_gcm_encrypt(unsigned char* key, unsigned char* iv, unsigned char* ct, unsigned char* tag, char* plaintext, int length); +EVP_PKEY* dscrdrpc_rsa_read_pubkey(char* pubkey); +int dscrdrpc_rsa_oaep_encrypt(EVP_PKEY* pkey, char* plaintext, int length, char** ct); +void dscrdrpc_form_outerJSON(char* iv, char* tag, char* ct, char* checksum, char** jsonOutput); + +int dscrdrpc_encrypt(dscrdrpc_data** dscrdrpc_struct); + +#endif diff --git a/src/external/cJSON.c b/src/external/cJSON.c new file mode 100644 index 0000000..61483d9 --- /dev/null +++ b/src/external/cJSON.c @@ -0,0 +1,3143 @@ +/* + Copyright (c) 2009-2017 Dave Gamble and cJSON contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +/* cJSON */ +/* JSON parser in C. */ + +/* disable warnings about old C89 functions in MSVC */ +#if !defined(_CRT_SECURE_NO_DEPRECATE) && defined(_MSC_VER) +#define _CRT_SECURE_NO_DEPRECATE +#endif + +#ifdef __GNUC__ +#pragma GCC visibility push(default) +#endif +#if defined(_MSC_VER) +#pragma warning (push) +/* disable warning about single line comments in system headers */ +#pragma warning (disable : 4001) +#endif + +#include +#include +#include +#include +#include +#include +#include + +#ifdef ENABLE_LOCALES +#include +#endif + +#if defined(_MSC_VER) +#pragma warning (pop) +#endif +#ifdef __GNUC__ +#pragma GCC visibility pop +#endif + +#include "cJSON.h" + +/* define our own boolean type */ +#ifdef true +#undef true +#endif +#define true ((cJSON_bool)1) + +#ifdef false +#undef false +#endif +#define false ((cJSON_bool)0) + +/* define isnan and isinf for ANSI C, if in C99 or above, isnan and isinf has been defined in math.h */ +#ifndef isinf +#define isinf(d) (isnan((d - d)) && !isnan(d)) +#endif +#ifndef isnan +#define isnan(d) (d != d) +#endif + +#ifndef NAN +#ifdef _WIN32 +#define NAN sqrt(-1.0) +#else +#define NAN 0.0/0.0 +#endif +#endif + +typedef struct { + const unsigned char *json; + size_t position; +} error; +static error global_error = { NULL, 0 }; + +CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void) +{ + return (const char*) (global_error.json + global_error.position); +} + +CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item) +{ + if (!cJSON_IsString(item)) + { + return NULL; + } + + return item->valuestring; +} + +CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item) +{ + if (!cJSON_IsNumber(item)) + { + return (double) NAN; + } + + return item->valuedouble; +} + +/* This is a safeguard to prevent copy-pasters from using incompatible C and header files */ +#if (CJSON_VERSION_MAJOR != 1) || (CJSON_VERSION_MINOR != 7) || (CJSON_VERSION_PATCH != 18) + #error cJSON.h and cJSON.c have different versions. Make sure that both have the same. +#endif + +CJSON_PUBLIC(const char*) cJSON_Version(void) +{ + static char version[15]; + sprintf(version, "%i.%i.%i", CJSON_VERSION_MAJOR, CJSON_VERSION_MINOR, CJSON_VERSION_PATCH); + + return version; +} + +/* Case insensitive string comparison, doesn't consider two NULL pointers equal though */ +static int case_insensitive_strcmp(const unsigned char *string1, const unsigned char *string2) +{ + if ((string1 == NULL) || (string2 == NULL)) + { + return 1; + } + + if (string1 == string2) + { + return 0; + } + + for(; tolower(*string1) == tolower(*string2); (void)string1++, string2++) + { + if (*string1 == '\0') + { + return 0; + } + } + + return tolower(*string1) - tolower(*string2); +} + +typedef struct internal_hooks +{ + void *(CJSON_CDECL *allocate)(size_t size); + void (CJSON_CDECL *deallocate)(void *pointer); + void *(CJSON_CDECL *reallocate)(void *pointer, size_t size); +} internal_hooks; + +#if defined(_MSC_VER) +/* work around MSVC error C2322: '...' address of dllimport '...' is not static */ +static void * CJSON_CDECL internal_malloc(size_t size) +{ + return malloc(size); +} +static void CJSON_CDECL internal_free(void *pointer) +{ + free(pointer); +} +static void * CJSON_CDECL internal_realloc(void *pointer, size_t size) +{ + return realloc(pointer, size); +} +#else +#define internal_malloc malloc +#define internal_free free +#define internal_realloc realloc +#endif + +/* strlen of character literals resolved at compile time */ +#define static_strlen(string_literal) (sizeof(string_literal) - sizeof("")) + +static internal_hooks global_hooks = { internal_malloc, internal_free, internal_realloc }; + +static unsigned char* cJSON_strdup(const unsigned char* string, const internal_hooks * const hooks) +{ + size_t length = 0; + unsigned char *copy = NULL; + + if (string == NULL) + { + return NULL; + } + + length = strlen((const char*)string) + sizeof(""); + copy = (unsigned char*)hooks->allocate(length); + if (copy == NULL) + { + return NULL; + } + memcpy(copy, string, length); + + return copy; +} + +CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks) +{ + if (hooks == NULL) + { + /* Reset hooks */ + global_hooks.allocate = malloc; + global_hooks.deallocate = free; + global_hooks.reallocate = realloc; + return; + } + + global_hooks.allocate = malloc; + if (hooks->malloc_fn != NULL) + { + global_hooks.allocate = hooks->malloc_fn; + } + + global_hooks.deallocate = free; + if (hooks->free_fn != NULL) + { + global_hooks.deallocate = hooks->free_fn; + } + + /* use realloc only if both free and malloc are used */ + global_hooks.reallocate = NULL; + if ((global_hooks.allocate == malloc) && (global_hooks.deallocate == free)) + { + global_hooks.reallocate = realloc; + } +} + +/* Internal constructor. */ +static cJSON *cJSON_New_Item(const internal_hooks * const hooks) +{ + cJSON* node = (cJSON*)hooks->allocate(sizeof(cJSON)); + if (node) + { + memset(node, '\0', sizeof(cJSON)); + } + + return node; +} + +/* Delete a cJSON structure. */ +CJSON_PUBLIC(void) cJSON_Delete(cJSON *item) +{ + cJSON *next = NULL; + while (item != NULL) + { + next = item->next; + if (!(item->type & cJSON_IsReference) && (item->child != NULL)) + { + cJSON_Delete(item->child); + } + if (!(item->type & cJSON_IsReference) && (item->valuestring != NULL)) + { + global_hooks.deallocate(item->valuestring); + item->valuestring = NULL; + } + if (!(item->type & cJSON_StringIsConst) && (item->string != NULL)) + { + global_hooks.deallocate(item->string); + item->string = NULL; + } + global_hooks.deallocate(item); + item = next; + } +} + +/* get the decimal point character of the current locale */ +static unsigned char get_decimal_point(void) +{ +#ifdef ENABLE_LOCALES + struct lconv *lconv = localeconv(); + return (unsigned char) lconv->decimal_point[0]; +#else + return '.'; +#endif +} + +typedef struct +{ + const unsigned char *content; + size_t length; + size_t offset; + size_t depth; /* How deeply nested (in arrays/objects) is the input at the current offset. */ + internal_hooks hooks; +} parse_buffer; + +/* check if the given size is left to read in a given parse buffer (starting with 1) */ +#define can_read(buffer, size) ((buffer != NULL) && (((buffer)->offset + size) <= (buffer)->length)) +/* check if the buffer can be accessed at the given index (starting with 0) */ +#define can_access_at_index(buffer, index) ((buffer != NULL) && (((buffer)->offset + index) < (buffer)->length)) +#define cannot_access_at_index(buffer, index) (!can_access_at_index(buffer, index)) +/* get a pointer to the buffer at the position */ +#define buffer_at_offset(buffer) ((buffer)->content + (buffer)->offset) + +/* Parse the input text to generate a number, and populate the result into item. */ +static cJSON_bool parse_number(cJSON * const item, parse_buffer * const input_buffer) +{ + double number = 0; + unsigned char *after_end = NULL; + unsigned char number_c_string[64]; + unsigned char decimal_point = get_decimal_point(); + size_t i = 0; + + if ((input_buffer == NULL) || (input_buffer->content == NULL)) + { + return false; + } + + /* copy the number into a temporary buffer and replace '.' with the decimal point + * of the current locale (for strtod) + * This also takes care of '\0' not necessarily being available for marking the end of the input */ + for (i = 0; (i < (sizeof(number_c_string) - 1)) && can_access_at_index(input_buffer, i); i++) + { + switch (buffer_at_offset(input_buffer)[i]) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '+': + case '-': + case 'e': + case 'E': + number_c_string[i] = buffer_at_offset(input_buffer)[i]; + break; + + case '.': + number_c_string[i] = decimal_point; + break; + + default: + goto loop_end; + } + } +loop_end: + number_c_string[i] = '\0'; + + number = strtod((const char*)number_c_string, (char**)&after_end); + if (number_c_string == after_end) + { + return false; /* parse_error */ + } + + item->valuedouble = number; + + /* use saturation in case of overflow */ + if (number >= INT_MAX) + { + item->valueint = INT_MAX; + } + else if (number <= (double)INT_MIN) + { + item->valueint = INT_MIN; + } + else + { + item->valueint = (int)number; + } + + item->type = cJSON_Number; + + input_buffer->offset += (size_t)(after_end - number_c_string); + return true; +} + +/* don't ask me, but the original cJSON_SetNumberValue returns an integer or double */ +CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number) +{ + if (number >= INT_MAX) + { + object->valueint = INT_MAX; + } + else if (number <= (double)INT_MIN) + { + object->valueint = INT_MIN; + } + else + { + object->valueint = (int)number; + } + + return object->valuedouble = number; +} + +/* Note: when passing a NULL valuestring, cJSON_SetValuestring treats this as an error and return NULL */ +CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring) +{ + char *copy = NULL; + /* if object's type is not cJSON_String or is cJSON_IsReference, it should not set valuestring */ + if ((object == NULL) || !(object->type & cJSON_String) || (object->type & cJSON_IsReference)) + { + return NULL; + } + /* return NULL if the object is corrupted or valuestring is NULL */ + if (object->valuestring == NULL || valuestring == NULL) + { + return NULL; + } + if (strlen(valuestring) <= strlen(object->valuestring)) + { + strcpy(object->valuestring, valuestring); + return object->valuestring; + } + copy = (char*) cJSON_strdup((const unsigned char*)valuestring, &global_hooks); + if (copy == NULL) + { + return NULL; + } + if (object->valuestring != NULL) + { + cJSON_free(object->valuestring); + } + object->valuestring = copy; + + return copy; +} + +typedef struct +{ + unsigned char *buffer; + size_t length; + size_t offset; + size_t depth; /* current nesting depth (for formatted printing) */ + cJSON_bool noalloc; + cJSON_bool format; /* is this print a formatted print */ + internal_hooks hooks; +} printbuffer; + +/* realloc printbuffer if necessary to have at least "needed" bytes more */ +static unsigned char* ensure(printbuffer * const p, size_t needed) +{ + unsigned char *newbuffer = NULL; + size_t newsize = 0; + + if ((p == NULL) || (p->buffer == NULL)) + { + return NULL; + } + + if ((p->length > 0) && (p->offset >= p->length)) + { + /* make sure that offset is valid */ + return NULL; + } + + if (needed > INT_MAX) + { + /* sizes bigger than INT_MAX are currently not supported */ + return NULL; + } + + needed += p->offset + 1; + if (needed <= p->length) + { + return p->buffer + p->offset; + } + + if (p->noalloc) { + return NULL; + } + + /* calculate new buffer size */ + if (needed > (INT_MAX / 2)) + { + /* overflow of int, use INT_MAX if possible */ + if (needed <= INT_MAX) + { + newsize = INT_MAX; + } + else + { + return NULL; + } + } + else + { + newsize = needed * 2; + } + + if (p->hooks.reallocate != NULL) + { + /* reallocate with realloc if available */ + newbuffer = (unsigned char*)p->hooks.reallocate(p->buffer, newsize); + if (newbuffer == NULL) + { + p->hooks.deallocate(p->buffer); + p->length = 0; + p->buffer = NULL; + + return NULL; + } + } + else + { + /* otherwise reallocate manually */ + newbuffer = (unsigned char*)p->hooks.allocate(newsize); + if (!newbuffer) + { + p->hooks.deallocate(p->buffer); + p->length = 0; + p->buffer = NULL; + + return NULL; + } + + memcpy(newbuffer, p->buffer, p->offset + 1); + p->hooks.deallocate(p->buffer); + } + p->length = newsize; + p->buffer = newbuffer; + + return newbuffer + p->offset; +} + +/* calculate the new length of the string in a printbuffer and update the offset */ +static void update_offset(printbuffer * const buffer) +{ + const unsigned char *buffer_pointer = NULL; + if ((buffer == NULL) || (buffer->buffer == NULL)) + { + return; + } + buffer_pointer = buffer->buffer + buffer->offset; + + buffer->offset += strlen((const char*)buffer_pointer); +} + +/* securely comparison of floating-point variables */ +static cJSON_bool compare_double(double a, double b) +{ + double maxVal = fabs(a) > fabs(b) ? fabs(a) : fabs(b); + return (fabs(a - b) <= maxVal * DBL_EPSILON); +} + +/* Render the number nicely from the given item into a string. */ +static cJSON_bool print_number(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output_pointer = NULL; + double d = item->valuedouble; + int length = 0; + size_t i = 0; + unsigned char number_buffer[26] = {0}; /* temporary buffer to print the number into */ + unsigned char decimal_point = get_decimal_point(); + double test = 0.0; + + if (output_buffer == NULL) + { + return false; + } + + /* This checks for NaN and Infinity */ + if (isnan(d) || isinf(d)) + { + length = sprintf((char*)number_buffer, "null"); + } + else if(d == (double)item->valueint) + { + length = sprintf((char*)number_buffer, "%d", item->valueint); + } + else + { + /* Try 15 decimal places of precision to avoid nonsignificant nonzero digits */ + length = sprintf((char*)number_buffer, "%1.15g", d); + + /* Check whether the original double can be recovered */ + if ((sscanf((char*)number_buffer, "%lg", &test) != 1) || !compare_double((double)test, d)) + { + /* If not, print with 17 decimal places of precision */ + length = sprintf((char*)number_buffer, "%1.17g", d); + } + } + + /* sprintf failed or buffer overrun occurred */ + if ((length < 0) || (length > (int)(sizeof(number_buffer) - 1))) + { + return false; + } + + /* reserve appropriate space in the output */ + output_pointer = ensure(output_buffer, (size_t)length + sizeof("")); + if (output_pointer == NULL) + { + return false; + } + + /* copy the printed number to the output and replace locale + * dependent decimal point with '.' */ + for (i = 0; i < ((size_t)length); i++) + { + if (number_buffer[i] == decimal_point) + { + output_pointer[i] = '.'; + continue; + } + + output_pointer[i] = number_buffer[i]; + } + output_pointer[i] = '\0'; + + output_buffer->offset += (size_t)length; + + return true; +} + +/* parse 4 digit hexadecimal number */ +static unsigned parse_hex4(const unsigned char * const input) +{ + unsigned int h = 0; + size_t i = 0; + + for (i = 0; i < 4; i++) + { + /* parse digit */ + if ((input[i] >= '0') && (input[i] <= '9')) + { + h += (unsigned int) input[i] - '0'; + } + else if ((input[i] >= 'A') && (input[i] <= 'F')) + { + h += (unsigned int) 10 + input[i] - 'A'; + } + else if ((input[i] >= 'a') && (input[i] <= 'f')) + { + h += (unsigned int) 10 + input[i] - 'a'; + } + else /* invalid */ + { + return 0; + } + + if (i < 3) + { + /* shift left to make place for the next nibble */ + h = h << 4; + } + } + + return h; +} + +/* converts a UTF-16 literal to UTF-8 + * A literal can be one or two sequences of the form \uXXXX */ +static unsigned char utf16_literal_to_utf8(const unsigned char * const input_pointer, const unsigned char * const input_end, unsigned char **output_pointer) +{ + long unsigned int codepoint = 0; + unsigned int first_code = 0; + const unsigned char *first_sequence = input_pointer; + unsigned char utf8_length = 0; + unsigned char utf8_position = 0; + unsigned char sequence_length = 0; + unsigned char first_byte_mark = 0; + + if ((input_end - first_sequence) < 6) + { + /* input ends unexpectedly */ + goto fail; + } + + /* get the first utf16 sequence */ + first_code = parse_hex4(first_sequence + 2); + + /* check that the code is valid */ + if (((first_code >= 0xDC00) && (first_code <= 0xDFFF))) + { + goto fail; + } + + /* UTF16 surrogate pair */ + if ((first_code >= 0xD800) && (first_code <= 0xDBFF)) + { + const unsigned char *second_sequence = first_sequence + 6; + unsigned int second_code = 0; + sequence_length = 12; /* \uXXXX\uXXXX */ + + if ((input_end - second_sequence) < 6) + { + /* input ends unexpectedly */ + goto fail; + } + + if ((second_sequence[0] != '\\') || (second_sequence[1] != 'u')) + { + /* missing second half of the surrogate pair */ + goto fail; + } + + /* get the second utf16 sequence */ + second_code = parse_hex4(second_sequence + 2); + /* check that the code is valid */ + if ((second_code < 0xDC00) || (second_code > 0xDFFF)) + { + /* invalid second half of the surrogate pair */ + goto fail; + } + + + /* calculate the unicode codepoint from the surrogate pair */ + codepoint = 0x10000 + (((first_code & 0x3FF) << 10) | (second_code & 0x3FF)); + } + else + { + sequence_length = 6; /* \uXXXX */ + codepoint = first_code; + } + + /* encode as UTF-8 + * takes at maximum 4 bytes to encode: + * 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx */ + if (codepoint < 0x80) + { + /* normal ascii, encoding 0xxxxxxx */ + utf8_length = 1; + } + else if (codepoint < 0x800) + { + /* two bytes, encoding 110xxxxx 10xxxxxx */ + utf8_length = 2; + first_byte_mark = 0xC0; /* 11000000 */ + } + else if (codepoint < 0x10000) + { + /* three bytes, encoding 1110xxxx 10xxxxxx 10xxxxxx */ + utf8_length = 3; + first_byte_mark = 0xE0; /* 11100000 */ + } + else if (codepoint <= 0x10FFFF) + { + /* four bytes, encoding 1110xxxx 10xxxxxx 10xxxxxx 10xxxxxx */ + utf8_length = 4; + first_byte_mark = 0xF0; /* 11110000 */ + } + else + { + /* invalid unicode codepoint */ + goto fail; + } + + /* encode as utf8 */ + for (utf8_position = (unsigned char)(utf8_length - 1); utf8_position > 0; utf8_position--) + { + /* 10xxxxxx */ + (*output_pointer)[utf8_position] = (unsigned char)((codepoint | 0x80) & 0xBF); + codepoint >>= 6; + } + /* encode first byte */ + if (utf8_length > 1) + { + (*output_pointer)[0] = (unsigned char)((codepoint | first_byte_mark) & 0xFF); + } + else + { + (*output_pointer)[0] = (unsigned char)(codepoint & 0x7F); + } + + *output_pointer += utf8_length; + + return sequence_length; + +fail: + return 0; +} + +/* Parse the input text into an unescaped cinput, and populate item. */ +static cJSON_bool parse_string(cJSON * const item, parse_buffer * const input_buffer) +{ + const unsigned char *input_pointer = buffer_at_offset(input_buffer) + 1; + const unsigned char *input_end = buffer_at_offset(input_buffer) + 1; + unsigned char *output_pointer = NULL; + unsigned char *output = NULL; + + /* not a string */ + if (buffer_at_offset(input_buffer)[0] != '\"') + { + goto fail; + } + + { + /* calculate approximate size of the output (overestimate) */ + size_t allocation_length = 0; + size_t skipped_bytes = 0; + while (((size_t)(input_end - input_buffer->content) < input_buffer->length) && (*input_end != '\"')) + { + /* is escape sequence */ + if (input_end[0] == '\\') + { + if ((size_t)(input_end + 1 - input_buffer->content) >= input_buffer->length) + { + /* prevent buffer overflow when last input character is a backslash */ + goto fail; + } + skipped_bytes++; + input_end++; + } + input_end++; + } + if (((size_t)(input_end - input_buffer->content) >= input_buffer->length) || (*input_end != '\"')) + { + goto fail; /* string ended unexpectedly */ + } + + /* This is at most how much we need for the output */ + allocation_length = (size_t) (input_end - buffer_at_offset(input_buffer)) - skipped_bytes; + output = (unsigned char*)input_buffer->hooks.allocate(allocation_length + sizeof("")); + if (output == NULL) + { + goto fail; /* allocation failure */ + } + } + + output_pointer = output; + /* loop through the string literal */ + while (input_pointer < input_end) + { + if (*input_pointer != '\\') + { + *output_pointer++ = *input_pointer++; + } + /* escape sequence */ + else + { + unsigned char sequence_length = 2; + if ((input_end - input_pointer) < 1) + { + goto fail; + } + + switch (input_pointer[1]) + { + case 'b': + *output_pointer++ = '\b'; + break; + case 'f': + *output_pointer++ = '\f'; + break; + case 'n': + *output_pointer++ = '\n'; + break; + case 'r': + *output_pointer++ = '\r'; + break; + case 't': + *output_pointer++ = '\t'; + break; + case '\"': + case '\\': + case '/': + *output_pointer++ = input_pointer[1]; + break; + + /* UTF-16 literal */ + case 'u': + sequence_length = utf16_literal_to_utf8(input_pointer, input_end, &output_pointer); + if (sequence_length == 0) + { + /* failed to convert UTF16-literal to UTF-8 */ + goto fail; + } + break; + + default: + goto fail; + } + input_pointer += sequence_length; + } + } + + /* zero terminate the output */ + *output_pointer = '\0'; + + item->type = cJSON_String; + item->valuestring = (char*)output; + + input_buffer->offset = (size_t) (input_end - input_buffer->content); + input_buffer->offset++; + + return true; + +fail: + if (output != NULL) + { + input_buffer->hooks.deallocate(output); + output = NULL; + } + + if (input_pointer != NULL) + { + input_buffer->offset = (size_t)(input_pointer - input_buffer->content); + } + + return false; +} + +/* Render the cstring provided to an escaped version that can be printed. */ +static cJSON_bool print_string_ptr(const unsigned char * const input, printbuffer * const output_buffer) +{ + const unsigned char *input_pointer = NULL; + unsigned char *output = NULL; + unsigned char *output_pointer = NULL; + size_t output_length = 0; + /* numbers of additional characters needed for escaping */ + size_t escape_characters = 0; + + if (output_buffer == NULL) + { + return false; + } + + /* empty string */ + if (input == NULL) + { + output = ensure(output_buffer, sizeof("\"\"")); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "\"\""); + + return true; + } + + /* set "flag" to 1 if something needs to be escaped */ + for (input_pointer = input; *input_pointer; input_pointer++) + { + switch (*input_pointer) + { + case '\"': + case '\\': + case '\b': + case '\f': + case '\n': + case '\r': + case '\t': + /* one character escape sequence */ + escape_characters++; + break; + default: + if (*input_pointer < 32) + { + /* UTF-16 escape sequence uXXXX */ + escape_characters += 5; + } + break; + } + } + output_length = (size_t)(input_pointer - input) + escape_characters; + + output = ensure(output_buffer, output_length + sizeof("\"\"")); + if (output == NULL) + { + return false; + } + + /* no characters have to be escaped */ + if (escape_characters == 0) + { + output[0] = '\"'; + memcpy(output + 1, input, output_length); + output[output_length + 1] = '\"'; + output[output_length + 2] = '\0'; + + return true; + } + + output[0] = '\"'; + output_pointer = output + 1; + /* copy the string */ + for (input_pointer = input; *input_pointer != '\0'; (void)input_pointer++, output_pointer++) + { + if ((*input_pointer > 31) && (*input_pointer != '\"') && (*input_pointer != '\\')) + { + /* normal character, copy */ + *output_pointer = *input_pointer; + } + else + { + /* character needs to be escaped */ + *output_pointer++ = '\\'; + switch (*input_pointer) + { + case '\\': + *output_pointer = '\\'; + break; + case '\"': + *output_pointer = '\"'; + break; + case '\b': + *output_pointer = 'b'; + break; + case '\f': + *output_pointer = 'f'; + break; + case '\n': + *output_pointer = 'n'; + break; + case '\r': + *output_pointer = 'r'; + break; + case '\t': + *output_pointer = 't'; + break; + default: + /* escape and print as unicode codepoint */ + sprintf((char*)output_pointer, "u%04x", *input_pointer); + output_pointer += 4; + break; + } + } + } + output[output_length + 1] = '\"'; + output[output_length + 2] = '\0'; + + return true; +} + +/* Invoke print_string_ptr (which is useful) on an item. */ +static cJSON_bool print_string(const cJSON * const item, printbuffer * const p) +{ + return print_string_ptr((unsigned char*)item->valuestring, p); +} + +/* Predeclare these prototypes. */ +static cJSON_bool parse_value(cJSON * const item, parse_buffer * const input_buffer); +static cJSON_bool print_value(const cJSON * const item, printbuffer * const output_buffer); +static cJSON_bool parse_array(cJSON * const item, parse_buffer * const input_buffer); +static cJSON_bool print_array(const cJSON * const item, printbuffer * const output_buffer); +static cJSON_bool parse_object(cJSON * const item, parse_buffer * const input_buffer); +static cJSON_bool print_object(const cJSON * const item, printbuffer * const output_buffer); + +/* Utility to jump whitespace and cr/lf */ +static parse_buffer *buffer_skip_whitespace(parse_buffer * const buffer) +{ + if ((buffer == NULL) || (buffer->content == NULL)) + { + return NULL; + } + + if (cannot_access_at_index(buffer, 0)) + { + return buffer; + } + + while (can_access_at_index(buffer, 0) && (buffer_at_offset(buffer)[0] <= 32)) + { + buffer->offset++; + } + + if (buffer->offset == buffer->length) + { + buffer->offset--; + } + + return buffer; +} + +/* skip the UTF-8 BOM (byte order mark) if it is at the beginning of a buffer */ +static parse_buffer *skip_utf8_bom(parse_buffer * const buffer) +{ + if ((buffer == NULL) || (buffer->content == NULL) || (buffer->offset != 0)) + { + return NULL; + } + + if (can_access_at_index(buffer, 4) && (strncmp((const char*)buffer_at_offset(buffer), "\xEF\xBB\xBF", 3) == 0)) + { + buffer->offset += 3; + } + + return buffer; +} + +CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated) +{ + size_t buffer_length; + + if (NULL == value) + { + return NULL; + } + + /* Adding null character size due to require_null_terminated. */ + buffer_length = strlen(value) + sizeof(""); + + return cJSON_ParseWithLengthOpts(value, buffer_length, return_parse_end, require_null_terminated); +} + +/* Parse an object - create a new root, and populate. */ +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated) +{ + parse_buffer buffer = { 0, 0, 0, 0, { 0, 0, 0 } }; + cJSON *item = NULL; + + /* reset error position */ + global_error.json = NULL; + global_error.position = 0; + + if (value == NULL || 0 == buffer_length) + { + goto fail; + } + + buffer.content = (const unsigned char*)value; + buffer.length = buffer_length; + buffer.offset = 0; + buffer.hooks = global_hooks; + + item = cJSON_New_Item(&global_hooks); + if (item == NULL) /* memory fail */ + { + goto fail; + } + + if (!parse_value(item, buffer_skip_whitespace(skip_utf8_bom(&buffer)))) + { + /* parse failure. ep is set. */ + goto fail; + } + + /* if we require null-terminated JSON without appended garbage, skip and then check for a null terminator */ + if (require_null_terminated) + { + buffer_skip_whitespace(&buffer); + if ((buffer.offset >= buffer.length) || buffer_at_offset(&buffer)[0] != '\0') + { + goto fail; + } + } + if (return_parse_end) + { + *return_parse_end = (const char*)buffer_at_offset(&buffer); + } + + return item; + +fail: + if (item != NULL) + { + cJSON_Delete(item); + } + + if (value != NULL) + { + error local_error; + local_error.json = (const unsigned char*)value; + local_error.position = 0; + + if (buffer.offset < buffer.length) + { + local_error.position = buffer.offset; + } + else if (buffer.length > 0) + { + local_error.position = buffer.length - 1; + } + + if (return_parse_end != NULL) + { + *return_parse_end = (const char*)local_error.json + local_error.position; + } + + global_error = local_error; + } + + return NULL; +} + +/* Default options for cJSON_Parse */ +CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value) +{ + return cJSON_ParseWithOpts(value, 0, 0); +} + +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length) +{ + return cJSON_ParseWithLengthOpts(value, buffer_length, 0, 0); +} + +#define cjson_min(a, b) (((a) < (b)) ? (a) : (b)) + +static unsigned char *print(const cJSON * const item, cJSON_bool format, const internal_hooks * const hooks) +{ + static const size_t default_buffer_size = 256; + printbuffer buffer[1]; + unsigned char *printed = NULL; + + memset(buffer, 0, sizeof(buffer)); + + /* create buffer */ + buffer->buffer = (unsigned char*) hooks->allocate(default_buffer_size); + buffer->length = default_buffer_size; + buffer->format = format; + buffer->hooks = *hooks; + if (buffer->buffer == NULL) + { + goto fail; + } + + /* print the value */ + if (!print_value(item, buffer)) + { + goto fail; + } + update_offset(buffer); + + /* check if reallocate is available */ + if (hooks->reallocate != NULL) + { + printed = (unsigned char*) hooks->reallocate(buffer->buffer, buffer->offset + 1); + if (printed == NULL) { + goto fail; + } + buffer->buffer = NULL; + } + else /* otherwise copy the JSON over to a new buffer */ + { + printed = (unsigned char*) hooks->allocate(buffer->offset + 1); + if (printed == NULL) + { + goto fail; + } + memcpy(printed, buffer->buffer, cjson_min(buffer->length, buffer->offset + 1)); + printed[buffer->offset] = '\0'; /* just to be sure */ + + /* free the buffer */ + hooks->deallocate(buffer->buffer); + buffer->buffer = NULL; + } + + return printed; + +fail: + if (buffer->buffer != NULL) + { + hooks->deallocate(buffer->buffer); + buffer->buffer = NULL; + } + + if (printed != NULL) + { + hooks->deallocate(printed); + printed = NULL; + } + + return NULL; +} + +/* Render a cJSON item/entity/structure to text. */ +CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item) +{ + return (char*)print(item, true, &global_hooks); +} + +CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item) +{ + return (char*)print(item, false, &global_hooks); +} + +CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt) +{ + printbuffer p = { 0, 0, 0, 0, 0, 0, { 0, 0, 0 } }; + + if (prebuffer < 0) + { + return NULL; + } + + p.buffer = (unsigned char*)global_hooks.allocate((size_t)prebuffer); + if (!p.buffer) + { + return NULL; + } + + p.length = (size_t)prebuffer; + p.offset = 0; + p.noalloc = false; + p.format = fmt; + p.hooks = global_hooks; + + if (!print_value(item, &p)) + { + global_hooks.deallocate(p.buffer); + p.buffer = NULL; + return NULL; + } + + return (char*)p.buffer; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format) +{ + printbuffer p = { 0, 0, 0, 0, 0, 0, { 0, 0, 0 } }; + + if ((length < 0) || (buffer == NULL)) + { + return false; + } + + p.buffer = (unsigned char*)buffer; + p.length = (size_t)length; + p.offset = 0; + p.noalloc = true; + p.format = format; + p.hooks = global_hooks; + + return print_value(item, &p); +} + +/* Parser core - when encountering text, process appropriately. */ +static cJSON_bool parse_value(cJSON * const item, parse_buffer * const input_buffer) +{ + if ((input_buffer == NULL) || (input_buffer->content == NULL)) + { + return false; /* no input */ + } + + /* parse the different types of values */ + /* null */ + if (can_read(input_buffer, 4) && (strncmp((const char*)buffer_at_offset(input_buffer), "null", 4) == 0)) + { + item->type = cJSON_NULL; + input_buffer->offset += 4; + return true; + } + /* false */ + if (can_read(input_buffer, 5) && (strncmp((const char*)buffer_at_offset(input_buffer), "false", 5) == 0)) + { + item->type = cJSON_False; + input_buffer->offset += 5; + return true; + } + /* true */ + if (can_read(input_buffer, 4) && (strncmp((const char*)buffer_at_offset(input_buffer), "true", 4) == 0)) + { + item->type = cJSON_True; + item->valueint = 1; + input_buffer->offset += 4; + return true; + } + /* string */ + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '\"')) + { + return parse_string(item, input_buffer); + } + /* number */ + if (can_access_at_index(input_buffer, 0) && ((buffer_at_offset(input_buffer)[0] == '-') || ((buffer_at_offset(input_buffer)[0] >= '0') && (buffer_at_offset(input_buffer)[0] <= '9')))) + { + return parse_number(item, input_buffer); + } + /* array */ + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '[')) + { + return parse_array(item, input_buffer); + } + /* object */ + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '{')) + { + return parse_object(item, input_buffer); + } + + return false; +} + +/* Render a value to text. */ +static cJSON_bool print_value(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output = NULL; + + if ((item == NULL) || (output_buffer == NULL)) + { + return false; + } + + switch ((item->type) & 0xFF) + { + case cJSON_NULL: + output = ensure(output_buffer, 5); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "null"); + return true; + + case cJSON_False: + output = ensure(output_buffer, 6); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "false"); + return true; + + case cJSON_True: + output = ensure(output_buffer, 5); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "true"); + return true; + + case cJSON_Number: + return print_number(item, output_buffer); + + case cJSON_Raw: + { + size_t raw_length = 0; + if (item->valuestring == NULL) + { + return false; + } + + raw_length = strlen(item->valuestring) + sizeof(""); + output = ensure(output_buffer, raw_length); + if (output == NULL) + { + return false; + } + memcpy(output, item->valuestring, raw_length); + return true; + } + + case cJSON_String: + return print_string(item, output_buffer); + + case cJSON_Array: + return print_array(item, output_buffer); + + case cJSON_Object: + return print_object(item, output_buffer); + + default: + return false; + } +} + +/* Build an array from input text. */ +static cJSON_bool parse_array(cJSON * const item, parse_buffer * const input_buffer) +{ + cJSON *head = NULL; /* head of the linked list */ + cJSON *current_item = NULL; + + if (input_buffer->depth >= CJSON_NESTING_LIMIT) + { + return false; /* to deeply nested */ + } + input_buffer->depth++; + + if (buffer_at_offset(input_buffer)[0] != '[') + { + /* not an array */ + goto fail; + } + + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ']')) + { + /* empty array */ + goto success; + } + + /* check if we skipped to the end of the buffer */ + if (cannot_access_at_index(input_buffer, 0)) + { + input_buffer->offset--; + goto fail; + } + + /* step back to character in front of the first element */ + input_buffer->offset--; + /* loop through the comma separated array elements */ + do + { + /* allocate next item */ + cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks)); + if (new_item == NULL) + { + goto fail; /* allocation failure */ + } + + /* attach next item to list */ + if (head == NULL) + { + /* start the linked list */ + current_item = head = new_item; + } + else + { + /* add to the end and advance */ + current_item->next = new_item; + new_item->prev = current_item; + current_item = new_item; + } + + /* parse next value */ + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (!parse_value(current_item, input_buffer)) + { + goto fail; /* failed to parse value */ + } + buffer_skip_whitespace(input_buffer); + } + while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ',')); + + if (cannot_access_at_index(input_buffer, 0) || buffer_at_offset(input_buffer)[0] != ']') + { + goto fail; /* expected end of array */ + } + +success: + input_buffer->depth--; + + if (head != NULL) { + head->prev = current_item; + } + + item->type = cJSON_Array; + item->child = head; + + input_buffer->offset++; + + return true; + +fail: + if (head != NULL) + { + cJSON_Delete(head); + } + + return false; +} + +/* Render an array to text */ +static cJSON_bool print_array(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output_pointer = NULL; + size_t length = 0; + cJSON *current_element = item->child; + + if (output_buffer == NULL) + { + return false; + } + + /* Compose the output array. */ + /* opening square bracket */ + output_pointer = ensure(output_buffer, 1); + if (output_pointer == NULL) + { + return false; + } + + *output_pointer = '['; + output_buffer->offset++; + output_buffer->depth++; + + while (current_element != NULL) + { + if (!print_value(current_element, output_buffer)) + { + return false; + } + update_offset(output_buffer); + if (current_element->next) + { + length = (size_t) (output_buffer->format ? 2 : 1); + output_pointer = ensure(output_buffer, length + 1); + if (output_pointer == NULL) + { + return false; + } + *output_pointer++ = ','; + if(output_buffer->format) + { + *output_pointer++ = ' '; + } + *output_pointer = '\0'; + output_buffer->offset += length; + } + current_element = current_element->next; + } + + output_pointer = ensure(output_buffer, 2); + if (output_pointer == NULL) + { + return false; + } + *output_pointer++ = ']'; + *output_pointer = '\0'; + output_buffer->depth--; + + return true; +} + +/* Build an object from the text. */ +static cJSON_bool parse_object(cJSON * const item, parse_buffer * const input_buffer) +{ + cJSON *head = NULL; /* linked list head */ + cJSON *current_item = NULL; + + if (input_buffer->depth >= CJSON_NESTING_LIMIT) + { + return false; /* to deeply nested */ + } + input_buffer->depth++; + + if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '{')) + { + goto fail; /* not an object */ + } + + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '}')) + { + goto success; /* empty object */ + } + + /* check if we skipped to the end of the buffer */ + if (cannot_access_at_index(input_buffer, 0)) + { + input_buffer->offset--; + goto fail; + } + + /* step back to character in front of the first element */ + input_buffer->offset--; + /* loop through the comma separated array elements */ + do + { + /* allocate next item */ + cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks)); + if (new_item == NULL) + { + goto fail; /* allocation failure */ + } + + /* attach next item to list */ + if (head == NULL) + { + /* start the linked list */ + current_item = head = new_item; + } + else + { + /* add to the end and advance */ + current_item->next = new_item; + new_item->prev = current_item; + current_item = new_item; + } + + if (cannot_access_at_index(input_buffer, 1)) + { + goto fail; /* nothing comes after the comma */ + } + + /* parse the name of the child */ + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (!parse_string(current_item, input_buffer)) + { + goto fail; /* failed to parse name */ + } + buffer_skip_whitespace(input_buffer); + + /* swap valuestring and string, because we parsed the name */ + current_item->string = current_item->valuestring; + current_item->valuestring = NULL; + + if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != ':')) + { + goto fail; /* invalid object */ + } + + /* parse the value */ + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (!parse_value(current_item, input_buffer)) + { + goto fail; /* failed to parse value */ + } + buffer_skip_whitespace(input_buffer); + } + while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ',')); + + if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '}')) + { + goto fail; /* expected end of object */ + } + +success: + input_buffer->depth--; + + if (head != NULL) { + head->prev = current_item; + } + + item->type = cJSON_Object; + item->child = head; + + input_buffer->offset++; + return true; + +fail: + if (head != NULL) + { + cJSON_Delete(head); + } + + return false; +} + +/* Render an object to text. */ +static cJSON_bool print_object(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output_pointer = NULL; + size_t length = 0; + cJSON *current_item = item->child; + + if (output_buffer == NULL) + { + return false; + } + + /* Compose the output: */ + length = (size_t) (output_buffer->format ? 2 : 1); /* fmt: {\n */ + output_pointer = ensure(output_buffer, length + 1); + if (output_pointer == NULL) + { + return false; + } + + *output_pointer++ = '{'; + output_buffer->depth++; + if (output_buffer->format) + { + *output_pointer++ = '\n'; + } + output_buffer->offset += length; + + while (current_item) + { + if (output_buffer->format) + { + size_t i; + output_pointer = ensure(output_buffer, output_buffer->depth); + if (output_pointer == NULL) + { + return false; + } + for (i = 0; i < output_buffer->depth; i++) + { + *output_pointer++ = '\t'; + } + output_buffer->offset += output_buffer->depth; + } + + /* print key */ + if (!print_string_ptr((unsigned char*)current_item->string, output_buffer)) + { + return false; + } + update_offset(output_buffer); + + length = (size_t) (output_buffer->format ? 2 : 1); + output_pointer = ensure(output_buffer, length); + if (output_pointer == NULL) + { + return false; + } + *output_pointer++ = ':'; + if (output_buffer->format) + { + *output_pointer++ = '\t'; + } + output_buffer->offset += length; + + /* print value */ + if (!print_value(current_item, output_buffer)) + { + return false; + } + update_offset(output_buffer); + + /* print comma if not last */ + length = ((size_t)(output_buffer->format ? 1 : 0) + (size_t)(current_item->next ? 1 : 0)); + output_pointer = ensure(output_buffer, length + 1); + if (output_pointer == NULL) + { + return false; + } + if (current_item->next) + { + *output_pointer++ = ','; + } + + if (output_buffer->format) + { + *output_pointer++ = '\n'; + } + *output_pointer = '\0'; + output_buffer->offset += length; + + current_item = current_item->next; + } + + output_pointer = ensure(output_buffer, output_buffer->format ? (output_buffer->depth + 1) : 2); + if (output_pointer == NULL) + { + return false; + } + if (output_buffer->format) + { + size_t i; + for (i = 0; i < (output_buffer->depth - 1); i++) + { + *output_pointer++ = '\t'; + } + } + *output_pointer++ = '}'; + *output_pointer = '\0'; + output_buffer->depth--; + + return true; +} + +/* Get Array size/item / object item. */ +CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array) +{ + cJSON *child = NULL; + size_t size = 0; + + if (array == NULL) + { + return 0; + } + + child = array->child; + + while(child != NULL) + { + size++; + child = child->next; + } + + /* FIXME: Can overflow here. Cannot be fixed without breaking the API */ + + return (int)size; +} + +static cJSON* get_array_item(const cJSON *array, size_t index) +{ + cJSON *current_child = NULL; + + if (array == NULL) + { + return NULL; + } + + current_child = array->child; + while ((current_child != NULL) && (index > 0)) + { + index--; + current_child = current_child->next; + } + + return current_child; +} + +CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index) +{ + if (index < 0) + { + return NULL; + } + + return get_array_item(array, (size_t)index); +} + +static cJSON *get_object_item(const cJSON * const object, const char * const name, const cJSON_bool case_sensitive) +{ + cJSON *current_element = NULL; + + if ((object == NULL) || (name == NULL)) + { + return NULL; + } + + current_element = object->child; + if (case_sensitive) + { + while ((current_element != NULL) && (current_element->string != NULL) && (strcmp(name, current_element->string) != 0)) + { + current_element = current_element->next; + } + } + else + { + while ((current_element != NULL) && (case_insensitive_strcmp((const unsigned char*)name, (const unsigned char*)(current_element->string)) != 0)) + { + current_element = current_element->next; + } + } + + if ((current_element == NULL) || (current_element->string == NULL)) { + return NULL; + } + + return current_element; +} + +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string) +{ + return get_object_item(object, string, false); +} + +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string) +{ + return get_object_item(object, string, true); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string) +{ + return cJSON_GetObjectItem(object, string) ? 1 : 0; +} + +/* Utility for array list handling. */ +static void suffix_object(cJSON *prev, cJSON *item) +{ + prev->next = item; + item->prev = prev; +} + +/* Utility for handling references. */ +static cJSON *create_reference(const cJSON *item, const internal_hooks * const hooks) +{ + cJSON *reference = NULL; + if (item == NULL) + { + return NULL; + } + + reference = cJSON_New_Item(hooks); + if (reference == NULL) + { + return NULL; + } + + memcpy(reference, item, sizeof(cJSON)); + reference->string = NULL; + reference->type |= cJSON_IsReference; + reference->next = reference->prev = NULL; + return reference; +} + +static cJSON_bool add_item_to_array(cJSON *array, cJSON *item) +{ + cJSON *child = NULL; + + if ((item == NULL) || (array == NULL) || (array == item)) + { + return false; + } + + child = array->child; + /* + * To find the last item in array quickly, we use prev in array + */ + if (child == NULL) + { + /* list is empty, start new one */ + array->child = item; + item->prev = item; + item->next = NULL; + } + else + { + /* append to the end */ + if (child->prev) + { + suffix_object(child->prev, item); + array->child->prev = item; + } + } + + return true; +} + +/* Add item to array/object. */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item) +{ + return add_item_to_array(array, item); +} + +#if defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5)))) + #pragma GCC diagnostic push +#endif +#ifdef __GNUC__ +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif +/* helper function to cast away const */ +static void* cast_away_const(const void* string) +{ + return (void*)string; +} +#if defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5)))) + #pragma GCC diagnostic pop +#endif + + +static cJSON_bool add_item_to_object(cJSON * const object, const char * const string, cJSON * const item, const internal_hooks * const hooks, const cJSON_bool constant_key) +{ + char *new_key = NULL; + int new_type = cJSON_Invalid; + + if ((object == NULL) || (string == NULL) || (item == NULL) || (object == item)) + { + return false; + } + + if (constant_key) + { + new_key = (char*)cast_away_const(string); + new_type = item->type | cJSON_StringIsConst; + } + else + { + new_key = (char*)cJSON_strdup((const unsigned char*)string, hooks); + if (new_key == NULL) + { + return false; + } + + new_type = item->type & ~cJSON_StringIsConst; + } + + if (!(item->type & cJSON_StringIsConst) && (item->string != NULL)) + { + hooks->deallocate(item->string); + } + + item->string = new_key; + item->type = new_type; + + return add_item_to_array(object, item); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item) +{ + return add_item_to_object(object, string, item, &global_hooks, false); +} + +/* Add an item to an object with constant string as key */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item) +{ + return add_item_to_object(object, string, item, &global_hooks, true); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item) +{ + if (array == NULL) + { + return false; + } + + return add_item_to_array(array, create_reference(item, &global_hooks)); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item) +{ + if ((object == NULL) || (string == NULL)) + { + return false; + } + + return add_item_to_object(object, string, create_reference(item, &global_hooks), &global_hooks, false); +} + +CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name) +{ + cJSON *null = cJSON_CreateNull(); + if (add_item_to_object(object, name, null, &global_hooks, false)) + { + return null; + } + + cJSON_Delete(null); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name) +{ + cJSON *true_item = cJSON_CreateTrue(); + if (add_item_to_object(object, name, true_item, &global_hooks, false)) + { + return true_item; + } + + cJSON_Delete(true_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name) +{ + cJSON *false_item = cJSON_CreateFalse(); + if (add_item_to_object(object, name, false_item, &global_hooks, false)) + { + return false_item; + } + + cJSON_Delete(false_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean) +{ + cJSON *bool_item = cJSON_CreateBool(boolean); + if (add_item_to_object(object, name, bool_item, &global_hooks, false)) + { + return bool_item; + } + + cJSON_Delete(bool_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number) +{ + cJSON *number_item = cJSON_CreateNumber(number); + if (add_item_to_object(object, name, number_item, &global_hooks, false)) + { + return number_item; + } + + cJSON_Delete(number_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string) +{ + cJSON *string_item = cJSON_CreateString(string); + if (add_item_to_object(object, name, string_item, &global_hooks, false)) + { + return string_item; + } + + cJSON_Delete(string_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw) +{ + cJSON *raw_item = cJSON_CreateRaw(raw); + if (add_item_to_object(object, name, raw_item, &global_hooks, false)) + { + return raw_item; + } + + cJSON_Delete(raw_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name) +{ + cJSON *object_item = cJSON_CreateObject(); + if (add_item_to_object(object, name, object_item, &global_hooks, false)) + { + return object_item; + } + + cJSON_Delete(object_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name) +{ + cJSON *array = cJSON_CreateArray(); + if (add_item_to_object(object, name, array, &global_hooks, false)) + { + return array; + } + + cJSON_Delete(array); + return NULL; +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item) +{ + if ((parent == NULL) || (item == NULL)) + { + return NULL; + } + + if (item != parent->child) + { + /* not the first element */ + item->prev->next = item->next; + } + if (item->next != NULL) + { + /* not the last element */ + item->next->prev = item->prev; + } + + if (item == parent->child) + { + /* first element */ + parent->child = item->next; + } + else if (item->next == NULL) + { + /* last element */ + parent->child->prev = item->prev; + } + + /* make sure the detached item doesn't point anywhere anymore */ + item->prev = NULL; + item->next = NULL; + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which) +{ + if (which < 0) + { + return NULL; + } + + return cJSON_DetachItemViaPointer(array, get_array_item(array, (size_t)which)); +} + +CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which) +{ + cJSON_Delete(cJSON_DetachItemFromArray(array, which)); +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string) +{ + cJSON *to_detach = cJSON_GetObjectItem(object, string); + + return cJSON_DetachItemViaPointer(object, to_detach); +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string) +{ + cJSON *to_detach = cJSON_GetObjectItemCaseSensitive(object, string); + + return cJSON_DetachItemViaPointer(object, to_detach); +} + +CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string) +{ + cJSON_Delete(cJSON_DetachItemFromObject(object, string)); +} + +CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string) +{ + cJSON_Delete(cJSON_DetachItemFromObjectCaseSensitive(object, string)); +} + +/* Replace array/object items with new ones. */ +CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem) +{ + cJSON *after_inserted = NULL; + + if (which < 0 || newitem == NULL) + { + return false; + } + + after_inserted = get_array_item(array, (size_t)which); + if (after_inserted == NULL) + { + return add_item_to_array(array, newitem); + } + + if (after_inserted != array->child && after_inserted->prev == NULL) { + /* return false if after_inserted is a corrupted array item */ + return false; + } + + newitem->next = after_inserted; + newitem->prev = after_inserted->prev; + after_inserted->prev = newitem; + if (after_inserted == array->child) + { + array->child = newitem; + } + else + { + newitem->prev->next = newitem; + } + return true; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement) +{ + if ((parent == NULL) || (parent->child == NULL) || (replacement == NULL) || (item == NULL)) + { + return false; + } + + if (replacement == item) + { + return true; + } + + replacement->next = item->next; + replacement->prev = item->prev; + + if (replacement->next != NULL) + { + replacement->next->prev = replacement; + } + if (parent->child == item) + { + if (parent->child->prev == parent->child) + { + replacement->prev = replacement; + } + parent->child = replacement; + } + else + { /* + * To find the last item in array quickly, we use prev in array. + * We can't modify the last item's next pointer where this item was the parent's child + */ + if (replacement->prev != NULL) + { + replacement->prev->next = replacement; + } + if (replacement->next == NULL) + { + parent->child->prev = replacement; + } + } + + item->next = NULL; + item->prev = NULL; + cJSON_Delete(item); + + return true; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem) +{ + if (which < 0) + { + return false; + } + + return cJSON_ReplaceItemViaPointer(array, get_array_item(array, (size_t)which), newitem); +} + +static cJSON_bool replace_item_in_object(cJSON *object, const char *string, cJSON *replacement, cJSON_bool case_sensitive) +{ + if ((replacement == NULL) || (string == NULL)) + { + return false; + } + + /* replace the name in the replacement */ + if (!(replacement->type & cJSON_StringIsConst) && (replacement->string != NULL)) + { + cJSON_free(replacement->string); + } + replacement->string = (char*)cJSON_strdup((const unsigned char*)string, &global_hooks); + if (replacement->string == NULL) + { + return false; + } + + replacement->type &= ~cJSON_StringIsConst; + + return cJSON_ReplaceItemViaPointer(object, get_object_item(object, string, case_sensitive), replacement); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object, const char *string, cJSON *newitem) +{ + return replace_item_in_object(object, string, newitem, false); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object, const char *string, cJSON *newitem) +{ + return replace_item_in_object(object, string, newitem, true); +} + +/* Create basic types: */ +CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_NULL; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_True; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_False; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = boolean ? cJSON_True : cJSON_False; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_Number; + item->valuedouble = num; + + /* use saturation in case of overflow */ + if (num >= INT_MAX) + { + item->valueint = INT_MAX; + } + else if (num <= (double)INT_MIN) + { + item->valueint = INT_MIN; + } + else + { + item->valueint = (int)num; + } + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_String; + item->valuestring = (char*)cJSON_strdup((const unsigned char*)string, &global_hooks); + if(!item->valuestring) + { + cJSON_Delete(item); + return NULL; + } + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item != NULL) + { + item->type = cJSON_String | cJSON_IsReference; + item->valuestring = (char*)cast_away_const(string); + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item != NULL) { + item->type = cJSON_Object | cJSON_IsReference; + item->child = (cJSON*)cast_away_const(child); + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child) { + cJSON *item = cJSON_New_Item(&global_hooks); + if (item != NULL) { + item->type = cJSON_Array | cJSON_IsReference; + item->child = (cJSON*)cast_away_const(child); + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_Raw; + item->valuestring = (char*)cJSON_strdup((const unsigned char*)raw, &global_hooks); + if(!item->valuestring) + { + cJSON_Delete(item); + return NULL; + } + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type=cJSON_Array; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item) + { + item->type = cJSON_Object; + } + + return item; +} + +/* Create Arrays: */ +CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (numbers == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for(i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateNumber(numbers[i]); + if (!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (numbers == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for(i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateNumber((double)numbers[i]); + if(!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (numbers == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for(i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateNumber(numbers[i]); + if(!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (strings == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for (i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateString(strings[i]); + if(!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p,n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +/* Duplication */ +CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse) +{ + cJSON *newitem = NULL; + cJSON *child = NULL; + cJSON *next = NULL; + cJSON *newchild = NULL; + + /* Bail on bad ptr */ + if (!item) + { + goto fail; + } + /* Create new item */ + newitem = cJSON_New_Item(&global_hooks); + if (!newitem) + { + goto fail; + } + /* Copy over all vars */ + newitem->type = item->type & (~cJSON_IsReference); + newitem->valueint = item->valueint; + newitem->valuedouble = item->valuedouble; + if (item->valuestring) + { + newitem->valuestring = (char*)cJSON_strdup((unsigned char*)item->valuestring, &global_hooks); + if (!newitem->valuestring) + { + goto fail; + } + } + if (item->string) + { + newitem->string = (item->type&cJSON_StringIsConst) ? item->string : (char*)cJSON_strdup((unsigned char*)item->string, &global_hooks); + if (!newitem->string) + { + goto fail; + } + } + /* If non-recursive, then we're done! */ + if (!recurse) + { + return newitem; + } + /* Walk the ->next chain for the child. */ + child = item->child; + while (child != NULL) + { + newchild = cJSON_Duplicate(child, true); /* Duplicate (with recurse) each item in the ->next chain */ + if (!newchild) + { + goto fail; + } + if (next != NULL) + { + /* If newitem->child already set, then crosswire ->prev and ->next and move on */ + next->next = newchild; + newchild->prev = next; + next = newchild; + } + else + { + /* Set newitem->child and move to it */ + newitem->child = newchild; + next = newchild; + } + child = child->next; + } + if (newitem && newitem->child) + { + newitem->child->prev = newchild; + } + + return newitem; + +fail: + if (newitem != NULL) + { + cJSON_Delete(newitem); + } + + return NULL; +} + +static void skip_oneline_comment(char **input) +{ + *input += static_strlen("//"); + + for (; (*input)[0] != '\0'; ++(*input)) + { + if ((*input)[0] == '\n') { + *input += static_strlen("\n"); + return; + } + } +} + +static void skip_multiline_comment(char **input) +{ + *input += static_strlen("/*"); + + for (; (*input)[0] != '\0'; ++(*input)) + { + if (((*input)[0] == '*') && ((*input)[1] == '/')) + { + *input += static_strlen("*/"); + return; + } + } +} + +static void minify_string(char **input, char **output) { + (*output)[0] = (*input)[0]; + *input += static_strlen("\""); + *output += static_strlen("\""); + + + for (; (*input)[0] != '\0'; (void)++(*input), ++(*output)) { + (*output)[0] = (*input)[0]; + + if ((*input)[0] == '\"') { + (*output)[0] = '\"'; + *input += static_strlen("\""); + *output += static_strlen("\""); + return; + } else if (((*input)[0] == '\\') && ((*input)[1] == '\"')) { + (*output)[1] = (*input)[1]; + *input += static_strlen("\""); + *output += static_strlen("\""); + } + } +} + +CJSON_PUBLIC(void) cJSON_Minify(char *json) +{ + char *into = json; + + if (json == NULL) + { + return; + } + + while (json[0] != '\0') + { + switch (json[0]) + { + case ' ': + case '\t': + case '\r': + case '\n': + json++; + break; + + case '/': + if (json[1] == '/') + { + skip_oneline_comment(&json); + } + else if (json[1] == '*') + { + skip_multiline_comment(&json); + } else { + json++; + } + break; + + case '\"': + minify_string(&json, (char**)&into); + break; + + default: + into[0] = json[0]; + json++; + into++; + } + } + + /* and null-terminate. */ + *into = '\0'; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Invalid; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_False; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xff) == cJSON_True; +} + + +CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & (cJSON_True | cJSON_False)) != 0; +} +CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_NULL; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Number; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_String; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Array; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Object; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Raw; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive) +{ + if ((a == NULL) || (b == NULL) || ((a->type & 0xFF) != (b->type & 0xFF))) + { + return false; + } + + /* check if type is valid */ + switch (a->type & 0xFF) + { + case cJSON_False: + case cJSON_True: + case cJSON_NULL: + case cJSON_Number: + case cJSON_String: + case cJSON_Raw: + case cJSON_Array: + case cJSON_Object: + break; + + default: + return false; + } + + /* identical objects are equal */ + if (a == b) + { + return true; + } + + switch (a->type & 0xFF) + { + /* in these cases and equal type is enough */ + case cJSON_False: + case cJSON_True: + case cJSON_NULL: + return true; + + case cJSON_Number: + if (compare_double(a->valuedouble, b->valuedouble)) + { + return true; + } + return false; + + case cJSON_String: + case cJSON_Raw: + if ((a->valuestring == NULL) || (b->valuestring == NULL)) + { + return false; + } + if (strcmp(a->valuestring, b->valuestring) == 0) + { + return true; + } + + return false; + + case cJSON_Array: + { + cJSON *a_element = a->child; + cJSON *b_element = b->child; + + for (; (a_element != NULL) && (b_element != NULL);) + { + if (!cJSON_Compare(a_element, b_element, case_sensitive)) + { + return false; + } + + a_element = a_element->next; + b_element = b_element->next; + } + + /* one of the arrays is longer than the other */ + if (a_element != b_element) { + return false; + } + + return true; + } + + case cJSON_Object: + { + cJSON *a_element = NULL; + cJSON *b_element = NULL; + cJSON_ArrayForEach(a_element, a) + { + /* TODO This has O(n^2) runtime, which is horrible! */ + b_element = get_object_item(b, a_element->string, case_sensitive); + if (b_element == NULL) + { + return false; + } + + if (!cJSON_Compare(a_element, b_element, case_sensitive)) + { + return false; + } + } + + /* doing this twice, once on a and b to prevent true comparison if a subset of b + * TODO: Do this the proper way, this is just a fix for now */ + cJSON_ArrayForEach(b_element, b) + { + a_element = get_object_item(a, b_element->string, case_sensitive); + if (a_element == NULL) + { + return false; + } + + if (!cJSON_Compare(b_element, a_element, case_sensitive)) + { + return false; + } + } + + return true; + } + + default: + return false; + } +} + +CJSON_PUBLIC(void *) cJSON_malloc(size_t size) +{ + return global_hooks.allocate(size); +} + +CJSON_PUBLIC(void) cJSON_free(void *object) +{ + global_hooks.deallocate(object); + object = NULL; +} diff --git a/src/external/cJSON.h b/src/external/cJSON.h new file mode 100644 index 0000000..88cf0bc --- /dev/null +++ b/src/external/cJSON.h @@ -0,0 +1,300 @@ +/* + Copyright (c) 2009-2017 Dave Gamble and cJSON contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +#ifndef cJSON__h +#define cJSON__h + +#ifdef __cplusplus +extern "C" +{ +#endif + +#if !defined(__WINDOWS__) && (defined(WIN32) || defined(WIN64) || defined(_MSC_VER) || defined(_WIN32)) +#define __WINDOWS__ +#endif + +#ifdef __WINDOWS__ + +/* When compiling for windows, we specify a specific calling convention to avoid issues where we are being called from a project with a different default calling convention. For windows you have 3 define options: + +CJSON_HIDE_SYMBOLS - Define this in the case where you don't want to ever dllexport symbols +CJSON_EXPORT_SYMBOLS - Define this on library build when you want to dllexport symbols (default) +CJSON_IMPORT_SYMBOLS - Define this if you want to dllimport symbol + +For *nix builds that support visibility attribute, you can define similar behavior by + +setting default visibility to hidden by adding +-fvisibility=hidden (for gcc) +or +-xldscope=hidden (for sun cc) +to CFLAGS + +then using the CJSON_API_VISIBILITY flag to "export" the same symbols the way CJSON_EXPORT_SYMBOLS does + +*/ + +#define CJSON_CDECL __cdecl +#define CJSON_STDCALL __stdcall + +/* export symbols by default, this is necessary for copy pasting the C and header file */ +#if !defined(CJSON_HIDE_SYMBOLS) && !defined(CJSON_IMPORT_SYMBOLS) && !defined(CJSON_EXPORT_SYMBOLS) +#define CJSON_EXPORT_SYMBOLS +#endif + +#if defined(CJSON_HIDE_SYMBOLS) +#define CJSON_PUBLIC(type) type CJSON_STDCALL +#elif defined(CJSON_EXPORT_SYMBOLS) +#define CJSON_PUBLIC(type) __declspec(dllexport) type CJSON_STDCALL +#elif defined(CJSON_IMPORT_SYMBOLS) +#define CJSON_PUBLIC(type) __declspec(dllimport) type CJSON_STDCALL +#endif +#else /* !__WINDOWS__ */ +#define CJSON_CDECL +#define CJSON_STDCALL + +#if (defined(__GNUC__) || defined(__SUNPRO_CC) || defined (__SUNPRO_C)) && defined(CJSON_API_VISIBILITY) +#define CJSON_PUBLIC(type) __attribute__((visibility("default"))) type +#else +#define CJSON_PUBLIC(type) type +#endif +#endif + +/* project version */ +#define CJSON_VERSION_MAJOR 1 +#define CJSON_VERSION_MINOR 7 +#define CJSON_VERSION_PATCH 18 + +#include + +/* cJSON Types: */ +#define cJSON_Invalid (0) +#define cJSON_False (1 << 0) +#define cJSON_True (1 << 1) +#define cJSON_NULL (1 << 2) +#define cJSON_Number (1 << 3) +#define cJSON_String (1 << 4) +#define cJSON_Array (1 << 5) +#define cJSON_Object (1 << 6) +#define cJSON_Raw (1 << 7) /* raw json */ + +#define cJSON_IsReference 256 +#define cJSON_StringIsConst 512 + +/* The cJSON structure: */ +typedef struct cJSON +{ + /* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */ + struct cJSON *next; + struct cJSON *prev; + /* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */ + struct cJSON *child; + + /* The type of the item, as above. */ + int type; + + /* The item's string, if type==cJSON_String and type == cJSON_Raw */ + char *valuestring; + /* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */ + int valueint; + /* The item's number, if type==cJSON_Number */ + double valuedouble; + + /* The item's name string, if this item is the child of, or is in the list of subitems of an object. */ + char *string; +} cJSON; + +typedef struct cJSON_Hooks +{ + /* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */ + void *(CJSON_CDECL *malloc_fn)(size_t sz); + void (CJSON_CDECL *free_fn)(void *ptr); +} cJSON_Hooks; + +typedef int cJSON_bool; + +/* Limits how deeply nested arrays/objects can be before cJSON rejects to parse them. + * This is to prevent stack overflows. */ +#ifndef CJSON_NESTING_LIMIT +#define CJSON_NESTING_LIMIT 1000 +#endif + +/* returns the version of cJSON as a string */ +CJSON_PUBLIC(const char*) cJSON_Version(void); + +/* Supply malloc, realloc and free functions to cJSON */ +CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks); + +/* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */ +/* Supply a block of JSON, and this returns a cJSON object you can interrogate. */ +CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value); +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length); +/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */ +/* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */ +CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated); +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated); + +/* Render a cJSON entity to text for transfer/storage. */ +CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item); +/* Render a cJSON entity to text for transfer/storage without any formatting. */ +CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item); +/* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */ +CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt); +/* Render a cJSON entity to text using a buffer already allocated in memory with given length. Returns 1 on success and 0 on failure. */ +/* NOTE: cJSON is not always 100% accurate in estimating how much memory it will use, so to be safe allocate 5 bytes more than you actually need */ +CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format); +/* Delete a cJSON entity and all subentities. */ +CJSON_PUBLIC(void) cJSON_Delete(cJSON *item); + +/* Returns the number of items in an array (or object). */ +CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array); +/* Retrieve item number "index" from array "array". Returns NULL if unsuccessful. */ +CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index); +/* Get item "string" from object. Case insensitive. */ +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string); +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string); +CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string); +/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */ +CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void); + +/* Check item type and return its value */ +CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item); +CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item); + +/* These functions check the type of an item */ +CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item); + +/* These calls create a cJSON item of the appropriate type. */ +CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean); +CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num); +CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string); +/* raw json */ +CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw); +CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void); + +/* Create a string where valuestring references a string so + * it will not be freed by cJSON_Delete */ +CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string); +/* Create an object/array that only references it's elements so + * they will not be freed by cJSON_Delete */ +CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child); +CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child); + +/* These utilities create an Array of count items. + * The parameter count cannot be greater than the number of elements in the number array, otherwise array access will be out of bounds.*/ +CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count); +CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count); +CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count); +CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count); + +/* Append item to the specified array/object. */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item); +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item); +/* Use this when string is definitely const (i.e. a literal, or as good as), and will definitely survive the cJSON object. + * WARNING: When this function was used, make sure to always check that (item->type & cJSON_StringIsConst) is zero before + * writing to `item->string` */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item); +/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item); +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item); + +/* Remove/Detach items from Arrays/Objects. */ +CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item); +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which); +CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which); +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string); +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string); +CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string); +CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string); + +/* Update array items. */ +CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem); /* Shifts pre-existing items to the right. */ +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement); +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem); +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem); +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object,const char *string,cJSON *newitem); + +/* Duplicate a cJSON item */ +CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse); +/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will + * need to be released. With recurse!=0, it will duplicate any children connected to the item. + * The item->next and ->prev pointers are always zero on return from Duplicate. */ +/* Recursively compare two cJSON items for equality. If either a or b is NULL or invalid, they will be considered unequal. + * case_sensitive determines if object keys are treated case sensitive (1) or case insensitive (0) */ +CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive); + +/* Minify a strings, remove blank characters(such as ' ', '\t', '\r', '\n') from strings. + * The input pointer json cannot point to a read-only address area, such as a string constant, + * but should point to a readable and writable address area. */ +CJSON_PUBLIC(void) cJSON_Minify(char *json); + +/* Helper functions for creating and adding items to an object at the same time. + * They return the added item or NULL on failure. */ +CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean); +CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number); +CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string); +CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw); +CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name); + +/* When assigning an integer value, it needs to be propagated to valuedouble too. */ +#define cJSON_SetIntValue(object, number) ((object) ? (object)->valueint = (object)->valuedouble = (number) : (number)) +/* helper for the cJSON_SetNumberValue macro */ +CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number); +#define cJSON_SetNumberValue(object, number) ((object != NULL) ? cJSON_SetNumberHelper(object, (double)number) : (number)) +/* Change the valuestring of a cJSON_String object, only takes effect when type of object is cJSON_String */ +CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring); + +/* If the object is not a boolean type this does nothing and returns cJSON_Invalid else it returns the new type*/ +#define cJSON_SetBoolValue(object, boolValue) ( \ + (object != NULL && ((object)->type & (cJSON_False|cJSON_True))) ? \ + (object)->type=((object)->type &(~(cJSON_False|cJSON_True)))|((boolValue)?cJSON_True:cJSON_False) : \ + cJSON_Invalid\ +) + +/* Macro for iterating over an array or object */ +#define cJSON_ArrayForEach(element, array) for(element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next) + +/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */ +CJSON_PUBLIC(void *) cJSON_malloc(size_t size); +CJSON_PUBLIC(void) cJSON_free(void *object); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/external/cJSON_Utils.c b/src/external/cJSON_Utils.c new file mode 100644 index 0000000..63651df --- /dev/null +++ b/src/external/cJSON_Utils.c @@ -0,0 +1,1481 @@ +/* + Copyright (c) 2009-2017 Dave Gamble and cJSON contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +/* disable warnings about old C89 functions in MSVC */ +#if !defined(_CRT_SECURE_NO_DEPRECATE) && defined(_MSC_VER) +#define _CRT_SECURE_NO_DEPRECATE +#endif + +#ifdef __GNUCC__ +#pragma GCC visibility push(default) +#endif +#if defined(_MSC_VER) +#pragma warning (push) +/* disable warning about single line comments in system headers */ +#pragma warning (disable : 4001) +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_MSC_VER) +#pragma warning (pop) +#endif +#ifdef __GNUCC__ +#pragma GCC visibility pop +#endif + +#include "cJSON_Utils.h" + +/* define our own boolean type */ +#ifdef true +#undef true +#endif +#define true ((cJSON_bool)1) + +#ifdef false +#undef false +#endif +#define false ((cJSON_bool)0) + +static unsigned char* cJSONUtils_strdup(const unsigned char* const string) +{ + size_t length = 0; + unsigned char *copy = NULL; + + length = strlen((const char*)string) + sizeof(""); + copy = (unsigned char*) cJSON_malloc(length); + if (copy == NULL) + { + return NULL; + } + memcpy(copy, string, length); + + return copy; +} + +/* string comparison which doesn't consider NULL pointers equal */ +static int compare_strings(const unsigned char *string1, const unsigned char *string2, const cJSON_bool case_sensitive) +{ + if ((string1 == NULL) || (string2 == NULL)) + { + return 1; + } + + if (string1 == string2) + { + return 0; + } + + if (case_sensitive) + { + return strcmp((const char*)string1, (const char*)string2); + } + + for(; tolower(*string1) == tolower(*string2); (void)string1++, string2++) + { + if (*string1 == '\0') + { + return 0; + } + } + + return tolower(*string1) - tolower(*string2); +} + +/* securely comparison of floating-point variables */ +static cJSON_bool compare_double(double a, double b) +{ + double maxVal = fabs(a) > fabs(b) ? fabs(a) : fabs(b); + return (fabs(a - b) <= maxVal * DBL_EPSILON); +} + + +/* Compare the next path element of two JSON pointers, two NULL pointers are considered unequal: */ +static cJSON_bool compare_pointers(const unsigned char *name, const unsigned char *pointer, const cJSON_bool case_sensitive) +{ + if ((name == NULL) || (pointer == NULL)) + { + return false; + } + + for (; (*name != '\0') && (*pointer != '\0') && (*pointer != '/'); (void)name++, pointer++) /* compare until next '/' */ + { + if (*pointer == '~') + { + /* check for escaped '~' (~0) and '/' (~1) */ + if (((pointer[1] != '0') || (*name != '~')) && ((pointer[1] != '1') || (*name != '/'))) + { + /* invalid escape sequence or wrong character in *name */ + return false; + } + else + { + pointer++; + } + } + else if ((!case_sensitive && (tolower(*name) != tolower(*pointer))) || (case_sensitive && (*name != *pointer))) + { + return false; + } + } + if (((*pointer != 0) && (*pointer != '/')) != (*name != 0)) + { + /* one string has ended, the other not */ + return false;; + } + + return true; +} + +/* calculate the length of a string if encoded as JSON pointer with ~0 and ~1 escape sequences */ +static size_t pointer_encoded_length(const unsigned char *string) +{ + size_t length; + for (length = 0; *string != '\0'; (void)string++, length++) + { + /* character needs to be escaped? */ + if ((*string == '~') || (*string == '/')) + { + length++; + } + } + + return length; +} + +/* copy a string while escaping '~' and '/' with ~0 and ~1 JSON pointer escape codes */ +static void encode_string_as_pointer(unsigned char *destination, const unsigned char *source) +{ + for (; source[0] != '\0'; (void)source++, destination++) + { + if (source[0] == '/') + { + destination[0] = '~'; + destination[1] = '1'; + destination++; + } + else if (source[0] == '~') + { + destination[0] = '~'; + destination[1] = '0'; + destination++; + } + else + { + destination[0] = source[0]; + } + } + + destination[0] = '\0'; +} + +CJSON_PUBLIC(char *) cJSONUtils_FindPointerFromObjectTo(const cJSON * const object, const cJSON * const target) +{ + size_t child_index = 0; + cJSON *current_child = 0; + + if ((object == NULL) || (target == NULL)) + { + return NULL; + } + + if (object == target) + { + /* found */ + return (char*)cJSONUtils_strdup((const unsigned char*)""); + } + + /* recursively search all children of the object or array */ + for (current_child = object->child; current_child != NULL; (void)(current_child = current_child->next), child_index++) + { + unsigned char *target_pointer = (unsigned char*)cJSONUtils_FindPointerFromObjectTo(current_child, target); + /* found the target? */ + if (target_pointer != NULL) + { + if (cJSON_IsArray(object)) + { + /* reserve enough memory for a 64 bit integer + '/' and '\0' */ + unsigned char *full_pointer = (unsigned char*)cJSON_malloc(strlen((char*)target_pointer) + 20 + sizeof("/")); + /* check if conversion to unsigned long is valid + * This should be eliminated at compile time by dead code elimination + * if size_t is an alias of unsigned long, or if it is bigger */ + if (child_index > ULONG_MAX) + { + cJSON_free(target_pointer); + cJSON_free(full_pointer); + return NULL; + } + sprintf((char*)full_pointer, "/%lu%s", (unsigned long)child_index, target_pointer); /* / */ + cJSON_free(target_pointer); + + return (char*)full_pointer; + } + + if (cJSON_IsObject(object)) + { + unsigned char *full_pointer = (unsigned char*)cJSON_malloc(strlen((char*)target_pointer) + pointer_encoded_length((unsigned char*)current_child->string) + 2); + full_pointer[0] = '/'; + encode_string_as_pointer(full_pointer + 1, (unsigned char*)current_child->string); + strcat((char*)full_pointer, (char*)target_pointer); + cJSON_free(target_pointer); + + return (char*)full_pointer; + } + + /* reached leaf of the tree, found nothing */ + cJSON_free(target_pointer); + return NULL; + } + } + + /* not found */ + return NULL; +} + +/* non broken version of cJSON_GetArrayItem */ +static cJSON *get_array_item(const cJSON *array, size_t item) +{ + cJSON *child = array ? array->child : NULL; + while ((child != NULL) && (item > 0)) + { + item--; + child = child->next; + } + + return child; +} + +static cJSON_bool decode_array_index_from_pointer(const unsigned char * const pointer, size_t * const index) +{ + size_t parsed_index = 0; + size_t position = 0; + + if ((pointer[0] == '0') && ((pointer[1] != '\0') && (pointer[1] != '/'))) + { + /* leading zeroes are not permitted */ + return 0; + } + + for (position = 0; (pointer[position] >= '0') && (pointer[0] <= '9'); position++) + { + parsed_index = (10 * parsed_index) + (size_t)(pointer[position] - '0'); + + } + + if ((pointer[position] != '\0') && (pointer[position] != '/')) + { + return 0; + } + + *index = parsed_index; + + return 1; +} + +static cJSON *get_item_from_pointer(cJSON * const object, const char * pointer, const cJSON_bool case_sensitive) +{ + cJSON *current_element = object; + + if (pointer == NULL) + { + return NULL; + } + + /* follow path of the pointer */ + while ((pointer[0] == '/') && (current_element != NULL)) + { + pointer++; + if (cJSON_IsArray(current_element)) + { + size_t index = 0; + if (!decode_array_index_from_pointer((const unsigned char*)pointer, &index)) + { + return NULL; + } + + current_element = get_array_item(current_element, index); + } + else if (cJSON_IsObject(current_element)) + { + current_element = current_element->child; + /* GetObjectItem. */ + while ((current_element != NULL) && !compare_pointers((unsigned char*)current_element->string, (const unsigned char*)pointer, case_sensitive)) + { + current_element = current_element->next; + } + } + else + { + return NULL; + } + + /* skip to the next path token or end of string */ + while ((pointer[0] != '\0') && (pointer[0] != '/')) + { + pointer++; + } + } + + return current_element; +} + +CJSON_PUBLIC(cJSON *) cJSONUtils_GetPointer(cJSON * const object, const char *pointer) +{ + return get_item_from_pointer(object, pointer, false); +} + +CJSON_PUBLIC(cJSON *) cJSONUtils_GetPointerCaseSensitive(cJSON * const object, const char *pointer) +{ + return get_item_from_pointer(object, pointer, true); +} + +/* JSON Patch implementation. */ +static void decode_pointer_inplace(unsigned char *string) +{ + unsigned char *decoded_string = string; + + if (string == NULL) { + return; + } + + for (; *string; (void)decoded_string++, string++) + { + if (string[0] == '~') + { + if (string[1] == '0') + { + decoded_string[0] = '~'; + } + else if (string[1] == '1') + { + decoded_string[1] = '/'; + } + else + { + /* invalid escape sequence */ + return; + } + + string++; + } + } + + decoded_string[0] = '\0'; +} + +/* non-broken cJSON_DetachItemFromArray */ +static cJSON *detach_item_from_array(cJSON *array, size_t which) +{ + cJSON *c = array->child; + while (c && (which > 0)) + { + c = c->next; + which--; + } + if (!c) + { + /* item doesn't exist */ + return NULL; + } + if (c != array->child) + { + /* not the first element */ + c->prev->next = c->next; + } + if (c->next) + { + c->next->prev = c->prev; + } + if (c == array->child) + { + array->child = c->next; + } + else if (c->next == NULL) + { + array->child->prev = c->prev; + } + /* make sure the detached item doesn't point anywhere anymore */ + c->prev = c->next = NULL; + + return c; +} + +/* detach an item at the given path */ +static cJSON *detach_path(cJSON *object, const unsigned char *path, const cJSON_bool case_sensitive) +{ + unsigned char *parent_pointer = NULL; + unsigned char *child_pointer = NULL; + cJSON *parent = NULL; + cJSON *detached_item = NULL; + + /* copy path and split it in parent and child */ + parent_pointer = cJSONUtils_strdup(path); + if (parent_pointer == NULL) { + goto cleanup; + } + + child_pointer = (unsigned char*)strrchr((char*)parent_pointer, '/'); /* last '/' */ + if (child_pointer == NULL) + { + goto cleanup; + } + /* split strings */ + child_pointer[0] = '\0'; + child_pointer++; + + parent = get_item_from_pointer(object, (char*)parent_pointer, case_sensitive); + decode_pointer_inplace(child_pointer); + + if (cJSON_IsArray(parent)) + { + size_t index = 0; + if (!decode_array_index_from_pointer(child_pointer, &index)) + { + goto cleanup; + } + detached_item = detach_item_from_array(parent, index); + } + else if (cJSON_IsObject(parent)) + { + detached_item = cJSON_DetachItemFromObject(parent, (char*)child_pointer); + } + else + { + /* Couldn't find object to remove child from. */ + goto cleanup; + } + +cleanup: + if (parent_pointer != NULL) + { + cJSON_free(parent_pointer); + } + + return detached_item; +} + +/* sort lists using mergesort */ +static cJSON *sort_list(cJSON *list, const cJSON_bool case_sensitive) +{ + cJSON *first = list; + cJSON *second = list; + cJSON *current_item = list; + cJSON *result = list; + cJSON *result_tail = NULL; + + if ((list == NULL) || (list->next == NULL)) + { + /* One entry is sorted already. */ + return result; + } + + while ((current_item != NULL) && (current_item->next != NULL) && (compare_strings((unsigned char*)current_item->string, (unsigned char*)current_item->next->string, case_sensitive) < 0)) + { + /* Test for list sorted. */ + current_item = current_item->next; + } + if ((current_item == NULL) || (current_item->next == NULL)) + { + /* Leave sorted lists unmodified. */ + return result; + } + + /* reset pointer to the beginning */ + current_item = list; + while (current_item != NULL) + { + /* Walk two pointers to find the middle. */ + second = second->next; + current_item = current_item->next; + /* advances current_item two steps at a time */ + if (current_item != NULL) + { + current_item = current_item->next; + } + } + if ((second != NULL) && (second->prev != NULL)) + { + /* Split the lists */ + second->prev->next = NULL; + second->prev = NULL; + } + + /* Recursively sort the sub-lists. */ + first = sort_list(first, case_sensitive); + second = sort_list(second, case_sensitive); + result = NULL; + + /* Merge the sub-lists */ + while ((first != NULL) && (second != NULL)) + { + cJSON *smaller = NULL; + if (compare_strings((unsigned char*)first->string, (unsigned char*)second->string, case_sensitive) < 0) + { + smaller = first; + } + else + { + smaller = second; + } + + if (result == NULL) + { + /* start merged list with the smaller element */ + result_tail = smaller; + result = smaller; + } + else + { + /* add smaller element to the list */ + result_tail->next = smaller; + smaller->prev = result_tail; + result_tail = smaller; + } + + if (first == smaller) + { + first = first->next; + } + else + { + second = second->next; + } + } + + if (first != NULL) + { + /* Append rest of first list. */ + if (result == NULL) + { + return first; + } + result_tail->next = first; + first->prev = result_tail; + } + if (second != NULL) + { + /* Append rest of second list */ + if (result == NULL) + { + return second; + } + result_tail->next = second; + second->prev = result_tail; + } + + return result; +} + +static void sort_object(cJSON * const object, const cJSON_bool case_sensitive) +{ + if (object == NULL) + { + return; + } + object->child = sort_list(object->child, case_sensitive); +} + +static cJSON_bool compare_json(cJSON *a, cJSON *b, const cJSON_bool case_sensitive) +{ + if ((a == NULL) || (b == NULL) || ((a->type & 0xFF) != (b->type & 0xFF))) + { + /* mismatched type. */ + return false; + } + switch (a->type & 0xFF) + { + case cJSON_Number: + /* numeric mismatch. */ + if ((a->valueint != b->valueint) || (!compare_double(a->valuedouble, b->valuedouble))) + { + return false; + } + else + { + return true; + } + + case cJSON_String: + /* string mismatch. */ + if (strcmp(a->valuestring, b->valuestring) != 0) + { + return false; + } + else + { + return true; + } + + case cJSON_Array: + for ((void)(a = a->child), b = b->child; (a != NULL) && (b != NULL); (void)(a = a->next), b = b->next) + { + cJSON_bool identical = compare_json(a, b, case_sensitive); + if (!identical) + { + return false; + } + } + + /* array size mismatch? (one of both children is not NULL) */ + if ((a != NULL) || (b != NULL)) + { + return false; + } + else + { + return true; + } + + case cJSON_Object: + sort_object(a, case_sensitive); + sort_object(b, case_sensitive); + for ((void)(a = a->child), b = b->child; (a != NULL) && (b != NULL); (void)(a = a->next), b = b->next) + { + cJSON_bool identical = false; + /* compare object keys */ + if (compare_strings((unsigned char*)a->string, (unsigned char*)b->string, case_sensitive)) + { + /* missing member */ + return false; + } + identical = compare_json(a, b, case_sensitive); + if (!identical) + { + return false; + } + } + + /* object length mismatch (one of both children is not null) */ + if ((a != NULL) || (b != NULL)) + { + return false; + } + else + { + return true; + } + + default: + break; + } + + /* null, true or false */ + return true; +} + +/* non broken version of cJSON_InsertItemInArray */ +static cJSON_bool insert_item_in_array(cJSON *array, size_t which, cJSON *newitem) +{ + cJSON *child = array->child; + while (child && (which > 0)) + { + child = child->next; + which--; + } + if (which > 0) + { + /* item is after the end of the array */ + return 0; + } + if (child == NULL) + { + cJSON_AddItemToArray(array, newitem); + return 1; + } + + /* insert into the linked list */ + newitem->next = child; + newitem->prev = child->prev; + child->prev = newitem; + + /* was it at the beginning */ + if (child == array->child) + { + array->child = newitem; + } + else + { + newitem->prev->next = newitem; + } + + return 1; +} + +static cJSON *get_object_item(const cJSON * const object, const char* name, const cJSON_bool case_sensitive) +{ + if (case_sensitive) + { + return cJSON_GetObjectItemCaseSensitive(object, name); + } + + return cJSON_GetObjectItem(object, name); +} + +enum patch_operation { INVALID, ADD, REMOVE, REPLACE, MOVE, COPY, TEST }; + +static enum patch_operation decode_patch_operation(const cJSON * const patch, const cJSON_bool case_sensitive) +{ + cJSON *operation = get_object_item(patch, "op", case_sensitive); + if (!cJSON_IsString(operation)) + { + return INVALID; + } + + if (strcmp(operation->valuestring, "add") == 0) + { + return ADD; + } + + if (strcmp(operation->valuestring, "remove") == 0) + { + return REMOVE; + } + + if (strcmp(operation->valuestring, "replace") == 0) + { + return REPLACE; + } + + if (strcmp(operation->valuestring, "move") == 0) + { + return MOVE; + } + + if (strcmp(operation->valuestring, "copy") == 0) + { + return COPY; + } + + if (strcmp(operation->valuestring, "test") == 0) + { + return TEST; + } + + return INVALID; +} + +/* overwrite and existing item with another one and free resources on the way */ +static void overwrite_item(cJSON * const root, const cJSON replacement) +{ + if (root == NULL) + { + return; + } + + if (root->string != NULL) + { + cJSON_free(root->string); + } + if (root->valuestring != NULL) + { + cJSON_free(root->valuestring); + } + if (root->child != NULL) + { + cJSON_Delete(root->child); + } + + memcpy(root, &replacement, sizeof(cJSON)); +} + +static int apply_patch(cJSON *object, const cJSON *patch, const cJSON_bool case_sensitive) +{ + cJSON *path = NULL; + cJSON *value = NULL; + cJSON *parent = NULL; + enum patch_operation opcode = INVALID; + unsigned char *parent_pointer = NULL; + unsigned char *child_pointer = NULL; + int status = 0; + + path = get_object_item(patch, "path", case_sensitive); + if (!cJSON_IsString(path)) + { + /* malformed patch. */ + status = 2; + goto cleanup; + } + + opcode = decode_patch_operation(patch, case_sensitive); + if (opcode == INVALID) + { + status = 3; + goto cleanup; + } + else if (opcode == TEST) + { + /* compare value: {...} with the given path */ + status = !compare_json(get_item_from_pointer(object, path->valuestring, case_sensitive), get_object_item(patch, "value", case_sensitive), case_sensitive); + goto cleanup; + } + + /* special case for replacing the root */ + if (path->valuestring[0] == '\0') + { + if (opcode == REMOVE) + { + static const cJSON invalid = { NULL, NULL, NULL, cJSON_Invalid, NULL, 0, 0, NULL}; + + overwrite_item(object, invalid); + + status = 0; + goto cleanup; + } + + if ((opcode == REPLACE) || (opcode == ADD)) + { + value = get_object_item(patch, "value", case_sensitive); + if (value == NULL) + { + /* missing "value" for add/replace. */ + status = 7; + goto cleanup; + } + + value = cJSON_Duplicate(value, 1); + if (value == NULL) + { + /* out of memory for add/replace. */ + status = 8; + goto cleanup; + } + + overwrite_item(object, *value); + + /* delete the duplicated value */ + cJSON_free(value); + value = NULL; + + /* the string "value" isn't needed */ + if (object->string != NULL) + { + cJSON_free(object->string); + object->string = NULL; + } + + status = 0; + goto cleanup; + } + } + + if ((opcode == REMOVE) || (opcode == REPLACE)) + { + /* Get rid of old. */ + cJSON *old_item = detach_path(object, (unsigned char*)path->valuestring, case_sensitive); + if (old_item == NULL) + { + status = 13; + goto cleanup; + } + cJSON_Delete(old_item); + if (opcode == REMOVE) + { + /* For Remove, this job is done. */ + status = 0; + goto cleanup; + } + } + + /* Copy/Move uses "from". */ + if ((opcode == MOVE) || (opcode == COPY)) + { + cJSON *from = get_object_item(patch, "from", case_sensitive); + if (from == NULL) + { + /* missing "from" for copy/move. */ + status = 4; + goto cleanup; + } + + if (opcode == MOVE) + { + value = detach_path(object, (unsigned char*)from->valuestring, case_sensitive); + } + if (opcode == COPY) + { + value = get_item_from_pointer(object, from->valuestring, case_sensitive); + } + if (value == NULL) + { + /* missing "from" for copy/move. */ + status = 5; + goto cleanup; + } + if (opcode == COPY) + { + value = cJSON_Duplicate(value, 1); + } + if (value == NULL) + { + /* out of memory for copy/move. */ + status = 6; + goto cleanup; + } + } + else /* Add/Replace uses "value". */ + { + value = get_object_item(patch, "value", case_sensitive); + if (value == NULL) + { + /* missing "value" for add/replace. */ + status = 7; + goto cleanup; + } + value = cJSON_Duplicate(value, 1); + if (value == NULL) + { + /* out of memory for add/replace. */ + status = 8; + goto cleanup; + } + } + + /* Now, just add "value" to "path". */ + + /* split pointer in parent and child */ + parent_pointer = cJSONUtils_strdup((unsigned char*)path->valuestring); + if (parent_pointer) { + child_pointer = (unsigned char*)strrchr((char*)parent_pointer, '/'); + } + if (child_pointer != NULL) + { + child_pointer[0] = '\0'; + child_pointer++; + } + parent = get_item_from_pointer(object, (char*)parent_pointer, case_sensitive); + decode_pointer_inplace(child_pointer); + + /* add, remove, replace, move, copy, test. */ + if ((parent == NULL) || (child_pointer == NULL)) + { + /* Couldn't find object to add to. */ + status = 9; + goto cleanup; + } + else if (cJSON_IsArray(parent)) + { + if (strcmp((char*)child_pointer, "-") == 0) + { + cJSON_AddItemToArray(parent, value); + value = NULL; + } + else + { + size_t index = 0; + if (!decode_array_index_from_pointer(child_pointer, &index)) + { + status = 11; + goto cleanup; + } + + if (!insert_item_in_array(parent, index, value)) + { + status = 10; + goto cleanup; + } + value = NULL; + } + } + else if (cJSON_IsObject(parent)) + { + if (case_sensitive) + { + cJSON_DeleteItemFromObjectCaseSensitive(parent, (char*)child_pointer); + } + else + { + cJSON_DeleteItemFromObject(parent, (char*)child_pointer); + } + cJSON_AddItemToObject(parent, (char*)child_pointer, value); + value = NULL; + } + else /* parent is not an object */ + { + /* Couldn't find object to add to. */ + status = 9; + goto cleanup; + } + +cleanup: + if (value != NULL) + { + cJSON_Delete(value); + } + if (parent_pointer != NULL) + { + cJSON_free(parent_pointer); + } + + return status; +} + +CJSON_PUBLIC(int) cJSONUtils_ApplyPatches(cJSON * const object, const cJSON * const patches) +{ + const cJSON *current_patch = NULL; + int status = 0; + + if (!cJSON_IsArray(patches)) + { + /* malformed patches. */ + return 1; + } + + if (patches != NULL) + { + current_patch = patches->child; + } + + while (current_patch != NULL) + { + status = apply_patch(object, current_patch, false); + if (status != 0) + { + return status; + } + current_patch = current_patch->next; + } + + return 0; +} + +CJSON_PUBLIC(int) cJSONUtils_ApplyPatchesCaseSensitive(cJSON * const object, const cJSON * const patches) +{ + const cJSON *current_patch = NULL; + int status = 0; + + if (!cJSON_IsArray(patches)) + { + /* malformed patches. */ + return 1; + } + + if (patches != NULL) + { + current_patch = patches->child; + } + + while (current_patch != NULL) + { + status = apply_patch(object, current_patch, true); + if (status != 0) + { + return status; + } + current_patch = current_patch->next; + } + + return 0; +} + +static void compose_patch(cJSON * const patches, const unsigned char * const operation, const unsigned char * const path, const unsigned char *suffix, const cJSON * const value) +{ + cJSON *patch = NULL; + + if ((patches == NULL) || (operation == NULL) || (path == NULL)) + { + return; + } + + patch = cJSON_CreateObject(); + if (patch == NULL) + { + return; + } + cJSON_AddItemToObject(patch, "op", cJSON_CreateString((const char*)operation)); + + if (suffix == NULL) + { + cJSON_AddItemToObject(patch, "path", cJSON_CreateString((const char*)path)); + } + else + { + size_t suffix_length = pointer_encoded_length(suffix); + size_t path_length = strlen((const char*)path); + unsigned char *full_path = (unsigned char*)cJSON_malloc(path_length + suffix_length + sizeof("/")); + + sprintf((char*)full_path, "%s/", (const char*)path); + encode_string_as_pointer(full_path + path_length + 1, suffix); + + cJSON_AddItemToObject(patch, "path", cJSON_CreateString((const char*)full_path)); + cJSON_free(full_path); + } + + if (value != NULL) + { + cJSON_AddItemToObject(patch, "value", cJSON_Duplicate(value, 1)); + } + cJSON_AddItemToArray(patches, patch); +} + +CJSON_PUBLIC(void) cJSONUtils_AddPatchToArray(cJSON * const array, const char * const operation, const char * const path, const cJSON * const value) +{ + compose_patch(array, (const unsigned char*)operation, (const unsigned char*)path, NULL, value); +} + +static void create_patches(cJSON * const patches, const unsigned char * const path, cJSON * const from, cJSON * const to, const cJSON_bool case_sensitive) +{ + if ((from == NULL) || (to == NULL)) + { + return; + } + + if ((from->type & 0xFF) != (to->type & 0xFF)) + { + compose_patch(patches, (const unsigned char*)"replace", path, 0, to); + return; + } + + switch (from->type & 0xFF) + { + case cJSON_Number: + if ((from->valueint != to->valueint) || !compare_double(from->valuedouble, to->valuedouble)) + { + compose_patch(patches, (const unsigned char*)"replace", path, NULL, to); + } + return; + + case cJSON_String: + if (strcmp(from->valuestring, to->valuestring) != 0) + { + compose_patch(patches, (const unsigned char*)"replace", path, NULL, to); + } + return; + + case cJSON_Array: + { + size_t index = 0; + cJSON *from_child = from->child; + cJSON *to_child = to->child; + unsigned char *new_path = (unsigned char*)cJSON_malloc(strlen((const char*)path) + 20 + sizeof("/")); /* Allow space for 64bit int. log10(2^64) = 20 */ + + /* generate patches for all array elements that exist in both "from" and "to" */ + for (index = 0; (from_child != NULL) && (to_child != NULL); (void)(from_child = from_child->next), (void)(to_child = to_child->next), index++) + { + /* check if conversion to unsigned long is valid + * This should be eliminated at compile time by dead code elimination + * if size_t is an alias of unsigned long, or if it is bigger */ + if (index > ULONG_MAX) + { + cJSON_free(new_path); + return; + } + sprintf((char*)new_path, "%s/%lu", path, (unsigned long)index); /* path of the current array element */ + create_patches(patches, new_path, from_child, to_child, case_sensitive); + } + + /* remove leftover elements from 'from' that are not in 'to' */ + for (; (from_child != NULL); (void)(from_child = from_child->next)) + { + /* check if conversion to unsigned long is valid + * This should be eliminated at compile time by dead code elimination + * if size_t is an alias of unsigned long, or if it is bigger */ + if (index > ULONG_MAX) + { + cJSON_free(new_path); + return; + } + sprintf((char*)new_path, "%lu", (unsigned long)index); + compose_patch(patches, (const unsigned char*)"remove", path, new_path, NULL); + } + /* add new elements in 'to' that were not in 'from' */ + for (; (to_child != NULL); (void)(to_child = to_child->next), index++) + { + compose_patch(patches, (const unsigned char*)"add", path, (const unsigned char*)"-", to_child); + } + cJSON_free(new_path); + return; + } + + case cJSON_Object: + { + cJSON *from_child = NULL; + cJSON *to_child = NULL; + sort_object(from, case_sensitive); + sort_object(to, case_sensitive); + + from_child = from->child; + to_child = to->child; + /* for all object values in the object with more of them */ + while ((from_child != NULL) || (to_child != NULL)) + { + int diff; + if (from_child == NULL) + { + diff = 1; + } + else if (to_child == NULL) + { + diff = -1; + } + else + { + diff = compare_strings((unsigned char*)from_child->string, (unsigned char*)to_child->string, case_sensitive); + } + + if (diff == 0) + { + /* both object keys are the same */ + size_t path_length = strlen((const char*)path); + size_t from_child_name_length = pointer_encoded_length((unsigned char*)from_child->string); + unsigned char *new_path = (unsigned char*)cJSON_malloc(path_length + from_child_name_length + sizeof("/")); + + sprintf((char*)new_path, "%s/", path); + encode_string_as_pointer(new_path + path_length + 1, (unsigned char*)from_child->string); + + /* create a patch for the element */ + create_patches(patches, new_path, from_child, to_child, case_sensitive); + cJSON_free(new_path); + + from_child = from_child->next; + to_child = to_child->next; + } + else if (diff < 0) + { + /* object element doesn't exist in 'to' --> remove it */ + compose_patch(patches, (const unsigned char*)"remove", path, (unsigned char*)from_child->string, NULL); + + from_child = from_child->next; + } + else + { + /* object element doesn't exist in 'from' --> add it */ + compose_patch(patches, (const unsigned char*)"add", path, (unsigned char*)to_child->string, to_child); + + to_child = to_child->next; + } + } + return; + } + + default: + break; + } +} + +CJSON_PUBLIC(cJSON *) cJSONUtils_GeneratePatches(cJSON * const from, cJSON * const to) +{ + cJSON *patches = NULL; + + if ((from == NULL) || (to == NULL)) + { + return NULL; + } + + patches = cJSON_CreateArray(); + create_patches(patches, (const unsigned char*)"", from, to, false); + + return patches; +} + +CJSON_PUBLIC(cJSON *) cJSONUtils_GeneratePatchesCaseSensitive(cJSON * const from, cJSON * const to) +{ + cJSON *patches = NULL; + + if ((from == NULL) || (to == NULL)) + { + return NULL; + } + + patches = cJSON_CreateArray(); + create_patches(patches, (const unsigned char*)"", from, to, true); + + return patches; +} + +CJSON_PUBLIC(void) cJSONUtils_SortObject(cJSON * const object) +{ + sort_object(object, false); +} + +CJSON_PUBLIC(void) cJSONUtils_SortObjectCaseSensitive(cJSON * const object) +{ + sort_object(object, true); +} + +static cJSON *merge_patch(cJSON *target, const cJSON * const patch, const cJSON_bool case_sensitive) +{ + cJSON *patch_child = NULL; + + if (!cJSON_IsObject(patch)) + { + /* scalar value, array or NULL, just duplicate */ + cJSON_Delete(target); + return cJSON_Duplicate(patch, 1); + } + + if (!cJSON_IsObject(target)) + { + cJSON_Delete(target); + target = cJSON_CreateObject(); + } + + patch_child = patch->child; + while (patch_child != NULL) + { + if (cJSON_IsNull(patch_child)) + { + /* NULL is the indicator to remove a value, see RFC7396 */ + if (case_sensitive) + { + cJSON_DeleteItemFromObjectCaseSensitive(target, patch_child->string); + } + else + { + cJSON_DeleteItemFromObject(target, patch_child->string); + } + } + else + { + cJSON *replace_me = NULL; + cJSON *replacement = NULL; + + if (case_sensitive) + { + replace_me = cJSON_DetachItemFromObjectCaseSensitive(target, patch_child->string); + } + else + { + replace_me = cJSON_DetachItemFromObject(target, patch_child->string); + } + + replacement = merge_patch(replace_me, patch_child, case_sensitive); + if (replacement == NULL) + { + cJSON_Delete(target); + return NULL; + } + + cJSON_AddItemToObject(target, patch_child->string, replacement); + } + patch_child = patch_child->next; + } + return target; +} + +CJSON_PUBLIC(cJSON *) cJSONUtils_MergePatch(cJSON *target, const cJSON * const patch) +{ + return merge_patch(target, patch, false); +} + +CJSON_PUBLIC(cJSON *) cJSONUtils_MergePatchCaseSensitive(cJSON *target, const cJSON * const patch) +{ + return merge_patch(target, patch, true); +} + +static cJSON *generate_merge_patch(cJSON * const from, cJSON * const to, const cJSON_bool case_sensitive) +{ + cJSON *from_child = NULL; + cJSON *to_child = NULL; + cJSON *patch = NULL; + if (to == NULL) + { + /* patch to delete everything */ + return cJSON_CreateNull(); + } + if (!cJSON_IsObject(to) || !cJSON_IsObject(from)) + { + return cJSON_Duplicate(to, 1); + } + + sort_object(from, case_sensitive); + sort_object(to, case_sensitive); + + from_child = from->child; + to_child = to->child; + patch = cJSON_CreateObject(); + if (patch == NULL) + { + return NULL; + } + while (from_child || to_child) + { + int diff; + if (from_child != NULL) + { + if (to_child != NULL) + { + diff = strcmp(from_child->string, to_child->string); + } + else + { + diff = -1; + } + } + else + { + diff = 1; + } + + if (diff < 0) + { + /* from has a value that to doesn't have -> remove */ + cJSON_AddItemToObject(patch, from_child->string, cJSON_CreateNull()); + + from_child = from_child->next; + } + else if (diff > 0) + { + /* to has a value that from doesn't have -> add to patch */ + cJSON_AddItemToObject(patch, to_child->string, cJSON_Duplicate(to_child, 1)); + + to_child = to_child->next; + } + else + { + /* object key exists in both objects */ + if (!compare_json(from_child, to_child, case_sensitive)) + { + /* not identical --> generate a patch */ + cJSON_AddItemToObject(patch, to_child->string, cJSONUtils_GenerateMergePatch(from_child, to_child)); + } + + /* next key in the object */ + from_child = from_child->next; + to_child = to_child->next; + } + } + if (patch->child == NULL) + { + /* no patch generated */ + cJSON_Delete(patch); + return NULL; + } + + return patch; +} + +CJSON_PUBLIC(cJSON *) cJSONUtils_GenerateMergePatch(cJSON * const from, cJSON * const to) +{ + return generate_merge_patch(from, to, false); +} + +CJSON_PUBLIC(cJSON *) cJSONUtils_GenerateMergePatchCaseSensitive(cJSON * const from, cJSON * const to) +{ + return generate_merge_patch(from, to, true); +} diff --git a/src/external/cJSON_Utils.h b/src/external/cJSON_Utils.h new file mode 100644 index 0000000..a970c65 --- /dev/null +++ b/src/external/cJSON_Utils.h @@ -0,0 +1,88 @@ +/* + Copyright (c) 2009-2017 Dave Gamble and cJSON contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +#ifndef cJSON_Utils__h +#define cJSON_Utils__h + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "cJSON.h" + +/* Implement RFC6901 (https://tools.ietf.org/html/rfc6901) JSON Pointer spec. */ +CJSON_PUBLIC(cJSON *) cJSONUtils_GetPointer(cJSON * const object, const char *pointer); +CJSON_PUBLIC(cJSON *) cJSONUtils_GetPointerCaseSensitive(cJSON * const object, const char *pointer); + +/* Implement RFC6902 (https://tools.ietf.org/html/rfc6902) JSON Patch spec. */ +/* NOTE: This modifies objects in 'from' and 'to' by sorting the elements by their key */ +CJSON_PUBLIC(cJSON *) cJSONUtils_GeneratePatches(cJSON * const from, cJSON * const to); +CJSON_PUBLIC(cJSON *) cJSONUtils_GeneratePatchesCaseSensitive(cJSON * const from, cJSON * const to); +/* Utility for generating patch array entries. */ +CJSON_PUBLIC(void) cJSONUtils_AddPatchToArray(cJSON * const array, const char * const operation, const char * const path, const cJSON * const value); +/* Returns 0 for success. */ +CJSON_PUBLIC(int) cJSONUtils_ApplyPatches(cJSON * const object, const cJSON * const patches); +CJSON_PUBLIC(int) cJSONUtils_ApplyPatchesCaseSensitive(cJSON * const object, const cJSON * const patches); + +/* +// Note that ApplyPatches is NOT atomic on failure. To implement an atomic ApplyPatches, use: +//int cJSONUtils_AtomicApplyPatches(cJSON **object, cJSON *patches) +//{ +// cJSON *modme = cJSON_Duplicate(*object, 1); +// int error = cJSONUtils_ApplyPatches(modme, patches); +// if (!error) +// { +// cJSON_Delete(*object); +// *object = modme; +// } +// else +// { +// cJSON_Delete(modme); +// } +// +// return error; +//} +// Code not added to library since this strategy is a LOT slower. +*/ + +/* Implement RFC7386 (https://tools.ietf.org/html/rfc7396) JSON Merge Patch spec. */ +/* target will be modified by patch. return value is new ptr for target. */ +CJSON_PUBLIC(cJSON *) cJSONUtils_MergePatch(cJSON *target, const cJSON * const patch); +CJSON_PUBLIC(cJSON *) cJSONUtils_MergePatchCaseSensitive(cJSON *target, const cJSON * const patch); +/* generates a patch to move from -> to */ +/* NOTE: This modifies objects in 'from' and 'to' by sorting the elements by their key */ +CJSON_PUBLIC(cJSON *) cJSONUtils_GenerateMergePatch(cJSON * const from, cJSON * const to); +CJSON_PUBLIC(cJSON *) cJSONUtils_GenerateMergePatchCaseSensitive(cJSON * const from, cJSON * const to); + +/* Given a root object and a target object, construct a pointer from one to the other. */ +CJSON_PUBLIC(char *) cJSONUtils_FindPointerFromObjectTo(const cJSON * const object, const cJSON * const target); + +/* Sorts the members of the object into alphabetical order. */ +CJSON_PUBLIC(void) cJSONUtils_SortObject(cJSON * const object); +CJSON_PUBLIC(void) cJSONUtils_SortObjectCaseSensitive(cJSON * const object); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/external/libcurl_uriescape.c b/src/external/libcurl_uriescape.c new file mode 100644 index 0000000..e066e58 --- /dev/null +++ b/src/external/libcurl_uriescape.c @@ -0,0 +1,64 @@ +/* + * Libcurl curl_easy_escape out-of-tree and reimplemented + * Based on libcurl 8.15.0 + * Libcurl Copyright (C) Steve Holme, + * Written for use in OSSP by Goldenkrew3000, + * Acronyms are used to avoid conflict when in use in combination with libcurl (lcue -> Libcurl URI Encode) + */ + +#include +#include +#include +#include "libcurl_uriescape.h" + +#define LCUE_ISUPPER(x) (((x) >= 'A') && ((x) <= 'Z')) +#define LCUE_ISLOWER(x) (((x) >= 'a') && ((x) <= 'z')) +#define LCUE_ISDIGIT(x) (((x) >= '0') && ((x) <= '9')) +#define LCUE_ISALNUM(x) (LCUE_ISDIGIT(x) || LCUE_ISLOWER(x) || LCUE_ISUPPER(x)) +#define LCUE_ISURLPUNTCS(x) (((x) == '-') || ((x) == '.') || ((x) == '_') || ((x) == '~')) +#define LCUE_ISUNRESERVED(x) (LCUE_ISALNUM(x) || LCUE_ISURLPUNTCS(x)) +const unsigned char lcue_udigits[] = "0123456789ABCDEF"; +const unsigned char lcue_ldigits[] = "0123456789abcdef"; + +static void lcue_hexbyte(unsigned char* dest, unsigned char val) { + dest[0] = lcue_udigits[val >> 4]; + dest[1] = lcue_udigits[val & 0x0F]; +} + +char* lcue_uriescape(char* string, int inlength) { + size_t length = 0; + char* d = NULL; + int d_idx = 0; + + if (!string || (inlength < 0)) { + return NULL; + } + + length = (inlength ? (size_t)inlength : strlen(string)); + if(!length) { + return NULL; + } + + d = calloc(length * 3 + 1, sizeof(char)); + if (d == NULL) { + return NULL; + } + + while (length--) { + unsigned char in = (unsigned char)*string++; + + if (LCUE_ISUNRESERVED(in)) { + d[d_idx] = in; + d_idx++; + } else { + unsigned char out[3] = { '%' }; + lcue_hexbyte(&out[1], in); + d[d_idx] = out[0]; + d[d_idx + 1] = out[1]; + d[d_idx + 2] = out[2]; + d_idx += 3; + } + } + + return d; +} diff --git a/src/external/libcurl_uriescape.h b/src/external/libcurl_uriescape.h new file mode 100644 index 0000000..ac7369a --- /dev/null +++ b/src/external/libcurl_uriescape.h @@ -0,0 +1,14 @@ +/* + * Libcurl curl_easy_escape out-of-tree and reimplemented + * Based on libcurl 8.15.0 + * Libcurl Copyright (C) Steve Holme, + * Written for use in OSSP by Goldenkrew3000, + * Acronyms are used to avoid conflict when in use in combination with libcurl (lsue -> Libcurl URI Encode) + */ + +#ifndef _LIBCURL_URIESCAPE_H +#define _LIBCURL_URIESCAPE_H + +char* lcue_uriescape(char* string, int inlength); + +#endif diff --git a/src/external/md5.c b/src/external/md5.c new file mode 100644 index 0000000..f79a427 --- /dev/null +++ b/src/external/md5.c @@ -0,0 +1,223 @@ +/* + * Derived from the RSA Data Security, Inc. MD5 Message-Digest Algorithm + * and modified slightly to be functionally identical but condensed into control structures. + */ + +#include "md5.h" + +/* + * Constants defined by the MD5 algorithm + */ +#define A 0x67452301 +#define B 0xefcdab89 +#define C 0x98badcfe +#define D 0x10325476 + +static uint32_t S[] = {7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, + 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, + 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, + 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21}; + +static uint32_t K[] = {0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, + 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, + 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, + 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, + 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, + 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, + 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, + 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, + 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, + 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, + 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, + 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, + 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, + 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, + 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, + 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391}; + +/* + * Padding used to make the size (in bits) of the input congruent to 448 mod 512 + */ +static uint8_t PADDING[] = {0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +/* + * Bit-manipulation functions defined by the MD5 algorithm + */ +#define F(X, Y, Z) ((X & Y) | (~X & Z)) +#define G(X, Y, Z) ((X & Z) | (Y & ~Z)) +#define H(X, Y, Z) (X ^ Y ^ Z) +#define I(X, Y, Z) (Y ^ (X | ~Z)) + +/* + * Rotates a 32-bit word left by n bits + */ +uint32_t rotateLeft(uint32_t x, uint32_t n){ + return (x << n) | (x >> (32 - n)); +} + + +/* + * Initialize a context + */ +void md5Init(MD5Context *ctx){ + ctx->size = (uint64_t)0; + + ctx->buffer[0] = (uint32_t)A; + ctx->buffer[1] = (uint32_t)B; + ctx->buffer[2] = (uint32_t)C; + ctx->buffer[3] = (uint32_t)D; +} + +/* + * Add some amount of input to the context + * + * If the input fills out a block of 512 bits, apply the algorithm (md5Step) + * and save the result in the buffer. Also updates the overall size. + */ +void md5Update(MD5Context *ctx, uint8_t *input_buffer, size_t input_len){ + uint32_t input[16]; + unsigned int offset = ctx->size % 64; + ctx->size += (uint64_t)input_len; + + // Copy each byte in input_buffer into the next space in our context input + for(unsigned int i = 0; i < input_len; ++i){ + ctx->input[offset++] = (uint8_t)*(input_buffer + i); + + // If we've filled our context input, copy it into our local array input + // then reset the offset to 0 and fill in a new buffer. + // Every time we fill out a chunk, we run it through the algorithm + // to enable some back and forth between cpu and i/o + if(offset % 64 == 0){ + for(unsigned int j = 0; j < 16; ++j){ + // Convert to little-endian + // The local variable `input` our 512-bit chunk separated into 32-bit words + // we can use in calculations + input[j] = (uint32_t)(ctx->input[(j * 4) + 3]) << 24 | + (uint32_t)(ctx->input[(j * 4) + 2]) << 16 | + (uint32_t)(ctx->input[(j * 4) + 1]) << 8 | + (uint32_t)(ctx->input[(j * 4)]); + } + md5Step(ctx->buffer, input); + offset = 0; + } + } +} + +/* + * Pad the current input to get to 448 bytes, append the size in bits to the very end, + * and save the result of the final iteration into digest. + */ +void md5Finalize(MD5Context *ctx){ + uint32_t input[16]; + unsigned int offset = ctx->size % 64; + unsigned int padding_length = offset < 56 ? 56 - offset : (56 + 64) - offset; + + // Fill in the padding and undo the changes to size that resulted from the update + md5Update(ctx, PADDING, padding_length); + ctx->size -= (uint64_t)padding_length; + + // Do a final update (internal to this function) + // Last two 32-bit words are the two halves of the size (converted from bytes to bits) + for(unsigned int j = 0; j < 14; ++j){ + input[j] = (uint32_t)(ctx->input[(j * 4) + 3]) << 24 | + (uint32_t)(ctx->input[(j * 4) + 2]) << 16 | + (uint32_t)(ctx->input[(j * 4) + 1]) << 8 | + (uint32_t)(ctx->input[(j * 4)]); + } + input[14] = (uint32_t)(ctx->size * 8); + input[15] = (uint32_t)((ctx->size * 8) >> 32); + + md5Step(ctx->buffer, input); + + // Move the result into digest (convert from little-endian) + for(unsigned int i = 0; i < 4; ++i){ + ctx->digest[(i * 4) + 0] = (uint8_t)((ctx->buffer[i] & 0x000000FF)); + ctx->digest[(i * 4) + 1] = (uint8_t)((ctx->buffer[i] & 0x0000FF00) >> 8); + ctx->digest[(i * 4) + 2] = (uint8_t)((ctx->buffer[i] & 0x00FF0000) >> 16); + ctx->digest[(i * 4) + 3] = (uint8_t)((ctx->buffer[i] & 0xFF000000) >> 24); + } +} + +/* + * Step on 512 bits of input with the main MD5 algorithm. + */ +void md5Step(uint32_t *buffer, uint32_t *input){ + uint32_t AA = buffer[0]; + uint32_t BB = buffer[1]; + uint32_t CC = buffer[2]; + uint32_t DD = buffer[3]; + + uint32_t E; + + unsigned int j; + + for(unsigned int i = 0; i < 64; ++i){ + switch(i / 16){ + case 0: + E = F(BB, CC, DD); + j = i; + break; + case 1: + E = G(BB, CC, DD); + j = ((i * 5) + 1) % 16; + break; + case 2: + E = H(BB, CC, DD); + j = ((i * 3) + 5) % 16; + break; + default: + E = I(BB, CC, DD); + j = (i * 7) % 16; + break; + } + + uint32_t temp = DD; + DD = CC; + CC = BB; + BB = BB + rotateLeft(AA + E + K[i] + input[j], S[i]); + AA = temp; + } + + buffer[0] += AA; + buffer[1] += BB; + buffer[2] += CC; + buffer[3] += DD; +} + +/* + * Functions that run the algorithm on the provided input and put the digest into result. + * result should be able to store 16 bytes. + */ +void md5String(char *input, uint8_t *result){ + MD5Context ctx; + md5Init(&ctx); + md5Update(&ctx, (uint8_t *)input, strlen(input)); + md5Finalize(&ctx); + + memcpy(result, ctx.digest, 16); +} + +void md5File(FILE *file, uint8_t *result){ + char *input_buffer = malloc(1024); + size_t input_size = 0; + + MD5Context ctx; + md5Init(&ctx); + + while((input_size = fread(input_buffer, 1, 1024, file)) > 0){ + md5Update(&ctx, (uint8_t *)input_buffer, input_size); + } + + md5Finalize(&ctx); + + free(input_buffer); + + memcpy(result, ctx.digest, 16); +} diff --git a/src/external/md5.h b/src/external/md5.h new file mode 100644 index 0000000..ca172f1 --- /dev/null +++ b/src/external/md5.h @@ -0,0 +1,24 @@ +#ifndef MD5_H +#define MD5_H + +#include +#include +#include +#include + +typedef struct{ + uint64_t size; // Size of input in bytes + uint32_t buffer[4]; // Current accumulation of hash + uint8_t input[64]; // Input to be used in the next step + uint8_t digest[16]; // Result of algorithm +}MD5Context; + +void md5Init(MD5Context *ctx); +void md5Update(MD5Context *ctx, uint8_t *input, size_t input_len); +void md5Finalize(MD5Context *ctx); +void md5Step(uint32_t *buffer, uint32_t *input); + +void md5String(char *input, uint8_t *result); +void md5File(FILE *file, uint8_t *result); + +#endif diff --git a/src/libopensubsonic/crypto.c b/src/libopensubsonic/crypto.c new file mode 100644 index 0000000..535154a --- /dev/null +++ b/src/libopensubsonic/crypto.c @@ -0,0 +1,100 @@ +#include +#include "crypto.h" +#include "../external/md5.h" +#include "../configHandler.h" +#include "logger.h" + +#if __NetBSD__ // Functions for NetBSD to use KERN_ARND +#include +#include +#endif + +static int rc = 0; +extern configHandler_config_t* configObj; + +// Use arc4random() to generate cryptographically secure bytes. Should work on all BSD-style systems +void crypto_secure_arc4random_generate(unsigned char* bytes, size_t length) { + for (size_t i = 0; i < length; i++) { + bytes[i] = arc4random() & 0xFF; + } +} + +// Use the arandom sysctl on NetBSD to generate cryptographically secure bytes. +#if __NetBSD__ +void crypto_secure_netbsd_arandom_generate(unsigned char* bytes, size_t length) { + // Setup the sysctl MIB for kern.arandom + int mib[2]; + mib[0] = CTL_KERN; + mib[1] = KERN_ARND; + + // Read random bytes + if (sysctl(mib, 2, bytes, &length, NULL, 0) == -1) { + logger_log_error(__func__, "sysctl() error."); + exit(EXIT_FAILURE); // TODO handle error better + } +} +#endif + +// Generate an Opensubsonic Login Salt +void crypto_secure_generate_salt(void) { + uint8_t salt_bytes[8]; + + // Generate cryptographically secure salt bytes using OS-native functions +#if __NetBSD__ + crypto_secure_netbsd_arandom_generate(salt_bytes, 8); +#else + crypto_secure_arc4random_generate(salt_bytes, 8); +#endif + + // Convert to a string hex representation + char* loginSalt = NULL; + rc = asprintf(&loginSalt, "%02x%02x%02x%02x%02x%02x%02x%02x", + salt_bytes[0], salt_bytes[1], salt_bytes[2], salt_bytes[3], + salt_bytes[4], salt_bytes[5], salt_bytes[6], salt_bytes[7]); + if (rc == -1) { + logger_log_error(__func__, "asprintf() failed."); + return; // TODO return proper error + } + + // TODO Fix this hack - Copy login salt to config + configObj->internal_opensubsonic_loginSalt = strdup(loginSalt); + free(loginSalt); +} + +// Generate an MD5 checksum in string hex representation of the account password and salt +void crypto_secure_generate_token(void) { + uint8_t md5_bytes[16]; + char* token_plaintext = NULL; + + // Concatenate account password and salt into single string + rc = asprintf(&token_plaintext, "%s%s", configObj->opensubsonic_password, configObj->internal_opensubsonic_loginSalt); + if (rc == -1) { + logger_log_error(__func__, "asprintf() failed."); + return; // TODO return error + } + + // Generate an MD5 checksum of the plaintext token + md5String(token_plaintext, md5_bytes); + free(token_plaintext); + + // Convert the MD5 checksum bytes into string hex representation + char* loginToken = NULL; + rc = asprintf(&loginToken, "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + md5_bytes[0], md5_bytes[1], md5_bytes[2], md5_bytes[3], + md5_bytes[4], md5_bytes[5], md5_bytes[6], md5_bytes[7], + md5_bytes[8], md5_bytes[9], md5_bytes[10], md5_bytes[11], + md5_bytes[12], md5_bytes[13], md5_bytes[14], md5_bytes[15]); + if (rc == -1) { + logger_log_error(__func__, "asprintf() failed."); + return; // TODO return error + } + + // TODO Fix this hack - Copy login token to config + configObj->internal_opensubsonic_loginToken = strdup(loginToken); + free(loginToken); +} + +void opensubsonic_crypto_generateLogin(void) { + crypto_secure_generate_salt(); + crypto_secure_generate_token(); +} diff --git a/src/libopensubsonic/crypto.h b/src/libopensubsonic/crypto.h new file mode 100644 index 0000000..c107768 --- /dev/null +++ b/src/libopensubsonic/crypto.h @@ -0,0 +1,14 @@ +#ifndef _CRYPTO_H +#define _CRYPTO_H + +// OS-native cryptographic generation functions +void crypto_secure_arc4random_generate(unsigned char* bytes, size_t length); +#if __NetBSD__ +void crypto_secure_netbsd_arandom_generate(unsigned char* bytes, size_t length); +#endif + +void crypto_secure_generate_salt(void); +void crypto_secure_generate_token(void); +void opensubsonic_crypto_generateLogin(void); + +#endif diff --git a/src/libopensubsonic/endpoint_getAlbum.c b/src/libopensubsonic/endpoint_getAlbum.c new file mode 100644 index 0000000..075758a --- /dev/null +++ b/src/libopensubsonic/endpoint_getAlbum.c @@ -0,0 +1,174 @@ +#include +#include +#include +#include "../external/cJSON.h" +#include "logger.h" +#include "utils.h" +#include "endpoint_getAlbum.h" + +// Returns 1 if failure occured, else 0 +int opensubsonic_getAlbum_parse(char* data, opensubsonic_getAlbum_struct** getAlbumStruct) { + // Allocate and initialize + (*getAlbumStruct) = (opensubsonic_getAlbum_struct*)malloc(sizeof(opensubsonic_getAlbum_struct)); + (*getAlbumStruct)->status = NULL; + (*getAlbumStruct)->errorCode = 0; + (*getAlbumStruct)->errorMessage = NULL; + (*getAlbumStruct)->id = NULL; + (*getAlbumStruct)->parent = NULL; + (*getAlbumStruct)->album = NULL; + (*getAlbumStruct)->title = NULL; + (*getAlbumStruct)->name = NULL; + (*getAlbumStruct)->coverArt = NULL; + (*getAlbumStruct)->created = NULL; + (*getAlbumStruct)->duration = 0; + (*getAlbumStruct)->playCount = 0; + (*getAlbumStruct)->artistId = NULL; + (*getAlbumStruct)->artist = NULL; + (*getAlbumStruct)->year = 0; + (*getAlbumStruct)->genre = NULL; + (*getAlbumStruct)->songCount = 0; + (*getAlbumStruct)->songs = NULL; + + // Parse the JSON + cJSON* root = cJSON_Parse(data); + if (root == NULL) { + logger_log_error(__func__, "Error parsing JSON."); + return 1; + } + + // Make an object from subsonic-response + cJSON* subsonic_root = cJSON_GetObjectItemCaseSensitive(root, "subsonic-response"); + if (subsonic_root == NULL) { + logger_log_error(__func__, "Error handling JSON - subsonic-response does not exist."); + cJSON_Delete(root); + return 1; + } + + // Check for error + OSS_Psoj(&(*getAlbumStruct)->status, subsonic_root, "status"); + if (strstr((*getAlbumStruct)->status, "ok") == NULL) { + cJSON* subsonic_error = cJSON_GetObjectItemCaseSensitive(subsonic_root, "error"); + if (subsonic_error == NULL) { + logger_log_error(__func__, "API has indicated failure through status, but error does not exist."); + cJSON_Delete(root); + return 1; + } + + OSS_Pioj(&(*getAlbumStruct)->errorCode, subsonic_error, "code"); + OSS_Psoj(&(*getAlbumStruct)->errorMessage, subsonic_error, "message"); + logger_log_error(__func__, "Error noted in JSON - Code %d: %s", (*getAlbumStruct)->errorCode, (*getAlbumStruct)->errorMessage); + cJSON_Delete(root); + return 1; + } + + // Process contents + cJSON* album_root = cJSON_GetObjectItemCaseSensitive(subsonic_root, "album"); + if (album_root == NULL) { + logger_log_error(__func__, "Error handling JSON - album does not exist."); + cJSON_Delete(root); + return 1; + } + + OSS_Psoj(&(*getAlbumStruct)->id, album_root, "id"); + OSS_Psoj(&(*getAlbumStruct)->parent, album_root, "parent"); + OSS_Psoj(&(*getAlbumStruct)->album, album_root, "album"); + OSS_Psoj(&(*getAlbumStruct)->title, album_root, "title"); + OSS_Psoj(&(*getAlbumStruct)->name, album_root, "name"); + OSS_Psoj(&(*getAlbumStruct)->coverArt, album_root, "coverArt"); + OSS_Psoj(&(*getAlbumStruct)->created, album_root, "created"); + OSS_Ploj(&(*getAlbumStruct)->duration, album_root, "duration"); + OSS_Pioj(&(*getAlbumStruct)->playCount, album_root, "playCount"); + OSS_Psoj(&(*getAlbumStruct)->artistId, album_root, "artistId"); + OSS_Psoj(&(*getAlbumStruct)->artist, album_root, "artist"); + OSS_Pioj(&(*getAlbumStruct)->year, album_root, "year"); + OSS_Psoj(&(*getAlbumStruct)->genre, album_root, "genre"); + + cJSON* song_root = cJSON_GetObjectItemCaseSensitive(album_root, "song"); + if (song_root == NULL) { + logger_log_error(__func__, "Error handling JSON - song does not exist."); + cJSON_Delete(root); + return 1; + } + + (*getAlbumStruct)->songCount = cJSON_GetArraySize(song_root); + + // Allocate and initialize + (*getAlbumStruct)->songs = (opensubsonic_getAlbum_song_struct*)malloc((*getAlbumStruct)->songCount * sizeof(opensubsonic_getAlbum_song_struct)); + + for (int i = 0; i < (*getAlbumStruct)->songCount; i++) { + (*getAlbumStruct)->songs[i].id = NULL; + (*getAlbumStruct)->songs[i].parent = NULL; + (*getAlbumStruct)->songs[i].title = NULL; + (*getAlbumStruct)->songs[i].albumId = NULL; + (*getAlbumStruct)->songs[i].album = NULL; + (*getAlbumStruct)->songs[i].artistId = NULL; + (*getAlbumStruct)->songs[i].artist = NULL; + (*getAlbumStruct)->songs[i].coverArt = NULL; + (*getAlbumStruct)->songs[i].duration = 0; + (*getAlbumStruct)->songs[i].bitRate = 0; + (*getAlbumStruct)->songs[i].bitDepth = 0; + (*getAlbumStruct)->songs[i].samplingRate = 0; + (*getAlbumStruct)->songs[i].channelCount = 0; + (*getAlbumStruct)->songs[i].track = 0; + (*getAlbumStruct)->songs[i].year = 0; + (*getAlbumStruct)->songs[i].genre = NULL; + (*getAlbumStruct)->songs[i].size = 0; + (*getAlbumStruct)->songs[i].discNumber = 0; + } + + for (int i = 0; i < (*getAlbumStruct)->songCount; i++) { + cJSON* array_song_root = cJSON_GetArrayItem(song_root, i); + if (array_song_root != NULL) { + OSS_Psoj(&(*getAlbumStruct)->songs[i].id, array_song_root, "id"); + OSS_Psoj(&(*getAlbumStruct)->songs[i].parent, array_song_root, "parent"); + OSS_Psoj(&(*getAlbumStruct)->songs[i].title, array_song_root, "title"); + OSS_Psoj(&(*getAlbumStruct)->songs[i].albumId, array_song_root, "albumId"); + OSS_Psoj(&(*getAlbumStruct)->songs[i].album, array_song_root, "album"); + OSS_Psoj(&(*getAlbumStruct)->songs[i].artistId, array_song_root, "artistId"); + OSS_Psoj(&(*getAlbumStruct)->songs[i].artist, array_song_root, "artist"); + OSS_Psoj(&(*getAlbumStruct)->songs[i].coverArt, array_song_root, "coverArt"); + OSS_Ploj(&(*getAlbumStruct)->songs[i].duration, array_song_root, "duration"); + OSS_Pioj(&(*getAlbumStruct)->songs[i].bitRate, array_song_root, "bitRate"); + OSS_Pioj(&(*getAlbumStruct)->songs[i].bitDepth, array_song_root, "bitDepth"); + OSS_Ploj(&(*getAlbumStruct)->songs[i].samplingRate, array_song_root, "samplingRate"); + OSS_Pioj(&(*getAlbumStruct)->songs[i].channelCount, array_song_root, "channelCount"); + OSS_Pioj(&(*getAlbumStruct)->songs[i].track, array_song_root, "track"); + OSS_Pioj(&(*getAlbumStruct)->songs[i].year, array_song_root, "year"); + OSS_Psoj(&(*getAlbumStruct)->songs[i].genre, array_song_root, "genre"); + OSS_Ploj(&(*getAlbumStruct)->songs[i].size, array_song_root, "size"); + OSS_Pioj(&(*getAlbumStruct)->songs[i].discNumber, array_song_root, "discNumber"); + } + } + + cJSON_Delete(root); + return 0; +} + +void opensubsonic_getAlbum_struct_free(opensubsonic_getAlbum_struct** getAlbumStruct) { + logger_log_general(__func__, "Freeing /getAlbum endpoint heap objects."); + if ((*getAlbumStruct)->status != NULL) { free((*getAlbumStruct)->status); } + if ((*getAlbumStruct)->errorMessage != NULL) { free((*getAlbumStruct)->errorMessage); } + if ((*getAlbumStruct)->id != NULL) { free((*getAlbumStruct)->id); } + if ((*getAlbumStruct)->parent != NULL) { free((*getAlbumStruct)->parent); } + if ((*getAlbumStruct)->album != NULL) { free((*getAlbumStruct)->album); } + if ((*getAlbumStruct)->title != NULL) { free((*getAlbumStruct)->title); } + if ((*getAlbumStruct)->name != NULL) { free((*getAlbumStruct)->name); } + if ((*getAlbumStruct)->coverArt != NULL) { free((*getAlbumStruct)->coverArt); } + if ((*getAlbumStruct)->created != NULL) { free((*getAlbumStruct)->created); } + if ((*getAlbumStruct)->artistId != NULL) { free((*getAlbumStruct)->artistId); } + if ((*getAlbumStruct)->artist != NULL) { free((*getAlbumStruct)->artist); } + if ((*getAlbumStruct)->genre != NULL) { free((*getAlbumStruct)->genre); } + for (int i = 0; i < (*getAlbumStruct)->songCount; i++) { + if ((*getAlbumStruct)->songs[i].id != NULL) { free((*getAlbumStruct)->songs[i].id); } + if ((*getAlbumStruct)->songs[i].parent != NULL) { free((*getAlbumStruct)->songs[i].parent); } + if ((*getAlbumStruct)->songs[i].title != NULL) { free((*getAlbumStruct)->songs[i].title); } + if ((*getAlbumStruct)->songs[i].albumId != NULL) { free((*getAlbumStruct)->songs[i].albumId); } + if ((*getAlbumStruct)->songs[i].album != NULL) { free((*getAlbumStruct)->songs[i].album); } + if ((*getAlbumStruct)->songs[i].artistId != NULL) { free((*getAlbumStruct)->songs[i].artistId); } + if ((*getAlbumStruct)->songs[i].artist != NULL) { free((*getAlbumStruct)->songs[i].artist); } + if ((*getAlbumStruct)->songs[i].coverArt != NULL) { free((*getAlbumStruct)->songs[i].coverArt); } + if ((*getAlbumStruct)->songs[i].genre != NULL) { free((*getAlbumStruct)->songs[i].genre); } + } + if ((*getAlbumStruct)->songs != NULL) { free((*getAlbumStruct)->songs); } + if (*getAlbumStruct != NULL) { free(*getAlbumStruct); } +} diff --git a/src/libopensubsonic/endpoint_getAlbum.h b/src/libopensubsonic/endpoint_getAlbum.h new file mode 100644 index 0000000..d3ad60e --- /dev/null +++ b/src/libopensubsonic/endpoint_getAlbum.h @@ -0,0 +1,52 @@ +#ifndef _ENDPOINT_GETALBUM_H +#define _ENDPOINT_GETALBUM_H + +typedef struct { + char* id; + char* parent; + char* title; + // 'isDir', 'isVideo', 'type' excluded + char* albumId; + char* album; + char* artistId; + char* artist; + char* coverArt; + long duration; + int bitRate; + int bitDepth; + long samplingRate; + int channelCount; + int track; // Use songCount index instead + int year; + char* genre; + long size; + int discNumber; + // 'suffix', 'contentType', 'path' excluded +} opensubsonic_getAlbum_song_struct; + +typedef struct { + char* status; + int errorCode; + char* errorMessage; + char* id; + char* parent; + char* album; + char* title; + char* name; + // 'isDir' excluded + char* coverArt; + char* created; + long duration; + int playCount; + char* artistId; + char* artist; + int year; + char* genre; + int songCount; // Counted, not retrieved from JSON + opensubsonic_getAlbum_song_struct* songs; +} opensubsonic_getAlbum_struct; + +int opensubsonic_getAlbum_parse(char* data, opensubsonic_getAlbum_struct** getAlbumStruct); +void opensubsonic_getAlbum_struct_free(opensubsonic_getAlbum_struct** getAlbumStruct); + +#endif diff --git a/src/libopensubsonic/endpoint_getAlbumList.c b/src/libopensubsonic/endpoint_getAlbumList.c new file mode 100644 index 0000000..4040a26 --- /dev/null +++ b/src/libopensubsonic/endpoint_getAlbumList.c @@ -0,0 +1,134 @@ +#include +#include +#include +#include "../external/cJSON.h" +#include "logger.h" +#include "utils.h" +#include "endpoint_getAlbumList.h" + +// Returns 1 if failure occured, else 0 +int opensubsonic_getAlbumList_parse(char* data, opensubsonic_getAlbumList_struct** getAlbumListStruct) { + // Allocate struct and initialize variables + (*getAlbumListStruct) = (opensubsonic_getAlbumList_struct*)malloc(sizeof(opensubsonic_getAlbumList_struct)); + (*getAlbumListStruct)->status = NULL; + (*getAlbumListStruct)->errorCode = 0; + (*getAlbumListStruct)->errorMessage = NULL; + (*getAlbumListStruct)->albumCount = 0; + (*getAlbumListStruct)->albums = NULL; + + // Parse the JSON + cJSON* root = cJSON_Parse(data); + if (root == NULL) { + logger_log_error(__func__, "Error parsing JSON."); + return 1; + } + + // Make an object from subsonic-response + cJSON* subsonic_root = cJSON_GetObjectItemCaseSensitive(root, "subsonic-response"); + if (subsonic_root == NULL) { + logger_log_error(__func__, "Error handling JSON - subsonic-response does not exist."); + cJSON_Delete(root); + return 1; + } + + OSS_Psoj(&(*getAlbumListStruct)->status, subsonic_root, "status"); + + // Check if API has returned an error + if (strstr((*getAlbumListStruct)->status, "ok") == NULL) { + // API has not returned 'ok' in status, fetch error, and return + // Check if an error is present + cJSON* subsonic_error = cJSON_GetObjectItemCaseSensitive(subsonic_root, "error"); + if (subsonic_error == NULL) { + // Error not defined in JSON + logger_log_error(__func__, "API has indicated failure through status, but error does not exist."); + cJSON_Delete(root); + return 1; + } + + OSS_Pioj(&(*getAlbumListStruct)->errorCode, subsonic_error, "code"); + OSS_Psoj(&(*getAlbumListStruct)->errorMessage, subsonic_error, "message"); + + logger_log_error(__func__, "Error noted in JSON - Code %d: %s", (*getAlbumListStruct)->errorCode, (*getAlbumListStruct)->errorMessage); + cJSON_Delete(root); + return 1; + } + + cJSON* albumList_root = cJSON_GetObjectItemCaseSensitive(subsonic_root, "albumList"); + if (albumList_root == NULL) { + logger_log_error(__func__, "Error handling JSON - albumList does not exist."); + cJSON_Delete(root); + return 1; + } + + cJSON* album_root = cJSON_GetObjectItemCaseSensitive(albumList_root, "album"); + if (album_root == NULL) { + logger_log_error(__func__, "Error handling JSON - album does not exist."); + cJSON_Delete(root); + return 1; + } + + (*getAlbumListStruct)->albumCount = cJSON_GetArraySize(album_root); + + // Allocate and initialize + (*getAlbumListStruct)->albums = (opensubsonic_getAlbumList_album_struct*)malloc((*getAlbumListStruct)->albumCount * sizeof(opensubsonic_getAlbumList_album_struct)); + for (int i = 0; i < (*getAlbumListStruct)->albumCount; i++) { + (*getAlbumListStruct)->albums[i].id = NULL; + (*getAlbumListStruct)->albums[i].parent = NULL; + (*getAlbumListStruct)->albums[i].album = NULL; + (*getAlbumListStruct)->albums[i].title = NULL; + (*getAlbumListStruct)->albums[i].name = NULL; + (*getAlbumListStruct)->albums[i].coverArt = NULL; + (*getAlbumListStruct)->albums[i].songCount = 0; + (*getAlbumListStruct)->albums[i].created = NULL; + (*getAlbumListStruct)->albums[i].duration = 0; + (*getAlbumListStruct)->albums[i].playCount = 0; + (*getAlbumListStruct)->albums[i].artistId = NULL; + (*getAlbumListStruct)->albums[i].artist = NULL; + (*getAlbumListStruct)->albums[i].year = 0; + (*getAlbumListStruct)->albums[i].genre = NULL; + + } + + for (int i = 0; i < (*getAlbumListStruct)->albumCount; i++) { + cJSON* array_album_root = cJSON_GetArrayItem(album_root, i); + if (array_album_root != NULL) { + OSS_Psoj(&(*getAlbumListStruct)->albums[i].id, array_album_root, "id"); + OSS_Psoj(&(*getAlbumListStruct)->albums[i].parent, array_album_root, "parent"); + OSS_Psoj(&(*getAlbumListStruct)->albums[i].album, array_album_root, "album"); + OSS_Psoj(&(*getAlbumListStruct)->albums[i].title, array_album_root, "title"); + OSS_Psoj(&(*getAlbumListStruct)->albums[i].name, array_album_root, "name"); + OSS_Psoj(&(*getAlbumListStruct)->albums[i].coverArt, array_album_root, "coverArt"); + OSS_Pioj(&(*getAlbumListStruct)->albums[i].songCount, array_album_root, "songCount"); + OSS_Psoj(&(*getAlbumListStruct)->albums[i].created, array_album_root, "created"); + OSS_Ploj(&(*getAlbumListStruct)->albums[i].duration, array_album_root, "duration"); + OSS_Pioj(&(*getAlbumListStruct)->albums[i].playCount, array_album_root, "playCount"); + OSS_Psoj(&(*getAlbumListStruct)->albums[i].artistId, array_album_root, "artistId"); + OSS_Psoj(&(*getAlbumListStruct)->albums[i].artist, array_album_root, "artist"); + OSS_Pioj(&(*getAlbumListStruct)->albums[i].year, array_album_root, "year"); + OSS_Psoj(&(*getAlbumListStruct)->albums[i].genre, array_album_root, "genre"); + } + } + + cJSON_Delete(root); + return 0; +} + +void opensubsonic_getAlbumList_struct_free(opensubsonic_getAlbumList_struct** getAlbumListStruct) { + logger_log_general(__func__, "Freeing /getAlbumList endpoint heap objects."); + if ((*getAlbumListStruct)->status != NULL) { free((*getAlbumListStruct)->status); } + if ((*getAlbumListStruct)->errorMessage != NULL) { free((*getAlbumListStruct)->errorMessage); } + for (int i = 0; i < (*getAlbumListStruct)->albumCount; i++) { + if ((*getAlbumListStruct)->albums[i].id != NULL) { free((*getAlbumListStruct)->albums[i].id); } + if ((*getAlbumListStruct)->albums[i].parent != NULL) { free((*getAlbumListStruct)->albums[i].parent); } + if ((*getAlbumListStruct)->albums[i].album != NULL) { free((*getAlbumListStruct)->albums[i].album); } + if ((*getAlbumListStruct)->albums[i].title != NULL) { free((*getAlbumListStruct)->albums[i].title); } + if ((*getAlbumListStruct)->albums[i].name != NULL) { free((*getAlbumListStruct)->albums[i].name); } + if ((*getAlbumListStruct)->albums[i].coverArt != NULL) { free((*getAlbumListStruct)->albums[i].coverArt); } + if ((*getAlbumListStruct)->albums[i].created != NULL) { free((*getAlbumListStruct)->albums[i].created); } + if ((*getAlbumListStruct)->albums[i].artistId != NULL) { free((*getAlbumListStruct)->albums[i].artistId); } + if ((*getAlbumListStruct)->albums[i].artist != NULL) { free((*getAlbumListStruct)->albums[i].artist); } + if ((*getAlbumListStruct)->albums[i].genre != NULL) { free((*getAlbumListStruct)->albums[i].genre); } + } + if ((*getAlbumListStruct)->albums != NULL) { free((*getAlbumListStruct)->albums); } + if (*getAlbumListStruct != NULL) { free(*getAlbumListStruct); } +} diff --git a/src/libopensubsonic/endpoint_getAlbumList.h b/src/libopensubsonic/endpoint_getAlbumList.h new file mode 100644 index 0000000..6cb0922 --- /dev/null +++ b/src/libopensubsonic/endpoint_getAlbumList.h @@ -0,0 +1,33 @@ +#ifndef _ENDPOINT_GETALBUMLIST_H +#define _ENDPOINT_GETALBUMLIST_H + +typedef struct { + char* id; + char* parent; + char* album; + char* title; + char* name; + // 'isDir' excluded + char* coverArt; + int songCount; // Only use as a guideline, count manually when possible + char* created; + long duration; + int playCount; + char* artistId; + char* artist; + int year; + char* genre; +} opensubsonic_getAlbumList_album_struct; + +typedef struct { + char* status; + int errorCode; + char* errorMessage; + int albumCount; // Album count (Counted from array, NOT from JSON) + opensubsonic_getAlbumList_album_struct* albums; +} opensubsonic_getAlbumList_struct; + +int opensubsonic_getAlbumList_parse(char* data, opensubsonic_getAlbumList_struct** getAlbumListStruct); +void opensubsonic_getAlbumList_struct_free(opensubsonic_getAlbumList_struct** getAlbumListStruct); + +#endif diff --git a/src/libopensubsonic/endpoint_getArtist.c b/src/libopensubsonic/endpoint_getArtist.c new file mode 100644 index 0000000..9667c9b --- /dev/null +++ b/src/libopensubsonic/endpoint_getArtist.c @@ -0,0 +1,163 @@ +#include +#include +#include +#include "../external/cJSON.h" +#include "logger.h" +#include "utils.h" +#include "endpoint_getArtist.h" + +// Parse the JSON returned from the /rest/getArtist endpoint +// Returns 1 if failure occured, else 0 +int opensubsonic_getArtist_parse(char* data, opensubsonic_getArtist_struct** getArtistStruct) { + // Allocate struct + *getArtistStruct = (opensubsonic_getArtist_struct*)malloc(sizeof(opensubsonic_getArtist_struct)); + + // Initialize struct variables + (*getArtistStruct)->status = NULL; + (*getArtistStruct)->errorCode = 0; + (*getArtistStruct)->errorMessage = NULL; + (*getArtistStruct)->artistId = NULL; + (*getArtistStruct)->artistName = NULL; + (*getArtistStruct)->coverArt = NULL; + (*getArtistStruct)->albumCount = 0; + (*getArtistStruct)->userRating = 0; + (*getArtistStruct)->artistImageUrl = NULL; + (*getArtistStruct)->starred = NULL; + (*getArtistStruct)->albums = NULL; + + // Parse the JSON + cJSON* root = cJSON_Parse(data); + if (root == NULL) { + logger_log_error(__func__, "Error parsing JSON."); + return 1; + } + + // Make an object from subsonic-response + cJSON* subsonic_root = cJSON_GetObjectItemCaseSensitive(root, "subsonic-response"); + if (subsonic_root == NULL) { + logger_log_error(__func__, "Error handling JSON - subsonic-response does not exist."); + cJSON_Delete(root); + return 1; + } + + OSS_Psoj(&(*getArtistStruct)->status, subsonic_root, "status"); + + // Check if API has returned an error + if (strstr((*getArtistStruct)->status, "ok") == NULL) { + // API has not returned 'ok' in status, fetch error, and return + // Check if an error is present + cJSON* subsonic_error = cJSON_GetObjectItemCaseSensitive(subsonic_root, "error"); + if (subsonic_error == NULL) { + // Error not defined in JSON + logger_log_error(__func__, "API has indicated failure through status, but error does not exist."); + cJSON_Delete(root); + return 1; + } + + OSS_Pioj(&(*getArtistStruct)->errorCode, subsonic_error, "code"); + OSS_Psoj(&(*getArtistStruct)->errorMessage, subsonic_error, "message"); + + logger_log_error(__func__, "Error noted in JSON - Code %d: %s", (*getArtistStruct)->errorCode, (*getArtistStruct)->errorMessage); + cJSON_Delete(root); + return 1; + } + + // Make an object from artist + cJSON* artist_root = cJSON_GetObjectItemCaseSensitive(subsonic_root, "artist"); + if (artist_root == NULL) { + logger_log_error(__func__, "Error handling JSON - artist does not exist."); + cJSON_Delete(root); + return 1; + } + + OSS_Psoj(&(*getArtistStruct)->artistId, artist_root, "id"); + OSS_Psoj(&(*getArtistStruct)->artistName, artist_root, "name"); + OSS_Psoj(&(*getArtistStruct)->coverArt, artist_root, "coverArt"); + OSS_Pioj(&(*getArtistStruct)->userRating, artist_root, "userRating"); + OSS_Psoj(&(*getArtistStruct)->artistImageUrl, artist_root, "artistImageUrl"); + OSS_Psoj(&(*getArtistStruct)->starred, artist_root, "starred"); + + // Make an object from album + cJSON* album_root = cJSON_GetObjectItemCaseSensitive(artist_root, "album"); + if (album_root == NULL) { + logger_log_error(__func__, "Error handling JSON - album does not exist."); + cJSON_Delete(root); + return 1; + } + + // Count the amount of albums + (*getArtistStruct)->albumCount = cJSON_GetArraySize(album_root); + + // Allocate memory for albums + (*getArtistStruct)->albums = (opensubsonic_getArtist_album_struct*)malloc((*getArtistStruct)->albumCount * sizeof(opensubsonic_getArtist_album_struct)); + + // Initialize variables + for (int i = 0; i < (*getArtistStruct)->albumCount; i++) { + (*getArtistStruct)->albums[i].id = NULL; + (*getArtistStruct)->albums[i].parent = NULL; + (*getArtistStruct)->albums[i].title = NULL; + (*getArtistStruct)->albums[i].name = NULL; + (*getArtistStruct)->albums[i].coverArt = NULL; + (*getArtistStruct)->albums[i].songCount = 0; + (*getArtistStruct)->albums[i].created = NULL; + (*getArtistStruct)->albums[i].duration = 0; + (*getArtistStruct)->albums[i].playCount = 0; + (*getArtistStruct)->albums[i].artistId = NULL; + (*getArtistStruct)->albums[i].artist = NULL; + (*getArtistStruct)->albums[i].year = 0; + (*getArtistStruct)->albums[i].genre = NULL; + (*getArtistStruct)->albums[i].userRating = 0; + (*getArtistStruct)->albums[i].starred = NULL; + } + + // Go through the album array + for (int i = 0; i < (*getArtistStruct)->albumCount; i++) { + cJSON* array_album_root = cJSON_GetArrayItem(album_root, i); + if (array_album_root != NULL) { + OSS_Psoj(&(*getArtistStruct)->albums[i].id, array_album_root, "id"); + OSS_Psoj(&(*getArtistStruct)->albums[i].parent, array_album_root, "parent"); + OSS_Psoj(&(*getArtistStruct)->albums[i].title, array_album_root, "title"); + OSS_Psoj(&(*getArtistStruct)->albums[i].name, array_album_root, "name"); + OSS_Psoj(&(*getArtistStruct)->albums[i].coverArt, array_album_root, "coverArt"); + OSS_Pioj(&(*getArtistStruct)->albums[i].songCount, array_album_root, "songCount"); + OSS_Psoj(&(*getArtistStruct)->albums[i].created, array_album_root, "created"); + OSS_Ploj(&(*getArtistStruct)->albums[i].duration, array_album_root, "duration"); + OSS_Pioj(&(*getArtistStruct)->albums[i].playCount, array_album_root, "playCount"); + OSS_Psoj(&(*getArtistStruct)->albums[i].artistId, array_album_root, "artistId"); + OSS_Psoj(&(*getArtistStruct)->albums[i].artist, array_album_root, "artist"); + OSS_Pioj(&(*getArtistStruct)->albums[i].year, array_album_root, "year"); + OSS_Psoj(&(*getArtistStruct)->albums[i].genre, array_album_root, "genre"); + OSS_Pioj(&(*getArtistStruct)->albums[i].userRating, array_album_root, "userRating"); + OSS_Psoj(&(*getArtistStruct)->albums[i].starred, array_album_root, "starred"); + } + } + + cJSON_Delete(root); + return 0; +} + +// Free the dynamically allocated elements of the opensubsonic_getArtist_struct structure and the opensubsonic_getArtist_album_struct array structs. +void opensubsonic_getArtist_struct_free(opensubsonic_getArtist_struct** getArtistStruct) { + logger_log_general(__func__, "Freeing /getArtist endpoint heap objects."); + if((*getArtistStruct)->status != NULL) { free((*getArtistStruct)->status); } + if((*getArtistStruct)->errorMessage != NULL) { free((*getArtistStruct)->errorMessage); } + if((*getArtistStruct)->artistId != NULL) { free((*getArtistStruct)->artistId); } + if((*getArtistStruct)->artistName != NULL) { free((*getArtistStruct)->artistName); } + if((*getArtistStruct)->coverArt != NULL) { free((*getArtistStruct)->coverArt); } + if((*getArtistStruct)->artistImageUrl != NULL) { free((*getArtistStruct)->artistImageUrl); } + if((*getArtistStruct)->starred != NULL) { free((*getArtistStruct)->starred); } + for (int i = 0; i < (*getArtistStruct)->albumCount; i++) { + if((*getArtistStruct)->albums[i].id != NULL) { free((*getArtistStruct)->albums[i].id); } + if((*getArtistStruct)->albums[i].parent != NULL) { free((*getArtistStruct)->albums[i].parent); } + if((*getArtistStruct)->albums[i].title != NULL) { free((*getArtistStruct)->albums[i].title); } + if((*getArtistStruct)->albums[i].name != NULL) { free((*getArtistStruct)->albums[i].name); } + if((*getArtistStruct)->albums[i].coverArt != NULL) { free((*getArtistStruct)->albums[i].coverArt); } + if((*getArtistStruct)->albums[i].created != NULL) { free((*getArtistStruct)->albums[i].created); } + if((*getArtistStruct)->albums[i].artistId != NULL) { free((*getArtistStruct)->albums[i].artistId); } + if((*getArtistStruct)->albums[i].artist != NULL) { free((*getArtistStruct)->albums[i].artist); } + if((*getArtistStruct)->albums[i].genre != NULL) { free((*getArtistStruct)->albums[i].genre); } + if((*getArtistStruct)->albums[i].starred != NULL) { free((*getArtistStruct)->albums[i].starred); } + } + if((*getArtistStruct)->albums != NULL) { free((*getArtistStruct)->albums); } + if(*getArtistStruct != NULL) { free(*getArtistStruct); } +} diff --git a/src/libopensubsonic/endpoint_getArtist.h b/src/libopensubsonic/endpoint_getArtist.h new file mode 100644 index 0000000..ce8afbb --- /dev/null +++ b/src/libopensubsonic/endpoint_getArtist.h @@ -0,0 +1,41 @@ +#ifndef _ENDPOINT_GETARTIST_H +#define _ENDPOINT_GETARTIST_H + +typedef struct { + char* id; // Album ID + char* parent; // Parent ID + char* title; // Album Title + char* name; // Album Name + // 'isDir' excluded + char* coverArt; // Cover art ID + int songCount; + char* created; // Time created + long duration; // Duration of the album in seconds + int playCount; + char* artistId; // Artist ID + char* artist; // Artist Name + int year; // Year released + char* genre; + int userRating; + char* starred; // Time starred +} opensubsonic_getArtist_album_struct; + +typedef struct { + char* status; + int errorCode; + char* errorMessage; + char* artistId; // Artist ID + char* artistName; // Artist name + char* coverArt; // Artist cover art ID + int userRating; + char* artistImageUrl; + char* starred; // Time starred + // 'musicBrainzId', 'sorted', 'roles' excluded + int albumCount; // Album count (Counted from array, NOT from JSON) + opensubsonic_getArtist_album_struct* albums; +} opensubsonic_getArtist_struct; + +int opensubsonic_getArtist_parse(char* data, opensubsonic_getArtist_struct** getArtistStruct); +void opensubsonic_getArtist_struct_free(opensubsonic_getArtist_struct** getArtistStruct); + +#endif diff --git a/src/libopensubsonic/endpoint_getArtists.c b/src/libopensubsonic/endpoint_getArtists.c new file mode 100644 index 0000000..0855abe --- /dev/null +++ b/src/libopensubsonic/endpoint_getArtists.c @@ -0,0 +1,144 @@ +#include +#include +#include +#include "../external/cJSON.h" +#include "logger.h" +#include "utils.h" +#include "endpoint_getArtists.h" + +// Parse the JSON returned from the /rest/getArtists endpoint +// Returns 1 if failure occured, else 0 +int opensubsonic_getArtists_parse(char* data, opensubsonic_getArtists_struct** getArtistsStruct) { + // Allocate struct + *getArtistsStruct = (opensubsonic_getArtists_struct*)malloc(sizeof(opensubsonic_getArtists_struct)); + + // Initialize struct variables + (*getArtistsStruct)->status = NULL; + (*getArtistsStruct)->errorMessage = NULL; + (*getArtistsStruct)->errorCode = 0; + (*getArtistsStruct)->artistCount = 0; + (*getArtistsStruct)->artists = NULL; + + // Parse the JSON + cJSON* root = cJSON_Parse(data); + if (root == NULL) { + logger_log_error(__func__, "Error parsing JSON."); + return 1; + } + + // Make an object from subsonic-response + cJSON* subsonic_root = cJSON_GetObjectItemCaseSensitive(root, "subsonic-response"); + if (subsonic_root == NULL) { + logger_log_error(__func__, "Error handling JSON - subsonic-response does not exist."); + cJSON_Delete(root); + return 1; + } + + OSS_Psoj(&(*getArtistsStruct)->status, subsonic_root, "status"); + + // Check if API has returned an error + if (strstr((*getArtistsStruct)->status, "ok") == NULL) { + // API has not returned 'ok' in status, fetch error, and return + // Check if an error is present + cJSON* subsonic_error = cJSON_GetObjectItemCaseSensitive(subsonic_root, "error"); + if (subsonic_error == NULL) { + // Error not defined in JSON + logger_log_error(__func__, "API has indicated failure through status, but error does not exist."); + cJSON_Delete(root); + return 1; + } + + OSS_Pioj(&(*getArtistsStruct)->errorCode, subsonic_error, "code"); + OSS_Psoj(&(*getArtistsStruct)->errorMessage, subsonic_error, "message"); + + logger_log_error(__func__, "Error noted in JSON - Code %d: %s", (*getArtistsStruct)->errorCode, (*getArtistsStruct)->errorMessage); + cJSON_Delete(root); + return 1; + } + + // Make an object from artists + cJSON* artists_root = cJSON_GetObjectItemCaseSensitive(subsonic_root, "artists"); + if (artists_root == NULL) { + logger_log_error(__func__, "Error handling JSON - artists does not exist."); + cJSON_Delete(root); + return 1; + } + + // Make an object from index + cJSON* index_root = cJSON_GetObjectItemCaseSensitive(artists_root, "index"); + if (index_root == NULL) { + logger_log_error(__func__, "Error handling JSON - index does not exist."); + cJSON_Delete(root); + return 1; + } + + // Count the amount of artists present for malloc (Copied from function below) + int totalArtistIndex = 0; + for (int i = 0; i < cJSON_GetArraySize(index_root); i++) { + cJSON* array_letter_root = cJSON_GetArrayItem(index_root, i); + + cJSON* current_letter_artists_array = cJSON_GetObjectItemCaseSensitive(array_letter_root, "artist"); + if (current_letter_artists_array != NULL) { + // Array of artists starting with the same letter + for (int j = 0; j < cJSON_GetArraySize(current_letter_artists_array); j++) { + cJSON* current_letter_artist_array_layer_b = cJSON_GetArrayItem(current_letter_artists_array, j); + if (current_letter_artist_array_layer_b != NULL) { + // Increment total artist index + (*getArtistsStruct)->artistCount++; + } + } + } + } + + // Allocate memory for opensubsonic_getArtists_artist_struct inside opensubsonic_getArtists_struct (Where the artist data is held) + (*getArtistsStruct)->artists = (opensubsonic_getArtists_artist_struct*)malloc((*getArtistsStruct)->artistCount * sizeof(opensubsonic_getArtists_artist_struct)); + + // Initialize struct variables + for (int i = 0; i < (*getArtistsStruct)->artistCount; i++) { + (*getArtistsStruct)->artists[i].name = NULL; + (*getArtistsStruct)->artists[i].id = NULL; + (*getArtistsStruct)->artists[i].coverArt = NULL; + (*getArtistsStruct)->artists[i].albumCount = 0; + } + + // Go through the alphabet array (Each element in this array is the first letter of the artist, organized) + int currentArtistIndex = 0; + for (int i = 0; i < cJSON_GetArraySize(index_root); i++) { + cJSON* array_letter_root = cJSON_GetArrayItem(index_root, i); + + cJSON* current_letter_artists_array = cJSON_GetObjectItemCaseSensitive(array_letter_root, "artist"); + if (current_letter_artists_array != NULL) { + // Array of artists starting with the same letter + for (int j = 0; j < cJSON_GetArraySize(current_letter_artists_array); j++) { + cJSON* current_letter_artist_array_layer_b = cJSON_GetArrayItem(current_letter_artists_array, j); + if (current_letter_artist_array_layer_b != NULL) { + // Fetch information + OSS_Psoj(&(*getArtistsStruct)->artists[currentArtistIndex].name, current_letter_artist_array_layer_b, "name"); + OSS_Psoj(&(*getArtistsStruct)->artists[currentArtistIndex].id, current_letter_artist_array_layer_b, "id"); + OSS_Psoj(&(*getArtistsStruct)->artists[currentArtistIndex].coverArt, current_letter_artist_array_layer_b, "coverArt"); + OSS_Pioj(&(*getArtistsStruct)->artists[currentArtistIndex].albumCount, current_letter_artist_array_layer_b, "albumCount"); + + // Increment current artist index + currentArtistIndex++; + } + } + } + } + + cJSON_Delete(root); + return 0; +} + +// Free the opensubsonic_getArtists_struct structure. +void opensubsonic_getArtists_struct_free(opensubsonic_getArtists_struct** getArtistsStruct) { + logger_log_general(__func__, "Freeing /getArtists endpoint heap objects."); + if ((*getArtistsStruct)->status != NULL) { free((*getArtistsStruct)->status); } + if ((*getArtistsStruct)->errorMessage != NULL) { free((*getArtistsStruct)->errorMessage); } + for (size_t i = 0; i < (*getArtistsStruct)->artistCount; i++) { + if ((*getArtistsStruct)->artists[i].name != NULL) { free((*getArtistsStruct)->artists[i].name); } + if ((*getArtistsStruct)->artists[i].id != NULL) { free((*getArtistsStruct)->artists[i].id); } + if ((*getArtistsStruct)->artists[i].coverArt != NULL) { free((*getArtistsStruct)->artists[i].coverArt); } + } + if ((*getArtistsStruct)->artists != NULL) { free((*getArtistsStruct)->artists); } + if (*getArtistsStruct != NULL) { free(*getArtistsStruct); } +} diff --git a/src/libopensubsonic/endpoint_getArtists.h b/src/libopensubsonic/endpoint_getArtists.h new file mode 100644 index 0000000..b29706c --- /dev/null +++ b/src/libopensubsonic/endpoint_getArtists.h @@ -0,0 +1,22 @@ +#ifndef _ENDPOINT_GETARTISTS_H +#define _ENDPOINT_GETARTISTS_H + +typedef struct { + char* id; + char* name; + char* coverArt; + int albumCount; +} opensubsonic_getArtists_artist_struct; + +typedef struct { + char* status; + int errorCode; + char* errorMessage; + int artistCount; + opensubsonic_getArtists_artist_struct* artists; +} opensubsonic_getArtists_struct; + +int opensubsonic_getArtists_parse(char* data, opensubsonic_getArtists_struct** getArtistsStruct); +void opensubsonic_getArtists_struct_free(opensubsonic_getArtists_struct** getArtistsStruct); + +#endif diff --git a/src/libopensubsonic/endpoint_getLyricsBySongId.c b/src/libopensubsonic/endpoint_getLyricsBySongId.c new file mode 100644 index 0000000..d70b590 --- /dev/null +++ b/src/libopensubsonic/endpoint_getLyricsBySongId.c @@ -0,0 +1,129 @@ +#include +#include +#include +#include "../external/cJSON.h" +#include "logger.h" +#include "utils.h" +#include "endpoint_getLyricsBySongId.h" + +// Returns 1 if failure occured, else 0 +int opensubsonic_getLyricsBySongId_parse(char* data, opensubsonic_getLyricsBySongId_struct** getLyricsBySongIdStruct) { + // Allocate struct + *getLyricsBySongIdStruct = malloc(sizeof(opensubsonic_getLyricsBySongId_struct)); + + // Initialize struct variables + (*getLyricsBySongIdStruct)->status = NULL; + (*getLyricsBySongIdStruct)->errorCode = 0; + (*getLyricsBySongIdStruct)->errorMessage = NULL; + (*getLyricsBySongIdStruct)->displayArtist = NULL; + (*getLyricsBySongIdStruct)->displayTitle = NULL; + (*getLyricsBySongIdStruct)->lyricsAmount = 0; + (*getLyricsBySongIdStruct)->lyrics = NULL; + + // Parse the JSON + cJSON* root = cJSON_Parse(data); + if (root == NULL) { + logger_log_error(__func__, "Error parsing JSON."); + return 1; + } + + // Make an object from subsonic-response + cJSON* subsonic_root = cJSON_GetObjectItemCaseSensitive(root, "subsonic-response"); + if (subsonic_root == NULL) { + logger_log_error(__func__, "Error handling JSON - subsonic-response does not exist."); + cJSON_Delete(root); + return 1; + } + + OSS_Psoj(&(*getLyricsBySongIdStruct)->status, subsonic_root, "status"); + + // Check if API has returned an error + if (strstr((*getLyricsBySongIdStruct)->status, "ok") == NULL) { + // API has not returned 'ok' in status, fetch error, and return + // Check if an error is present + cJSON* subsonic_error = cJSON_GetObjectItemCaseSensitive(subsonic_root, "error"); + if (subsonic_error == NULL) { + // Error not defined in JSON + logger_log_error(__func__, "API has indicated failure through status, but error does not exist."); + cJSON_Delete(root); + return 1; + } + + OSS_Pioj(&(*getLyricsBySongIdStruct)->errorCode, subsonic_error, "code"); + OSS_Psoj(&(*getLyricsBySongIdStruct)->errorMessage, subsonic_error, "message"); + + logger_log_error(__func__, "Error noted in JSON - Code %d: %s", (*getLyricsBySongIdStruct)->errorCode, (*getLyricsBySongIdStruct)->errorMessage); + cJSON_Delete(root); + return 1; + } + + // Make an object from 'lyricsList' + cJSON* lyricsList_root = cJSON_GetObjectItemCaseSensitive(subsonic_root, "lyricsList"); + if (lyricsList_root == NULL) { + logger_log_error(__func__, "Error handling JSON - lyricsList does not exist."); + cJSON_Delete(root); + return 1; + } + + // Make an object from 'structuredLyrics' + cJSON* structuredLyrics_root = cJSON_GetObjectItemCaseSensitive(lyricsList_root, "structuredLyrics"); + if (structuredLyrics_root == NULL) { + logger_log_error(__func__, "Error handling JSON - structuredLyrics does not exist."); + cJSON_Delete(root); + return 1; + } + + // Make an object from the first index of 'structuredLyrics' + cJSON* structuredLyrics_idx0_root = cJSON_GetArrayItem(structuredLyrics_root, 0); + if (structuredLyrics_idx0_root == NULL) { + logger_log_error(__func__, "Error handling JSON - structuredLyrics (idx 0) does not exist."); + cJSON_Delete(root); + return 1; + } + + OSS_Psoj(&(*getLyricsBySongIdStruct)->displayArtist, structuredLyrics_idx0_root, "displayArtist"); + OSS_Psoj(&(*getLyricsBySongIdStruct)->displayTitle, structuredLyrics_idx0_root, "displayTitle"); + + // Make an object from 'line' + cJSON* line_root = cJSON_GetObjectItemCaseSensitive(structuredLyrics_idx0_root, "line"); + if (line_root == NULL) { + logger_log_error(__func__, "Error handling JSON - line does not exist."); + cJSON_Delete(root); + return 1; + } + + // Count amount of objects in 'line' and allocate memory + (*getLyricsBySongIdStruct)->lyricsAmount = cJSON_GetArraySize(line_root); + (*getLyricsBySongIdStruct)->lyrics = (opensubsonic_getLyricsBySongId_lyric_struct*)malloc((*getLyricsBySongIdStruct)->lyricsAmount * sizeof(opensubsonic_getLyricsBySongId_lyric_struct)); + + // Initialize variables + for (int i = 0; i < (*getLyricsBySongIdStruct)->lyricsAmount; i++) { + (*getLyricsBySongIdStruct)->lyrics[i].data = NULL; + (*getLyricsBySongIdStruct)->lyrics[i].offset = 0; + } + + // Extract lyrics + for (int i = 0; i < (*getLyricsBySongIdStruct)->lyricsAmount; i++) { + cJSON* curr_idx_root = cJSON_GetArrayItem(line_root, i); + if (curr_idx_root != NULL) { + OSS_Psoj(&(*getLyricsBySongIdStruct)->lyrics[i].data, curr_idx_root, "value"); + OSS_Ploj(&(*getLyricsBySongIdStruct)->lyrics[i].offset, curr_idx_root, "start"); + } + } + + cJSON_Delete(root); + return 0; +} + +void opensubsonic_getLyricsBySongId_struct_free(opensubsonic_getLyricsBySongId_struct** getLyricsBySongIdStruct) { + logger_log_general(__func__, "Freeing /getLyricsBySongId endpoint heap objects."); + if ((*getLyricsBySongIdStruct)->status != NULL) { free((*getLyricsBySongIdStruct)->status); } + if ((*getLyricsBySongIdStruct)->errorMessage != NULL) { free((*getLyricsBySongIdStruct)->errorMessage); } + if ((*getLyricsBySongIdStruct)->displayArtist != NULL) { free((*getLyricsBySongIdStruct)->displayArtist); } + if ((*getLyricsBySongIdStruct)->displayTitle != NULL) { free((*getLyricsBySongIdStruct)->displayTitle); } + for (int i = 0; i < (*getLyricsBySongIdStruct)->lyricsAmount; i++) { + if ((*getLyricsBySongIdStruct)->lyrics[i].data != NULL) { free((*getLyricsBySongIdStruct)->lyrics[i].data); } + } + if ((*getLyricsBySongIdStruct)->lyrics != NULL) { free((*getLyricsBySongIdStruct)->lyrics); } + if (*getLyricsBySongIdStruct != NULL) { free(*getLyricsBySongIdStruct); } +} diff --git a/src/libopensubsonic/endpoint_getLyricsBySongId.h b/src/libopensubsonic/endpoint_getLyricsBySongId.h new file mode 100644 index 0000000..4fd2403 --- /dev/null +++ b/src/libopensubsonic/endpoint_getLyricsBySongId.h @@ -0,0 +1,22 @@ +#ifndef _ENDPOINT_GETLYRICSBYSONGID_H +#define _ENDPOINT_GETLYRICSBYSONGID_H + +typedef struct { + char* data; + long offset; +} opensubsonic_getLyricsBySongId_lyric_struct; + +typedef struct { + char* status; + int errorCode; + char* errorMessage; + char* displayArtist; + char* displayTitle; + int lyricsAmount; + opensubsonic_getLyricsBySongId_lyric_struct* lyrics; +} opensubsonic_getLyricsBySongId_struct; + +int opensubsonic_getLyricsBySongId_parse(char* data, opensubsonic_getLyricsBySongId_struct** getLyricsBySongIdStruct); +void opensubsonic_getLyricsBySongId_struct_free(opensubsonic_getLyricsBySongId_struct** getLyricsBySongIdStruct); + +#endif diff --git a/src/libopensubsonic/endpoint_getPlaylist.c b/src/libopensubsonic/endpoint_getPlaylist.c new file mode 100644 index 0000000..27048dd --- /dev/null +++ b/src/libopensubsonic/endpoint_getPlaylist.c @@ -0,0 +1,166 @@ +#include +#include +#include +#include "../external/cJSON.h" +#include "logger.h" +#include "utils.h" +#include "endpoint_getPlaylist.h" + +int opensubsonic_getPlaylist_parse(char* data, opensubsonic_getPlaylist_struct** getPlaylistStruct) { + // Allocate struct + *getPlaylistStruct = (opensubsonic_getPlaylist_struct*)malloc(sizeof(opensubsonic_getPlaylist_struct)); + + // Initialize struct variables + (*getPlaylistStruct)->status = NULL; + (*getPlaylistStruct)->errorCode = 0; + (*getPlaylistStruct)->errorMessage = NULL; + (*getPlaylistStruct)->id = NULL; + (*getPlaylistStruct)->name = NULL; + (*getPlaylistStruct)->owner = NULL; + (*getPlaylistStruct)->isPublic = false; + (*getPlaylistStruct)->created = NULL; + (*getPlaylistStruct)->changed = NULL; + (*getPlaylistStruct)->songCount = 0; + (*getPlaylistStruct)->duration = 0; + (*getPlaylistStruct)->songs = NULL; + + // Parse the JSON + cJSON* root = cJSON_Parse(data); + if (root == NULL) { + logger_log_error(__func__, "Error parsing JSON."); + return 1; + } + + // Make an object from subsonic-response + cJSON* subsonic_root = cJSON_GetObjectItemCaseSensitive(root, "subsonic-response"); + if (subsonic_root == NULL) { + logger_log_error(__func__, "Error handling JSON - subsonic-response does not exist."); + cJSON_Delete(root); + return 1; + } + + OSS_Psoj(&(*getPlaylistStruct)->status, subsonic_root, "status"); + + // Check if API has returned an error + if (strstr((*getPlaylistStruct)->status, "ok") == NULL) { + // API has not returned 'ok' in status, fetch error, and return + // Check if an error is present + cJSON* subsonic_error = cJSON_GetObjectItemCaseSensitive(subsonic_root, "error"); + if (subsonic_error == NULL) { + // Error not defined in JSON + logger_log_error(__func__, "API has indicated failure through status, but error does not exist."); + cJSON_Delete(root); + return 1; + } + + OSS_Pioj(&(*getPlaylistStruct)->errorCode, subsonic_error, "code"); + OSS_Psoj(&(*getPlaylistStruct)->errorMessage, subsonic_error, "message"); + + logger_log_error(__func__, "Error noted in JSON - Code %d: %s", (*getPlaylistStruct)->errorCode, (*getPlaylistStruct)->errorMessage); + cJSON_Delete(root); + return 1; + } + + // Make an object from 'playlist' + cJSON* playlist_root = cJSON_GetObjectItemCaseSensitive(subsonic_root, "playlist"); + if (playlist_root == NULL) { + logger_log_error(__func__, "Error handling JSON - playlist does not exist."); + cJSON_Delete(root); + return 1; + } + + OSS_Psoj(&(*getPlaylistStruct)->id, playlist_root, "id"); + OSS_Psoj(&(*getPlaylistStruct)->name, playlist_root, "name"); + OSS_Psoj(&(*getPlaylistStruct)->owner, playlist_root, "owner"); + OSS_Pboj(&(*getPlaylistStruct)->isPublic, playlist_root, "public"); + OSS_Psoj(&(*getPlaylistStruct)->created, playlist_root, "created"); + OSS_Psoj(&(*getPlaylistStruct)->changed, playlist_root, "changed"); + OSS_Ploj(&(*getPlaylistStruct)->duration, playlist_root, "duration"); + + // Make an object from 'entry' + cJSON* entry_root = cJSON_GetObjectItemCaseSensitive(playlist_root, "entry"); + if (entry_root == NULL) { + logger_log_error(__func__, "Error handling JSON - entry does not exist."); + cJSON_Delete(root); + return 1; + } + + // Get the amount of songs in the playlist, and allocate memory + (*getPlaylistStruct)->songCount = cJSON_GetArraySize(entry_root); + (*getPlaylistStruct)->songs = (opensubsonic_getPlaylist_songs_struct*)malloc((*getPlaylistStruct)->songCount * sizeof(opensubsonic_getPlaylist_songs_struct)); + + // Initialize struct variables + for (int i = 0; i < (*getPlaylistStruct)->songCount; i++) { + (*getPlaylistStruct)->songs[i].id = NULL; + (*getPlaylistStruct)->songs[i].parent = NULL; + (*getPlaylistStruct)->songs[i].title = NULL; + (*getPlaylistStruct)->songs[i].albumId = NULL; + (*getPlaylistStruct)->songs[i].album = NULL; + (*getPlaylistStruct)->songs[i].artistId = NULL; + (*getPlaylistStruct)->songs[i].artist = NULL; + (*getPlaylistStruct)->songs[i].coverArt = NULL; + (*getPlaylistStruct)->songs[i].duration = 0; + (*getPlaylistStruct)->songs[i].bitRate = 0; + (*getPlaylistStruct)->songs[i].bitDepth = 0; + (*getPlaylistStruct)->songs[i].samplingRate = 0; + (*getPlaylistStruct)->songs[i].channelCount = 0; + (*getPlaylistStruct)->songs[i].userRating = 0; + (*getPlaylistStruct)->songs[i].track = 0; + (*getPlaylistStruct)->songs[i].year = 0; + (*getPlaylistStruct)->songs[i].genre = NULL; + (*getPlaylistStruct)->songs[i].size = 0; + (*getPlaylistStruct)->songs[i].discNumber = 0; + } + + for (int i = 0; i < (*getPlaylistStruct)->songCount; i++) { + cJSON* curr_idx_root = cJSON_GetArrayItem(entry_root, i); + if (curr_idx_root != NULL) { + OSS_Psoj(&(*getPlaylistStruct)->songs[i].id, curr_idx_root, "id"); + OSS_Psoj(&(*getPlaylistStruct)->songs[i].parent, curr_idx_root, "parent"); + OSS_Psoj(&(*getPlaylistStruct)->songs[i].title, curr_idx_root, "title"); + OSS_Psoj(&(*getPlaylistStruct)->songs[i].albumId, curr_idx_root, "albumId"); + OSS_Psoj(&(*getPlaylistStruct)->songs[i].album, curr_idx_root, "album"); + OSS_Psoj(&(*getPlaylistStruct)->songs[i].artistId, curr_idx_root, "artistId"); + OSS_Psoj(&(*getPlaylistStruct)->songs[i].artist, curr_idx_root, "artist"); + OSS_Psoj(&(*getPlaylistStruct)->songs[i].coverArt, curr_idx_root, "coverArt"); + OSS_Ploj(&(*getPlaylistStruct)->songs[i].duration, curr_idx_root, "duration"); + OSS_Pioj(&(*getPlaylistStruct)->songs[i].bitRate, curr_idx_root, "bitRate"); + OSS_Pioj(&(*getPlaylistStruct)->songs[i].bitDepth, curr_idx_root, "bitDepth"); + OSS_Ploj(&(*getPlaylistStruct)->songs[i].samplingRate, curr_idx_root, "samplingRate"); + OSS_Pioj(&(*getPlaylistStruct)->songs[i].channelCount, curr_idx_root, "channelCount"); + OSS_Pioj(&(*getPlaylistStruct)->songs[i].userRating, curr_idx_root, "userRating"); + OSS_Pioj(&(*getPlaylistStruct)->songs[i].track, curr_idx_root, "track"); + OSS_Pioj(&(*getPlaylistStruct)->songs[i].year, curr_idx_root, "year"); + OSS_Psoj(&(*getPlaylistStruct)->songs[i].genre, curr_idx_root, "genre"); + OSS_Ploj(&(*getPlaylistStruct)->songs[i].size, curr_idx_root, "size"); + OSS_Pioj(&(*getPlaylistStruct)->songs[i].discNumber, curr_idx_root, "discNumber"); + } + } + + cJSON_Delete(root); + return 0; +} + +void opensubsonic_getPlaylist_struct_free(opensubsonic_getPlaylist_struct** getPlaylistStruct) { + logger_log_general(__func__, "Freeing /getPlaylist endpoint heap objects."); + if ((*getPlaylistStruct)->status != NULL) { free((*getPlaylistStruct)->status); } + if ((*getPlaylistStruct)->errorMessage != NULL) { free((*getPlaylistStruct)->errorMessage); } + if ((*getPlaylistStruct)->id != NULL) { free((*getPlaylistStruct)->id); } + if ((*getPlaylistStruct)->name != NULL) { free((*getPlaylistStruct)->name); } + if ((*getPlaylistStruct)->owner != NULL) { free((*getPlaylistStruct)->owner); } + if ((*getPlaylistStruct)->created != NULL) { free((*getPlaylistStruct)->created); } + if ((*getPlaylistStruct)->changed != NULL) { free((*getPlaylistStruct)->changed); } + for (int i = 0; i < (*getPlaylistStruct)->songCount; i++) { + if((*getPlaylistStruct)->songs[i].id != NULL) { free((*getPlaylistStruct)->songs[i].id); } + if((*getPlaylistStruct)->songs[i].parent != NULL) { free((*getPlaylistStruct)->songs[i].parent); } + if((*getPlaylistStruct)->songs[i].title != NULL) { free((*getPlaylistStruct)->songs[i].title); } + if((*getPlaylistStruct)->songs[i].albumId != NULL) { free((*getPlaylistStruct)->songs[i].albumId); } + if((*getPlaylistStruct)->songs[i].album != NULL) { free((*getPlaylistStruct)->songs[i].album); } + if((*getPlaylistStruct)->songs[i].artistId != NULL) { free((*getPlaylistStruct)->songs[i].artistId); } + if((*getPlaylistStruct)->songs[i].artist != NULL) { free((*getPlaylistStruct)->songs[i].artist); } + if((*getPlaylistStruct)->songs[i].coverArt != NULL) { free((*getPlaylistStruct)->songs[i].coverArt); } + if((*getPlaylistStruct)->songs[i].genre != NULL) { free((*getPlaylistStruct)->songs[i].genre); } + } + if ((*getPlaylistStruct)->songs != NULL) { free((*getPlaylistStruct)->songs); } + if (*getPlaylistStruct != NULL) { free(*getPlaylistStruct); } +} diff --git a/src/libopensubsonic/endpoint_getPlaylist.h b/src/libopensubsonic/endpoint_getPlaylist.h new file mode 100644 index 0000000..3461586 --- /dev/null +++ b/src/libopensubsonic/endpoint_getPlaylist.h @@ -0,0 +1,49 @@ +#ifndef _ENDPOINT_GETPLAYLIST_H +#define _ENDPOINT_GETPLAYLIST_H +#include + +typedef struct { + char* id; + char* parent; + char* title; + // 'isDir', 'isVideo', and 'type' are not needed + char* albumId; + char* album; // Album Name + char* artistId; + char* artist; // Artist Name + char* coverArt; // Cover Art ID + long duration; // Duration (seconds) + // Do not completely rely on the next 4 values if you can detect them manually + int bitRate; + int bitDepth; + long samplingRate; + int channelCount; + int userRating; + // Not sure what 'averageRating' is used for + int track; + int year; // Year released + char* genre; + long size; // Size in bytes + int discNumber; + // 'suffix', 'contentType', and 'path' not needed +} opensubsonic_getPlaylist_songs_struct; + +typedef struct { + char* status; + int errorCode; + char* errorMessage; + char* id; + char* name; + char* owner; + bool isPublic; // 'public' + char* created; + char* changed; + long duration; + int songCount; // Counts are not reliable from navidrome, counted manually + opensubsonic_getPlaylist_songs_struct* songs; +} opensubsonic_getPlaylist_struct; + +int opensubsonic_getPlaylist_parse(char* data, opensubsonic_getPlaylist_struct** getPlaylistStruct); +void opensubsonic_getPlaylist_struct_free(opensubsonic_getPlaylist_struct** getPlaylistStruct); + +#endif diff --git a/src/libopensubsonic/endpoint_getPlaylists.c b/src/libopensubsonic/endpoint_getPlaylists.c new file mode 100644 index 0000000..8bcd862 --- /dev/null +++ b/src/libopensubsonic/endpoint_getPlaylists.c @@ -0,0 +1,121 @@ +#include +#include +#include +#include "../external/cJSON.h" +#include "logger.h" +#include "utils.h" +#include "endpoint_getPlaylists.h" + +// Parse the JSON returned from the /rest/getPlaylists endpoint +// Returns 1 if failure occured, else 0 +int opensubsonic_getPlaylists_parse(char* data, opensubsonic_getPlaylists_struct** getPlaylistsStruct) { + // Allocate struct + *getPlaylistsStruct = (opensubsonic_getPlaylists_struct*)malloc(sizeof(opensubsonic_getPlaylists_struct)); + + // Initialize struct variables + (*getPlaylistsStruct)->status = NULL; + (*getPlaylistsStruct)->errorCode = 0; + (*getPlaylistsStruct)->errorMessage = NULL; + (*getPlaylistsStruct)->playlistCount = 0; + (*getPlaylistsStruct)->playlists = NULL; + + // Parse the JSON + cJSON* root = cJSON_Parse(data); + if (root == NULL) { + logger_log_error(__func__, "Error parsing JSON."); + return 1; + } + + // Make an object from subsonic-response + cJSON* subsonic_root = cJSON_GetObjectItemCaseSensitive(root, "subsonic-response"); + if (subsonic_root == NULL) { + logger_log_error(__func__, "Error handling JSON - subsonic-response does not exist."); + cJSON_Delete(root); + return 1; + } + + OSS_Psoj(&(*getPlaylistsStruct)->status, subsonic_root, "status"); + + // Check if API has returned an error + if (strstr((*getPlaylistsStruct)->status, "ok") == NULL) { + // API has not returned 'ok' in status, fetch error, and return + // Check if an error is present + cJSON* subsonic_error = cJSON_GetObjectItemCaseSensitive(subsonic_root, "error"); + if (subsonic_error == NULL) { + // Error not defined in JSON + logger_log_error(__func__, "API has indicated failure through status, but error does not exist."); + cJSON_Delete(root); + return 1; + } + + OSS_Pioj(&(*getPlaylistsStruct)->errorCode, subsonic_error, "code"); + OSS_Psoj(&(*getPlaylistsStruct)->errorMessage, subsonic_error, "message"); + + logger_log_error(__func__, "Error noted in JSON - Code %d: %s", (*getPlaylistsStruct)->errorCode, (*getPlaylistsStruct)->errorMessage); + cJSON_Delete(root); + return 1; + } + + // Make an object from playlists + cJSON* playlists_root = cJSON_GetObjectItemCaseSensitive(subsonic_root, "playlists"); + if (playlists_root == NULL) { + logger_log_error(__func__, "Error handling JSON - playlists does not exist."); + cJSON_Delete(root); + return 1; + } + + // Make an object from playlist + cJSON* playlist_root = cJSON_GetObjectItemCaseSensitive(playlists_root, "playlist"); + if (playlist_root == NULL) { + logger_log_error(__func__, "Error handling JSON - playlist does not exist."); + cJSON_Delete(root); + return 1; + } + + // Get the amount of playlists + (*getPlaylistsStruct)->playlistCount = cJSON_GetArraySize(playlist_root); + + // Allocate memory for opensubsonic_getPlaylists_playlist_struct inside opensubsonic_getPlaylists_struct + (*getPlaylistsStruct)->playlists = (opensubsonic_getPlaylists_playlist_struct*)malloc((*getPlaylistsStruct)->playlistCount * sizeof(opensubsonic_getPlaylists_playlist_struct)); + + // Initialize struct variables + for (int i = 0; i < (*getPlaylistsStruct)->playlistCount; i++) { + (*getPlaylistsStruct)->playlists[i].id = NULL; + (*getPlaylistsStruct)->playlists[i].name = NULL; + (*getPlaylistsStruct)->playlists[i].songCount = 0; + (*getPlaylistsStruct)->playlists[i].duration = 0; + (*getPlaylistsStruct)->playlists[i].owner = NULL; + (*getPlaylistsStruct)->playlists[i].coverArt = NULL; + } + + // Extract the data from the playlist array + for (int i = 0; i < (*getPlaylistsStruct)->playlistCount; i++) { + cJSON* current_index_root = cJSON_GetArrayItem(playlist_root, i); + if (current_index_root != NULL) { + // Fetch playlist information + OSS_Psoj(&(*getPlaylistsStruct)->playlists[i].id, current_index_root, "id"); + OSS_Psoj(&(*getPlaylistsStruct)->playlists[i].name, current_index_root, "name"); + OSS_Pioj(&(*getPlaylistsStruct)->playlists[i].songCount, current_index_root, "songCount"); + OSS_Ploj(&(*getPlaylistsStruct)->playlists[i].duration, current_index_root, "duration"); + OSS_Psoj(&(*getPlaylistsStruct)->playlists[i].owner, current_index_root, "owner"); + OSS_Psoj(&(*getPlaylistsStruct)->playlists[i].coverArt, current_index_root, "coverArt"); + } + } + + cJSON_Delete(root); + return 0; +} + +void opensubsonic_getPlaylists_struct_free(opensubsonic_getPlaylists_struct** getPlaylistsStruct) { + logger_log_general(__func__, "Freeing /getPlaylists endpoint heap objects."); + if ((*getPlaylistsStruct)->status != NULL) { free((*getPlaylistsStruct)->status); } + if ((*getPlaylistsStruct)->errorMessage != NULL) { free((*getPlaylistsStruct)->errorMessage); } + for (size_t i = 0; i < (*getPlaylistsStruct)->playlistCount; i++) { + if ((*getPlaylistsStruct)->playlists[i].id != NULL) { free((*getPlaylistsStruct)->playlists[i].id); } + if ((*getPlaylistsStruct)->playlists[i].name != NULL) { free((*getPlaylistsStruct)->playlists[i].name); } + if ((*getPlaylistsStruct)->playlists[i].owner != NULL) { free((*getPlaylistsStruct)->playlists[i].owner); } + if ((*getPlaylistsStruct)->playlists[i].coverArt != NULL) { free((*getPlaylistsStruct)->playlists[i].coverArt); } + } + if ((*getPlaylistsStruct)->playlists != NULL) { free((*getPlaylistsStruct)->playlists); } + if (*getPlaylistsStruct != NULL) { free(*getPlaylistsStruct); } +} diff --git a/src/libopensubsonic/endpoint_getPlaylists.h b/src/libopensubsonic/endpoint_getPlaylists.h new file mode 100644 index 0000000..da1a47a --- /dev/null +++ b/src/libopensubsonic/endpoint_getPlaylists.h @@ -0,0 +1,26 @@ +#ifndef _ENDPOINT_GETPLAYLISTS_H +#define _ENDPOINT_GETPLAYLISTS_H + +typedef struct { + char* id; // Album ID + char* name; // Album name + int songCount; // Number of songs in the album + long duration; // Duration of the album in seconds + // 'public' doesn't seem useful here + char* owner; // Username of the owner of the album + // 'created' and 'changed' do not seem useful here either + char* coverArt; // Cover art ID of the album +} opensubsonic_getPlaylists_playlist_struct; + +typedef struct { + char* status; + int errorCode; + char* errorMessage; + int playlistCount; + opensubsonic_getPlaylists_playlist_struct* playlists; +} opensubsonic_getPlaylists_struct; + +int opensubsonic_getPlaylists_parse(char* data, opensubsonic_getPlaylists_struct** getPlaylistsStruct); +void opensubsonic_getPlaylists_struct_free(opensubsonic_getPlaylists_struct** getPlaylistsStruct); + +#endif diff --git a/src/libopensubsonic/endpoint_getSong.c b/src/libopensubsonic/endpoint_getSong.c new file mode 100644 index 0000000..ef1b025 --- /dev/null +++ b/src/libopensubsonic/endpoint_getSong.c @@ -0,0 +1,137 @@ +#include +#include +#include +#include "../external/cJSON.h" +#include "logger.h" +#include "utils.h" +#include "endpoint_getSong.h" + +// Parse the JSON returned from the /rest/getSong endpoint +// Returns 1 if failure occured, else 0 +int opensubsonic_getSong_parse(char* data, opensubsonic_getSong_struct** getSongStruct) { + // Allocate struct + *getSongStruct = (opensubsonic_getSong_struct*)malloc(sizeof(opensubsonic_getSong_struct)); + + // Initialize struct variables + (*getSongStruct)->status = NULL; + (*getSongStruct)->errorCode = 0; + (*getSongStruct)->errorMessage = NULL; + (*getSongStruct)->id = NULL; + (*getSongStruct)->parent = NULL; + (*getSongStruct)->title = NULL; + (*getSongStruct)->album = NULL; + (*getSongStruct)->artist = NULL; + (*getSongStruct)->track = 0; + (*getSongStruct)->year = 0; + (*getSongStruct)->coverArt = NULL; + (*getSongStruct)->size = 0; + (*getSongStruct)->starred = NULL; + (*getSongStruct)->duration = 0; + (*getSongStruct)->bitRate = 0; + (*getSongStruct)->bitDepth = 0; + (*getSongStruct)->samplingRate = 0; + (*getSongStruct)->channelCount = 0; + (*getSongStruct)->played = NULL; + (*getSongStruct)->discNumber = 0; + (*getSongStruct)->created = NULL; + (*getSongStruct)->albumId = NULL; + (*getSongStruct)->artistId = NULL; + (*getSongStruct)->displayArtist = NULL; + (*getSongStruct)->displayAlbumArtist = NULL; + (*getSongStruct)->displayComposer = NULL; + + // Parse the JSON + cJSON* root = cJSON_Parse(data); + if (root == NULL) { + logger_log_error(__func__, "Error parsing JSON."); + return 1; + } + + // Make an object from subsonic-response + cJSON* subsonic_root = cJSON_GetObjectItemCaseSensitive(root, "subsonic-response"); + if (subsonic_root == NULL) { + logger_log_error(__func__, "Error handling JSON - subsonic-response does not exist."); + cJSON_Delete(root); + return 1; + } + + OSS_Psoj(&(*getSongStruct)->status, subsonic_root, "status"); + + // Check if API has returned an error + if (strstr((*getSongStruct)->status, "ok") == NULL) { + // API has not returned 'ok' in status, fetch error, and return + // Check if an error is present + cJSON* subsonic_error = cJSON_GetObjectItemCaseSensitive(subsonic_root, "error"); + if (subsonic_error == NULL) { + // Error not defined in JSON + logger_log_error(__func__, "API has indicated failure through status, but error does not exist."); + cJSON_Delete(root); + return 1; + } + + OSS_Pioj(&(*getSongStruct)->errorCode, subsonic_error, "code"); + OSS_Psoj(&(*getSongStruct)->errorMessage, subsonic_error, "message"); + + logger_log_error(__func__, "Error noted in JSON - Code %d: %s", (*getSongStruct)->errorCode, (*getSongStruct)->errorMessage); + cJSON_Delete(root); + return 1; + } + + // Make an object from song + cJSON* song_root = cJSON_GetObjectItemCaseSensitive(subsonic_root, "song"); + if (song_root == NULL) { + logger_log_error(__func__, "Error handling JSON - song does not exist."); + cJSON_Delete(root); + return 1; + } + + // Fetch song information + OSS_Psoj(&(*getSongStruct)->id, song_root, "id"); + OSS_Psoj(&(*getSongStruct)->parent, song_root, "parent"); + OSS_Psoj(&(*getSongStruct)->title, song_root, "title"); + OSS_Psoj(&(*getSongStruct)->album, song_root, "album"); + OSS_Psoj(&(*getSongStruct)->artist, song_root, "artist"); + OSS_Pioj(&(*getSongStruct)->track, song_root, "track"); + OSS_Pioj(&(*getSongStruct)->year, song_root, "year"); + OSS_Psoj(&(*getSongStruct)->coverArt, song_root, "coverArt"); + OSS_Ploj(&(*getSongStruct)->size, song_root, "size"); + OSS_Psoj(&(*getSongStruct)->starred, song_root, "starred"); + OSS_Ploj(&(*getSongStruct)->duration, song_root, "duration"); + OSS_Pioj(&(*getSongStruct)->bitRate, song_root, "bitRate"); + OSS_Pioj(&(*getSongStruct)->bitDepth, song_root, "bitDepth"); + OSS_Ploj(&(*getSongStruct)->samplingRate, song_root, "samplingRate"); + OSS_Pioj(&(*getSongStruct)->channelCount, song_root, "channelCount"); + OSS_Psoj(&(*getSongStruct)->played, song_root, "played"); + OSS_Pioj(&(*getSongStruct)->discNumber, song_root, "discNumber"); + OSS_Psoj(&(*getSongStruct)->created, song_root, "created"); + OSS_Psoj(&(*getSongStruct)->albumId, song_root, "albumId"); + OSS_Psoj(&(*getSongStruct)->artistId, song_root, "artistId"); + OSS_Psoj(&(*getSongStruct)->displayArtist, song_root, "displayArtist"); + OSS_Psoj(&(*getSongStruct)->displayAlbumArtist, song_root, "displayAlbumArtist"); + OSS_Psoj(&(*getSongStruct)->displayComposer, song_root, "displayComposer"); + + cJSON_Delete(root); + return 0; +} + +// Free the dynamically allocated elements of the opensubsonic_getSong_struct structure +void opensubsonic_getSong_struct_free(opensubsonic_getSong_struct** getSongStruct) { + logger_log_general(__func__, "Freeing /getSong endpoint heap objects."); + if ((*getSongStruct)->status != NULL) { free((*getSongStruct)->status); } + if ((*getSongStruct)->errorMessage != NULL) { free((*getSongStruct)->errorMessage); } + if ((*getSongStruct)->id != NULL) { free((*getSongStruct)->id); } + if ((*getSongStruct)->parent != NULL) { free((*getSongStruct)->parent); } + if ((*getSongStruct)->title != NULL) { free((*getSongStruct)->title); } + if ((*getSongStruct)->album != NULL) { free((*getSongStruct)->album); } + if ((*getSongStruct)->artist != NULL) { free((*getSongStruct)->artist); } + if ((*getSongStruct)->coverArt != NULL) { free((*getSongStruct)->coverArt); } + if ((*getSongStruct)->starred != NULL) { free((*getSongStruct)->starred); } + if ((*getSongStruct)->played != NULL) { free((*getSongStruct)->played); } + if ((*getSongStruct)->created != NULL) { free((*getSongStruct)->created); } + if ((*getSongStruct)->albumId != NULL) { free((*getSongStruct)->albumId); } + if ((*getSongStruct)->artistId != NULL) { free((*getSongStruct)->artistId); } + if ((*getSongStruct)->displayArtist != NULL) { free((*getSongStruct)->displayArtist); } + if ((*getSongStruct)->displayAlbumArtist != NULL) { free((*getSongStruct)->displayAlbumArtist); } + if ((*getSongStruct)->displayComposer != NULL) { free((*getSongStruct)->displayComposer); } + if (*getSongStruct != NULL) { free(*getSongStruct); } +} diff --git a/src/libopensubsonic/endpoint_getSong.h b/src/libopensubsonic/endpoint_getSong.h new file mode 100644 index 0000000..6659268 --- /dev/null +++ b/src/libopensubsonic/endpoint_getSong.h @@ -0,0 +1,39 @@ +#ifndef _ENDPOINT_GETSONG_H +#define _ENDPOINT_GETSONG_H + +typedef struct { + char* status; // Request status + int errorCode; // Request error code (0 if none) + char* errorMessage; // Request error message (NULL if none) + char* id; // Song ID + char* parent; // Song parent + // 'isDir' excluded + char* title; // Song title + char* album; // Song album + char* artist; // Song artist + int track; // Track number + int year; // Year released + char* coverArt; // Cover art ID + long size; // Size of the song in bytes + // 'contentType', 'suffix' excluded + char* starred; // Time starred + long duration; // Duration of the song in seconds + int bitRate; // Bitrate of the song + int bitDepth; // Bit depth + long samplingRate; // Sample rate + int channelCount; // Channel count + char* played; // Time last played (TODO probably) + int discNumber; // Disc number + char* created; // Date added (TODO probably) + char* albumId; // Album ID + char* artistId; // Artist ID + // 'type', 'mediaType', 'isVideo', 'bpm', 'comment', 'sortName', 'musicBrainzId', 'genres', 'artists', 'albumArtists', 'contributors', 'moods', 'replayGain' excluded + char* displayArtist; // Display Artist + char* displayAlbumArtist; // Display Album Artist + char* displayComposer; // Display Composer +} opensubsonic_getSong_struct; + +int opensubsonic_getSong_parse(char* data, opensubsonic_getSong_struct** getSongStruct); +void opensubsonic_getSong_struct_free(opensubsonic_getSong_struct** getSongStruct); + +#endif diff --git a/src/libopensubsonic/endpoint_getStarred.c b/src/libopensubsonic/endpoint_getStarred.c new file mode 100644 index 0000000..34086e0 --- /dev/null +++ b/src/libopensubsonic/endpoint_getStarred.c @@ -0,0 +1,260 @@ +#include +#include +#include +#include "../external/cJSON.h" +#include "logger.h" +#include "utils.h" +#include "endpoint_getStarred.h" + +// Returns 1 if failure occured, else 0 +int opensubsonic_getStarred_parse(char* data, opensubsonic_getStarred_struct** getStarredStruct) { + // Allocate and initialize + *getStarredStruct = (opensubsonic_getStarred_struct*)malloc(sizeof(opensubsonic_getStarred_struct)); + (*getStarredStruct)->status = NULL; + (*getStarredStruct)->errorMessage = NULL; + (*getStarredStruct)->errorCode = 0; + (*getStarredStruct)->artistCount = -1; + (*getStarredStruct)->albumCount = -1; + (*getStarredStruct)->songCount = -1; + (*getStarredStruct)->artists = NULL; + (*getStarredStruct)->albums = NULL; + (*getStarredStruct)->songs = NULL; + + // Parse the JSON + cJSON* root = cJSON_Parse(data); + if (root == NULL) { + logger_log_error(__func__, "Error parsing JSON."); + return 1; + } + + // Make an object from subsonic-response + cJSON* subsonic_root = cJSON_GetObjectItemCaseSensitive(root, "subsonic-response"); + if (subsonic_root == NULL) { + logger_log_error(__func__, "Error handling JSON - subsonic-response does not exist."); + cJSON_Delete(root); + return 1; + } + + // Check for error + OSS_Psoj(&(*getStarredStruct)->status, subsonic_root, "status"); + if (strstr((*getStarredStruct)->status, "ok") == NULL) { + cJSON* subsonic_error = cJSON_GetObjectItemCaseSensitive(subsonic_root, "error"); + if (subsonic_error == NULL) { + logger_log_error(__func__, "API has indicated failure through status, but error does not exist."); + cJSON_Delete(root); + return 1; + } + + OSS_Pioj(&(*getStarredStruct)->errorCode, subsonic_error, "code"); + OSS_Psoj(&(*getStarredStruct)->errorMessage, subsonic_error, "message"); + logger_log_error(__func__, "Error noted in JSON - Code %d: %s", (*getStarredStruct)->errorCode, (*getStarredStruct)->errorMessage); + cJSON_Delete(root); + return 1; + } + + // Make an object from starred + cJSON* starred_root = cJSON_GetObjectItemCaseSensitive(subsonic_root, "starred"); + if (starred_root == NULL) { + logger_log_error(__func__, "Error handling JSON - starred does not exist."); + cJSON_Delete(root); + return 1; + } + + // Make an object from artist, album, and song + cJSON* artist_root = cJSON_GetObjectItemCaseSensitive(starred_root, "artist"); + if (artist_root == NULL) { + logger_log_error(__func__, "Error handling JSON - artist does not exist."); + } else { + (*getStarredStruct)->artistCount = 0; + } + + cJSON* album_root = cJSON_GetObjectItemCaseSensitive(starred_root, "album"); + if (album_root == NULL) { + logger_log_error(__func__, "Error handling JSON - album does not exist."); + } else { + (*getStarredStruct)->albumCount = 0; + } + + cJSON* song_root = cJSON_GetObjectItemCaseSensitive(starred_root, "song"); + if (song_root == NULL) { + logger_log_error(__func__, "Error handling JSON - song does not exist."); + } else { + (*getStarredStruct)->songCount = 0; + } + + // Extract starred artists + if ((*getStarredStruct)->artistCount != -1) { + // Count, allocate, initialize, and extract + (*getStarredStruct)->artistCount = cJSON_GetArraySize(artist_root); + (*getStarredStruct)->artists = (opensubsonic_getStarred_artist_struct*)malloc((*getStarredStruct)->artistCount * sizeof(opensubsonic_getStarred_artist_struct)); + + for (int i = 0; i < (*getStarredStruct)->artistCount; i++) { + (*getStarredStruct)->artists[i].id = NULL; + (*getStarredStruct)->artists[i].name = NULL; + (*getStarredStruct)->artists[i].coverArt = NULL; + (*getStarredStruct)->artists[i].starred = NULL; + } + + for (int i = 0; i < (*getStarredStruct)->artistCount; i++) { + cJSON* array_artist_root = cJSON_GetArrayItem(artist_root, i); + if (array_artist_root != NULL) { + OSS_Psoj(&(*getStarredStruct)->artists[i].id, array_artist_root, "id"); + OSS_Psoj(&(*getStarredStruct)->artists[i].name, array_artist_root, "name"); + OSS_Psoj(&(*getStarredStruct)->artists[i].coverArt, array_artist_root, "coverArt"); + OSS_Psoj(&(*getStarredStruct)->artists[i].starred, array_artist_root, "starred"); + } + } + } else { + // No starred artists + (*getStarredStruct)->artistCount = 0; + } + + // Extract starred albums + if ((*getStarredStruct)->albumCount != -1) { + // Count, allocate, initialize, and extract + (*getStarredStruct)->albumCount = cJSON_GetArraySize(album_root); + (*getStarredStruct)->albums = (opensubsonic_getStarred_album_struct*)malloc((*getStarredStruct)->albumCount * sizeof(opensubsonic_getStarred_album_struct)); + + for (int i = 0; i < (*getStarredStruct)->albumCount; i++) { + (*getStarredStruct)->albums[i].id = NULL; + (*getStarredStruct)->albums[i].parent = NULL; + (*getStarredStruct)->albums[i].album = NULL; + (*getStarredStruct)->albums[i].title = NULL; + (*getStarredStruct)->albums[i].name = NULL; + (*getStarredStruct)->albums[i].coverArt = NULL; + (*getStarredStruct)->albums[i].songCount = 0; + (*getStarredStruct)->albums[i].created = NULL; + (*getStarredStruct)->albums[i].duration = 0; + (*getStarredStruct)->albums[i].playCount = 0; + (*getStarredStruct)->albums[i].artistId = NULL; + (*getStarredStruct)->albums[i].artist = NULL; + (*getStarredStruct)->albums[i].year = 0; + (*getStarredStruct)->albums[i].genre = NULL; + } + + for (int i = 0; i < (*getStarredStruct)->albumCount; i++) { + cJSON* array_album_root = cJSON_GetArrayItem(album_root, i); + if (array_album_root != NULL) { + OSS_Psoj(&(*getStarredStruct)->albums[i].id, array_album_root, "id"); + OSS_Psoj(&(*getStarredStruct)->albums[i].parent, array_album_root, "parent"); + OSS_Psoj(&(*getStarredStruct)->albums[i].album, array_album_root, "album"); + OSS_Psoj(&(*getStarredStruct)->albums[i].title, array_album_root, "title"); + OSS_Psoj(&(*getStarredStruct)->albums[i].name, array_album_root, "name"); + OSS_Psoj(&(*getStarredStruct)->albums[i].coverArt, array_album_root, "coverArt"); + OSS_Pioj(&(*getStarredStruct)->albums[i].songCount, array_album_root, "songCount"); + OSS_Psoj(&(*getStarredStruct)->albums[i].created, array_album_root, "created"); + OSS_Ploj(&(*getStarredStruct)->albums[i].duration, array_album_root, "duration"); + OSS_Pioj(&(*getStarredStruct)->albums[i].playCount, array_album_root, "playCount"); + OSS_Psoj(&(*getStarredStruct)->albums[i].artistId, array_album_root, "artistId"); + OSS_Psoj(&(*getStarredStruct)->albums[i].artist, array_album_root, "artist"); + OSS_Pioj(&(*getStarredStruct)->albums[i].year, array_album_root, "year"); + OSS_Psoj(&(*getStarredStruct)->albums[i].genre, array_album_root, "genre"); + } + } + } else { + // No starred albums + (*getStarredStruct)->albumCount = 0; + } + + // Extract starred songs + if ((*getStarredStruct)->songCount != -1) { + // Count, allocate, initialize, and extract + (*getStarredStruct)->songCount = cJSON_GetArraySize(song_root); + (*getStarredStruct)->songs = (opensubsonic_getStarred_song_struct*)malloc((*getStarredStruct)->songCount * sizeof(opensubsonic_getStarred_song_struct)); + + for (int i = 0; i < (*getStarredStruct)->songCount; i++) { + (*getStarredStruct)->songs[i].id = NULL; + (*getStarredStruct)->songs[i].parent = NULL; + (*getStarredStruct)->songs[i].title = NULL; + (*getStarredStruct)->songs[i].album = NULL; + (*getStarredStruct)->songs[i].artist = NULL; + (*getStarredStruct)->songs[i].track = 0; + (*getStarredStruct)->songs[i].year = 0; + (*getStarredStruct)->songs[i].coverArt = NULL; + (*getStarredStruct)->songs[i].size = 0; + (*getStarredStruct)->songs[i].starred = NULL; + (*getStarredStruct)->songs[i].duration = 0; + (*getStarredStruct)->songs[i].bitRate = 0; + (*getStarredStruct)->songs[i].bitDepth = 0; + (*getStarredStruct)->songs[i].samplingRate = 0; + (*getStarredStruct)->songs[i].channelCount = 0; + (*getStarredStruct)->songs[i].playCount = 0; + (*getStarredStruct)->songs[i].discNumber = 0; + (*getStarredStruct)->songs[i].created = NULL; + (*getStarredStruct)->songs[i].albumId = NULL; + (*getStarredStruct)->songs[i].artistId = NULL; + } + + for (int i = 0; i < (*getStarredStruct)->songCount; i++) { + cJSON* array_song_root = cJSON_GetArrayItem(song_root, i); + if (array_song_root != NULL) { + OSS_Psoj(&(*getStarredStruct)->songs[i].id, array_song_root, "id"); + OSS_Psoj(&(*getStarredStruct)->songs[i].parent, array_song_root, "parent"); + OSS_Psoj(&(*getStarredStruct)->songs[i].title, array_song_root, "title"); + OSS_Psoj(&(*getStarredStruct)->songs[i].album, array_song_root, "album"); + OSS_Psoj(&(*getStarredStruct)->songs[i].artist, array_song_root, "artist"); + OSS_Pioj(&(*getStarredStruct)->songs[i].track, array_song_root, "track"); + OSS_Pioj(&(*getStarredStruct)->songs[i].year, array_song_root, "year"); + OSS_Psoj(&(*getStarredStruct)->songs[i].coverArt, array_song_root, "coverArt"); + OSS_Ploj(&(*getStarredStruct)->songs[i].size, array_song_root, "size"); + OSS_Psoj(&(*getStarredStruct)->songs[i].starred, array_song_root, "starred"); + OSS_Ploj(&(*getStarredStruct)->songs[i].duration, array_song_root, "duration"); + OSS_Pioj(&(*getStarredStruct)->songs[i].bitRate, array_song_root, "bitRate"); + OSS_Pioj(&(*getStarredStruct)->songs[i].bitDepth, array_song_root, "bitDepth"); + OSS_Ploj(&(*getStarredStruct)->songs[i].samplingRate, array_song_root, "samplingRate"); + OSS_Pioj(&(*getStarredStruct)->songs[i].channelCount, array_song_root, "channelCount"); + OSS_Pioj(&(*getStarredStruct)->songs[i].playCount, array_song_root, "playCount"); + OSS_Pioj(&(*getStarredStruct)->songs[i].discNumber, array_song_root, "discNumber"); + OSS_Psoj(&(*getStarredStruct)->songs[i].created, array_song_root, "created"); + OSS_Psoj(&(*getStarredStruct)->songs[i].albumId, array_song_root, "albumId"); + OSS_Psoj(&(*getStarredStruct)->songs[i].artistId, array_song_root, "artistId"); + } + } + } else { + // No starred songs + (*getStarredStruct)->songCount = 0; + } + + cJSON_Delete(root); + return 0; +} + +void opensubsonic_getStarred_struct_free(opensubsonic_getStarred_struct** getStarredStruct) { + logger_log_general(__func__, "Freeing /getStarred endpoint heap objects."); + if ((*getStarredStruct)->status != NULL) { free((*getStarredStruct)->status); } + if ((*getStarredStruct)->errorMessage != NULL) { free((*getStarredStruct)->errorMessage); } + for (int i = 0; i < (*getStarredStruct)->artistCount; i++) { + if ((*getStarredStruct)->artists[i].id != NULL) { free((*getStarredStruct)->artists[i].id); } + if ((*getStarredStruct)->artists[i].name != NULL) { free((*getStarredStruct)->artists[i].name); } + if ((*getStarredStruct)->artists[i].coverArt != NULL) { free((*getStarredStruct)->artists[i].coverArt); } + if ((*getStarredStruct)->artists[i].starred != NULL) { free((*getStarredStruct)->artists[i].starred); } + } + for (int i = 0; i < (*getStarredStruct)->albumCount; i++) { + if ((*getStarredStruct)->albums[i].id != NULL) { free((*getStarredStruct)->albums[i].id); } + if ((*getStarredStruct)->albums[i].parent != NULL) { free((*getStarredStruct)->albums[i].parent); } + if ((*getStarredStruct)->albums[i].album != NULL) { free((*getStarredStruct)->albums[i].album); } + if ((*getStarredStruct)->albums[i].title != NULL) { free((*getStarredStruct)->albums[i].title); } + if ((*getStarredStruct)->albums[i].name != NULL) { free((*getStarredStruct)->albums[i].name); } + if ((*getStarredStruct)->albums[i].coverArt != NULL) { free((*getStarredStruct)->albums[i].coverArt); } + if ((*getStarredStruct)->albums[i].created != NULL) { free((*getStarredStruct)->albums[i].created); } + if ((*getStarredStruct)->albums[i].artistId != NULL) { free((*getStarredStruct)->albums[i].artistId); } + if ((*getStarredStruct)->albums[i].artist != NULL) { free((*getStarredStruct)->albums[i].artist); } + if ((*getStarredStruct)->albums[i].genre != NULL) { free((*getStarredStruct)->albums[i].genre); } + } + for (int i = 0; i < (*getStarredStruct)->songCount; i++) { + if ((*getStarredStruct)->songs[i].id != NULL) { free((*getStarredStruct)->songs[i].id); } + if ((*getStarredStruct)->songs[i].parent != NULL) { free((*getStarredStruct)->songs[i].parent); } + if ((*getStarredStruct)->songs[i].title != NULL) { free((*getStarredStruct)->songs[i].title); } + if ((*getStarredStruct)->songs[i].album != NULL) { free((*getStarredStruct)->songs[i].album); } + if ((*getStarredStruct)->songs[i].artist != NULL) { free((*getStarredStruct)->songs[i].artist); } + if ((*getStarredStruct)->songs[i].coverArt != NULL) { free((*getStarredStruct)->songs[i].coverArt); } + if ((*getStarredStruct)->songs[i].starred != NULL) { free((*getStarredStruct)->songs[i].starred); } + if ((*getStarredStruct)->songs[i].created != NULL) { free((*getStarredStruct)->songs[i].created); } + if ((*getStarredStruct)->songs[i].albumId != NULL) { free((*getStarredStruct)->songs[i].albumId); } + if ((*getStarredStruct)->songs[i].artistId != NULL) { free((*getStarredStruct)->songs[i].artistId); } + } + if ((*getStarredStruct)->artists != NULL) { free((*getStarredStruct)->artists); } + if ((*getStarredStruct)->albums != NULL) { free((*getStarredStruct)->albums); } + if ((*getStarredStruct)->songs != NULL) { free((*getStarredStruct)->songs); } + if (*getStarredStruct != NULL) { free(*getStarredStruct); } +} diff --git a/src/libopensubsonic/endpoint_getStarred.h b/src/libopensubsonic/endpoint_getStarred.h new file mode 100644 index 0000000..39d1912 --- /dev/null +++ b/src/libopensubsonic/endpoint_getStarred.h @@ -0,0 +1,71 @@ +#ifndef _ENDPOINT_GETSTARRED_H +#define _ENDPOINT_GETSTARRED_H + +typedef struct { + char* id; + char* name; + char* coverArt; + char* starred; +} opensubsonic_getStarred_artist_struct; + +typedef struct { + char* id; + char* parent; + char* album; + char* title; + char* name; + // 'isDir' excluded + char* coverArt; + int songCount; // Do not rely on this + char* created; + long duration; + int playCount; + char* artistId; + char* artist; + int year; + char* genre; +} opensubsonic_getStarred_album_struct; + +typedef struct { + char* id; + char* parent; + // 'isDir' excluded + char* title; + char* album; + char* artist; + int track; + int year; + char* coverArt; + long size; + // 'contentType', 'suffix' excluded + char* starred; + long duration; + int bitRate; + int bitDepth; + long samplingRate; + int channelCount; + // 'path' excluded + int playCount; + int discNumber; + char* created; + char* albumId; + char* artistId; + // 'type', 'isVideo' excluded +} opensubsonic_getStarred_song_struct; + +typedef struct { + char* status; + int errorCode; + char* errorMessage; + int artistCount; + int albumCount; + int songCount; + opensubsonic_getStarred_artist_struct* artists; + opensubsonic_getStarred_album_struct* albums; + opensubsonic_getStarred_song_struct* songs; +} opensubsonic_getStarred_struct; + +int opensubsonic_getStarred_parse(char* data, opensubsonic_getStarred_struct** getStarredStruct); +void opensubsonic_getStarred_struct_free(opensubsonic_getStarred_struct** getStarredStruct); + +#endif diff --git a/src/libopensubsonic/endpoint_ping.c b/src/libopensubsonic/endpoint_ping.c new file mode 100644 index 0000000..3e9ef6f --- /dev/null +++ b/src/libopensubsonic/endpoint_ping.c @@ -0,0 +1,73 @@ +#include +#include +#include +#include "../external/cJSON.h" +#include "logger.h" +#include "utils.h" +#include "endpoint_ping.h" + +// Parse the JSON returned from the /rest/ping endpoint +// Returns 1 if failure occured, else 0 +int opensubsonic_ping_parse(char* data, opensubsonic_ping_struct** pingStruct) { + // Allocate on the heap + *pingStruct = (opensubsonic_ping_struct*)malloc(sizeof(opensubsonic_ping_struct)); + + // Initialize struct variables + (*pingStruct)->status = NULL; + (*pingStruct)->version = NULL; + (*pingStruct)->serverType = NULL; + (*pingStruct)->serverVersion = NULL; + (*pingStruct)->openSubsonicCapable = false; + (*pingStruct)->error = false; + (*pingStruct)->errorCode = 0; + (*pingStruct)->errorMessage = NULL; + + // Parse the JSON + cJSON* root = cJSON_Parse(data); + if (root == NULL) { + logger_log_error(__func__, "Error parsing JSON."); + return 1; + } + + // Make an object from subsonic-response + cJSON* subsonic_root = cJSON_GetObjectItemCaseSensitive(root, "subsonic-response"); + if (subsonic_root == NULL) { + logger_log_error(__func__, "Error handling JSON - subsonic-response does not exist."); + cJSON_Delete(root); + return 1; + } + + OSS_Psoj(&(*pingStruct)->status, subsonic_root, "status"); + OSS_Psoj(&(*pingStruct)->version, subsonic_root, "version"); + OSS_Psoj(&(*pingStruct)->serverType, subsonic_root, "type"); + OSS_Psoj(&(*pingStruct)->serverVersion, subsonic_root, "serverVersion"); + OSS_Pboj(&(*pingStruct)->openSubsonicCapable, subsonic_root, "openSubsonic"); + + // Check if an error is present + cJSON* subsonic_error = cJSON_GetObjectItemCaseSensitive(subsonic_root, "error"); + if (subsonic_error == NULL) { + // Error did not occur, return + cJSON_Delete(root); + return 0; + } + (*pingStruct)->error = true; + + // From this point on, error has occured, capture error information + OSS_Pioj(&(*pingStruct)->errorCode, subsonic_error, "code"); + OSS_Psoj(&(*pingStruct)->errorMessage, subsonic_error, "message"); + logger_log_error(__func__, "Error noted in JSON - Code %d: %s", (*pingStruct)->errorCode, (*pingStruct)->errorMessage); + + cJSON_Delete(root); + return 1; +} + +// Free the dynamically allocated elements of the opensubsonic_ping_struct structure +void opensubsonic_ping_struct_free(opensubsonic_ping_struct** pingStruct) { + logger_log_general(__func__, "Freeing /ping endpoint heap objects."); + if ((*pingStruct)->status != NULL) { free((*pingStruct)->status); } + if ((*pingStruct)->version != NULL) { free((*pingStruct)->version); } + if ((*pingStruct)->serverType != NULL) { free((*pingStruct)->serverType); } + if ((*pingStruct)->serverVersion != NULL) { free((*pingStruct)->serverVersion); } + if ((*pingStruct)->errorMessage != NULL) { free((*pingStruct)->errorMessage); } + if (*pingStruct != NULL) { free(*pingStruct); } +} diff --git a/src/libopensubsonic/endpoint_ping.h b/src/libopensubsonic/endpoint_ping.h new file mode 100644 index 0000000..9c166bd --- /dev/null +++ b/src/libopensubsonic/endpoint_ping.h @@ -0,0 +1,20 @@ +#ifndef _ENDPOINT_PING_H +#define _ENDPOINT_PING_H +#include +#include + +typedef struct { + char* status; + char* version; + char* serverType; + char* serverVersion; + bool openSubsonicCapable; + bool error; + int errorCode; + char* errorMessage; +} opensubsonic_ping_struct; + +int opensubsonic_ping_parse(char* data, opensubsonic_ping_struct** pingStruct); +void opensubsonic_ping_struct_free(opensubsonic_ping_struct** pingStruct); + +#endif diff --git a/src/libopensubsonic/endpoint_scrobble.c b/src/libopensubsonic/endpoint_scrobble.c new file mode 100644 index 0000000..46b484c --- /dev/null +++ b/src/libopensubsonic/endpoint_scrobble.c @@ -0,0 +1,58 @@ +#include +#include +#include +#include "../external/cJSON.h" +#include "logger.h" +#include "utils.h" +#include "endpoint_scrobble.h" + +// Returns 1 if failure occured, else 0 +int opensubsonic_scrobble_parse(char* data, opensubsonic_scrobble_struct** scrobbleStruct) { + // Allocate and initialize + (*scrobbleStruct) = (opensubsonic_scrobble_struct*)malloc(sizeof(opensubsonic_scrobble_struct)); + (*scrobbleStruct)->status = NULL; + (*scrobbleStruct)->errorCode = 0; + (*scrobbleStruct)->errorMessage = NULL; + + // Parse the JSON + cJSON* root = cJSON_Parse(data); + if (root == NULL) { + logger_log_error(__func__, "Error parsing JSON."); + return 1; + } + + // Make an object from subsonic-response + cJSON* subsonic_root = cJSON_GetObjectItemCaseSensitive(root, "subsonic-response"); + if (subsonic_root == NULL) { + logger_log_error(__func__, "Error handling JSON - subsonic-response does not exist."); + cJSON_Delete(root); + return 1; + } + + // Check for error + OSS_Psoj(&(*scrobbleStruct)->status, subsonic_root, "status"); + if (strstr((*scrobbleStruct)->status, "ok") == NULL) { + cJSON* subsonic_error = cJSON_GetObjectItemCaseSensitive(subsonic_root, "error"); + if (subsonic_error == NULL) { + logger_log_error(__func__, "API has indicated failure through status, but error does not exist."); + cJSON_Delete(root); + return 1; + } + + OSS_Pioj(&(*scrobbleStruct)->errorCode, subsonic_error, "code"); + OSS_Psoj(&(*scrobbleStruct)->errorMessage, subsonic_error, "message"); + logger_log_error(__func__, "Error noted in JSON - Code %d: %s", (*scrobbleStruct)->errorCode, (*scrobbleStruct)->errorMessage); + cJSON_Delete(root); + return 1; + } + + cJSON_Delete(root); + return 0; +} + +void opensubsonic_scrobble_struct_free(opensubsonic_scrobble_struct** scrobbleStruct) { + logger_log_general(__func__, "Freeing /scrobble endpoint heap objects."); + if ((*scrobbleStruct)->status != NULL) { free((*scrobbleStruct)->status); } + if ((*scrobbleStruct)->errorMessage != NULL) { free((*scrobbleStruct)->errorMessage); } + if (*scrobbleStruct != NULL) { free(*scrobbleStruct); } +} diff --git a/src/libopensubsonic/endpoint_scrobble.h b/src/libopensubsonic/endpoint_scrobble.h new file mode 100644 index 0000000..90413c1 --- /dev/null +++ b/src/libopensubsonic/endpoint_scrobble.h @@ -0,0 +1,13 @@ +#ifndef _ENDPOINT_SCROBBLE_H +#define _ENDPOINT_SCROBBLE_H + +typedef struct { + char* status; + int errorCode; + char* errorMessage; +} opensubsonic_scrobble_struct; + +int opensubsonic_scrobble_parse(char* data, opensubsonic_scrobble_struct** scrobbleStruct); +void opensubsonic_scrobble_struct_free(opensubsonic_scrobble_struct** scrobbleStruct); + +#endif diff --git a/src/libopensubsonic/httpclient.c b/src/libopensubsonic/httpclient.c new file mode 100644 index 0000000..d082e03 --- /dev/null +++ b/src/libopensubsonic/httpclient.c @@ -0,0 +1,428 @@ +#include +#include +#include +#include +#include "../external/cJSON.h" +#include "httpclient.h" +#include "logger.h" +#include "../configHandler.h" +#include "logger.h" + +#include "endpoint_ping.h" +#include "endpoint_getStarred.h" +#include "endpoint_getSong.h" +#include "endpoint_getPlaylists.h" +#include "endpoint_getPlaylist.h" +#include "endpoint_getArtists.h" +#include "endpoint_getArtist.h" +#include "endpoint_getLyricsBySongId.h" +#include "endpoint_getAlbumList.h" +#include "endpoint_getAlbum.h" +#include "endpoint_scrobble.h" + +static int rc = 0; +extern configHandler_config_t* configObj; + +void opensubsonic_httpClient_URL_prepare(opensubsonic_httpClient_URL_t** urlObj) { + // Initialize struct variables + (*urlObj)->endpoint = 0; + (*urlObj)->id = NULL; + (*urlObj)->type = 0; + (*urlObj)->amount = 0; + (*urlObj)->submit = false; + (*urlObj)->formedUrl = NULL; +} + +void opensubsonic_httpClient_URL_cleanup(opensubsonic_httpClient_URL_t** urlObj) { + if ((*urlObj)->formedUrl != NULL) { free((*urlObj)->formedUrl); } + if ((*urlObj)->id != NULL) { free((*urlObj)->id); } + if (*urlObj != NULL) { free(*urlObj); } +} + +void opensubsonic_httpClient_formUrl(opensubsonic_httpClient_URL_t** urlObj) { + // TODO fix hack, add error checking, + + char* url = NULL; + + switch ((*urlObj)->endpoint) { + case OPENSUBSONIC_ENDPOINT_PING: + rc = asprintf(&url, "%s://%s/rest/ping?u=%s&t=%s&s=%s&f=json&v=%s&c=%s", + configObj->opensubsonic_protocol, configObj->opensubsonic_server, configObj->opensubsonic_username, + configObj->internal_opensubsonic_loginToken, configObj->internal_opensubsonic_loginSalt, + configObj->internal_opensubsonic_version, configObj->internal_opensubsonic_clientName); + break; + case OPENSUBSONIC_ENDPOINT_GETSTARRED: + rc = asprintf(&url, "%s://%s/rest/getStarred?u=%s&t=%s&s=%s&f=json&v=%s&c=%s", + configObj->opensubsonic_protocol, configObj->opensubsonic_server, configObj->opensubsonic_username, + configObj->internal_opensubsonic_loginToken, configObj->internal_opensubsonic_loginSalt, + configObj->internal_opensubsonic_version, configObj->internal_opensubsonic_clientName); + break; + case OPENSUBSONIC_ENDPOINT_GETSONG: + if ((*urlObj)->id == NULL) { + logger_log_error(__func__, "(getSong) ID is null."); + // TODO handle error + break; + } + rc = asprintf(&url, "%s://%s/rest/getSong?u=%s&t=%s&s=%s&f=json&v=%s&c=%s&id=%s", + configObj->opensubsonic_protocol, configObj->opensubsonic_server, configObj->opensubsonic_username, + configObj->internal_opensubsonic_loginToken, configObj->internal_opensubsonic_loginSalt, + configObj->internal_opensubsonic_version, configObj->internal_opensubsonic_clientName, + (*urlObj)->id); + break; + case OPENSUBSONIC_ENDPOINT_STREAM: // Does not have a fetchResponse counterpart + if ((*urlObj)->id == NULL) { + logger_log_error(__func__, "(stream) ID is null."); + // TODO handle error + break; + } + rc = asprintf(&url, "%s://%s/rest/stream?u=%s&t=%s&s=%s&f=json&v=%s&c=%s&id=%s", + configObj->opensubsonic_protocol, configObj->opensubsonic_server, configObj->opensubsonic_username, + configObj->internal_opensubsonic_loginToken, configObj->internal_opensubsonic_loginSalt, + configObj->internal_opensubsonic_version, configObj->internal_opensubsonic_clientName, + (*urlObj)->id); + break; + case OPENSUBSONIC_ENDPOINT_GETCOVERART: // Does not have a fetchResponse counterpart + if ((*urlObj)->id == NULL) { + logger_log_error(__func__, "(getCoverArt) ID is null."); + // TODO handle error + break; + } + rc = asprintf(&url, "%s://%s/rest/getCoverArt?u=%s&t=%s&s=%s&f=json&v=%s&c=%s&id=%s", + configObj->opensubsonic_protocol, configObj->opensubsonic_server, configObj->opensubsonic_username, + configObj->internal_opensubsonic_loginToken, configObj->internal_opensubsonic_loginSalt, + configObj->internal_opensubsonic_version, configObj->internal_opensubsonic_clientName, + (*urlObj)->id); + break; + case OPENSUBSONIC_ENDPOINT_GETALBUM: + if ((*urlObj)->id == NULL) { + logger_log_error(__func__, "(getAlbum) ID is null."); + // TODO handle error + break; + } + rc = asprintf(&url, "%s://%s/rest/getAlbum?u=%s&t=%s&s=%s&f=json&v=%s&c=%s&id=%s", + configObj->opensubsonic_protocol, configObj->opensubsonic_server, configObj->opensubsonic_username, + configObj->internal_opensubsonic_loginToken, configObj->internal_opensubsonic_loginSalt, + configObj->internal_opensubsonic_version, configObj->internal_opensubsonic_clientName, + (*urlObj)->id); + break; + case OPENSUBSONIC_ENDPOINT_GETPLAYLISTS: + rc = asprintf(&url, "%s://%s/rest/getPlaylists?u=%s&t=%s&s=%s&f=json&v=%s&c=%s", + configObj->opensubsonic_protocol, configObj->opensubsonic_server, configObj->opensubsonic_username, + configObj->internal_opensubsonic_loginToken, configObj->internal_opensubsonic_loginSalt, + configObj->internal_opensubsonic_version, configObj->internal_opensubsonic_clientName); + break; + case OPENSUBSONIC_ENDPOINT_GETPLAYLIST: + if ((*urlObj)->id == NULL) { + logger_log_error(__func__, "(getPlaylist) ID is null."); + // TODO handle error + break; + } + rc = asprintf(&url, "%s://%s/rest/getPlaylist?u=%s&t=%s&s=%s&f=json&v=%s&c=%s&id=%s", + configObj->opensubsonic_protocol, configObj->opensubsonic_server, configObj->opensubsonic_username, + configObj->internal_opensubsonic_loginToken, configObj->internal_opensubsonic_loginSalt, + configObj->internal_opensubsonic_version, configObj->internal_opensubsonic_clientName, + (*urlObj)->id); + break; + case OPENSUBSONIC_ENDPOINT_GETARTISTS: + rc = asprintf(&url, "%s://%s/rest/getArtists?u=%s&t=%s&s=%s&f=json&v=%s&c=%s", + configObj->opensubsonic_protocol, configObj->opensubsonic_server, configObj->opensubsonic_username, + configObj->internal_opensubsonic_loginToken, configObj->internal_opensubsonic_loginSalt, + configObj->internal_opensubsonic_version, configObj->internal_opensubsonic_clientName); + break; + case OPENSUBSONIC_ENDPOINT_GETARTIST: + if ((*urlObj)->id == NULL) { + logger_log_error(__func__, "(getArtist) ID is null."); + // TODO handle error + break; + } + rc = asprintf(&url, "%s://%s/rest/getArtist?u=%s&t=%s&s=%s&f=json&v=%s&c=%s&id=%s", + configObj->opensubsonic_protocol, configObj->opensubsonic_server, configObj->opensubsonic_username, + configObj->internal_opensubsonic_loginToken, configObj->internal_opensubsonic_loginSalt, + configObj->internal_opensubsonic_version, configObj->internal_opensubsonic_clientName, + (*urlObj)->id); + break; + case OPENSUBSONIC_ENDPOINT_GETLYRICSBYSONGID: + if ((*urlObj)->id == NULL) { + logger_log_error(__func__, "(getArtist) ID is null."); + // TODO handle error + break; + } + rc = asprintf(&url, "%s://%s/rest/getLyricsBySongId?u=%s&t=%s&s=%s&f=json&v=%s&c=%s&id=%s", + configObj->opensubsonic_protocol, configObj->opensubsonic_server, configObj->opensubsonic_username, + configObj->internal_opensubsonic_loginToken, configObj->internal_opensubsonic_loginSalt, + configObj->internal_opensubsonic_version, configObj->internal_opensubsonic_clientName, + (*urlObj)->id); + break; + case OPENSUBSONIC_ENDPOINT_GETALBUMLIST: + if ((*urlObj)->type == 0) { + logger_log_error(__func__, "(getAlbumList) Type is 0."); + // TODO handle error + break; + } + if ((*urlObj)->amount == 0) { + logger_log_error(__func__, "(getAlbumList) Amount is 0."); + // TODO handle error + break; + } + + char* typeString = NULL; + switch ((*urlObj)->type) { + case OPENSUBSONIC_ENDPOINT_GETALBUMLIST_RANDOM: + rc = asprintf(&typeString, "random"); + break; + case OPENSUBSONIC_ENDPOINT_GETALBUMLIST_NEWEST: + rc = asprintf(&typeString, "newest"); + break; + case OPENSUBSONIC_ENDPOINT_GETALBUMLIST_HIGHEST: + rc = asprintf(&typeString, "highest"); + break; + case OPENSUBSONIC_ENDPOINT_GETALBUMLIST_FREQUENT: + rc = asprintf(&typeString, "frequent"); + break; + case OPENSUBSONIC_ENDPOINT_GETALBUMLIST_RECENT: + rc = asprintf(&typeString, "recent"); + break; + default: + logger_log_error(__func__, "(getAlbumList) Unknown type requested."); + // TODO handle error + } + + rc = asprintf(&url, "%s://%s/rest/getAlbumList?u=%s&t=%s&s=%s&f=json&v=%s&c=%s&type=%s&size=%d", + configObj->opensubsonic_protocol, configObj->opensubsonic_server, configObj->opensubsonic_username, + configObj->internal_opensubsonic_loginToken, configObj->internal_opensubsonic_loginSalt, + configObj->internal_opensubsonic_version, configObj->internal_opensubsonic_clientName, + typeString, (*urlObj)->amount); + free(typeString); + break; + case OPENSUBSONIC_ENDPOINT_SCROBBLE: + if ((*urlObj)->id == NULL) { + logger_log_error(__func__, "(scrobble) ID is null."); + // TODO handle error + break; + } + + char* submitString = NULL; + if ((*urlObj)->submit) { + rc = asprintf(&submitString, "true"); + } else { + rc = asprintf(&submitString, "false"); + } + rc = asprintf(&url, "%s://%s/rest/scrobble?u=%s&t=%s&s=%s&f=json&v=%s&c=%s&id=%s&submission=%s", + configObj->opensubsonic_protocol, configObj->opensubsonic_server, configObj->opensubsonic_username, + configObj->internal_opensubsonic_loginToken, configObj->internal_opensubsonic_loginSalt, + configObj->internal_opensubsonic_version, configObj->internal_opensubsonic_clientName, + (*urlObj)->id, submitString); + free(submitString); + break; + default: + logger_log_error(__func__, "Unknown endpoint requested."); + break; + } + + if (rc == -1) { + logger_log_error(__func__, "asprintf() error."); + // TODO handle error + } + + // HACK + (*urlObj)->formedUrl = strdup(url); free(url); +} + +void opensubsonic_httpClient_fetchResponse(opensubsonic_httpClient_URL_t** urlObj, void** responseObj) { + // Make and prepare HTTP object + opensubsonic_httpClientRequest_t* httpReq; + opensubsonic_httpClient_prepareRequest(&httpReq); + httpReq->method = HTTP_METHOD_GET; + httpReq->requestUrl = strdup((*urlObj)->formedUrl); + opensubsonic_httpClient_request(&httpReq); + + // Cannot use a switch statement here due to maintaining compatibility with < C23 + if ((*urlObj)->endpoint == OPENSUBSONIC_ENDPOINT_PING) { + opensubsonic_ping_struct** pingStruct = (opensubsonic_ping_struct**)responseObj; + opensubsonic_ping_parse(httpReq->responseMsg, pingStruct); + } else if ((*urlObj)->endpoint == OPENSUBSONIC_ENDPOINT_GETSTARRED) { + opensubsonic_getStarred_struct** getStarredStruct = (opensubsonic_getStarred_struct**)responseObj; + opensubsonic_getStarred_parse(httpReq->responseMsg, getStarredStruct); + } else if ((*urlObj)->endpoint == OPENSUBSONIC_ENDPOINT_GETSONG) { + opensubsonic_getSong_struct** getSongStruct = (opensubsonic_getSong_struct**)responseObj; + opensubsonic_getSong_parse(httpReq->responseMsg, getSongStruct); + } else if ((*urlObj)->endpoint == OPENSUBSONIC_ENDPOINT_GETALBUM) { + opensubsonic_getAlbum_struct** getAlbumStruct = (opensubsonic_getAlbum_struct**)responseObj; + opensubsonic_getAlbum_parse(httpReq->responseMsg, getAlbumStruct); + } else if ((*urlObj)->endpoint == OPENSUBSONIC_ENDPOINT_GETPLAYLISTS) { + opensubsonic_getPlaylists_struct** getPlaylistsStruct = (opensubsonic_getPlaylists_struct**)responseObj; + opensubsonic_getPlaylists_parse(httpReq->responseMsg, getPlaylistsStruct); + } else if ((*urlObj)->endpoint == OPENSUBSONIC_ENDPOINT_GETPLAYLIST) { + opensubsonic_getPlaylist_struct** getPlaylistStruct = (opensubsonic_getPlaylist_struct**)responseObj; + opensubsonic_getPlaylist_parse(httpReq->responseMsg, getPlaylistStruct); + } else if ((*urlObj)->endpoint == OPENSUBSONIC_ENDPOINT_GETARTISTS) { + opensubsonic_getArtists_struct** getArtistsStruct = (opensubsonic_getArtists_struct**)responseObj; + opensubsonic_getArtists_parse(httpReq->responseMsg, getArtistsStruct); + } else if ((*urlObj)->endpoint == OPENSUBSONIC_ENDPOINT_GETARTIST) { + opensubsonic_getArtist_struct** getArtistStruct = (opensubsonic_getArtist_struct**)responseObj; + opensubsonic_getArtist_parse(httpReq->responseMsg, getArtistStruct); + } else if ((*urlObj)->endpoint == OPENSUBSONIC_ENDPOINT_GETLYRICSBYSONGID) { + opensubsonic_getLyricsBySongId_struct** getLyricsBySongIdStruct = (opensubsonic_getLyricsBySongId_struct**)responseObj; + opensubsonic_getLyricsBySongId_parse(httpReq->responseMsg, getLyricsBySongIdStruct); + } else if ((*urlObj)->endpoint == OPENSUBSONIC_ENDPOINT_GETALBUMLIST) { + opensubsonic_getAlbumList_struct** getAlbumListStruct = (opensubsonic_getAlbumList_struct**)responseObj; + opensubsonic_getAlbumList_parse(httpReq->responseMsg, getAlbumListStruct); + } else if ((*urlObj)->endpoint == OPENSUBSONIC_ENDPOINT_SCROBBLE) { + opensubsonic_scrobble_struct** scrobbleStruct = (opensubsonic_scrobble_struct**)responseObj; + opensubsonic_scrobble_parse(httpReq->responseMsg, scrobbleStruct); + } else { + logger_log_error(__func__, "Unknown endpoint requested."); + } + + // Cleanup HTTP object + opensubsonic_httpClient_cleanup(&httpReq); +} + + + + +// Contact the /rest/getAlbum endpoint +int opensubsonic_getAlbum(const char* protocol_ptr, const char* server_ptr, const char* user_ptr, char* login_token_ptr, char* login_salt_ptr, const char* opensubsonic_version_ptr, const char* client_name_ptr, char* id, char** response) { + // Generate full URL, perform HTTP GET, and free the full URL + int rc = 0; + char* full_url = malloc(256); + snprintf(full_url, 256, "%s://%s/rest/getAlbum?u=%s&t=%s&s=%s&f=json&v=%s&c=%s&id=%s", protocol_ptr, server_ptr, user_ptr, login_token_ptr, login_salt_ptr, opensubsonic_version_ptr, client_name_ptr, id); + //rc = opensubsonic_http_json_get(full_url, response); + free(full_url); + return rc; +} + + +// TODO COVER ART - Returns JSON on error. +// {"subsonic-response":{"status":"failed","version":"1.16.1","type":"navidrome","serverVersion":"0.53.1-FREEBSD (1ba390a)","openSubsonic":true,"error":{"code":70,"message":"Artwork not found"}}} +// Contact the /rest/getCoverArt endpoint (Returns binary data) + + + + + + +// Functions for preparing / freeing a HTTP Request struct +void opensubsonic_httpClient_prepareRequest(opensubsonic_httpClientRequest_t** httpReq) { + // Allocate struct + *httpReq = (opensubsonic_httpClientRequest_t*)malloc(sizeof(opensubsonic_httpClientRequest_t)); + + // Initialize struct variables + (*httpReq)->requestUrl = NULL; + (*httpReq)->requestBody = NULL; + (*httpReq)->method = 0; + (*httpReq)->isBodyRequired = false; + (*httpReq)->scrobbler = 0; + (*httpReq)->responseCode = 0; + (*httpReq)->responseMsg = NULL; +} + +void opensubsonic_httpClient_cleanup(opensubsonic_httpClientRequest_t** httpReq) { + // Free heap-allocated struct variables + if ((*httpReq)->requestUrl != NULL) { free((*httpReq)->requestUrl); } + if ((*httpReq)->requestBody != NULL) { free((*httpReq)->requestBody); } + if ((*httpReq)->responseMsg != NULL) { free((*httpReq)->responseMsg); } + + // Free struct + free(*httpReq); +} + +// Perform HTTP POST for Scrobbling (This function is a wrapper around OS-specific networking functions) +int opensubsonic_httpClient_request(opensubsonic_httpClientRequest_t** httpReq) { + logger_log_general(__func__, "Performing HTTP Request."); +//#if defined(__APPLE__) && defined(__MACH__) + //XNU_HttpRequest(httpReq); +//#elif defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) + UNIX_HttpRequest(httpReq); +//#endif + return 0; +} + + + + + + + +struct memory { + char *data; + size_t size; +}; + +static size_t write_to_memory(void *ptr, size_t size, size_t nmemb, void *userdata) { + struct memory *mem = (struct memory *)userdata; + size_t total_size = size * nmemb; + + mem->data = realloc(mem->data, mem->size + total_size + 1); + if (!mem->data) return 0; // Fail on OOM + + memcpy(&(mem->data[mem->size]), ptr, total_size); + mem->size += total_size; + mem->data[mem->size] = '\0'; // Null-terminate + + return total_size; +} + +void UNIX_HttpRequest(opensubsonic_httpClientRequest_t** httpReq) { + CURL* curl_handle = curl_easy_init(); + struct curl_slist* header_list = NULL; + struct memory chunk = {0}; + long httpCode = 0; + + if (curl_handle) { + // Set method + if ((*httpReq)->method == HTTP_METHOD_GET) { + curl_easy_setopt(curl_handle, CURLOPT_CUSTOMREQUEST, "GET"); + } else if ((*httpReq)->method == HTTP_METHOD_POST) { + curl_easy_setopt(curl_handle, CURLOPT_CUSTOMREQUEST, "POST"); + if ((*httpReq)->isBodyRequired == false) { + header_list = curl_slist_append(header_list, "Content-Length: 0"); + } + } + + // Set scrobbler information + if ((*httpReq)->scrobbler == SCROBBLER_LISTENBRAINZ && (*httpReq)->method == HTTP_METHOD_POST) { + header_list = curl_slist_append(header_list, "Content-Type: application/json"); + char* authString = NULL; + rc = asprintf(&authString, "Authorization: Token %s", configObj->listenbrainz_token); + if (rc == -1) { + logger_log_error(__func__, "asprintf() error."); + // TODO handle error + } + printf("CODE: %s\n", authString); + header_list = curl_slist_append(header_list, authString); // TODO Check does this copy the string? + // TODO free auth string + } + + if ((*httpReq)->isBodyRequired == true && (*httpReq)->scrobbler == 0) { + header_list = curl_slist_append(header_list, "X-Organization: Hojuix"); + header_list = curl_slist_append(header_list, "X-Application: OSSP"); + } + + if (header_list != NULL) { + curl_easy_setopt(curl_handle, CURLOPT_HTTPHEADER, header_list); + } + + if ((*httpReq)->isBodyRequired == true) { + curl_easy_setopt(curl_handle, CURLOPT_POSTFIELDS, (*httpReq)->requestBody); + curl_easy_setopt(curl_handle, CURLOPT_POSTFIELDSIZE, (long)strlen((*httpReq)->requestBody)); + } + + curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "OSSP/1.0 (avery@hojuix.org)"); + curl_easy_setopt(curl_handle, CURLOPT_URL, (*httpReq)->requestUrl); + curl_easy_setopt(curl_handle, CURLOPT_MAXREDIRS, 50L); + curl_easy_setopt(curl_handle, CURLOPT_TCP_KEEPALIVE,0L); + curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYHOST, 0L); + curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, write_to_memory); + curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void *)&chunk); + CURLcode res = curl_easy_perform(curl_handle); + curl_easy_getinfo(curl_handle, CURLINFO_RESPONSE_CODE, &httpCode); + } + + curl_easy_cleanup(curl_handle); + + (*httpReq)->responseMsg = strdup(chunk.data); + (*httpReq)->responseCode = (int)httpCode; + free(chunk.data); +} diff --git a/src/libopensubsonic/httpclient.h b/src/libopensubsonic/httpclient.h new file mode 100644 index 0000000..ffb40de --- /dev/null +++ b/src/libopensubsonic/httpclient.h @@ -0,0 +1,75 @@ +#ifndef _HTTPCLIENT_H +#define _HTTPCLIENT_H +#include +#include + +#define SCROBBLER_LISTENBRAINZ 101 +#define SCROBBLER_LASTFM 102 +#define HTTP_METHOD_GET 201 +#define HTTP_METHOD_POST 202 +#define HTTP_CODE_SUCCESS 200 +#define HTTP_CODE_NOT_AUTHORIZED 403 +#define OPENSUBSONIC_ENDPOINT_PING 301 +#define OPENSUBSONIC_ENDPOINT_GETSTARRED 302 +#define OPENSUBSONIC_ENDPOINT_GETSONG 303 +#define OPENSUBSONIC_ENDPOINT_STREAM 304 +#define OPENSUBSONIC_ENDPOINT_GETCOVERART 305 +#define OPENSUBSONIC_ENDPOINT_GETALBUM 306 +#define OPENSUBSONIC_ENDPOINT_GETPLAYLISTS 307 +#define OPENSUBSONIC_ENDPOINT_GETPLAYLIST 308 +#define OPENSUBSONIC_ENDPOINT_GETARTISTS 309 +#define OPENSUBSONIC_ENDPOINT_GETARTIST 310 +#define OPENSUBSONIC_ENDPOINT_GETLYRICSBYSONGID 311 +#define OPENSUBSONIC_ENDPOINT_GETALBUMLIST 312 +#define OPENSUBSONIC_ENDPOINT_SCROBBLE 313 +#define OPENSUBSONIC_ENDPOINT_GETALBUMLIST_RANDOM 501 +#define OPENSUBSONIC_ENDPOINT_GETALBUMLIST_NEWEST 502 +#define OPENSUBSONIC_ENDPOINT_GETALBUMLIST_HIGHEST 503 +#define OPENSUBSONIC_ENDPOINT_GETALBUMLIST_FREQUENT 504 +#define OPENSUBSONIC_ENDPOINT_GETALBUMLIST_RECENT 505 + +typedef struct { + int endpoint; // Endpoint + char* id; // ID + int type; // Type of request (As used in the /getAlbumList endpoint) + int amount; // Amount of items to return (Also as used in the /getAlbumList endpoint) + bool submit; // Submit scrobble (used for the /scrobble endpoint) + char* formedUrl; // Final URL +} opensubsonic_httpClient_URL_t; // Forms authenticated URLs with required parameters + +typedef struct { + // Request Information + char* requestUrl; + char* requestBody; + int method; + bool isBodyRequired; + int scrobbler; + + // Response Information + int responseCode; + char* responseMsg; +} opensubsonic_httpClientRequest_t; // OS-agnostic HTTP interface + +#if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) +void UNIX_HttpRequest(opensubsonic_httpClientRequest_t** httpReq); +#endif + +void opensubsonic_httpClient_URL_prepare(opensubsonic_httpClient_URL_t** urlObj); +void opensubsonic_httpClient_URL_cleanup(opensubsonic_httpClient_URL_t** urlObj); +void opensubsonic_httpClient_formUrl(opensubsonic_httpClient_URL_t** urlObj); +void opensubsonic_httpClient_fetchResponse(opensubsonic_httpClient_URL_t** urlObj, void** responseObj); +void opensubsonic_httpClient_prepareRequest(opensubsonic_httpClientRequest_t** httpReq); +void opensubsonic_httpClient_cleanup(opensubsonic_httpClientRequest_t** httpReq); +int opensubsonic_httpClient_request(opensubsonic_httpClientRequest_t** httpReq); + +void UNIX_HttpRequest(opensubsonic_httpClientRequest_t** httpReq); + +// DEPRECATED - TO BE REMOVED SOON - APART OF THE OLD INFRASTRUCTURE +typedef struct { + char* memory; + size_t size; +} binary_response_struct; + +int opensubsonic_getAlbum(const char* protocol_ptr, const char* server_ptr, const char* user_ptr, char* login_token_ptr, char* login_salt_ptr, const char* opensubsonic_version_ptr, const char* client_name_ptr, char* id, char** response); + +#endif diff --git a/src/libopensubsonic/logger.c b/src/libopensubsonic/logger.c new file mode 100644 index 0000000..9c42a8e --- /dev/null +++ b/src/libopensubsonic/logger.c @@ -0,0 +1,74 @@ +#include +#include +#include "logger.h" + +#define LOGGER_COLOR_BLACK "\033[0;30m" +#define LOGGER_COLOR_RED "\033[0;31m" +#define LOGGER_COLOR_GREEN "\033[0;32m" +#define LOGGER_COLOR_YELLOW "\033[0;33m" +#define LOGGER_COLOR_BLUE "\033[0;34m" +#define LOGGER_COLOR_PURPLE "\033[0;35m" +#define LOGGER_COLOR_CYAN "\033[0;36m" +#define LOGGER_COLOR_WHITE "\033[0;37m" + +#define LOGGER_MODE_EVERYTHING 1 + +void logger_init(int mode) { + printf("%s +\n", __func__); + + printf("%s HAHAHA %s HAHAHA\n", LOGGER_COLOR_RED, LOGGER_COLOR_WHITE); + + printf("%s -\n", __func__); +} + +// Log entry into / exit from function +// Action 1 = Entry, Action 2 = Exit +void logger_log_function(const char* function, int action) { + if (action == 1) { + printf("%s%s +%s\n", LOGGER_COLOR_YELLOW, function, LOGGER_COLOR_WHITE); + } else if (action == 2) { + printf("%s%s -%s\n", LOGGER_COLOR_YELLOW, function, LOGGER_COLOR_WHITE); + } else { + printf("%sERROR: Invalid logger action!%s\n", LOGGER_COLOR_RED, LOGGER_COLOR_WHITE); + } +} + +// Log error +void logger_log_error(const char* function, const char* format, ...) { + printf("%s(%s) ERROR - ", LOGGER_COLOR_RED, function); + va_list argptr; + va_start(argptr, format); + vprintf(format, argptr); + va_end(argptr); + printf("%s\n", LOGGER_COLOR_WHITE); +} + +// Log general information +void logger_log_general(const char* function, const char* format, ...) { + printf("%s(%s) ", LOGGER_COLOR_YELLOW, function); + va_list argptr; + va_start(argptr, format); + vprintf(format, argptr); + va_end(argptr); + printf("%s\n", LOGGER_COLOR_WHITE); +} + +// Log important information +void logger_log_important(const char* function, const char* format, ...) { + printf("%s(%s) ", LOGGER_COLOR_GREEN, function); + va_list argptr; + va_start(argptr, format); + vprintf(format, argptr); + va_end(argptr); + printf("%s\n", LOGGER_COLOR_WHITE); +} + +// Log title +void logger_log_title(const char* function, const char* format, ...) { + printf("%s(%s) ", LOGGER_COLOR_PURPLE, function); + va_list argptr; + va_start(argptr, format); + vprintf(format, argptr); + va_end(argptr); + printf("%s\n", LOGGER_COLOR_WHITE); +} diff --git a/src/libopensubsonic/logger.h b/src/libopensubsonic/logger.h new file mode 100644 index 0000000..02feecc --- /dev/null +++ b/src/libopensubsonic/logger.h @@ -0,0 +1,11 @@ +#ifndef _LOGGER_H +#define _LOGGER_H + +void logger_init(int mode); +void logger_log_function(const char* function, int action); +void logger_log_error(const char* function, const char* format, ...); +void logger_log_general(const char* function, const char* format, ...); +void logger_log_important(const char* function, const char* format, ...); +void logger_log_title(const char* function, const char* format, ...); + +#endif diff --git a/src/libopensubsonic/scrobble_lastFm.c b/src/libopensubsonic/scrobble_lastFm.c new file mode 100644 index 0000000..8214ff7 --- /dev/null +++ b/src/libopensubsonic/scrobble_lastFm.c @@ -0,0 +1,530 @@ +#include +#include +#include +#include "scrobble_lastFm.h" +#include "logger.h" +#include "httpclient.h" +#include "../external/cJSON.h" +#include "../external/md5.h" +#include "../external/libcurl_uriescape.h" +#include "../configHandler.h" +#include "../DarwinHttpClient.h" +#include "../configHandler.h" + +const char* lastFmScrobbleURL = "https://ws.audioscrobbler.com/2.0/"; +static int rc = 0; +extern configHandler_config_t* configObj; + +/* + * Authenticate with LastFM using the method.getMobileSession endpoint + * Receives credentials from the config file, and returns the token as a heap allocated char*, or NULL if failure + */ +char* opensubsonic_authenticate_lastFm(void) { + logger_log_general(__func__, "Attempting to authenticate with LastFM."); + + // Check to make sure there are credentials present + if (configObj->lastfm_username == NULL || configObj->lastfm_username[0] == '\0' || + configObj->lastfm_password == NULL || configObj->lastfm_password[0] == '\0' || + configObj->lastfm_api_key == NULL || configObj->lastfm_api_key[0] == '\0' || + configObj->lastfm_api_secret == NULL || configObj->lastfm_api_secret[0] == '\0') { + logger_log_error(__func__, "LastFM Username/Password/API Key/API Secret is not in the config file."); + return NULL; + } + + // Assemble the signature + char* sig_plaintext = NULL; + rc = asprintf(&sig_plaintext, "api_key%smethodauth.getMobileSessionpassword%susername%s%s", + configObj->lastfm_api_key, configObj->lastfm_password, + configObj->lastfm_username, configObj->lastfm_api_secret); + if (rc == -1) { + logger_log_error(__func__, "asprintf() failed."); + return NULL; + } + + uint8_t sig_md5_bytes[16]; + char* sig_md5_text = NULL; + md5String(sig_plaintext, sig_md5_bytes); + free(sig_plaintext); + rc = asprintf(&sig_md5_text, "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + sig_md5_bytes[0], sig_md5_bytes[1], sig_md5_bytes[2], sig_md5_bytes[3], + sig_md5_bytes[4], sig_md5_bytes[5], sig_md5_bytes[6], sig_md5_bytes[7], + sig_md5_bytes[8], sig_md5_bytes[9], sig_md5_bytes[10], sig_md5_bytes[11], + sig_md5_bytes[12], sig_md5_bytes[13], sig_md5_bytes[14], sig_md5_bytes[15]); + if (rc == -1) { + logger_log_error(__func__, "asprintf() failed."); + return NULL; + } + + // Assemble the payload + char* payload = NULL; + rc = asprintf(&payload, "%s?method=auth.getMobileSession&api_key=%s&username=%s&password=%s&api_sig=%s&format=json", + lastFmScrobbleURL, configObj->lastfm_api_key, configObj->lastfm_username, + configObj->lastfm_password, sig_md5_text); + free(sig_md5_text); + if (rc == -1) { + logger_log_error(__func__, "asprintf() failed."); + return NULL; + } + + // Send payload and receive JSON response + opensubsonic_httpClientRequest_t* httpReq; + opensubsonic_httpClient_prepareRequest(&httpReq); + + httpReq->requestUrl = strdup(payload); + free(payload); + httpReq->scrobbler = SCROBBLER_LASTFM; + httpReq->method = HTTP_METHOD_POST; + opensubsonic_httpClient_request(&httpReq); + + if (httpReq->responseCode != HTTP_CODE_SUCCESS && httpReq->responseCode != HTTP_CODE_NOT_AUTHORIZED) { + logger_log_error(__func__, "HTTP POST returned invalid code (%d).", httpReq->responseCode); + opensubsonic_httpClient_cleanup(&httpReq); + // TODO return error + } + + // Parse response JSON + cJSON* root = cJSON_Parse(httpReq->responseMsg); + if (root == NULL) { + logger_log_error(__func__, "Error parsing JSON."); + opensubsonic_httpClient_cleanup(&httpReq); + // TODO return error + } + + if (httpReq->responseCode == HTTP_CODE_SUCCESS) { + // Make an object from session + cJSON* session_root = cJSON_GetObjectItemCaseSensitive(root, "session"); + if (session_root == NULL) { + logger_log_error(__func__, "Error parsing JSON - session does not exist."); + opensubsonic_httpClient_cleanup(&httpReq); + cJSON_Delete(root); + // TODO return error + } + + cJSON* name = cJSON_GetObjectItemCaseSensitive(session_root, "name"); + if (cJSON_IsString(name) && name->valuestring != NULL) { + printf("Fetched username: %s\n", name->valuestring); + } + + cJSON* key = cJSON_GetObjectItemCaseSensitive(session_root, "key"); + if (cJSON_IsString(key) && key->valuestring != NULL) { + printf("Fetched key: %s\n", key->valuestring); + } else { + logger_log_error(__func__, "Error parsing JSON - key does not exist."); + opensubsonic_httpClient_cleanup(&httpReq); + cJSON_Delete(root); + // TODO return error + } + } else if (httpReq->responseCode == HTTP_CODE_NOT_AUTHORIZED) { + cJSON* error = cJSON_GetObjectItemCaseSensitive(root, "error"); + if (cJSON_IsNumber(error)) { + int code = error->valueint; + switch (code) { + case 2: + logger_log_error(__func__, "Invalid service (%d).", code); + break; + case 3: + logger_log_error(__func__, "Invalid method (%d).", code); + break; + case 4: + logger_log_error(__func__, "Auth error (%d).", code); + break; + case 5: + logger_log_error(__func__, "Invalid format (%d).", code); + break; + case 6: + logger_log_error(__func__, "Invalid parameters (%d).", code); + break; + case 7: + logger_log_error(__func__, "Invalid resource (%d).", code); + break; + case 8: + logger_log_error(__func__, "Operation failed (%d).", code); + break; + case 9: + logger_log_error(__func__, "Invalid session key (%d).", code); + break; + case 10: + logger_log_error(__func__, "Invalid API key (%d).", code); + break; + case 11: + logger_log_error(__func__, "Service offline (%d).", code); + break; + case 13: + logger_log_error(__func__, "Invalid method signature (%d).", code); + break; + case 16: + logger_log_error(__func__, "Temporary error processing request (%d).", code); + break; + case 26: + logger_log_error(__func__, "Suspended API key (%d).", code); + break; + case 29: + logger_log_error(__func__, "Rate limit exceeded (%d).", code); + break; + default: + logger_log_error(__func__, "Unknown error (%d).", code); + break; + } + } + + } + + opensubsonic_httpClient_cleanup(&httpReq); + cJSON_Delete(root); + + // TODO fix error codes, do something with the code etc + return NULL; +} + +/* + * Sends a scrobble to LastFM + * If 'finalize' is true, it sends to the track.scrobble endpoint + * If 'finalize' is false, it sends to the track.updateNowPlaying endpoint + */ +void opensubsonic_scrobble_lastFm(bool finalize, opensubsonic_getSong_struct* songStruct) { + if (finalize) { + logger_log_general(__func__, "Performing final scrobble to LastFM."); + } else { + logger_log_general(__func__, "Performing in-progress scrobble to LastFM."); + } + + // Fetch the current UNIX timestamp + time_t currentTime; + char* currentTime_string; + currentTime = time(NULL); + rc = asprintf(¤tTime_string, "%ld", currentTime); + if (rc == -1) { + logger_log_error(__func__, "asprintf() failed."); + return; // TODO return error + } + + // Assemble the signature + char* sig_plaintext = NULL; + if (finalize) { + rc = asprintf(&sig_plaintext, "album%salbumArtist%sapi_key%sartist%smethodtrack.scrobblesk%stimestamp%strack%s%s", + songStruct->album, songStruct->artist, configObj->lastfm_api_key, songStruct->artist, + configObj->lastfm_api_session_key, currentTime_string, songStruct->title, configObj->lastfm_api_secret); + } else { + rc = asprintf(&sig_plaintext, "album%salbumArtist%sapi_key%sartist%smethodtrack.updateNowPlayingsk%stimestamp%strack%s%s", + songStruct->album, songStruct->artist, configObj->lastfm_api_key, songStruct->artist, + configObj->lastfm_api_session_key, currentTime_string, songStruct->title, configObj->lastfm_api_secret); + } + if (rc == -1) { + logger_log_error(__func__, "asprintf() failed."); + return; // TODO handle error + } + + uint8_t sig_md5_bytes[16]; + char* sig_md5_text = NULL; // TODO do I have to free this? Also is be used in crypto.c + md5String(sig_plaintext, sig_md5_bytes); + free(sig_plaintext); + rc = asprintf(&sig_md5_text, "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + sig_md5_bytes[0], sig_md5_bytes[1], sig_md5_bytes[2], sig_md5_bytes[3], + sig_md5_bytes[4], sig_md5_bytes[5], sig_md5_bytes[6], sig_md5_bytes[7], + sig_md5_bytes[8], sig_md5_bytes[9], sig_md5_bytes[10], sig_md5_bytes[11], + sig_md5_bytes[12], sig_md5_bytes[13], sig_md5_bytes[14], sig_md5_bytes[15]); + if (rc == -1) { + logger_log_error(__func__, "asprintf() failed."); + return; // TODO handle error + } + + // URI encode strings + char* uri_songTitle = lcue_uriescape(songStruct->title, (unsigned int)strlen(songStruct->title)); + char* uri_songArtist = lcue_uriescape(songStruct->artist, (unsigned int)strlen(songStruct->artist)); + char* uri_songAlbum = lcue_uriescape(songStruct->album, (unsigned int)strlen(songStruct->album)); + if (uri_songTitle == NULL || uri_songArtist == NULL || uri_songAlbum == NULL) { + logger_log_error(__func__, "lcue_uriescape() error."); + free(currentTime_string); + free(sig_md5_text); + if (uri_songTitle != NULL) { free(uri_songTitle); } + if (uri_songArtist != NULL) { free(uri_songArtist); } + if (uri_songAlbum != NULL) { free(uri_songAlbum); } + return; // TODO return error + } + + // Assemble the payload + char* payload = NULL; + if (finalize) { + rc = asprintf(&payload, + "%s?method=track.scrobble&api_key=%s×tamp=%s&track=%s&artist=%s&album=%s&albumArtist=%s&sk=%s&api_sig=%s&format=json", + lastFmScrobbleURL, configObj->lastfm_api_key, currentTime_string, uri_songTitle, + uri_songArtist, uri_songAlbum, uri_songArtist, + configObj->lastfm_api_session_key, sig_md5_text); + } else { + rc = asprintf(&payload, + "%s?method=track.updateNowPlaying&api_key=%s×tamp=%s&track=%s&artist=%s&album=%s&albumArtist=%s&sk=%s&api_sig=%s&format=json", + lastFmScrobbleURL, configObj->lastfm_api_key, currentTime_string, uri_songTitle, + uri_songArtist, uri_songAlbum, uri_songArtist, + configObj->lastfm_api_session_key, sig_md5_text); + } + free(currentTime_string); + free(sig_md5_text); + free(uri_songTitle); + free(uri_songAlbum); + free(uri_songArtist); + if (rc == -1) { + logger_log_error(__func__, "asprintf() failed."); + return; // TODO return error + } + + // Send scrobble and receive response + opensubsonic_httpClientRequest_t* httpReq; + opensubsonic_httpClient_prepareRequest(&httpReq); + + httpReq->requestUrl = strdup(payload); + free(payload); + httpReq->scrobbler = SCROBBLER_LASTFM; + httpReq->method = HTTP_METHOD_POST; + opensubsonic_httpClient_request(&httpReq); + + if (httpReq->responseCode != HTTP_CODE_SUCCESS) { + logger_log_error(__func__, "HTTP POST did not return success (%d).", httpReq->responseCode); + opensubsonic_httpClient_cleanup(&httpReq); + // TODO return error + } + + // Parse the scrobble response + cJSON* root = cJSON_Parse(httpReq->responseMsg); + opensubsonic_httpClient_cleanup(&httpReq); + if (root == NULL) { + logger_log_error(__func__, "Error parsing JSON."); + // TODO return error + } + + cJSON* inner_root = NULL; + cJSON* scrobbles_root = NULL; // Parent of inner_root, only used on final scrobble + if (finalize) { + // Make an object from scrobbles + scrobbles_root = cJSON_GetObjectItemCaseSensitive(root, "scrobbles"); + if (scrobbles_root == NULL) { + logger_log_error(__func__, "Error parsing JSON - scrobbles does not exist."); + cJSON_Delete(root); + // TODO return error + } + + // Make an object from scrobble + inner_root = cJSON_GetObjectItemCaseSensitive(scrobbles_root, "scrobble"); + if (inner_root == NULL) { + logger_log_error(__func__, "Error parsing JSON - scrobble does not exist."); + cJSON_Delete(root); + // TODO return error + } + } else { + // Make an object from nowplaying + inner_root = cJSON_GetObjectItemCaseSensitive(root, "nowplaying"); + if (inner_root == NULL) { + logger_log_error(__func__, "Error parsing JSON - nowplaying does not exist."); + cJSON_Delete(root); + // TODO return error + } + } + + // Make an object from artist, track, albumArtist, and album, and fetch codes + cJSON* artist_root = cJSON_GetObjectItemCaseSensitive(inner_root, "artist"); + if (artist_root == NULL) { + logger_log_error(__func__, "Error parsing JSON - artist does not exist."); + cJSON_Delete(root); + // TODO return error + } + + cJSON* artist_corrected = cJSON_GetObjectItemCaseSensitive(artist_root, "corrected"); + if (cJSON_IsString(artist_corrected) && artist_corrected->valuestring != NULL) { + if (*(artist_corrected->valuestring) == '\0') { + logger_log_error(__func__, "Error parsing JSON - artist/corrected is empty."); + cJSON_Delete(root); + // TODO return error + } + char* endptr; + int corrected = (int)strtol(artist_corrected->valuestring, &endptr, 10); + if (*endptr != '\0') { + logger_log_error(__func__, "Error parsing JSON - artist/corrected strtol/endptr is not empty."); + cJSON_Delete(root); + // TODO return error + } + if (corrected == 1) { + logger_log_important(__func__, "Warning - Artist has been autocorrected."); + } + } else { + logger_log_error(__func__, "Error parsing JSON - artist/corrected does not exist."); + cJSON_Delete(root); + // TODO return error + } + + cJSON* track_root = cJSON_GetObjectItemCaseSensitive(inner_root, "track"); + if (track_root == NULL) { + logger_log_error(__func__, "Error parsing JSON - track does not exist."); + cJSON_Delete(root); + // TODO return error + } + + cJSON* track_corrected = cJSON_GetObjectItemCaseSensitive(track_root, "corrected"); + if (cJSON_IsString(track_corrected) && track_corrected->valuestring != NULL) { + if (*(track_corrected->valuestring) == '\0') { + logger_log_error(__func__, "Error parsing JSON - track/corrected is empty."); + cJSON_Delete(root); + // TODO return error + } + char* endptr; + int corrected = (int)strtol(track_corrected->valuestring, &endptr, 10); + if (*endptr != '\0') { + logger_log_error(__func__, "Error parsing JSON - track/corrected strtol/endptr is not empty."); + cJSON_Delete(root); + // TODO return error + } + if (corrected == 1) { + logger_log_important(__func__, "Warning - Track has been autocorrected."); + } + } else { + logger_log_error(__func__, "Error parsing JSON - track/corrected does not exist."); + cJSON_Delete(root); + // TODO return error + } + + cJSON* albumArtist_root = cJSON_GetObjectItemCaseSensitive(inner_root, "albumArtist"); + if (albumArtist_root == NULL) { + logger_log_error(__func__, "Error parsing JSON - albumArtist does not exist."); + cJSON_Delete(root); + // TODO return error + } + + cJSON* albumArtist_corrected = cJSON_GetObjectItemCaseSensitive(albumArtist_root, "corrected"); + if (cJSON_IsString(albumArtist_corrected) && albumArtist_corrected->valuestring != NULL) { + if (*(albumArtist_corrected->valuestring) == '\0') { + logger_log_error(__func__, "Error parsing JSON - albumArtist/corrected is empty."); + cJSON_Delete(root); + // TODO return error + } + char* endptr; + int corrected = (int)strtol(albumArtist_corrected->valuestring, &endptr, 10); + if (*endptr != '\0') { + logger_log_error(__func__, "Error parsing JSON - albumArtist/corrected strtol/endptr is not empty."); + cJSON_Delete(root); + // TODO return error + } + if (corrected == 1) { + logger_log_important(__func__, "Warning - Album Artist has been autocorrected."); + } + } else { + logger_log_error(__func__, "Error parsing JSON - albumArtist/corrected does not exist."); + cJSON_Delete(root); + // TODO return error + } + + cJSON* album_root = cJSON_GetObjectItemCaseSensitive(inner_root, "album"); + if (album_root == NULL) { + logger_log_error(__func__, "Error parsing JSON - album does not exist."); + cJSON_Delete(root); + // TODO return error + } + + cJSON* album_corrected = cJSON_GetObjectItemCaseSensitive(album_root, "corrected"); + if (cJSON_IsString(album_corrected) && album_corrected->valuestring != NULL) { + if (*(album_corrected->valuestring) == '\0') { + logger_log_error(__func__, "Error parsing JSON - album/corrected is empty."); + cJSON_Delete(root); + // TODO return error + } + char* endptr; + int corrected = (int)strtol(album_corrected->valuestring, &endptr, 10); + if (*endptr != '\0') { + logger_log_error(__func__, "Error parsing JSON - album/corrected strtol/endptr is not empty."); + cJSON_Delete(root); + // TODO return error + } + if (corrected == 1) { + logger_log_important(__func__, "Warning - Album has been autocorrected."); + } + } else { + logger_log_error(__func__, "Error parsing JSON - album/corrected does not exist."); + cJSON_Delete(root); + // TODO return error + } + + // Make an object from ignoredMessage, and check return code + cJSON* ignoredMessage_root = cJSON_GetObjectItemCaseSensitive(inner_root, "ignoredMessage"); + if (ignoredMessage_root == NULL) { + logger_log_error(__func__, "Error parsing JSON - ignoredMessage does not exist."); + cJSON_Delete(root); + // TODO return error + } + + cJSON* ignoredMessage_code = cJSON_GetObjectItemCaseSensitive(ignoredMessage_root, "code"); + if (cJSON_IsString(ignoredMessage_code) && ignoredMessage_code->valuestring != NULL) { + if (*(ignoredMessage_code->valuestring) == '\0') { + logger_log_error(__func__, "Error parsing JSON - ignoredMessage/code is empty."); + cJSON_Delete(root); + // TODO return error + } + char* endptr; + int code = (int)strtol(ignoredMessage_code->valuestring, &endptr, 10); + if (*endptr != '\0') { + logger_log_error(__func__, "Error parsing JSON - ignoredMessage/code strtol/endptr is not empty."); + cJSON_Delete(root); + // TODO return error + } + if (code == 0) { + if (!finalize) { + logger_log_general(__func__, "In progress scrobble was successful."); + } else { + logger_log_general(__func__, "Final scrobble 1/2 was successful."); + } + } else if (code == 1) { + logger_log_error(__func__, "Artist was ignored."); + } else if (code == 2) { + logger_log_error(__func__, "Track was ignored."); + } else if (code == 3) { + logger_log_error(__func__, "Timestamp was too old."); + } else if (code == 4) { + logger_log_error(__func__, "Timestamp was too new."); + } else if (code == 5) { + logger_log_error(__func__, "Daily scrobble limit exceeded."); + } else { + logger_log_error(__func__, "Unknown error code received (%d)", code); + } + } else { + logger_log_error(__func__, "Error parsing JSON - ignoredMessage/code does not exist."); + cJSON_Delete(root); + // TODO return error + } + + if (finalize) { + cJSON* attr_root = cJSON_GetObjectItemCaseSensitive(scrobbles_root, "@attr"); + if (attr_root == NULL) { + logger_log_error(__func__, "Error parsing JSON - @attr does not exist."); + cJSON_Delete(root); + // TODO return error + } + + cJSON* attr_ignored = cJSON_GetObjectItemCaseSensitive(attr_root, "ignored"); + if (cJSON_IsNumber(attr_ignored)) { + if (attr_ignored->valueint != 0) { + logger_log_important(__func__, "Warning - @attr/ignored is not 0 (%d).", attr_ignored->valueint); + } + } else { + logger_log_error(__func__, "Error parsing JSON - @attr/ignored does not exist."); + cJSON_Delete(root); + // TODO return error + } + + cJSON* attr_accepted = cJSON_GetObjectItemCaseSensitive(attr_root, "accepted"); + if (cJSON_IsNumber(attr_accepted)) { + if (attr_accepted->valueint != 1) { + logger_log_important(__func__, "Warning - @attr/accepted is not 1 (%d).", attr_accepted->valueint); + } + } else { + logger_log_error(__func__, "Error parsing JSON - @attr/accepted does not exist"); + cJSON_Delete(root); + // TODO return error + } + + // At this point, attr_ignored and attr_accepted are both known to be valid + if (attr_ignored->valueint == 0 && attr_accepted->valueint == 1) { + logger_log_general(__func__, "Final scrobble 2/2 was successful."); + } else { + logger_log_important(__func__, "Final scobble 2/2 was not successful (ignored: %d, accepted: %d", + attr_ignored->valueint, attr_accepted->valueint); + } + } + + cJSON_Delete(root); +} diff --git a/src/libopensubsonic/scrobble_lastFm.h b/src/libopensubsonic/scrobble_lastFm.h new file mode 100644 index 0000000..c1157d0 --- /dev/null +++ b/src/libopensubsonic/scrobble_lastFm.h @@ -0,0 +1,9 @@ +#ifndef _SCROBBLE_LASTFM_H +#define _SCROBBLE_LASTFM_H +#include +#include "endpoint_getSong.h" + +char* opensubsonic_authenticate_lastFm(void); +void opensubsonic_scrobble_lastFm(bool finalize, opensubsonic_getSong_struct* songStruct); + +#endif diff --git a/src/libopensubsonic/scrobble_listenBrainz.c b/src/libopensubsonic/scrobble_listenBrainz.c new file mode 100644 index 0000000..b4e7525 --- /dev/null +++ b/src/libopensubsonic/scrobble_listenBrainz.c @@ -0,0 +1,141 @@ +#include +#include +#include +#include +#include "../external/cJSON.h" +#include "logger.h" +#include "endpoint_getSong.h" +#include "httpclient.h" +#include "scrobble_listenBrainz.h" +#include "../DarwinHttpClient.h" + +const char* listenBrainzScrobbleURL = "https://api.listenbrainz.org/1/submit-listens"; + +void opensubsonic_scrobble_listenBrainz(bool finalize, opensubsonic_getSong_struct* songStruct) { + if (finalize) { + logger_log_general(__func__, "Performing final scrobble to ListenBrainz."); + } else { + logger_log_general(__func__, "Performing in-progress scrobble to ListenBrainz."); + } + + // Fetch the current UNIX timestamp + time_t currentTime; + currentTime = time(NULL); + + // Form the JSON body + cJSON* rootObj = cJSON_CreateObject(); + + // Add ["listen_type"] + if (finalize) { + cJSON* listenTypeObj = cJSON_CreateString("single"); + cJSON_AddItemToObject(rootObj, "listen_type", listenTypeObj); + } else { + cJSON* listenTypeObj = cJSON_CreateString("playing_now"); + cJSON_AddItemToObject(rootObj, "listen_type", listenTypeObj); + } + + // Add ["payload"] + cJSON* payloadObj = cJSON_CreateArray(); + cJSON_AddItemToObject(rootObj, "payload", payloadObj); + + // Add ["payload"][] + cJSON* payloadContainedObj = cJSON_CreateObject(); + cJSON_AddItemToArray(payloadObj, payloadContainedObj); + + // Add ["listened_at"] if 'finalize' is true + if (finalize) { + cJSON* listenedAtObj = cJSON_CreateNumber((long)currentTime); + cJSON_AddItemToObject(payloadContainedObj, "listened_at", listenedAtObj); + } + + // Add ["track_metadata"] + cJSON* trackMetadataObj = cJSON_CreateObject(); + cJSON_AddItemToObject(payloadContainedObj, "track_metadata", trackMetadataObj); + + // Add ["track_metadata"]["additional_info"] + cJSON* additionalInfoObj = cJSON_CreateObject(); + cJSON_AddItemToObject(trackMetadataObj, "additional_info", additionalInfoObj); + + // Add ["track_metadata"]["additional_info"]["media_player"] + cJSON* mediaPlayerObj = cJSON_CreateString("OSSP"); + cJSON_AddItemToObject(additionalInfoObj, "media_player", mediaPlayerObj); + + // Add ["track_metadata"]["additional_info"]["submission_client"] + cJSON* submissionClientObj = cJSON_CreateString("OSSP ListenBrainz Scrobbler"); + cJSON_AddItemToObject(additionalInfoObj, "submission_client", submissionClientObj); + + // Add ["track_metadata"]["additional_info"]["submission_client_version"] + cJSON* submissionClientVersionObj = cJSON_CreateString("1.0"); + cJSON_AddItemToObject(additionalInfoObj, "submission_client_version", submissionClientVersionObj); + + // Add ["track_metadata"]["artist_name"] + if (songStruct->artist != NULL) { + cJSON* artistNameObj = cJSON_CreateString(songStruct->artist); + cJSON_AddItemToObject(trackMetadataObj, "artist_name", artistNameObj); + } else { + printf("[ListenBrainz Scrobbler] Song Artist ([\"artist\"]) is null in songStruct\n"); + } + + // Add ["track_metadata"]["track_name"] + if (songStruct->title != NULL) { + cJSON* artistTitleObj = cJSON_CreateString(songStruct->title); + cJSON_AddItemToObject(trackMetadataObj, "track_name", artistTitleObj); + } else { + printf("[ListenBrainz Scrobbler] Song Title ([\"title\"]) is null in songStruct\n"); + } + + // Add ["track_metadata"]["release_name"] + if (songStruct->album != NULL) { + cJSON* artistReleaseObj = cJSON_CreateString(songStruct->artist); + cJSON_AddItemToObject(trackMetadataObj, "release_name", artistReleaseObj); + } else { + printf("[ListenBrainz Scrobbler] Song Album ([\"album\"]) is null in songStruct\n"); + } + + // Print the assembled JSON + char* payload = cJSON_PrintUnformatted(rootObj); + cJSON_Delete(rootObj); + + // Send payload and receive JSON response + opensubsonic_httpClientRequest_t* httpReq; + opensubsonic_httpClient_prepareRequest(&httpReq); + + httpReq->requestUrl = strdup(listenBrainzScrobbleURL); + httpReq->requestBody = strdup(payload); + free(payload); + httpReq->isBodyRequired = true; + httpReq->scrobbler = SCROBBLER_LISTENBRAINZ; + httpReq->method = HTTP_METHOD_POST; + opensubsonic_httpClient_request(&httpReq); + + // Check response + if (httpReq->responseCode != HTTP_CODE_SUCCESS) { + logger_log_error(__func__, "HTTP POST did not return success (%d).", httpReq->responseCode); + opensubsonic_httpClient_cleanup(&httpReq); + // TODO return error + } + + cJSON* responseObj = cJSON_Parse(httpReq->responseMsg); + opensubsonic_httpClient_cleanup(&httpReq); + if (responseObj == NULL) { + logger_log_error(__func__, "Error parsing JSON."); + // TODO return error + } + + cJSON* status = cJSON_GetObjectItemCaseSensitive(responseObj, "status"); + if (cJSON_IsString(status) && status->valuestring != NULL) { + if (strcmp(status->valuestring, "ok") == 0) { + if (finalize) { + logger_log_general(__func__, "Final scrobble was successful."); + } else { + logger_log_general(__func__, "In progress scrobble was successful."); + } + } else { + logger_log_error(__func__, "Something went wrong scrobbling to ListenBrainz."); + } + } else { + logger_log_error(__func__, "Something went wrong scrobbling to ListenBrainz."); + } + + cJSON_Delete(responseObj); +} diff --git a/src/libopensubsonic/scrobble_listenBrainz.h b/src/libopensubsonic/scrobble_listenBrainz.h new file mode 100644 index 0000000..5866b67 --- /dev/null +++ b/src/libopensubsonic/scrobble_listenBrainz.h @@ -0,0 +1,7 @@ +#ifndef _SCROBBLE_LISTENBRAINZ_H +#define _SCROBBLE_LISTENBRAINZ_H +#include + +void opensubsonic_scrobble_listenBrainz(bool finalize, opensubsonic_getSong_struct* songStruct); + +#endif diff --git a/src/libopensubsonic/utils.c b/src/libopensubsonic/utils.c new file mode 100644 index 0000000..97fe080 --- /dev/null +++ b/src/libopensubsonic/utils.c @@ -0,0 +1,106 @@ +#include +#include +#include +#include +#include "../external/cJSON.h" +#include "logger.h" +#include "utils.h" + +/* + * ACRONYMS: + * OSS_Psoj -> Opensubsonic_PullStringOutofJson + * OSS_Pioj -> Opensubsonic_PullIntOutofJson + * OSS_Ploj -> Opensubsonic_PullLongOutofJson + * OSS_Pboj -> Opensubsonic_PullBoolOutofJson + */ + +void OSS_Psoj(char** dest, cJSON* obj, char* child) { + if (obj == NULL) { + //void* ret_addr = __builtin_return_address(0); + //Dl_info info; + //if (dladdr(ret_addr, &info)) { + // logger_log_error(info.dli_sname, "Parent object is null."); + //} + } else { + cJSON* childObj = cJSON_GetObjectItem(obj, child); + + if (cJSON_IsString(childObj) && childObj->valuestring != NULL) { + *dest = strdup(childObj->valuestring); + } else { + //void* ret_addr = __builtin_return_address(0); + //Dl_info info; + //if (dladdr(ret_addr, &info)) { + // logger_log_error(info.dli_sname, "Object %s is not a string or string is null.", child); + //} + } + } +} + +void OSS_Pioj(int* dest, cJSON* obj, char* child) { + if (obj == NULL) { + //void* ret_addr = __builtin_return_address(0); + //Dl_info info; + //if (dladdr(ret_addr, &info)) { + // logger_log_error(info.dli_sname, "Parent object is null."); + //} + } else { + cJSON* childObj = cJSON_GetObjectItem(obj, child); + + if (cJSON_IsNumber(childObj)) { + *dest = childObj->valueint; + } else { + //void* ret_addr = __builtin_return_address(0); + //Dl_info info; + //if (dladdr(ret_addr, &info)) { + // logger_log_error(info.dli_sname, "Object %s is not an int.", child); + //} + } + } +} + +void OSS_Ploj(long* dest, cJSON* obj, char* child) { + if (obj == NULL) { + //void* ret_addr = __builtin_return_address(0); + //Dl_info info; + //if (dladdr(ret_addr, &info)) { + // logger_log_error(info.dli_sname, "Parent object is null."); + //} + } else { + cJSON* childObj = cJSON_GetObjectItem(obj, child); + + if (cJSON_IsNumber(childObj)) { + *dest = childObj->valueint; + } else { + //void* ret_addr = __builtin_return_address(0); + //Dl_info info; + //if (dladdr(ret_addr, &info)) { + // logger_log_error(info.dli_sname, "Object %s is not a long.", child); + //} + } + } +} + +void OSS_Pboj(bool* dest, cJSON* obj, char* child) { + if (obj == NULL) { + //void* ret_addr = __builtin_return_address(0); + //Dl_info info; + //if (dladdr(ret_addr, &info)) { + // logger_log_error(info.dli_sname, "Parent object is null."); + //} + } else { + cJSON* childObj = cJSON_GetObjectItem(obj, child); + + if (cJSON_IsBool(childObj)) { + if (cJSON_IsTrue(childObj)) { + *dest = true; + } + } else { + //void* ret_addr = __builtin_return_address(0); + //Dl_info info; + //if (dladdr(ret_addr, &info)) { + // logger_log_error(info.dli_sname, "Object is not a bool."); + //} + } + } +} + diff --git a/src/libopensubsonic/utils.h b/src/libopensubsonic/utils.h new file mode 100644 index 0000000..5952919 --- /dev/null +++ b/src/libopensubsonic/utils.h @@ -0,0 +1,11 @@ +#ifndef _UTILS_H +#define _UTILS_H +#include +#include "../external/cJSON.h" + +void OSS_Psoj(char** dest, cJSON* obj, char* child); +void OSS_Pioj(int* dest, cJSON* obj, char* child); +void OSS_Ploj(long* dest, cJSON* obj, char* child); +void OSS_Pboj(bool* dest, cJSON* obj, char* child); + +#endif diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..d39ed0b --- /dev/null +++ b/src/main.c @@ -0,0 +1,32 @@ +#include +#include +#include "libopensubsonic/logger.h" +#include "libopensubsonic/crypto.h" +#include "libopensubsonic/httpclient.h" +#include "libopensubsonic/endpoint_ping.h" +#include "configHandler.h" + +configHandler_config_t* configObj = NULL; + +int main() { + int rc = 0; + + rc = configHandler_Read(&configObj); + if (rc != 0) { + printf("Could not read config file!\n"); + return 1; + } + opensubsonic_crypto_generateLogin(); + + opensubsonic_httpClient_URL_t* url = malloc(sizeof(opensubsonic_httpClient_URL_t)); + opensubsonic_httpClient_URL_prepare(&url); + url->endpoint = OPENSUBSONIC_ENDPOINT_PING; + opensubsonic_httpClient_formUrl(&url); + printf("URL: %s\n", url->formedUrl); + opensubsonic_ping_struct* OSS_ping_struct; + opensubsonic_httpClient_fetchResponse(&url, (void**)&OSS_ping_struct); + printf("PING OK: %s\n", OSS_ping_struct->status); + + configHandler_Free(&configObj); + return 0; +} diff --git a/src/openSubSonic.c b/src/openSubSonic.c new file mode 100644 index 0000000..b2e889f --- /dev/null +++ b/src/openSubSonic.c @@ -0,0 +1,259 @@ +#include "OSSP_Bridge.h" +#include +#include +#include +#include +#include "libopensubsonic/crypto.h" +#include "libopensubsonic/httpclient.h" +#include "libopensubsonic/endpoint_ping.h" +#include "libopensubsonic/endpoint_getStarred.h" +#include "libopensubsonic/endpoint_getAlbum.h" + +#include "libopensubsonic/endpoint_getPlaylists.h" +#include "libopensubsonic/endpoint_getPlaylist.h" +#include "libopensubsonic/endpoint_getSong.h" +#include "libopensubsonic/endpoint_getLyricsBySongId.h" +#include "libopensubsonic/endpoint_getAlbumList.h" +#include "libopensubsonic/endpoint_scrobble.h" +#include "configHandler.h" + +#include "libopensubsonic/scrobble_lastFm.h" +#include "dscrdrpc.h" + + + +struct memory { + char *data; + size_t size; +}; + +static size_t write_to_memory(void *ptr, size_t size, size_t nmemb, void *userdata) { + struct memory *mem = (struct memory *)userdata; + size_t total_size = size * nmemb; + + mem->data = realloc(mem->data, mem->size + total_size + 1); + if (!mem->data) return 0; // Fail on OOM + + memcpy(&(mem->data[mem->size]), ptr, total_size); + mem->size += total_size; + mem->data[mem->size] = '\0'; // Null-terminate + + return total_size; +} + + +static int rc = 0; +configHandler_config_t* configObj = NULL; + +opensubsonic_ping_struct* LOSS_PerformLogin(C_LoginStruct loginStruct) { + // Initialize configuration + configHandler_Read(&configObj); + + // Generate the login token and salt + opensubsonic_crypto_generateLogin(); + + + opensubsonic_httpClient_URL_t* url = (opensubsonic_httpClient_URL_t*)malloc(sizeof(opensubsonic_httpClient_URL_t)); + opensubsonic_httpClient_URL_prepare(&url); + url->endpoint = OPENSUBSONIC_ENDPOINT_PING; + opensubsonic_httpClient_formUrl(&url); + + opensubsonic_ping_struct* OSS_ping_struct; + opensubsonic_httpClient_fetchResponse(&url, (void**)&OSS_ping_struct); + + // Print Response + //printf("Result: %s\n", OSS_ping_struct.status); + + //opensubsonic_ping_struct_free(&OSS_ping_struct); + opensubsonic_httpClient_URL_cleanup(&url); + + //configHandler_Free(&config); + //opensubsonic_authenticate_lastFm(); + + + + /* + opensubsonic_httpClient_URL_t* urlc = (opensubsonic_httpClient_URL_t*)malloc(sizeof(opensubsonic_httpClient_URL_t)); + opensubsonic_httpClient_URL_prepare(&urlc); + urlc->endpoint = OPENSUBSONIC_ENDPOINT_GETLYRICSBYSONGID; + urlc->id = strdup("pvu323svJrim683Xf8fBV8"); + printf("URLC Endponit: %d\n", urlc->endpoint); + opensubsonic_httpClient_formUrl(&urlc); + printf("URL: %s\n", urlc->formedUrl); + + opensubsonic_getLyricsBySongId_struct* plStruct; + opensubsonic_httpClient_fetchResponse(&urlc, (void**)&plStruct); + + printf("2nd: %s\n", plStruct->lyrics[1].data); + */ + + + + + + + + return OSS_ping_struct; // TODO rename +} + + +void LOSS_FreeLoginStruct() { + +} + + +opensubsonic_getStarred_struct* LOSS_getStarred(void) { + opensubsonic_httpClient_URL_t* url = (opensubsonic_httpClient_URL_t*)malloc(sizeof(opensubsonic_httpClient_URL_t)); + opensubsonic_httpClient_URL_prepare(&url); + url->endpoint = OPENSUBSONIC_ENDPOINT_GETSTARRED; + opensubsonic_httpClient_formUrl(&url); + printf("STARRED URL: %s\n", url->formedUrl); + + opensubsonic_getStarred_struct* getStarredStruct; + opensubsonic_httpClient_fetchResponse(&url, (void**)&getStarredStruct); + opensubsonic_httpClient_URL_cleanup(&url); + + return getStarredStruct; +} + +void LOSS_delete_getStarred(opensubsonic_getStarred_struct* starredStruct) { + opensubsonic_getStarred_struct_free(&starredStruct); +} + +opensubsonic_getSong_struct* LOSS_getSong(char* id) { + opensubsonic_httpClient_URL_t* url = (opensubsonic_httpClient_URL_t*)malloc(sizeof(opensubsonic_httpClient_URL_t)); + opensubsonic_httpClient_URL_prepare(&url); + url->endpoint = OPENSUBSONIC_ENDPOINT_GETSONG; + url->id = strdup(id); + opensubsonic_httpClient_formUrl(&url); + + opensubsonic_getSong_struct* getSongStruct; + opensubsonic_httpClient_fetchResponse(&url, (void**)&getSongStruct); + opensubsonic_httpClient_URL_cleanup(&url); + + return getSongStruct; +} + +void LOSS_delete_getSong(opensubsonic_getSong_struct* songStruct) { + opensubsonic_getSong_struct_free(&songStruct); +} + + + + + + +opensubsonic_getAlbum_struct* LOSS_getAlbum(char* id) { + opensubsonic_httpClient_URL_t* url = (opensubsonic_httpClient_URL_t*)malloc(sizeof(opensubsonic_httpClient_URL_t)); + opensubsonic_httpClient_URL_prepare(&url); + url->endpoint = OPENSUBSONIC_ENDPOINT_GETALBUM; + url->id = strdup(id); + opensubsonic_httpClient_formUrl(&url); + + opensubsonic_getAlbum_struct* alStruct; + opensubsonic_httpClient_fetchResponse(&url, (void**)&alStruct); + opensubsonic_httpClient_URL_cleanup(&url); + + return alStruct; +} + +void LOSS_deleteAlbum(opensubsonic_getAlbum_struct* albumStruct) { + //opensubsonic_getAlbum_struct_free(albumStruct); // TODO +} + + + +// no error checking right now because fuck that TODO +opensubsonic_getAlbumList_struct* LOSS_getAlbumList(int type, int amount) { + opensubsonic_httpClient_URL_t* url = (opensubsonic_httpClient_URL_t*)malloc(sizeof(opensubsonic_httpClient_URL_t)); + opensubsonic_httpClient_URL_prepare(&url); + url->endpoint = OPENSUBSONIC_ENDPOINT_GETALBUMLIST; + url->type = type; + url->amount = amount; + opensubsonic_httpClient_formUrl(&url); + printf("%s\n", url->formedUrl); + + opensubsonic_getAlbumList_struct* alStruct; + opensubsonic_httpClient_fetchResponse(&url, (void**)&alStruct); + opensubsonic_httpClient_URL_cleanup(&url); + + return alStruct; +} + +opensubsonic_getPlaylists_struct* LOSS_getPlaylists(void) { + opensubsonic_httpClient_URL_t* url = (opensubsonic_httpClient_URL_t*)malloc(sizeof(opensubsonic_httpClient_URL_t)); + opensubsonic_httpClient_URL_prepare(&url); + url->endpoint = OPENSUBSONIC_ENDPOINT_GETPLAYLISTS; + opensubsonic_httpClient_formUrl(&url); + + opensubsonic_getPlaylists_struct* alStruct; + opensubsonic_httpClient_fetchResponse(&url, (void**)&alStruct); + opensubsonic_httpClient_URL_cleanup(&url); + + return alStruct; +} + + + + + +char* LOSS_getCoverArt(char* id) { + opensubsonic_httpClient_URL_t* url = (opensubsonic_httpClient_URL_t*)malloc(sizeof(opensubsonic_httpClient_URL_t)); + opensubsonic_httpClient_URL_prepare(&url); + url->endpoint = OPENSUBSONIC_ENDPOINT_GETCOVERART; + url->id = strdup(id); + opensubsonic_httpClient_formUrl(&url); + + return url->formedUrl; + // TODO need to free URL struct +} + + +char* LOSS_Stream(char* id) { + // Perform scrobble to navidrome (Used purely for server side tracking) + opensubsonic_httpClient_URL_t* scrobble_url = (opensubsonic_httpClient_URL_t*)malloc(sizeof(opensubsonic_httpClient_URL_t)); + opensubsonic_httpClient_URL_prepare(&scrobble_url); + scrobble_url->endpoint = OPENSUBSONIC_ENDPOINT_SCROBBLE; + scrobble_url->id = strdup(id); + scrobble_url->submit = true; + opensubsonic_httpClient_formUrl(&scrobble_url); + opensubsonic_scrobble_struct* scrobbleStruct; + opensubsonic_httpClient_fetchResponse(&scrobble_url, (void**)&scrobbleStruct); + opensubsonic_scrobble_struct_free(&scrobbleStruct); + opensubsonic_httpClient_URL_cleanup(&scrobble_url); + + // Actually make the stream url + opensubsonic_httpClient_URL_t* url = (opensubsonic_httpClient_URL_t*)malloc(sizeof(opensubsonic_httpClient_URL_t)); + opensubsonic_httpClient_URL_prepare(&url); + url->endpoint = OPENSUBSONIC_ENDPOINT_STREAM; + url->id = strdup(id); + opensubsonic_httpClient_formUrl(&url); + + return url->formedUrl; + // TODO need to free URL struct +} + +void discordrpc_update(int type, opensubsonic_getSong_struct* songStruct) { + printf("Updating Discord RPC\n"); + + dscrdrpc_data* dscrdrpc = NULL; + dscrdrpc_struct_init(&dscrdrpc); + + if (type == 1) { + dscrdrpc->requestType = DSCRDRPC_REQTYPE_PLAYING; + } else if (type == 2) { + dscrdrpc->requestType = DSCRDRPC_REQTYPE_KEEPALIVE; + } else if (type == 3) { + dscrdrpc->requestType = DSCRDRPC_REQTYPE_PAUSED; + } + dscrdrpc->songTitle = strdup(songStruct->title); + dscrdrpc->songArtist = strdup(songStruct->artist); + dscrdrpc->coverArtUrl = LOSS_getCoverArt(songStruct->coverArt); + dscrdrpc->deviceInfo = strdup("iPhone (iOS 16.6)"); + dscrdrpc->songLength = songStruct->duration; + + dscrdrpc_encrypt(&dscrdrpc); + printf("Checksum: %s\n", dscrdrpc->checksum); + + dscrdrpc_struct_deinit(&dscrdrpc); +} diff --git a/src/runTests.c b/src/runTests.c new file mode 100644 index 0000000..6fc7001 --- /dev/null +++ b/src/runTests.c @@ -0,0 +1,200 @@ +#include +#include +#include + +// Other imports +#include "configHandler.h" +#include "libopensubsonic/logger.h" +#include "libopensubsonic/httpclient.h" +#include "libopensubsonic/crypto.h" + +// libopensubsonic imports +#include "libopensubsonic/endpoint_ping.h" +#include "libopensubsonic/endpoint_getStarred.h" +#include "libopensubsonic/endpoint_getPlaylists.h" +#include "libopensubsonic/endpoint_getPlaylist.h" +#include "libopensubsonic/endpoint_getSong.h" +#include "libopensubsonic/endpoint_getArtists.h" +#include "libopensubsonic/endpoint_getArtist.h" +#include "libopensubsonic/endpoint_getAlbumList.h" +#include "libopensubsonic/endpoint_getAlbum.h" +#include "libopensubsonic/endpoint_getLyricsBySongId.h" + +configHandler_config_t* test_configObj = NULL; + +void test_libopensubsonic_endpoint_ping(void) { + logger_log_general(__func__, "Testing ping endpoint."); + + opensubsonic_httpClient_URL_t* url = (opensubsonic_httpClient_URL_t*)malloc(sizeof(opensubsonic_httpClient_URL_t)); + opensubsonic_httpClient_URL_prepare(&url); + url->endpoint = OPENSUBSONIC_ENDPOINT_PING; + opensubsonic_httpClient_formUrl(&url); + + opensubsonic_ping_struct* OSS_ping_struct; + opensubsonic_httpClient_fetchResponse(&url, (void**)&OSS_ping_struct); + + opensubsonic_ping_struct_free(&OSS_ping_struct); + opensubsonic_httpClient_URL_cleanup(&url); +} + +void test_libopensubsonic_endpoint_getStarred(void) { + logger_log_general(__func__, "Testing getStarred endpoint."); + + opensubsonic_httpClient_URL_t* url = (opensubsonic_httpClient_URL_t*)malloc(sizeof(opensubsonic_httpClient_URL_t)); + opensubsonic_httpClient_URL_prepare(&url); + url->endpoint = OPENSUBSONIC_ENDPOINT_GETSTARRED; + opensubsonic_httpClient_formUrl(&url); + + opensubsonic_getStarred_struct* getStarredStruct; + opensubsonic_httpClient_fetchResponse(&url, (void**)&getStarredStruct); + + opensubsonic_getStarred_struct_free(&getStarredStruct); + opensubsonic_httpClient_URL_cleanup(&url); +} + +void test_libopensubsonic_endpoint_getPlaylists(void) { + logger_log_general(__func__, "Testing getPlaylists endpoint."); + + opensubsonic_httpClient_URL_t* url = (opensubsonic_httpClient_URL_t*)malloc(sizeof(opensubsonic_httpClient_URL_t)); + opensubsonic_httpClient_URL_prepare(&url); + url->endpoint = OPENSUBSONIC_ENDPOINT_GETPLAYLISTS; + opensubsonic_httpClient_formUrl(&url); + + opensubsonic_getPlaylists_struct* getPlaylistsStruct; + opensubsonic_httpClient_fetchResponse(&url, (void**)&getPlaylistsStruct); + + opensubsonic_getPlaylists_struct_free(&getPlaylistsStruct); + opensubsonic_httpClient_URL_cleanup(&url); +} + +void test_libopensubsonic_endpoint_getPlaylist(void) { + logger_log_general(__func__, "Testing getPlaylist endpoint."); + + opensubsonic_httpClient_URL_t* url = (opensubsonic_httpClient_URL_t*)malloc(sizeof(opensubsonic_httpClient_URL_t)); + opensubsonic_httpClient_URL_prepare(&url); + url->endpoint = OPENSUBSONIC_ENDPOINT_GETPLAYLIST; + url->id = strdup("dkBA3oVi5zpChEVIDVJn4i"); + opensubsonic_httpClient_formUrl(&url); + + opensubsonic_getPlaylist_struct* getPlaylistStruct; + opensubsonic_httpClient_fetchResponse(&url, (void**)&getPlaylistStruct); + + opensubsonic_getPlaylist_struct_free(&getPlaylistStruct); + opensubsonic_httpClient_URL_cleanup(&url); +} + +void test_libopensubsonic_endpoint_getSong(void) { + logger_log_general(__func__, "Testing getSong endpoint."); + + opensubsonic_httpClient_URL_t* url = (opensubsonic_httpClient_URL_t*)malloc(sizeof(opensubsonic_httpClient_URL_t)); + opensubsonic_httpClient_URL_prepare(&url); + url->endpoint = OPENSUBSONIC_ENDPOINT_GETSONG; + url->id = strdup("c24SgyyHpe86IsAwSV73rx"); + opensubsonic_httpClient_formUrl(&url); + + opensubsonic_getSong_struct* getSongStruct; + opensubsonic_httpClient_fetchResponse(&url, (void**)&getSongStruct); + + opensubsonic_getSong_struct_free(&getSongStruct); + opensubsonic_httpClient_URL_cleanup(&url); +} + +void test_libopensubsonic_endpoint_getArtists(void) { + logger_log_general(__func__, "Testing getArtists endpoint."); + + opensubsonic_httpClient_URL_t* url = (opensubsonic_httpClient_URL_t*)malloc(sizeof(opensubsonic_httpClient_URL_t)); + opensubsonic_httpClient_URL_prepare(&url); + url->endpoint = OPENSUBSONIC_ENDPOINT_GETARTISTS; + opensubsonic_httpClient_formUrl(&url); + + opensubsonic_getArtists_struct* getArtistsStruct; + opensubsonic_httpClient_fetchResponse(&url, (void**)&getArtistsStruct); + + opensubsonic_getArtists_struct_free(&getArtistsStruct); + opensubsonic_httpClient_URL_cleanup(&url); +} + +void test_libopensubsonic_endpoint_getArtist(void) { + logger_log_general(__func__, "Testing getArtist endpoint."); + + opensubsonic_httpClient_URL_t* url = (opensubsonic_httpClient_URL_t*)malloc(sizeof(opensubsonic_httpClient_URL_t)); + opensubsonic_httpClient_URL_prepare(&url); + url->endpoint = OPENSUBSONIC_ENDPOINT_GETARTIST; + url->id = strdup("3mZKW6zlodlLW4x4QLDhPr"); + opensubsonic_httpClient_formUrl(&url); + + opensubsonic_getArtist_struct* getArtistStruct; + opensubsonic_httpClient_fetchResponse(&url, (void**)&getArtistStruct); + + opensubsonic_getArtist_struct_free(&getArtistStruct); + opensubsonic_httpClient_URL_cleanup(&url); +} + +void test_libopensubsonic_endpoint_getAlbumList(void) { + logger_log_general(__func__, "Testing getAlbumList endpoint."); + + opensubsonic_httpClient_URL_t* url = (opensubsonic_httpClient_URL_t*)malloc(sizeof(opensubsonic_httpClient_URL_t)); + opensubsonic_httpClient_URL_prepare(&url); + url->endpoint = OPENSUBSONIC_ENDPOINT_GETALBUMLIST; + url->type = OPENSUBSONIC_ENDPOINT_GETALBUMLIST_RECENT; + url->amount = 5; + opensubsonic_httpClient_formUrl(&url); + + opensubsonic_getAlbumList_struct* getAlbumListStruct; + opensubsonic_httpClient_fetchResponse(&url, (void**)&getAlbumListStruct); + + opensubsonic_getAlbumList_struct_free(&getAlbumListStruct); + opensubsonic_httpClient_URL_cleanup(&url); +} + +void test_libopensubsonic_endpoint_getAlbum(void) { + logger_log_general(__func__, "Testing getAlbum endpoint."); + + opensubsonic_httpClient_URL_t* url = (opensubsonic_httpClient_URL_t*)malloc(sizeof(opensubsonic_httpClient_URL_t)); + opensubsonic_httpClient_URL_prepare(&url); + url->endpoint = OPENSUBSONIC_ENDPOINT_GETALBUM; + url->id = strdup("5OjqcCVp8LbDfamfubZxFN"); + opensubsonic_httpClient_formUrl(&url); + + opensubsonic_getAlbum_struct* getAlbumStruct; + opensubsonic_httpClient_fetchResponse(&url, (void**)&getAlbumStruct); + + opensubsonic_getAlbum_struct_free(&getAlbumStruct); + opensubsonic_httpClient_URL_cleanup(&url); +} + +void test_libopensubsonic_endpoint_getLyricsBySongId(void) { + logger_log_general(__func__, "Testing getLyricsBySongId endpoint."); + + opensubsonic_httpClient_URL_t* url = (opensubsonic_httpClient_URL_t*)malloc(sizeof(opensubsonic_httpClient_URL_t)); + opensubsonic_httpClient_URL_prepare(&url); + url->endpoint = OPENSUBSONIC_ENDPOINT_GETLYRICSBYSONGID; + url->id = strdup("pvu323svJrim683Xf8fBV8"); + opensubsonic_httpClient_formUrl(&url); + + opensubsonic_getLyricsBySongId_struct* getLyricsBySongIdStruct; + opensubsonic_httpClient_fetchResponse(&url, (void**)&getLyricsBySongIdStruct); + + opensubsonic_getLyricsBySongId_struct_free(&getLyricsBySongIdStruct); + opensubsonic_httpClient_URL_cleanup(&url); +} + +void runTests(void) { + // Read config file + configHandler_Read(&test_configObj); + + // Run tests + test_libopensubsonic_endpoint_ping(); + test_libopensubsonic_endpoint_getStarred(); + test_libopensubsonic_endpoint_getPlaylists(); + test_libopensubsonic_endpoint_getPlaylist(); + test_libopensubsonic_endpoint_getSong(); + test_libopensubsonic_endpoint_getArtists(); + test_libopensubsonic_endpoint_getAlbumList(); + test_libopensubsonic_endpoint_getAlbum(); + test_libopensubsonic_endpoint_getLyricsBySongId(); + + // Free config file + configHandler_Free(&test_configObj); +} +