gemba 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/THIRD_PARTY_NOTICES +37 -2
- data/assets/placeholder_boxart.png +0 -0
- data/bin/gemba +2 -2
- data/ext/gemba/extconf.rb +23 -1
- data/ext/gemba/gemba_ext.c +436 -2
- data/ext/gemba/gemba_ext.h +2 -0
- data/gemba.gemspec +5 -3
- data/lib/gemba/achievements/achievement.rb +23 -0
- data/lib/gemba/achievements/backend.rb +186 -0
- data/lib/gemba/achievements/cache.rb +70 -0
- data/lib/gemba/achievements/credentials_presenter.rb +142 -0
- data/lib/gemba/achievements/fake_backend.rb +205 -0
- data/lib/gemba/achievements/null_backend.rb +11 -0
- data/lib/gemba/achievements/offline_backend.rb +168 -0
- data/lib/gemba/achievements/retro_achievements/backend.rb +453 -0
- data/lib/gemba/achievements/retro_achievements/cli_sync_requester.rb +64 -0
- data/lib/gemba/achievements/retro_achievements/ping_worker.rb +27 -0
- data/lib/gemba/achievements.rb +19 -0
- data/lib/gemba/achievements_window.rb +556 -0
- data/lib/gemba/app_controller.rb +1015 -0
- data/lib/gemba/bios.rb +54 -0
- data/lib/gemba/boxart_fetcher/libretro_backend.rb +39 -0
- data/lib/gemba/boxart_fetcher/null_backend.rb +12 -0
- data/lib/gemba/boxart_fetcher.rb +79 -0
- data/lib/gemba/bus_emitter.rb +13 -0
- data/lib/gemba/child_window.rb +24 -1
- data/lib/gemba/cli/commands/config_cmd.rb +83 -0
- data/lib/gemba/cli/commands/decode.rb +154 -0
- data/lib/gemba/cli/commands/patch.rb +78 -0
- data/lib/gemba/cli/commands/play.rb +78 -0
- data/lib/gemba/cli/commands/record.rb +114 -0
- data/lib/gemba/cli/commands/replay.rb +161 -0
- data/lib/gemba/cli/commands/retro_achievements.rb +213 -0
- data/lib/gemba/cli/commands/version.rb +22 -0
- data/lib/gemba/cli.rb +52 -364
- data/lib/gemba/config.rb +134 -1
- data/lib/gemba/data/gb_games.json +1 -0
- data/lib/gemba/data/gb_md5.json +1 -0
- data/lib/gemba/data/gba_games.json +1 -0
- data/lib/gemba/data/gba_md5.json +1 -0
- data/lib/gemba/data/gbc_games.json +1 -0
- data/lib/gemba/data/gbc_md5.json +1 -0
- data/lib/gemba/emulator_frame.rb +1060 -0
- data/lib/gemba/event_bus.rb +48 -0
- data/lib/gemba/frame_stack.rb +60 -0
- data/lib/gemba/game_index.rb +84 -0
- data/lib/gemba/game_picker_frame.rb +268 -0
- data/lib/gemba/gamepad_map.rb +103 -0
- data/lib/gemba/headless.rb +6 -5
- data/lib/gemba/headless_player.rb +33 -3
- data/lib/gemba/help_window.rb +61 -0
- data/lib/gemba/hotkey_map.rb +3 -1
- data/lib/gemba/input_recorder.rb +107 -0
- data/lib/gemba/input_replayer.rb +119 -0
- data/lib/gemba/keyboard_map.rb +90 -0
- data/lib/gemba/locales/en.yml +97 -5
- data/lib/gemba/locales/ja.yml +97 -5
- data/lib/gemba/main_window.rb +56 -0
- data/lib/gemba/modal_stack.rb +81 -0
- data/lib/gemba/patcher_window.rb +223 -0
- data/lib/gemba/platform/gb.rb +21 -0
- data/lib/gemba/platform/gba.rb +21 -0
- data/lib/gemba/platform/gbc.rb +23 -0
- data/lib/gemba/platform.rb +20 -0
- data/lib/gemba/platform_open.rb +19 -0
- data/lib/gemba/recorder.rb +4 -3
- data/lib/gemba/replay_player.rb +691 -0
- data/lib/gemba/rom_info.rb +57 -0
- data/lib/gemba/rom_info_window.rb +16 -3
- data/lib/gemba/rom_library.rb +106 -0
- data/lib/gemba/rom_overrides.rb +47 -0
- data/lib/gemba/rom_patcher/bps.rb +161 -0
- data/lib/gemba/rom_patcher/ips.rb +101 -0
- data/lib/gemba/rom_patcher/ups.rb +118 -0
- data/lib/gemba/rom_patcher.rb +109 -0
- data/lib/gemba/{rom_loader.rb → rom_resolver.rb} +7 -6
- data/lib/gemba/runtime.rb +59 -26
- data/lib/gemba/save_state_manager.rb +4 -7
- data/lib/gemba/save_state_picker.rb +17 -4
- data/lib/gemba/session_logger.rb +64 -0
- data/lib/gemba/settings/audio_tab.rb +77 -0
- data/lib/gemba/settings/gamepad_tab.rb +351 -0
- data/lib/gemba/settings/hotkeys_tab.rb +259 -0
- data/lib/gemba/settings/paths.rb +11 -0
- data/lib/gemba/settings/recording_tab.rb +83 -0
- data/lib/gemba/settings/save_states_tab.rb +91 -0
- data/lib/gemba/settings/system_tab.rb +362 -0
- data/lib/gemba/settings/video_tab.rb +318 -0
- data/lib/gemba/settings_window.rb +162 -1036
- data/lib/gemba/version.rb +1 -1
- data/lib/gemba/virtual_keyboard.rb +19 -0
- data/lib/gemba.rb +2 -12
- data/test/achievements_window/test_bulk_sync.rb +218 -0
- data/test/achievements_window/test_bus_events.rb +125 -0
- data/test/achievements_window/test_close_confirmation.rb +201 -0
- data/test/achievements_window/test_initial_state.rb +164 -0
- data/test/achievements_window/test_sorting.rb +227 -0
- data/test/achievements_window/test_tree_rendering.rb +133 -0
- data/test/fixtures/fake_bios.bin +0 -0
- data/test/fixtures/pong.gba +0 -0
- data/test/fixtures/test.gb +0 -0
- data/test/fixtures/test.gbc +0 -0
- data/test/fixtures/test_quicksave.ss +0 -0
- data/test/screenshots/no_focus.png +0 -0
- data/test/shared/teek_test_worker.rb +17 -1
- data/test/shared/tk_test_helper.rb +91 -4
- data/test/support/achievements_window_helpers.rb +18 -0
- data/test/support/fake_core.rb +25 -0
- data/test/support/fake_ra_runtime.rb +74 -0
- data/test/support/fake_requester.rb +68 -0
- data/test/support/player_helpers.rb +20 -5
- data/test/test_achievement.rb +32 -0
- data/test/{test_player.rb → test_app_controller.rb} +353 -85
- data/test/test_bios.rb +123 -0
- data/test/test_boxart_fetcher.rb +150 -0
- data/test/test_cli.rb +17 -265
- data/test/test_cli_config.rb +64 -0
- data/test/test_cli_decode.rb +97 -0
- data/test/test_cli_patch.rb +58 -0
- data/test/test_cli_play.rb +213 -0
- data/test/test_cli_ra.rb +175 -0
- data/test/test_cli_record.rb +69 -0
- data/test/test_cli_replay.rb +72 -0
- data/test/test_cli_sync_requester.rb +152 -0
- data/test/test_cli_version.rb +27 -0
- data/test/test_config.rb +2 -3
- data/test/test_config_ra.rb +69 -0
- data/test/test_core.rb +62 -1
- data/test/test_credentials_presenter.rb +192 -0
- data/test/test_event_bus.rb +100 -0
- data/test/test_fake_backend_achievements.rb +130 -0
- data/test/test_fake_backend_auth.rb +68 -0
- data/test/test_game_index.rb +77 -0
- data/test/test_game_picker_frame.rb +310 -0
- data/test/test_gamepad_map.rb +1 -3
- data/test/test_headless_player.rb +17 -3
- data/test/test_help_window.rb +82 -0
- data/test/test_hotkey_map.rb +22 -1
- data/test/test_input_recorder.rb +179 -0
- data/test/test_input_replay_determinism.rb +113 -0
- data/test/test_input_replayer.rb +162 -0
- data/test/test_keyboard_map.rb +1 -3
- data/test/test_libretro_backend.rb +41 -0
- data/test/test_locale.rb +1 -1
- data/test/test_logging.rb +123 -0
- data/test/test_null_backend.rb +42 -0
- data/test/test_offline_backend.rb +116 -0
- data/test/test_overlay_renderer.rb +1 -1
- data/test/test_platform.rb +149 -0
- data/test/test_ra_backend.rb +313 -0
- data/test/test_ra_backend_unlock_gate.rb +56 -0
- data/test/test_recorder.rb +0 -3
- data/test/test_replay_player.rb +316 -0
- data/test/test_rom_info.rb +149 -0
- data/test/test_rom_overrides.rb +86 -0
- data/test/test_rom_patcher.rb +382 -0
- data/test/{test_rom_loader.rb → test_rom_resolver.rb} +25 -26
- data/test/test_save_state_manager.rb +2 -4
- data/test/test_settings_audio.rb +107 -0
- data/test/test_settings_hotkeys.rb +83 -66
- data/test/test_settings_recording.rb +49 -0
- data/test/test_settings_save_states.rb +97 -0
- data/test/test_settings_system.rb +133 -0
- data/test/test_settings_video.rb +450 -0
- data/test/test_settings_window.rb +76 -507
- data/test/test_tip_service.rb +6 -6
- data/test/test_toast_overlay.rb +1 -1
- data/test/test_virtual_events.rb +156 -0
- data/test/test_virtual_keyboard.rb +1 -1
- data/vendor/rcheevos/CHANGELOG.md +495 -0
- data/vendor/rcheevos/LICENSE +21 -0
- data/vendor/rcheevos/Package.swift +33 -0
- data/vendor/rcheevos/README.md +67 -0
- data/vendor/rcheevos/include/module.modulemap +70 -0
- data/vendor/rcheevos/include/rc_api_editor.h +296 -0
- data/vendor/rcheevos/include/rc_api_info.h +280 -0
- data/vendor/rcheevos/include/rc_api_request.h +77 -0
- data/vendor/rcheevos/include/rc_api_runtime.h +417 -0
- data/vendor/rcheevos/include/rc_api_user.h +262 -0
- data/vendor/rcheevos/include/rc_client.h +877 -0
- data/vendor/rcheevos/include/rc_client_raintegration.h +101 -0
- data/vendor/rcheevos/include/rc_consoles.h +138 -0
- data/vendor/rcheevos/include/rc_error.h +59 -0
- data/vendor/rcheevos/include/rc_export.h +100 -0
- data/vendor/rcheevos/include/rc_hash.h +200 -0
- data/vendor/rcheevos/include/rc_runtime.h +148 -0
- data/vendor/rcheevos/include/rc_runtime_types.h +452 -0
- data/vendor/rcheevos/include/rc_util.h +51 -0
- data/vendor/rcheevos/include/rcheevos.h +8 -0
- data/vendor/rcheevos/src/rapi/rc_api_common.c +1379 -0
- data/vendor/rcheevos/src/rapi/rc_api_common.h +88 -0
- data/vendor/rcheevos/src/rapi/rc_api_editor.c +625 -0
- data/vendor/rcheevos/src/rapi/rc_api_info.c +587 -0
- data/vendor/rcheevos/src/rapi/rc_api_runtime.c +901 -0
- data/vendor/rcheevos/src/rapi/rc_api_user.c +483 -0
- data/vendor/rcheevos/src/rc_client.c +6941 -0
- data/vendor/rcheevos/src/rc_client_external.c +281 -0
- data/vendor/rcheevos/src/rc_client_external.h +177 -0
- data/vendor/rcheevos/src/rc_client_external_versions.h +171 -0
- data/vendor/rcheevos/src/rc_client_internal.h +409 -0
- data/vendor/rcheevos/src/rc_client_raintegration.c +566 -0
- data/vendor/rcheevos/src/rc_client_raintegration_internal.h +61 -0
- data/vendor/rcheevos/src/rc_client_types.natvis +396 -0
- data/vendor/rcheevos/src/rc_compat.c +251 -0
- data/vendor/rcheevos/src/rc_compat.h +121 -0
- data/vendor/rcheevos/src/rc_libretro.c +915 -0
- data/vendor/rcheevos/src/rc_libretro.h +98 -0
- data/vendor/rcheevos/src/rc_util.c +199 -0
- data/vendor/rcheevos/src/rc_version.c +11 -0
- data/vendor/rcheevos/src/rc_version.h +32 -0
- data/vendor/rcheevos/src/rcheevos/alloc.c +312 -0
- data/vendor/rcheevos/src/rcheevos/condition.c +754 -0
- data/vendor/rcheevos/src/rcheevos/condset.c +777 -0
- data/vendor/rcheevos/src/rcheevos/consoleinfo.c +1215 -0
- data/vendor/rcheevos/src/rcheevos/format.c +330 -0
- data/vendor/rcheevos/src/rcheevos/lboard.c +287 -0
- data/vendor/rcheevos/src/rcheevos/memref.c +805 -0
- data/vendor/rcheevos/src/rcheevos/operand.c +607 -0
- data/vendor/rcheevos/src/rcheevos/rc_internal.h +390 -0
- data/vendor/rcheevos/src/rcheevos/rc_runtime_types.natvis +541 -0
- data/vendor/rcheevos/src/rcheevos/rc_validate.c +1406 -0
- data/vendor/rcheevos/src/rcheevos/rc_validate.h +18 -0
- data/vendor/rcheevos/src/rcheevos/richpresence.c +922 -0
- data/vendor/rcheevos/src/rcheevos/runtime.c +852 -0
- data/vendor/rcheevos/src/rcheevos/runtime_progress.c +1073 -0
- data/vendor/rcheevos/src/rcheevos/trigger.c +344 -0
- data/vendor/rcheevos/src/rcheevos/value.c +935 -0
- data/vendor/rcheevos/src/rhash/aes.c +480 -0
- data/vendor/rcheevos/src/rhash/aes.h +49 -0
- data/vendor/rcheevos/src/rhash/cdreader.c +838 -0
- data/vendor/rcheevos/src/rhash/hash.c +1402 -0
- data/vendor/rcheevos/src/rhash/hash_disc.c +1340 -0
- data/vendor/rcheevos/src/rhash/hash_encrypted.c +566 -0
- data/vendor/rcheevos/src/rhash/hash_rom.c +426 -0
- data/vendor/rcheevos/src/rhash/hash_zip.c +460 -0
- data/vendor/rcheevos/src/rhash/md5.c +382 -0
- data/vendor/rcheevos/src/rhash/md5.h +91 -0
- data/vendor/rcheevos/src/rhash/rc_hash_internal.h +116 -0
- data/vendor/rcheevos/test/libretro.h +205 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_common.c +941 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_editor.c +931 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_info.c +545 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_runtime.c +2213 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_user.c +998 -0
- data/vendor/rcheevos/test/rcheevos/mock_memory.h +32 -0
- data/vendor/rcheevos/test/rcheevos/test_condition.c +570 -0
- data/vendor/rcheevos/test/rcheevos/test_condset.c +5170 -0
- data/vendor/rcheevos/test/rcheevos/test_consoleinfo.c +203 -0
- data/vendor/rcheevos/test/rcheevos/test_format.c +112 -0
- data/vendor/rcheevos/test/rcheevos/test_lboard.c +746 -0
- data/vendor/rcheevos/test/rcheevos/test_memref.c +520 -0
- data/vendor/rcheevos/test/rcheevos/test_operand.c +692 -0
- data/vendor/rcheevos/test/rcheevos/test_rc_validate.c +502 -0
- data/vendor/rcheevos/test/rcheevos/test_richpresence.c +1564 -0
- data/vendor/rcheevos/test/rcheevos/test_runtime.c +1667 -0
- data/vendor/rcheevos/test/rcheevos/test_runtime_progress.c +1821 -0
- data/vendor/rcheevos/test/rcheevos/test_timing.c +166 -0
- data/vendor/rcheevos/test/rcheevos/test_trigger.c +2521 -0
- data/vendor/rcheevos/test/rcheevos/test_value.c +870 -0
- data/vendor/rcheevos/test/rcheevos-test.sln +46 -0
- data/vendor/rcheevos/test/rcheevos-test.vcxproj +239 -0
- data/vendor/rcheevos/test/rcheevos-test.vcxproj.filters +335 -0
- data/vendor/rcheevos/test/rhash/data.c +657 -0
- data/vendor/rcheevos/test/rhash/data.h +32 -0
- data/vendor/rcheevos/test/rhash/mock_filereader.c +236 -0
- data/vendor/rcheevos/test/rhash/mock_filereader.h +31 -0
- data/vendor/rcheevos/test/rhash/test_cdreader.c +920 -0
- data/vendor/rcheevos/test/rhash/test_hash.c +310 -0
- data/vendor/rcheevos/test/rhash/test_hash_disc.c +1450 -0
- data/vendor/rcheevos/test/rhash/test_hash_rom.c +899 -0
- data/vendor/rcheevos/test/rhash/test_hash_zip.c +551 -0
- data/vendor/rcheevos/test/test.c +113 -0
- data/vendor/rcheevos/test/test_framework.h +205 -0
- data/vendor/rcheevos/test/test_rc_client.c +10509 -0
- data/vendor/rcheevos/test/test_rc_client_external.c +2197 -0
- data/vendor/rcheevos/test/test_rc_client_raintegration.c +441 -0
- data/vendor/rcheevos/test/test_rc_libretro.c +952 -0
- data/vendor/rcheevos/test/test_types.natvis +9 -0
- data/vendor/rcheevos/validator/validator.c +658 -0
- data/vendor/rcheevos/validator/validator.vcxproj +152 -0
- data/vendor/rcheevos/validator/validator.vcxproj.filters +82 -0
- metadata +274 -11
- data/lib/gemba/input_mappings.rb +0 -214
- data/lib/gemba/player.rb +0 -1525
|
@@ -0,0 +1,1379 @@
|
|
|
1
|
+
#include "rc_api_common.h"
|
|
2
|
+
#include "rc_api_request.h"
|
|
3
|
+
#include "rc_api_runtime.h"
|
|
4
|
+
|
|
5
|
+
#include "../rc_compat.h"
|
|
6
|
+
|
|
7
|
+
#include <ctype.h>
|
|
8
|
+
#include <stdio.h>
|
|
9
|
+
#include <stdlib.h>
|
|
10
|
+
#include <string.h>
|
|
11
|
+
|
|
12
|
+
#define RETROACHIEVEMENTS_HOST "https://retroachievements.org"
|
|
13
|
+
#define RETROACHIEVEMENTS_IMAGE_HOST "https://media.retroachievements.org"
|
|
14
|
+
#define RETROACHIEVEMENTS_HOST_NONSSL "http://retroachievements.org"
|
|
15
|
+
#define RETROACHIEVEMENTS_IMAGE_HOST_NONSSL "http://media.retroachievements.org"
|
|
16
|
+
rc_api_host_t g_host = { NULL, NULL };
|
|
17
|
+
|
|
18
|
+
/* --- rc_json --- */
|
|
19
|
+
|
|
20
|
+
static int rc_json_parse_object(rc_json_iterator_t* iterator, rc_json_field_t* fields, size_t field_count, uint32_t* fields_seen);
|
|
21
|
+
static int rc_json_parse_array(rc_json_iterator_t* iterator, rc_json_field_t* field);
|
|
22
|
+
|
|
23
|
+
static int rc_json_match_char(rc_json_iterator_t* iterator, char c)
|
|
24
|
+
{
|
|
25
|
+
if (iterator->json < iterator->end && *iterator->json == c) {
|
|
26
|
+
++iterator->json;
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static void rc_json_skip_whitespace(rc_json_iterator_t* iterator)
|
|
34
|
+
{
|
|
35
|
+
while (iterator->json < iterator->end && isspace((unsigned char)*iterator->json))
|
|
36
|
+
++iterator->json;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static int rc_json_find_substring(rc_json_iterator_t* iterator, const char* substring)
|
|
40
|
+
{
|
|
41
|
+
const char first = *substring;
|
|
42
|
+
const size_t substring_len = strlen(substring);
|
|
43
|
+
const char* end = iterator->end - substring_len;
|
|
44
|
+
|
|
45
|
+
while (iterator->json <= end) {
|
|
46
|
+
if (*iterator->json == first) {
|
|
47
|
+
if (memcmp(iterator->json, substring, substring_len) == 0)
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
++iterator->json;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static int rc_json_find_closing_quote(rc_json_iterator_t* iterator)
|
|
58
|
+
{
|
|
59
|
+
while (iterator->json < iterator->end) {
|
|
60
|
+
if (*iterator->json == '"')
|
|
61
|
+
return 1;
|
|
62
|
+
|
|
63
|
+
if (*iterator->json == '\\') {
|
|
64
|
+
++iterator->json;
|
|
65
|
+
if (iterator->json == iterator->end)
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (*iterator->json == '\0')
|
|
70
|
+
return 0;
|
|
71
|
+
|
|
72
|
+
++iterator->json;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static int rc_json_parse_field(rc_json_iterator_t* iterator, rc_json_field_t* field) {
|
|
79
|
+
int result;
|
|
80
|
+
|
|
81
|
+
if (iterator->json >= iterator->end)
|
|
82
|
+
return RC_INVALID_JSON;
|
|
83
|
+
|
|
84
|
+
field->value_start = iterator->json;
|
|
85
|
+
|
|
86
|
+
switch (*iterator->json)
|
|
87
|
+
{
|
|
88
|
+
case '"': /* quoted string */
|
|
89
|
+
++iterator->json;
|
|
90
|
+
if (!rc_json_find_closing_quote(iterator))
|
|
91
|
+
return RC_INVALID_JSON;
|
|
92
|
+
++iterator->json;
|
|
93
|
+
break;
|
|
94
|
+
|
|
95
|
+
case '-':
|
|
96
|
+
case '+': /* signed number */
|
|
97
|
+
++iterator->json;
|
|
98
|
+
/* fallthrough to number */
|
|
99
|
+
case '0': case '1': case '2': case '3': case '4':
|
|
100
|
+
case '5': case '6': case '7': case '8': case '9': /* number */
|
|
101
|
+
while (iterator->json < iterator->end && *iterator->json >= '0' && *iterator->json <= '9')
|
|
102
|
+
++iterator->json;
|
|
103
|
+
|
|
104
|
+
if (rc_json_match_char(iterator, '.')) {
|
|
105
|
+
while (iterator->json < iterator->end && *iterator->json >= '0' && *iterator->json <= '9')
|
|
106
|
+
++iterator->json;
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
|
|
110
|
+
case '[': /* array */
|
|
111
|
+
result = rc_json_parse_array(iterator, field);
|
|
112
|
+
if (result != RC_OK)
|
|
113
|
+
return result;
|
|
114
|
+
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
case '{': /* object */
|
|
118
|
+
result = rc_json_parse_object(iterator, NULL, 0, &field->array_size);
|
|
119
|
+
if (result != RC_OK)
|
|
120
|
+
return result;
|
|
121
|
+
|
|
122
|
+
break;
|
|
123
|
+
|
|
124
|
+
default: /* non-quoted text [true,false,null] */
|
|
125
|
+
if (!isalpha((unsigned char)*iterator->json))
|
|
126
|
+
return RC_INVALID_JSON;
|
|
127
|
+
|
|
128
|
+
while (iterator->json < iterator->end && isalnum((unsigned char)*iterator->json))
|
|
129
|
+
++iterator->json;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
field->value_end = iterator->json;
|
|
134
|
+
return RC_OK;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
static int rc_json_parse_array(rc_json_iterator_t* iterator, rc_json_field_t* field) {
|
|
138
|
+
rc_json_field_t unused_field;
|
|
139
|
+
int result;
|
|
140
|
+
|
|
141
|
+
if (!rc_json_match_char(iterator, '['))
|
|
142
|
+
return RC_INVALID_JSON;
|
|
143
|
+
|
|
144
|
+
field->array_size = 0;
|
|
145
|
+
|
|
146
|
+
if (rc_json_match_char(iterator, ']')) /* empty array */
|
|
147
|
+
return RC_OK;
|
|
148
|
+
|
|
149
|
+
do
|
|
150
|
+
{
|
|
151
|
+
rc_json_skip_whitespace(iterator);
|
|
152
|
+
|
|
153
|
+
result = rc_json_parse_field(iterator, &unused_field);
|
|
154
|
+
if (result != RC_OK)
|
|
155
|
+
return result;
|
|
156
|
+
|
|
157
|
+
++field->array_size;
|
|
158
|
+
|
|
159
|
+
rc_json_skip_whitespace(iterator);
|
|
160
|
+
} while (rc_json_match_char(iterator, ','));
|
|
161
|
+
|
|
162
|
+
if (!rc_json_match_char(iterator, ']'))
|
|
163
|
+
return RC_INVALID_JSON;
|
|
164
|
+
|
|
165
|
+
return RC_OK;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
static int rc_json_get_next_field(rc_json_iterator_t* iterator, rc_json_field_t* field) {
|
|
169
|
+
rc_json_skip_whitespace(iterator);
|
|
170
|
+
|
|
171
|
+
if (!rc_json_match_char(iterator, '"'))
|
|
172
|
+
return RC_INVALID_JSON;
|
|
173
|
+
|
|
174
|
+
field->name = iterator->json;
|
|
175
|
+
while (iterator->json < iterator->end && *iterator->json != '"') {
|
|
176
|
+
if (!*iterator->json)
|
|
177
|
+
return RC_INVALID_JSON;
|
|
178
|
+
++iterator->json;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (iterator->json == iterator->end)
|
|
182
|
+
return RC_INVALID_JSON;
|
|
183
|
+
|
|
184
|
+
field->name_len = iterator->json - field->name;
|
|
185
|
+
++iterator->json;
|
|
186
|
+
|
|
187
|
+
rc_json_skip_whitespace(iterator);
|
|
188
|
+
|
|
189
|
+
if (!rc_json_match_char(iterator, ':'))
|
|
190
|
+
return RC_INVALID_JSON;
|
|
191
|
+
|
|
192
|
+
rc_json_skip_whitespace(iterator);
|
|
193
|
+
|
|
194
|
+
if (rc_json_parse_field(iterator, field) < 0)
|
|
195
|
+
return RC_INVALID_JSON;
|
|
196
|
+
|
|
197
|
+
rc_json_skip_whitespace(iterator);
|
|
198
|
+
|
|
199
|
+
return RC_OK;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
static int rc_json_parse_object(rc_json_iterator_t* iterator, rc_json_field_t* fields, size_t field_count, uint32_t* fields_seen) {
|
|
203
|
+
size_t i;
|
|
204
|
+
uint32_t num_fields = 0;
|
|
205
|
+
rc_json_field_t field;
|
|
206
|
+
int result;
|
|
207
|
+
|
|
208
|
+
if (fields_seen)
|
|
209
|
+
*fields_seen = 0;
|
|
210
|
+
|
|
211
|
+
for (i = 0; i < field_count; ++i)
|
|
212
|
+
fields[i].value_start = fields[i].value_end = NULL;
|
|
213
|
+
|
|
214
|
+
if (!rc_json_match_char(iterator, '{'))
|
|
215
|
+
return RC_INVALID_JSON;
|
|
216
|
+
|
|
217
|
+
if (rc_json_match_char(iterator, '}')) /* empty object */
|
|
218
|
+
return RC_OK;
|
|
219
|
+
|
|
220
|
+
do
|
|
221
|
+
{
|
|
222
|
+
result = rc_json_get_next_field(iterator, &field);
|
|
223
|
+
if (result != RC_OK)
|
|
224
|
+
return result;
|
|
225
|
+
|
|
226
|
+
for (i = 0; i < field_count; ++i) {
|
|
227
|
+
if (!fields[i].value_start && fields[i].name_len == field.name_len &&
|
|
228
|
+
memcmp(fields[i].name, field.name, field.name_len) == 0) {
|
|
229
|
+
fields[i].value_start = field.value_start;
|
|
230
|
+
fields[i].value_end = field.value_end;
|
|
231
|
+
fields[i].array_size = field.array_size;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
++num_fields;
|
|
237
|
+
|
|
238
|
+
} while (rc_json_match_char(iterator, ','));
|
|
239
|
+
|
|
240
|
+
if (!rc_json_match_char(iterator, '}'))
|
|
241
|
+
return RC_INVALID_JSON;
|
|
242
|
+
|
|
243
|
+
if (fields_seen)
|
|
244
|
+
*fields_seen = num_fields;
|
|
245
|
+
|
|
246
|
+
return RC_OK;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
int rc_json_get_next_object_field(rc_json_iterator_t* iterator, rc_json_field_t* field) {
|
|
250
|
+
if (!rc_json_match_char(iterator, ',') && !rc_json_match_char(iterator, '{'))
|
|
251
|
+
return 0;
|
|
252
|
+
|
|
253
|
+
return (rc_json_get_next_field(iterator, field) == RC_OK);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
int rc_json_get_object_string_length(const char* json) {
|
|
257
|
+
rc_json_iterator_t iterator;
|
|
258
|
+
memset(&iterator, 0, sizeof(iterator));
|
|
259
|
+
iterator.json = json;
|
|
260
|
+
iterator.end = json + (1024 * 1024 * 1024); /* arbitrary 1GB limit on JSON response */
|
|
261
|
+
|
|
262
|
+
rc_json_parse_object(&iterator, NULL, 0, NULL);
|
|
263
|
+
|
|
264
|
+
if (iterator.json == json) /* not JSON */
|
|
265
|
+
return (int)strlen(json);
|
|
266
|
+
|
|
267
|
+
return (int)(iterator.json - json);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
static int rc_json_extract_html_error(rc_api_response_t* response, const rc_api_server_response_t* server_response) {
|
|
271
|
+
rc_json_iterator_t iterator;
|
|
272
|
+
memset(&iterator, 0, sizeof(iterator));
|
|
273
|
+
iterator.json = server_response->body;
|
|
274
|
+
iterator.end = server_response->body + server_response->body_length;
|
|
275
|
+
|
|
276
|
+
/* assume the title contains the most appropriate message to display to the user */
|
|
277
|
+
if (rc_json_find_substring(&iterator, "<title>")) {
|
|
278
|
+
const char* title_start = iterator.json + 7;
|
|
279
|
+
if (rc_json_find_substring(&iterator, "</title>")) {
|
|
280
|
+
response->error_message = rc_buffer_strncpy(&response->buffer, title_start, iterator.json - title_start);
|
|
281
|
+
response->succeeded = 0;
|
|
282
|
+
return RC_INVALID_JSON;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/* title not found, return the first line of the response (up to 200 characters) */
|
|
287
|
+
iterator.json = server_response->body;
|
|
288
|
+
|
|
289
|
+
while (iterator.json < iterator.end && *iterator.json != '\n' &&
|
|
290
|
+
iterator.json - server_response->body < 200) {
|
|
291
|
+
++iterator.json;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (iterator.json > server_response->body && iterator.json[-1] == '\r')
|
|
295
|
+
--iterator.json;
|
|
296
|
+
|
|
297
|
+
if (iterator.json > server_response->body)
|
|
298
|
+
response->error_message = rc_buffer_strncpy(&response->buffer, server_response->body, iterator.json - server_response->body);
|
|
299
|
+
|
|
300
|
+
response->succeeded = 0;
|
|
301
|
+
return RC_INVALID_JSON;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
static int rc_json_convert_error_code(const char* server_error_code)
|
|
305
|
+
{
|
|
306
|
+
switch (server_error_code[0]) {
|
|
307
|
+
case 'a':
|
|
308
|
+
if (strcmp(server_error_code, "access_denied") == 0)
|
|
309
|
+
return RC_ACCESS_DENIED;
|
|
310
|
+
break;
|
|
311
|
+
|
|
312
|
+
case 'e':
|
|
313
|
+
if (strcmp(server_error_code, "expired_token") == 0)
|
|
314
|
+
return RC_EXPIRED_TOKEN;
|
|
315
|
+
break;
|
|
316
|
+
|
|
317
|
+
case 'i':
|
|
318
|
+
if (strcmp(server_error_code, "invalid_credentials") == 0)
|
|
319
|
+
return RC_INVALID_CREDENTIALS;
|
|
320
|
+
if (strcmp(server_error_code, "invalid_parameter") == 0)
|
|
321
|
+
return RC_INVALID_STATE;
|
|
322
|
+
break;
|
|
323
|
+
|
|
324
|
+
case 'm':
|
|
325
|
+
if (strcmp(server_error_code, "missing_parameter") == 0)
|
|
326
|
+
return RC_INVALID_STATE;
|
|
327
|
+
break;
|
|
328
|
+
|
|
329
|
+
case 'n':
|
|
330
|
+
if (strcmp(server_error_code, "not_found") == 0)
|
|
331
|
+
return RC_NOT_FOUND;
|
|
332
|
+
break;
|
|
333
|
+
|
|
334
|
+
default:
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return RC_API_FAILURE;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
int rc_json_parse_server_response(rc_api_response_t* response, const rc_api_server_response_t* server_response, rc_json_field_t* fields, size_t field_count) {
|
|
342
|
+
int result;
|
|
343
|
+
|
|
344
|
+
#ifndef NDEBUG
|
|
345
|
+
if (field_count < 2)
|
|
346
|
+
return RC_INVALID_STATE;
|
|
347
|
+
if (strcmp(fields[0].name, "Success") != 0)
|
|
348
|
+
return RC_INVALID_STATE;
|
|
349
|
+
if (strcmp(fields[1].name, "Error") != 0)
|
|
350
|
+
return RC_INVALID_STATE;
|
|
351
|
+
#endif
|
|
352
|
+
|
|
353
|
+
response->error_message = NULL;
|
|
354
|
+
|
|
355
|
+
if (!server_response) {
|
|
356
|
+
response->succeeded = 0;
|
|
357
|
+
return RC_NO_RESPONSE;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (server_response->http_status_code == RC_API_SERVER_RESPONSE_CLIENT_ERROR ||
|
|
361
|
+
server_response->http_status_code == RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR) {
|
|
362
|
+
/* client provided error message is passed as the response body */
|
|
363
|
+
response->error_message = server_response->body;
|
|
364
|
+
response->succeeded = 0;
|
|
365
|
+
return RC_NO_RESPONSE;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (!server_response->body || !*server_response->body) {
|
|
369
|
+
/* expect valid HTTP status codes to have bodies that we can extract the message from,
|
|
370
|
+
* but provide some default messages in case they don't. */
|
|
371
|
+
switch (server_response->http_status_code) {
|
|
372
|
+
case 504: /* 504 Gateway Timeout */
|
|
373
|
+
case 522: /* 522 Connection Timed Out */
|
|
374
|
+
case 524: /* 524 A Timeout Occurred */
|
|
375
|
+
response->error_message = "Request has timed out.";
|
|
376
|
+
break;
|
|
377
|
+
|
|
378
|
+
case 521: /* 521 Web Server is Down */
|
|
379
|
+
case 523: /* 523 Origin is Unreachable */
|
|
380
|
+
response->error_message = "Could not connect to server.";
|
|
381
|
+
break;
|
|
382
|
+
|
|
383
|
+
default:
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
response->succeeded = 0;
|
|
388
|
+
return RC_NO_RESPONSE;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (*server_response->body != '{') {
|
|
392
|
+
result = rc_json_extract_html_error(response, server_response);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
rc_json_iterator_t iterator;
|
|
396
|
+
memset(&iterator, 0, sizeof(iterator));
|
|
397
|
+
iterator.json = server_response->body;
|
|
398
|
+
iterator.end = server_response->body + server_response->body_length;
|
|
399
|
+
result = rc_json_parse_object(&iterator, fields, field_count, NULL);
|
|
400
|
+
|
|
401
|
+
rc_json_get_optional_string(&response->error_message, response, &fields[1], "Error", NULL);
|
|
402
|
+
rc_json_get_optional_bool(&response->succeeded, &fields[0], "Success", 1);
|
|
403
|
+
|
|
404
|
+
/* Code will be the third field in the fields array, but may not always be present */
|
|
405
|
+
if (field_count > 2 && strcmp(fields[2].name, "Code") == 0) {
|
|
406
|
+
rc_json_get_optional_string(&response->error_code, response, &fields[2], "Code", NULL);
|
|
407
|
+
if (response->error_code != NULL)
|
|
408
|
+
result = rc_json_convert_error_code(response->error_code);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return result;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
static int rc_json_missing_field(rc_api_response_t* response, const rc_json_field_t* field) {
|
|
416
|
+
const char* not_found = " not found in response";
|
|
417
|
+
const size_t not_found_len = strlen(not_found);
|
|
418
|
+
const size_t field_len = strlen(field->name);
|
|
419
|
+
|
|
420
|
+
uint8_t* write = rc_buffer_reserve(&response->buffer, field_len + not_found_len + 1);
|
|
421
|
+
if (write) {
|
|
422
|
+
response->error_message = (char*)write;
|
|
423
|
+
memcpy(write, field->name, field_len);
|
|
424
|
+
write += field_len;
|
|
425
|
+
memcpy(write, not_found, not_found_len + 1);
|
|
426
|
+
write += not_found_len + 1;
|
|
427
|
+
rc_buffer_consume(&response->buffer, (uint8_t*)response->error_message, write);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
response->succeeded = 0;
|
|
431
|
+
return 0;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
int rc_json_get_required_object(rc_json_field_t* fields, size_t field_count, rc_api_response_t* response, rc_json_field_t* field, const char* field_name) {
|
|
435
|
+
rc_json_iterator_t iterator;
|
|
436
|
+
|
|
437
|
+
#ifndef NDEBUG
|
|
438
|
+
if (strcmp(field->name, field_name) != 0)
|
|
439
|
+
return 0;
|
|
440
|
+
#else
|
|
441
|
+
(void)field_name;
|
|
442
|
+
#endif
|
|
443
|
+
|
|
444
|
+
if (!field->value_start)
|
|
445
|
+
return rc_json_missing_field(response, field);
|
|
446
|
+
|
|
447
|
+
memset(&iterator, 0, sizeof(iterator));
|
|
448
|
+
iterator.json = field->value_start;
|
|
449
|
+
iterator.end = field->value_end;
|
|
450
|
+
return (rc_json_parse_object(&iterator, fields, field_count, &field->array_size) == RC_OK);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
static int rc_json_get_array_entry_value(rc_json_field_t* field, rc_json_iterator_t* iterator) {
|
|
454
|
+
rc_json_skip_whitespace(iterator);
|
|
455
|
+
|
|
456
|
+
if (iterator->json >= iterator->end)
|
|
457
|
+
return 0;
|
|
458
|
+
|
|
459
|
+
if (rc_json_parse_field(iterator, field) != RC_OK)
|
|
460
|
+
return 0;
|
|
461
|
+
|
|
462
|
+
rc_json_skip_whitespace(iterator);
|
|
463
|
+
|
|
464
|
+
if (!rc_json_match_char(iterator, ','))
|
|
465
|
+
rc_json_match_char(iterator, ']');
|
|
466
|
+
|
|
467
|
+
return 1;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
int rc_json_get_required_unum_array(uint32_t** entries, uint32_t* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) {
|
|
471
|
+
rc_json_iterator_t iterator;
|
|
472
|
+
rc_json_field_t array;
|
|
473
|
+
rc_json_field_t value;
|
|
474
|
+
uint32_t* entry;
|
|
475
|
+
|
|
476
|
+
memset(&array, 0, sizeof(array));
|
|
477
|
+
if (!rc_json_get_required_array(num_entries, &array, response, field, field_name))
|
|
478
|
+
return RC_MISSING_VALUE;
|
|
479
|
+
|
|
480
|
+
if (*num_entries) {
|
|
481
|
+
*entries = (uint32_t*)rc_buffer_alloc(&response->buffer, *num_entries * sizeof(uint32_t));
|
|
482
|
+
if (!*entries)
|
|
483
|
+
return RC_OUT_OF_MEMORY;
|
|
484
|
+
|
|
485
|
+
value.name = field_name;
|
|
486
|
+
|
|
487
|
+
memset(&iterator, 0, sizeof(iterator));
|
|
488
|
+
iterator.json = array.value_start;
|
|
489
|
+
iterator.end = array.value_end;
|
|
490
|
+
|
|
491
|
+
entry = *entries;
|
|
492
|
+
while (rc_json_get_array_entry_value(&value, &iterator)) {
|
|
493
|
+
if (!rc_json_get_unum(entry, &value, field_name))
|
|
494
|
+
return RC_MISSING_VALUE;
|
|
495
|
+
|
|
496
|
+
++entry;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
*entries = NULL;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return RC_OK;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
int rc_json_get_required_array(uint32_t* num_entries, rc_json_field_t* array_field, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) {
|
|
507
|
+
#ifndef NDEBUG
|
|
508
|
+
if (strcmp(field->name, field_name) != 0)
|
|
509
|
+
return 0;
|
|
510
|
+
#endif
|
|
511
|
+
|
|
512
|
+
if (!rc_json_get_optional_array(num_entries, array_field, field, field_name))
|
|
513
|
+
return rc_json_missing_field(response, field);
|
|
514
|
+
|
|
515
|
+
return 1;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
int rc_json_get_optional_array(uint32_t* num_entries, rc_json_field_t* array_field, const rc_json_field_t* field, const char* field_name) {
|
|
519
|
+
#ifndef NDEBUG
|
|
520
|
+
if (strcmp(field->name, field_name) != 0)
|
|
521
|
+
return 0;
|
|
522
|
+
#else
|
|
523
|
+
(void)field_name;
|
|
524
|
+
#endif
|
|
525
|
+
|
|
526
|
+
if (!field->value_start || *field->value_start != '[') {
|
|
527
|
+
*num_entries = 0;
|
|
528
|
+
return 0;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
memcpy(array_field, field, sizeof(*array_field));
|
|
532
|
+
++array_field->value_start; /* skip [ */
|
|
533
|
+
|
|
534
|
+
*num_entries = field->array_size;
|
|
535
|
+
return 1;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_iterator_t* iterator) {
|
|
539
|
+
rc_json_skip_whitespace(iterator);
|
|
540
|
+
|
|
541
|
+
if (iterator->json >= iterator->end)
|
|
542
|
+
return 0;
|
|
543
|
+
|
|
544
|
+
if (rc_json_parse_object(iterator, fields, field_count, NULL) != RC_OK)
|
|
545
|
+
return 0;
|
|
546
|
+
|
|
547
|
+
rc_json_skip_whitespace(iterator);
|
|
548
|
+
|
|
549
|
+
if (!rc_json_match_char(iterator, ','))
|
|
550
|
+
rc_json_match_char(iterator, ']');
|
|
551
|
+
|
|
552
|
+
return 1;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
static uint32_t rc_json_decode_hex4(const char* input) {
|
|
556
|
+
char hex[5];
|
|
557
|
+
|
|
558
|
+
memcpy(hex, input, 4);
|
|
559
|
+
hex[4] = '\0';
|
|
560
|
+
|
|
561
|
+
return (uint32_t)strtoul(hex, NULL, 16);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
static int rc_json_ucs32_to_utf8(uint8_t* dst, uint32_t ucs32_char) {
|
|
565
|
+
if (ucs32_char < 0x80) {
|
|
566
|
+
dst[0] = (ucs32_char & 0x7F);
|
|
567
|
+
return 1;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (ucs32_char < 0x0800) {
|
|
571
|
+
dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;
|
|
572
|
+
dst[0] = 0xC0 | (ucs32_char & 0x1F);
|
|
573
|
+
return 2;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (ucs32_char < 0x010000) {
|
|
577
|
+
dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;
|
|
578
|
+
dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;
|
|
579
|
+
dst[0] = 0xE0 | (ucs32_char & 0x0F);
|
|
580
|
+
return 3;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (ucs32_char < 0x200000) {
|
|
584
|
+
dst[3] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;
|
|
585
|
+
dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;
|
|
586
|
+
dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;
|
|
587
|
+
dst[0] = 0xF0 | (ucs32_char & 0x07);
|
|
588
|
+
return 4;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (ucs32_char < 0x04000000) {
|
|
592
|
+
dst[4] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;
|
|
593
|
+
dst[3] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;
|
|
594
|
+
dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;
|
|
595
|
+
dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;
|
|
596
|
+
dst[0] = 0xF8 | (ucs32_char & 0x03);
|
|
597
|
+
return 5;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
dst[5] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;
|
|
601
|
+
dst[4] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;
|
|
602
|
+
dst[3] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;
|
|
603
|
+
dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;
|
|
604
|
+
dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6;
|
|
605
|
+
dst[0] = 0xFC | (ucs32_char & 0x01);
|
|
606
|
+
return 6;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
int rc_json_get_string(const char** out, rc_buffer_t* buffer, const rc_json_field_t* field, const char* field_name) {
|
|
610
|
+
const char* src = field->value_start;
|
|
611
|
+
size_t len = field->value_end - field->value_start;
|
|
612
|
+
char* dst;
|
|
613
|
+
|
|
614
|
+
#ifndef NDEBUG
|
|
615
|
+
if (strcmp(field->name, field_name) != 0)
|
|
616
|
+
return 0;
|
|
617
|
+
#else
|
|
618
|
+
(void)field_name;
|
|
619
|
+
#endif
|
|
620
|
+
|
|
621
|
+
if (!src) {
|
|
622
|
+
*out = NULL;
|
|
623
|
+
return 0;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (len == 4 && memcmp(field->value_start, "null", 4) == 0) {
|
|
627
|
+
*out = NULL;
|
|
628
|
+
return 1;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (*src == '\"') {
|
|
632
|
+
++src;
|
|
633
|
+
|
|
634
|
+
if (*src == '\"') {
|
|
635
|
+
/* simple optimization for empty string - don't allocate space */
|
|
636
|
+
*out = "";
|
|
637
|
+
return 1;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
*out = dst = (char*)rc_buffer_reserve(buffer, len - 1); /* -2 for quotes, +1 for null terminator */
|
|
641
|
+
|
|
642
|
+
do {
|
|
643
|
+
if (*src == '\\') {
|
|
644
|
+
++src;
|
|
645
|
+
if (*src == 'n') {
|
|
646
|
+
/* newline */
|
|
647
|
+
++src;
|
|
648
|
+
*dst++ = '\n';
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (*src == 'r') {
|
|
653
|
+
/* carriage return */
|
|
654
|
+
++src;
|
|
655
|
+
*dst++ = '\r';
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (*src == 'u') {
|
|
660
|
+
/* unicode character */
|
|
661
|
+
uint32_t ucs32_char = rc_json_decode_hex4(src + 1);
|
|
662
|
+
src += 5;
|
|
663
|
+
|
|
664
|
+
if (ucs32_char >= 0xD800 && ucs32_char < 0xE000) {
|
|
665
|
+
/* surrogate lead - look for surrogate tail */
|
|
666
|
+
if (ucs32_char < 0xDC00 && src[0] == '\\' && src[1] == 'u') {
|
|
667
|
+
const uint32_t surrogate = rc_json_decode_hex4(src + 2);
|
|
668
|
+
src += 6;
|
|
669
|
+
|
|
670
|
+
if (surrogate >= 0xDC00 && surrogate < 0xE000) {
|
|
671
|
+
/* found a surrogate tail, merge them */
|
|
672
|
+
ucs32_char = (((ucs32_char - 0xD800) << 10) | (surrogate - 0xDC00)) + 0x10000;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (!(ucs32_char & 0xFFFF0000)) {
|
|
677
|
+
/* invalid surrogate pair, fallback to replacement char */
|
|
678
|
+
ucs32_char = 0xFFFD;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
dst += rc_json_ucs32_to_utf8((unsigned char*)dst, ucs32_char);
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (*src == 't') {
|
|
687
|
+
/* tab */
|
|
688
|
+
++src;
|
|
689
|
+
*dst++ = '\t';
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/* just an escaped character, fallthrough to normal copy */
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
*dst++ = *src++;
|
|
697
|
+
} while (*src != '\"');
|
|
698
|
+
|
|
699
|
+
} else {
|
|
700
|
+
*out = dst = (char*)rc_buffer_reserve(buffer, len + 1); /* +1 for null terminator */
|
|
701
|
+
memcpy(dst, src, len);
|
|
702
|
+
dst += len;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
*dst++ = '\0';
|
|
706
|
+
rc_buffer_consume(buffer, (uint8_t*)(*out), (uint8_t*)dst);
|
|
707
|
+
return 1;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
int rc_json_field_string_matches(const rc_json_field_t* field, const char* text) {
|
|
711
|
+
int is_quoted = 0;
|
|
712
|
+
const char* ptr = field->value_start;
|
|
713
|
+
if (!ptr)
|
|
714
|
+
return 0;
|
|
715
|
+
|
|
716
|
+
if (*ptr == '"') {
|
|
717
|
+
is_quoted = 1;
|
|
718
|
+
++ptr;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
while (ptr < field->value_end) {
|
|
722
|
+
if (*ptr != *text) {
|
|
723
|
+
if (*ptr != '\\') {
|
|
724
|
+
if (*ptr == '"' && is_quoted && (*text == '\0')) {
|
|
725
|
+
is_quoted = 0;
|
|
726
|
+
++ptr;
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return 0;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
++ptr;
|
|
734
|
+
switch (*ptr) {
|
|
735
|
+
case 'n':
|
|
736
|
+
if (*text != '\n')
|
|
737
|
+
return 0;
|
|
738
|
+
break;
|
|
739
|
+
case 'r':
|
|
740
|
+
if (*text != '\r')
|
|
741
|
+
return 0;
|
|
742
|
+
break;
|
|
743
|
+
case 't':
|
|
744
|
+
if (*text != '\t')
|
|
745
|
+
return 0;
|
|
746
|
+
break;
|
|
747
|
+
default:
|
|
748
|
+
if (*text != *ptr)
|
|
749
|
+
return 0;
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
++text;
|
|
755
|
+
++ptr;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return !is_quoted && (*text == '\0');
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
void rc_json_get_optional_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name, const char* default_value) {
|
|
762
|
+
if (!rc_json_get_string(out, &response->buffer, field, field_name))
|
|
763
|
+
*out = default_value;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
int rc_json_get_required_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) {
|
|
767
|
+
if (rc_json_get_string(out, &response->buffer, field, field_name))
|
|
768
|
+
return 1;
|
|
769
|
+
|
|
770
|
+
return rc_json_missing_field(response, field);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
int rc_json_get_num(int32_t* out, const rc_json_field_t* field, const char* field_name) {
|
|
774
|
+
const char* src = field->value_start;
|
|
775
|
+
int32_t value = 0;
|
|
776
|
+
int negative = 0;
|
|
777
|
+
|
|
778
|
+
#ifndef NDEBUG
|
|
779
|
+
if (strcmp(field->name, field_name) != 0)
|
|
780
|
+
return 0;
|
|
781
|
+
#else
|
|
782
|
+
(void)field_name;
|
|
783
|
+
#endif
|
|
784
|
+
|
|
785
|
+
if (!src) {
|
|
786
|
+
*out = 0;
|
|
787
|
+
return 0;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/* assert: string contains only numerals and an optional sign per rc_json_parse_field */
|
|
791
|
+
if (*src == '-') {
|
|
792
|
+
negative = 1;
|
|
793
|
+
++src;
|
|
794
|
+
} else if (*src == '+') {
|
|
795
|
+
++src;
|
|
796
|
+
} else if (*src < '0' || *src > '9') {
|
|
797
|
+
*out = 0;
|
|
798
|
+
return 0;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
while (src < field->value_end && *src != '.') {
|
|
802
|
+
value *= 10;
|
|
803
|
+
value += *src - '0';
|
|
804
|
+
++src;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (negative)
|
|
808
|
+
*out = -value;
|
|
809
|
+
else
|
|
810
|
+
*out = value;
|
|
811
|
+
|
|
812
|
+
return 1;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
void rc_json_get_optional_num(int32_t* out, const rc_json_field_t* field, const char* field_name, int default_value) {
|
|
816
|
+
if (!rc_json_get_num(out, field, field_name))
|
|
817
|
+
*out = default_value;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
int rc_json_get_required_num(int32_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) {
|
|
821
|
+
if (rc_json_get_num(out, field, field_name))
|
|
822
|
+
return 1;
|
|
823
|
+
|
|
824
|
+
return rc_json_missing_field(response, field);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
int rc_json_get_unum(uint32_t* out, const rc_json_field_t* field, const char* field_name) {
|
|
828
|
+
const char* src = field->value_start;
|
|
829
|
+
uint32_t value = 0;
|
|
830
|
+
|
|
831
|
+
#ifndef NDEBUG
|
|
832
|
+
if (strcmp(field->name, field_name) != 0)
|
|
833
|
+
return 0;
|
|
834
|
+
#else
|
|
835
|
+
(void)field_name;
|
|
836
|
+
#endif
|
|
837
|
+
|
|
838
|
+
if (!src) {
|
|
839
|
+
*out = 0;
|
|
840
|
+
return 0;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (*src < '0' || *src > '9') {
|
|
844
|
+
*out = 0;
|
|
845
|
+
return 0;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/* assert: string contains only numerals per rc_json_parse_field */
|
|
849
|
+
while (src < field->value_end && *src != '.') {
|
|
850
|
+
value *= 10;
|
|
851
|
+
value += *src - '0';
|
|
852
|
+
++src;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
*out = value;
|
|
856
|
+
return 1;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
void rc_json_get_optional_unum(uint32_t* out, const rc_json_field_t* field, const char* field_name, uint32_t default_value) {
|
|
860
|
+
if (!rc_json_get_unum(out, field, field_name))
|
|
861
|
+
*out = default_value;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
int rc_json_get_required_unum(uint32_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) {
|
|
865
|
+
if (rc_json_get_unum(out, field, field_name))
|
|
866
|
+
return 1;
|
|
867
|
+
|
|
868
|
+
return rc_json_missing_field(response, field);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
int rc_json_get_float(float* out, const rc_json_field_t* field, const char* field_name) {
|
|
872
|
+
int32_t whole, fraction, fraction_denominator;
|
|
873
|
+
const char* decimal = field->value_start;
|
|
874
|
+
|
|
875
|
+
if (!decimal) {
|
|
876
|
+
*out = 0.0f;
|
|
877
|
+
return 0;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (!rc_json_get_num(&whole, field, field_name))
|
|
881
|
+
return 0;
|
|
882
|
+
|
|
883
|
+
while (decimal < field->value_end && *decimal != '.')
|
|
884
|
+
++decimal;
|
|
885
|
+
|
|
886
|
+
fraction = 0;
|
|
887
|
+
fraction_denominator = 1;
|
|
888
|
+
if (decimal) {
|
|
889
|
+
++decimal;
|
|
890
|
+
while (decimal < field->value_end && *decimal >= '0' && *decimal <= '9') {
|
|
891
|
+
fraction *= 10;
|
|
892
|
+
fraction += *decimal - '0';
|
|
893
|
+
fraction_denominator *= 10;
|
|
894
|
+
++decimal;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if (whole < 0)
|
|
899
|
+
fraction = -fraction;
|
|
900
|
+
|
|
901
|
+
*out = (float)whole + ((float)fraction / (float)fraction_denominator);
|
|
902
|
+
return 1;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
void rc_json_get_optional_float(float* out, const rc_json_field_t* field, const char* field_name, float default_value) {
|
|
906
|
+
if (!rc_json_get_float(out, field, field_name))
|
|
907
|
+
*out = default_value;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
int rc_json_get_required_float(float* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) {
|
|
911
|
+
if (rc_json_get_float(out, field, field_name))
|
|
912
|
+
return 1;
|
|
913
|
+
|
|
914
|
+
return rc_json_missing_field(response, field);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
int rc_json_get_datetime(time_t* out, const rc_json_field_t* field, const char* field_name) {
|
|
918
|
+
struct tm tm;
|
|
919
|
+
|
|
920
|
+
#ifndef NDEBUG
|
|
921
|
+
if (strcmp(field->name, field_name) != 0)
|
|
922
|
+
return 0;
|
|
923
|
+
#else
|
|
924
|
+
(void)field_name;
|
|
925
|
+
#endif
|
|
926
|
+
|
|
927
|
+
if (*field->value_start == '\"') {
|
|
928
|
+
memset(&tm, 0, sizeof(tm));
|
|
929
|
+
if (sscanf_s(field->value_start + 1, "%d-%d-%d %d:%d:%d", /* DB format "2013-10-20 22:12:21" */
|
|
930
|
+
&tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6 ||
|
|
931
|
+
/* NOTE: relies on sscanf stopping when it sees a non-digit after the seconds. could be 'Z', '.', '+', or '-' */
|
|
932
|
+
sscanf_s(field->value_start + 1, "%d-%d-%dT%d:%d:%d", /* ISO format "2013-10-20T22:12:21.000000Z */
|
|
933
|
+
&tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6) {
|
|
934
|
+
tm.tm_mon--; /* 0-based */
|
|
935
|
+
tm.tm_year -= 1900; /* 1900 based */
|
|
936
|
+
|
|
937
|
+
/* mktime converts a struct tm to a time_t using the local timezone.
|
|
938
|
+
* the input string is UTC. since timegm is not universally cross-platform,
|
|
939
|
+
* figure out the offset between UTC and local time by applying the
|
|
940
|
+
* timezone conversion twice and manually removing the difference */
|
|
941
|
+
{
|
|
942
|
+
time_t local_timet = mktime(&tm);
|
|
943
|
+
time_t skewed_timet, tz_offset;
|
|
944
|
+
struct tm gmt_tm;
|
|
945
|
+
gmtime_s(&gmt_tm, &local_timet);
|
|
946
|
+
skewed_timet = mktime(&gmt_tm); /* applies local time adjustment second time */
|
|
947
|
+
tz_offset = skewed_timet - local_timet;
|
|
948
|
+
*out = local_timet - tz_offset;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return 1;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
*out = 0;
|
|
956
|
+
return 0;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
int rc_json_get_required_datetime(time_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) {
|
|
960
|
+
if (rc_json_get_datetime(out, field, field_name))
|
|
961
|
+
return 1;
|
|
962
|
+
|
|
963
|
+
return rc_json_missing_field(response, field);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
int rc_json_get_bool(int* out, const rc_json_field_t* field, const char* field_name) {
|
|
967
|
+
const char* src = field->value_start;
|
|
968
|
+
|
|
969
|
+
#ifndef NDEBUG
|
|
970
|
+
if (strcmp(field->name, field_name) != 0)
|
|
971
|
+
return 0;
|
|
972
|
+
#else
|
|
973
|
+
(void)field_name;
|
|
974
|
+
#endif
|
|
975
|
+
|
|
976
|
+
if (src) {
|
|
977
|
+
const size_t len = field->value_end - field->value_start;
|
|
978
|
+
if (len == 4 && strncasecmp(src, "true", 4) == 0) {
|
|
979
|
+
*out = 1;
|
|
980
|
+
return 1;
|
|
981
|
+
} else if (len == 5 && strncasecmp(src, "false", 5) == 0) {
|
|
982
|
+
*out = 0;
|
|
983
|
+
return 1;
|
|
984
|
+
} else if (len == 1) {
|
|
985
|
+
*out = (*src != '0');
|
|
986
|
+
return 1;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
*out = 0;
|
|
991
|
+
return 0;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
void rc_json_get_optional_bool(int* out, const rc_json_field_t* field, const char* field_name, int default_value) {
|
|
995
|
+
if (!rc_json_get_bool(out, field, field_name))
|
|
996
|
+
*out = default_value;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
int rc_json_get_required_bool(int* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) {
|
|
1000
|
+
if (rc_json_get_bool(out, field, field_name))
|
|
1001
|
+
return 1;
|
|
1002
|
+
|
|
1003
|
+
return rc_json_missing_field(response, field);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
void rc_json_extract_filename(rc_json_field_t* field) {
|
|
1007
|
+
if (field->value_end) {
|
|
1008
|
+
const char* str = field->value_end;
|
|
1009
|
+
|
|
1010
|
+
/* remove the extension */
|
|
1011
|
+
while (str > field->value_start && str[-1] != '/') {
|
|
1012
|
+
--str;
|
|
1013
|
+
if (*str == '.') {
|
|
1014
|
+
field->value_end = str;
|
|
1015
|
+
break;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/* find the path separator */
|
|
1020
|
+
while (str > field->value_start && str[-1] != '/')
|
|
1021
|
+
--str;
|
|
1022
|
+
|
|
1023
|
+
field->value_start = str;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/* --- rc_api_request --- */
|
|
1028
|
+
|
|
1029
|
+
void rc_api_destroy_request(rc_api_request_t* request)
|
|
1030
|
+
{
|
|
1031
|
+
rc_buffer_destroy(&request->buffer);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/* --- rc_url_builder --- */
|
|
1035
|
+
|
|
1036
|
+
void rc_url_builder_init(rc_api_url_builder_t* builder, rc_buffer_t* buffer, size_t estimated_size) {
|
|
1037
|
+
rc_buffer_chunk_t* used_buffer;
|
|
1038
|
+
|
|
1039
|
+
memset(builder, 0, sizeof(*builder));
|
|
1040
|
+
builder->buffer = buffer;
|
|
1041
|
+
builder->write = builder->start = (char*)rc_buffer_reserve(buffer, estimated_size);
|
|
1042
|
+
|
|
1043
|
+
used_buffer = &buffer->chunk;
|
|
1044
|
+
while (used_buffer && used_buffer->write != (uint8_t*)builder->write)
|
|
1045
|
+
used_buffer = used_buffer->next;
|
|
1046
|
+
|
|
1047
|
+
builder->end = (used_buffer) ? (char*)used_buffer->end : builder->start + estimated_size;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
const char* rc_url_builder_finalize(rc_api_url_builder_t* builder) {
|
|
1051
|
+
rc_url_builder_append(builder, "", 1);
|
|
1052
|
+
|
|
1053
|
+
if (builder->result != RC_OK)
|
|
1054
|
+
return NULL;
|
|
1055
|
+
|
|
1056
|
+
rc_buffer_consume(builder->buffer, (uint8_t*)builder->start, (uint8_t*)builder->write);
|
|
1057
|
+
return builder->start;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
static int rc_url_builder_reserve(rc_api_url_builder_t* builder, size_t amount) {
|
|
1061
|
+
if (builder->result == RC_OK) {
|
|
1062
|
+
size_t remaining = builder->end - builder->write;
|
|
1063
|
+
if (remaining < amount) {
|
|
1064
|
+
const size_t used = builder->write - builder->start;
|
|
1065
|
+
const size_t current_size = builder->end - builder->start;
|
|
1066
|
+
const size_t buffer_prefix_size = sizeof(rc_buffer_chunk_t);
|
|
1067
|
+
char* new_start;
|
|
1068
|
+
size_t new_size = (current_size < 256) ? 256 : current_size * 2;
|
|
1069
|
+
do {
|
|
1070
|
+
remaining = new_size - used;
|
|
1071
|
+
if (remaining >= amount)
|
|
1072
|
+
break;
|
|
1073
|
+
|
|
1074
|
+
new_size *= 2;
|
|
1075
|
+
} while (1);
|
|
1076
|
+
|
|
1077
|
+
/* rc_buffer_reserve will align to 256 bytes after including the buffer prefix. attempt to account for that */
|
|
1078
|
+
if ((remaining - amount) > buffer_prefix_size)
|
|
1079
|
+
new_size -= buffer_prefix_size;
|
|
1080
|
+
|
|
1081
|
+
new_start = (char*)rc_buffer_reserve(builder->buffer, new_size);
|
|
1082
|
+
if (!new_start) {
|
|
1083
|
+
builder->result = RC_OUT_OF_MEMORY;
|
|
1084
|
+
return RC_OUT_OF_MEMORY;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (new_start != builder->start) {
|
|
1088
|
+
memcpy(new_start, builder->start, used);
|
|
1089
|
+
builder->start = new_start;
|
|
1090
|
+
builder->write = new_start + used;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
builder->end = builder->start + new_size;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
return builder->result;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
void rc_url_builder_append_encoded_str(rc_api_url_builder_t* builder, const char* str) {
|
|
1101
|
+
static const char hex[] = "0123456789abcdef";
|
|
1102
|
+
const char* start = str;
|
|
1103
|
+
size_t len = 0;
|
|
1104
|
+
for (;;) {
|
|
1105
|
+
const char c = *str++;
|
|
1106
|
+
switch (c) {
|
|
1107
|
+
case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j':
|
|
1108
|
+
case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't':
|
|
1109
|
+
case 'u': case 'v': case 'w': case 'x': case 'y': case 'z':
|
|
1110
|
+
case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': case 'H': case 'I': case 'J':
|
|
1111
|
+
case 'K': case 'L': case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T':
|
|
1112
|
+
case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z':
|
|
1113
|
+
case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9':
|
|
1114
|
+
case '-': case '_': case '.': case '~':
|
|
1115
|
+
len++;
|
|
1116
|
+
continue;
|
|
1117
|
+
|
|
1118
|
+
case '\0':
|
|
1119
|
+
if (len)
|
|
1120
|
+
rc_url_builder_append(builder, start, len);
|
|
1121
|
+
|
|
1122
|
+
return;
|
|
1123
|
+
|
|
1124
|
+
default:
|
|
1125
|
+
if (rc_url_builder_reserve(builder, len + 3) != RC_OK)
|
|
1126
|
+
return;
|
|
1127
|
+
|
|
1128
|
+
if (len) {
|
|
1129
|
+
memcpy(builder->write, start, len);
|
|
1130
|
+
builder->write += len;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
if (c == ' ') {
|
|
1134
|
+
*builder->write++ = '+';
|
|
1135
|
+
} else {
|
|
1136
|
+
*builder->write++ = '%';
|
|
1137
|
+
*builder->write++ = hex[((unsigned char)c) >> 4];
|
|
1138
|
+
*builder->write++ = hex[c & 0x0F];
|
|
1139
|
+
}
|
|
1140
|
+
break;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
start = str;
|
|
1144
|
+
len = 0;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
void rc_url_builder_append(rc_api_url_builder_t* builder, const char* data, size_t len) {
|
|
1149
|
+
if (rc_url_builder_reserve(builder, len) == RC_OK) {
|
|
1150
|
+
memcpy(builder->write, data, len);
|
|
1151
|
+
builder->write += len;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
static int rc_url_builder_append_param_equals(rc_api_url_builder_t* builder, const char* param) {
|
|
1156
|
+
size_t param_len = strlen(param);
|
|
1157
|
+
|
|
1158
|
+
if (rc_url_builder_reserve(builder, param_len + 2) == RC_OK) {
|
|
1159
|
+
if (builder->write > builder->start) {
|
|
1160
|
+
if (builder->write[-1] != '?')
|
|
1161
|
+
*builder->write++ = '&';
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
memcpy(builder->write, param, param_len);
|
|
1165
|
+
builder->write += param_len;
|
|
1166
|
+
*builder->write++ = '=';
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
return builder->result;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
void rc_url_builder_append_unum_param(rc_api_url_builder_t* builder, const char* param, uint32_t value) {
|
|
1173
|
+
if (rc_url_builder_append_param_equals(builder, param) == RC_OK) {
|
|
1174
|
+
char num[16];
|
|
1175
|
+
int chars = snprintf(num, sizeof(num), "%u", value);
|
|
1176
|
+
rc_url_builder_append(builder, num, chars);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
void rc_url_builder_append_num_param(rc_api_url_builder_t* builder, const char* param, int32_t value) {
|
|
1181
|
+
if (rc_url_builder_append_param_equals(builder, param) == RC_OK) {
|
|
1182
|
+
char num[16];
|
|
1183
|
+
int chars = snprintf(num, sizeof(num), "%d", value);
|
|
1184
|
+
rc_url_builder_append(builder, num, chars);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
void rc_url_builder_append_str_param(rc_api_url_builder_t* builder, const char* param, const char* value) {
|
|
1189
|
+
rc_url_builder_append_param_equals(builder, param);
|
|
1190
|
+
rc_url_builder_append_encoded_str(builder, value);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
void rc_api_url_build_dorequest_url(rc_api_request_t* request, const rc_api_host_t* host) {
|
|
1194
|
+
#define DOREQUEST_ENDPOINT "/dorequest.php"
|
|
1195
|
+
rc_buffer_init(&request->buffer);
|
|
1196
|
+
|
|
1197
|
+
if (!host || !host->host) {
|
|
1198
|
+
request->url = RETROACHIEVEMENTS_HOST DOREQUEST_ENDPOINT;
|
|
1199
|
+
}
|
|
1200
|
+
else {
|
|
1201
|
+
const size_t endpoint_len = sizeof(DOREQUEST_ENDPOINT);
|
|
1202
|
+
const size_t host_len = strlen(host->host);
|
|
1203
|
+
const size_t protocol_len = (strstr(host->host, "://")) ? 0 : 7;
|
|
1204
|
+
const size_t url_len = protocol_len + host_len + endpoint_len;
|
|
1205
|
+
uint8_t* url = rc_buffer_reserve(&request->buffer, url_len);
|
|
1206
|
+
|
|
1207
|
+
if (protocol_len)
|
|
1208
|
+
memcpy(url, "http://", protocol_len);
|
|
1209
|
+
|
|
1210
|
+
memcpy(url + protocol_len, host->host, host_len);
|
|
1211
|
+
memcpy(url + protocol_len + host_len, DOREQUEST_ENDPOINT, endpoint_len);
|
|
1212
|
+
rc_buffer_consume(&request->buffer, url, url + url_len);
|
|
1213
|
+
|
|
1214
|
+
request->url = (char*)url;
|
|
1215
|
+
}
|
|
1216
|
+
#undef DOREQUEST_ENDPOINT
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
int rc_api_url_build_dorequest(rc_api_url_builder_t* builder, const char* api, const char* username, const char* api_token) {
|
|
1220
|
+
if (!username || !*username || !api_token || !*api_token) {
|
|
1221
|
+
builder->result = RC_INVALID_STATE;
|
|
1222
|
+
return 0;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
rc_url_builder_append_str_param(builder, "r", api);
|
|
1226
|
+
rc_url_builder_append_str_param(builder, "u", username);
|
|
1227
|
+
rc_url_builder_append_str_param(builder, "t", api_token);
|
|
1228
|
+
|
|
1229
|
+
return (builder->result == RC_OK);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/* --- Set Host --- */
|
|
1233
|
+
|
|
1234
|
+
static void rc_api_update_host(const char** host, const char* hostname) {
|
|
1235
|
+
if (*host != NULL)
|
|
1236
|
+
free((void*)*host);
|
|
1237
|
+
|
|
1238
|
+
if (hostname != NULL) {
|
|
1239
|
+
if (strstr(hostname, "://")) {
|
|
1240
|
+
*host = strdup(hostname);
|
|
1241
|
+
}
|
|
1242
|
+
else {
|
|
1243
|
+
const size_t hostname_len = strlen(hostname);
|
|
1244
|
+
if (hostname_len == 0) {
|
|
1245
|
+
*host = NULL;
|
|
1246
|
+
}
|
|
1247
|
+
else {
|
|
1248
|
+
char* newhost = (char*)malloc(hostname_len + 7 + 1);
|
|
1249
|
+
if (newhost) {
|
|
1250
|
+
memcpy(newhost, "http://", 7);
|
|
1251
|
+
memcpy(&newhost[7], hostname, hostname_len + 1);
|
|
1252
|
+
*host = newhost;
|
|
1253
|
+
}
|
|
1254
|
+
else {
|
|
1255
|
+
*host = NULL;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
else {
|
|
1261
|
+
*host = NULL;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
const char* rc_api_default_host(void) {
|
|
1266
|
+
return RETROACHIEVEMENTS_HOST;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
void rc_api_set_host(const char* hostname) {
|
|
1270
|
+
if (hostname && strcmp(hostname, RETROACHIEVEMENTS_HOST) == 0)
|
|
1271
|
+
hostname = NULL;
|
|
1272
|
+
|
|
1273
|
+
rc_api_update_host(&g_host.host, hostname);
|
|
1274
|
+
|
|
1275
|
+
if (!hostname) {
|
|
1276
|
+
/* also clear out the image hostname */
|
|
1277
|
+
rc_api_set_image_host(NULL);
|
|
1278
|
+
}
|
|
1279
|
+
else if (strcmp(hostname, RETROACHIEVEMENTS_HOST_NONSSL) == 0) {
|
|
1280
|
+
/* if just pointing at the non-HTTPS host, explicitly use the default image host
|
|
1281
|
+
* so it doesn't try to use the web host directly */
|
|
1282
|
+
rc_api_set_image_host(RETROACHIEVEMENTS_IMAGE_HOST_NONSSL);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
void rc_api_set_image_host(const char* hostname) {
|
|
1287
|
+
rc_api_update_host(&g_host.media_host, hostname);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
/* --- Fetch Image --- */
|
|
1291
|
+
|
|
1292
|
+
int rc_api_init_fetch_image_request(rc_api_request_t* request, const rc_api_fetch_image_request_t* api_params) {
|
|
1293
|
+
return rc_api_init_fetch_image_request_hosted(request, api_params, &g_host);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
int rc_api_init_fetch_image_request_hosted(rc_api_request_t* request, const rc_api_fetch_image_request_t* api_params, const rc_api_host_t* host) {
|
|
1297
|
+
rc_api_url_builder_t builder;
|
|
1298
|
+
|
|
1299
|
+
rc_buffer_init(&request->buffer);
|
|
1300
|
+
rc_url_builder_init(&builder, &request->buffer, 64);
|
|
1301
|
+
|
|
1302
|
+
if (host && host->media_host) {
|
|
1303
|
+
/* custom media host provided */
|
|
1304
|
+
if (!strstr(host->host, "://"))
|
|
1305
|
+
rc_url_builder_append(&builder, "http://", 7);
|
|
1306
|
+
rc_url_builder_append(&builder, host->media_host, strlen(host->media_host));
|
|
1307
|
+
}
|
|
1308
|
+
else if (host && host->host) {
|
|
1309
|
+
if (strcmp(host->host, RETROACHIEVEMENTS_HOST_NONSSL) == 0) {
|
|
1310
|
+
/* if host specifically set to non-ssl host, and no media host provided, use non-ssl media host */
|
|
1311
|
+
rc_url_builder_append(&builder, RETROACHIEVEMENTS_IMAGE_HOST_NONSSL, sizeof(RETROACHIEVEMENTS_IMAGE_HOST_NONSSL) - 1);
|
|
1312
|
+
}
|
|
1313
|
+
else if (strcmp(host->host, RETROACHIEVEMENTS_HOST) == 0) {
|
|
1314
|
+
/* if host specifically set to ssl host, and no media host provided, use media host */
|
|
1315
|
+
rc_url_builder_append(&builder, RETROACHIEVEMENTS_IMAGE_HOST, sizeof(RETROACHIEVEMENTS_IMAGE_HOST) - 1);
|
|
1316
|
+
}
|
|
1317
|
+
else {
|
|
1318
|
+
/* custom host and no media host provided. assume custom host is also media host */
|
|
1319
|
+
if (!strstr(host->host, "://"))
|
|
1320
|
+
rc_url_builder_append(&builder, "http://", 7);
|
|
1321
|
+
rc_url_builder_append(&builder, host->host, strlen(host->host));
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
else {
|
|
1325
|
+
/* no custom host provided */
|
|
1326
|
+
rc_url_builder_append(&builder, RETROACHIEVEMENTS_IMAGE_HOST, sizeof(RETROACHIEVEMENTS_IMAGE_HOST) - 1);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
switch (api_params->image_type)
|
|
1330
|
+
{
|
|
1331
|
+
case RC_IMAGE_TYPE_GAME:
|
|
1332
|
+
rc_url_builder_append(&builder, "/Images/", 8);
|
|
1333
|
+
rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name));
|
|
1334
|
+
rc_url_builder_append(&builder, ".png", 4);
|
|
1335
|
+
break;
|
|
1336
|
+
|
|
1337
|
+
case RC_IMAGE_TYPE_ACHIEVEMENT:
|
|
1338
|
+
rc_url_builder_append(&builder, "/Badge/", 7);
|
|
1339
|
+
rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name));
|
|
1340
|
+
rc_url_builder_append(&builder, ".png", 4);
|
|
1341
|
+
break;
|
|
1342
|
+
|
|
1343
|
+
case RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED:
|
|
1344
|
+
rc_url_builder_append(&builder, "/Badge/", 7);
|
|
1345
|
+
rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name));
|
|
1346
|
+
rc_url_builder_append(&builder, "_lock.png", 9);
|
|
1347
|
+
break;
|
|
1348
|
+
|
|
1349
|
+
case RC_IMAGE_TYPE_USER:
|
|
1350
|
+
rc_url_builder_append(&builder, "/UserPic/", 9);
|
|
1351
|
+
rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name));
|
|
1352
|
+
rc_url_builder_append(&builder, ".png", 4);
|
|
1353
|
+
break;
|
|
1354
|
+
|
|
1355
|
+
default:
|
|
1356
|
+
return RC_INVALID_STATE;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
request->url = rc_url_builder_finalize(&builder);
|
|
1360
|
+
request->post_data = NULL;
|
|
1361
|
+
|
|
1362
|
+
return builder.result;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
const char* rc_api_build_avatar_url(rc_buffer_t* buffer, uint32_t image_type, const char* image_name) {
|
|
1366
|
+
rc_api_fetch_image_request_t image_request;
|
|
1367
|
+
rc_api_request_t request;
|
|
1368
|
+
int result;
|
|
1369
|
+
|
|
1370
|
+
memset(&image_request, 0, sizeof(image_request));
|
|
1371
|
+
image_request.image_type = image_type;
|
|
1372
|
+
image_request.image_name = image_name;
|
|
1373
|
+
|
|
1374
|
+
result = rc_api_init_fetch_image_request(&request, &image_request);
|
|
1375
|
+
if (result == RC_OK)
|
|
1376
|
+
return rc_buffer_strcpy(buffer, request.url);
|
|
1377
|
+
|
|
1378
|
+
return NULL;
|
|
1379
|
+
}
|