gemba 0.1.0 → 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.
Files changed (285) hide show
  1. checksums.yaml +4 -4
  2. data/THIRD_PARTY_NOTICES +37 -2
  3. data/assets/placeholder_boxart.png +0 -0
  4. data/bin/gemba +2 -2
  5. data/ext/gemba/extconf.rb +24 -1
  6. data/ext/gemba/gemba_ext.c +436 -2
  7. data/ext/gemba/gemba_ext.h +2 -0
  8. data/gemba.gemspec +5 -3
  9. data/lib/gemba/achievements/achievement.rb +23 -0
  10. data/lib/gemba/achievements/backend.rb +186 -0
  11. data/lib/gemba/achievements/cache.rb +70 -0
  12. data/lib/gemba/achievements/credentials_presenter.rb +142 -0
  13. data/lib/gemba/achievements/fake_backend.rb +205 -0
  14. data/lib/gemba/achievements/null_backend.rb +11 -0
  15. data/lib/gemba/achievements/offline_backend.rb +168 -0
  16. data/lib/gemba/achievements/retro_achievements/backend.rb +453 -0
  17. data/lib/gemba/achievements/retro_achievements/cli_sync_requester.rb +64 -0
  18. data/lib/gemba/achievements/retro_achievements/ping_worker.rb +27 -0
  19. data/lib/gemba/achievements.rb +19 -0
  20. data/lib/gemba/achievements_window.rb +556 -0
  21. data/lib/gemba/app_controller.rb +1015 -0
  22. data/lib/gemba/bios.rb +54 -0
  23. data/lib/gemba/boxart_fetcher/libretro_backend.rb +39 -0
  24. data/lib/gemba/boxart_fetcher/null_backend.rb +12 -0
  25. data/lib/gemba/boxart_fetcher.rb +79 -0
  26. data/lib/gemba/bus_emitter.rb +13 -0
  27. data/lib/gemba/child_window.rb +24 -1
  28. data/lib/gemba/cli/commands/config_cmd.rb +83 -0
  29. data/lib/gemba/cli/commands/decode.rb +154 -0
  30. data/lib/gemba/cli/commands/patch.rb +78 -0
  31. data/lib/gemba/cli/commands/play.rb +78 -0
  32. data/lib/gemba/cli/commands/record.rb +114 -0
  33. data/lib/gemba/cli/commands/replay.rb +161 -0
  34. data/lib/gemba/cli/commands/retro_achievements.rb +213 -0
  35. data/lib/gemba/cli/commands/version.rb +22 -0
  36. data/lib/gemba/cli.rb +52 -364
  37. data/lib/gemba/config.rb +135 -1
  38. data/lib/gemba/data/gb_games.json +1 -0
  39. data/lib/gemba/data/gb_md5.json +1 -0
  40. data/lib/gemba/data/gba_games.json +1 -0
  41. data/lib/gemba/data/gba_md5.json +1 -0
  42. data/lib/gemba/data/gbc_games.json +1 -0
  43. data/lib/gemba/data/gbc_md5.json +1 -0
  44. data/lib/gemba/emulator_frame.rb +1060 -0
  45. data/lib/gemba/event_bus.rb +48 -0
  46. data/lib/gemba/frame_stack.rb +60 -0
  47. data/lib/gemba/game_index.rb +84 -0
  48. data/lib/gemba/game_picker_frame.rb +268 -0
  49. data/lib/gemba/gamepad_map.rb +103 -0
  50. data/lib/gemba/headless.rb +6 -5
  51. data/lib/gemba/headless_player.rb +33 -3
  52. data/lib/gemba/help_window.rb +61 -0
  53. data/lib/gemba/hotkey_map.rb +3 -1
  54. data/lib/gemba/input_recorder.rb +107 -0
  55. data/lib/gemba/input_replayer.rb +119 -0
  56. data/lib/gemba/keyboard_map.rb +90 -0
  57. data/lib/gemba/locales/en.yml +97 -5
  58. data/lib/gemba/locales/ja.yml +97 -5
  59. data/lib/gemba/main_window.rb +56 -0
  60. data/lib/gemba/modal_stack.rb +81 -0
  61. data/lib/gemba/patcher_window.rb +223 -0
  62. data/lib/gemba/platform/gb.rb +21 -0
  63. data/lib/gemba/platform/gba.rb +21 -0
  64. data/lib/gemba/platform/gbc.rb +23 -0
  65. data/lib/gemba/platform.rb +20 -0
  66. data/lib/gemba/platform_open.rb +19 -0
  67. data/lib/gemba/recorder.rb +4 -3
  68. data/lib/gemba/replay_player.rb +691 -0
  69. data/lib/gemba/rom_info.rb +57 -0
  70. data/lib/gemba/rom_info_window.rb +16 -3
  71. data/lib/gemba/rom_library.rb +106 -0
  72. data/lib/gemba/rom_overrides.rb +47 -0
  73. data/lib/gemba/rom_patcher/bps.rb +161 -0
  74. data/lib/gemba/rom_patcher/ips.rb +101 -0
  75. data/lib/gemba/rom_patcher/ups.rb +118 -0
  76. data/lib/gemba/rom_patcher.rb +109 -0
  77. data/lib/gemba/{rom_loader.rb → rom_resolver.rb} +7 -6
  78. data/lib/gemba/runtime.rb +59 -26
  79. data/lib/gemba/save_state_manager.rb +4 -7
  80. data/lib/gemba/save_state_picker.rb +17 -4
  81. data/lib/gemba/session_logger.rb +64 -0
  82. data/lib/gemba/settings/audio_tab.rb +77 -0
  83. data/lib/gemba/settings/gamepad_tab.rb +351 -0
  84. data/lib/gemba/settings/hotkeys_tab.rb +259 -0
  85. data/lib/gemba/settings/paths.rb +11 -0
  86. data/lib/gemba/settings/recording_tab.rb +83 -0
  87. data/lib/gemba/settings/save_states_tab.rb +91 -0
  88. data/lib/gemba/settings/system_tab.rb +362 -0
  89. data/lib/gemba/settings/video_tab.rb +318 -0
  90. data/lib/gemba/settings_window.rb +162 -1036
  91. data/lib/gemba/version.rb +1 -1
  92. data/lib/gemba/virtual_keyboard.rb +19 -0
  93. data/lib/gemba.rb +2 -12
  94. data/test/achievements_window/test_bulk_sync.rb +218 -0
  95. data/test/achievements_window/test_bus_events.rb +125 -0
  96. data/test/achievements_window/test_close_confirmation.rb +201 -0
  97. data/test/achievements_window/test_initial_state.rb +164 -0
  98. data/test/achievements_window/test_sorting.rb +227 -0
  99. data/test/achievements_window/test_tree_rendering.rb +133 -0
  100. data/test/fixtures/fake_bios.bin +0 -0
  101. data/test/fixtures/pong.gba +0 -0
  102. data/test/fixtures/test.gb +0 -0
  103. data/test/fixtures/test.gbc +0 -0
  104. data/test/fixtures/test_quicksave.ss +0 -0
  105. data/test/screenshots/no_focus.png +0 -0
  106. data/test/shared/teek_test_worker.rb +17 -1
  107. data/test/shared/tk_test_helper.rb +91 -4
  108. data/test/support/achievements_window_helpers.rb +18 -0
  109. data/test/support/fake_core.rb +25 -0
  110. data/test/support/fake_ra_runtime.rb +74 -0
  111. data/test/support/fake_requester.rb +68 -0
  112. data/test/support/player_helpers.rb +20 -5
  113. data/test/test_achievement.rb +32 -0
  114. data/test/{test_player.rb → test_app_controller.rb} +353 -85
  115. data/test/test_bios.rb +123 -0
  116. data/test/test_boxart_fetcher.rb +150 -0
  117. data/test/test_cli.rb +17 -265
  118. data/test/test_cli_config.rb +64 -0
  119. data/test/test_cli_decode.rb +97 -0
  120. data/test/test_cli_patch.rb +58 -0
  121. data/test/test_cli_play.rb +213 -0
  122. data/test/test_cli_ra.rb +175 -0
  123. data/test/test_cli_record.rb +69 -0
  124. data/test/test_cli_replay.rb +72 -0
  125. data/test/test_cli_sync_requester.rb +152 -0
  126. data/test/test_cli_version.rb +27 -0
  127. data/test/test_config.rb +2 -3
  128. data/test/test_config_ra.rb +69 -0
  129. data/test/test_core.rb +62 -1
  130. data/test/test_credentials_presenter.rb +192 -0
  131. data/test/test_event_bus.rb +100 -0
  132. data/test/test_fake_backend_achievements.rb +130 -0
  133. data/test/test_fake_backend_auth.rb +68 -0
  134. data/test/test_game_index.rb +77 -0
  135. data/test/test_game_picker_frame.rb +310 -0
  136. data/test/test_gamepad_map.rb +1 -3
  137. data/test/test_headless_player.rb +17 -3
  138. data/test/test_help_window.rb +82 -0
  139. data/test/test_hotkey_map.rb +22 -1
  140. data/test/test_input_recorder.rb +179 -0
  141. data/test/test_input_replay_determinism.rb +113 -0
  142. data/test/test_input_replayer.rb +162 -0
  143. data/test/test_keyboard_map.rb +1 -3
  144. data/test/test_libretro_backend.rb +41 -0
  145. data/test/test_locale.rb +1 -1
  146. data/test/test_logging.rb +123 -0
  147. data/test/test_null_backend.rb +42 -0
  148. data/test/test_offline_backend.rb +116 -0
  149. data/test/test_overlay_renderer.rb +1 -1
  150. data/test/test_platform.rb +149 -0
  151. data/test/test_ra_backend.rb +313 -0
  152. data/test/test_ra_backend_unlock_gate.rb +56 -0
  153. data/test/test_recorder.rb +0 -3
  154. data/test/test_replay_player.rb +316 -0
  155. data/test/test_rom_info.rb +149 -0
  156. data/test/test_rom_overrides.rb +86 -0
  157. data/test/test_rom_patcher.rb +382 -0
  158. data/test/{test_rom_loader.rb → test_rom_resolver.rb} +25 -26
  159. data/test/test_save_state_manager.rb +2 -4
  160. data/test/test_settings_audio.rb +107 -0
  161. data/test/test_settings_hotkeys.rb +83 -66
  162. data/test/test_settings_recording.rb +49 -0
  163. data/test/test_settings_save_states.rb +97 -0
  164. data/test/test_settings_system.rb +133 -0
  165. data/test/test_settings_video.rb +450 -0
  166. data/test/test_settings_window.rb +76 -507
  167. data/test/test_tip_service.rb +6 -6
  168. data/test/test_toast_overlay.rb +1 -1
  169. data/test/test_virtual_events.rb +156 -0
  170. data/test/test_virtual_keyboard.rb +1 -1
  171. data/vendor/rcheevos/CHANGELOG.md +495 -0
  172. data/vendor/rcheevos/LICENSE +21 -0
  173. data/vendor/rcheevos/Package.swift +33 -0
  174. data/vendor/rcheevos/README.md +67 -0
  175. data/vendor/rcheevos/include/module.modulemap +70 -0
  176. data/vendor/rcheevos/include/rc_api_editor.h +296 -0
  177. data/vendor/rcheevos/include/rc_api_info.h +280 -0
  178. data/vendor/rcheevos/include/rc_api_request.h +77 -0
  179. data/vendor/rcheevos/include/rc_api_runtime.h +417 -0
  180. data/vendor/rcheevos/include/rc_api_user.h +262 -0
  181. data/vendor/rcheevos/include/rc_client.h +877 -0
  182. data/vendor/rcheevos/include/rc_client_raintegration.h +101 -0
  183. data/vendor/rcheevos/include/rc_consoles.h +138 -0
  184. data/vendor/rcheevos/include/rc_error.h +59 -0
  185. data/vendor/rcheevos/include/rc_export.h +100 -0
  186. data/vendor/rcheevos/include/rc_hash.h +200 -0
  187. data/vendor/rcheevos/include/rc_runtime.h +148 -0
  188. data/vendor/rcheevos/include/rc_runtime_types.h +452 -0
  189. data/vendor/rcheevos/include/rc_util.h +51 -0
  190. data/vendor/rcheevos/include/rcheevos.h +8 -0
  191. data/vendor/rcheevos/src/rapi/rc_api_common.c +1379 -0
  192. data/vendor/rcheevos/src/rapi/rc_api_common.h +88 -0
  193. data/vendor/rcheevos/src/rapi/rc_api_editor.c +625 -0
  194. data/vendor/rcheevos/src/rapi/rc_api_info.c +587 -0
  195. data/vendor/rcheevos/src/rapi/rc_api_runtime.c +901 -0
  196. data/vendor/rcheevos/src/rapi/rc_api_user.c +483 -0
  197. data/vendor/rcheevos/src/rc_client.c +6941 -0
  198. data/vendor/rcheevos/src/rc_client_external.c +281 -0
  199. data/vendor/rcheevos/src/rc_client_external.h +177 -0
  200. data/vendor/rcheevos/src/rc_client_external_versions.h +171 -0
  201. data/vendor/rcheevos/src/rc_client_internal.h +409 -0
  202. data/vendor/rcheevos/src/rc_client_raintegration.c +566 -0
  203. data/vendor/rcheevos/src/rc_client_raintegration_internal.h +61 -0
  204. data/vendor/rcheevos/src/rc_client_types.natvis +396 -0
  205. data/vendor/rcheevos/src/rc_compat.c +251 -0
  206. data/vendor/rcheevos/src/rc_compat.h +121 -0
  207. data/vendor/rcheevos/src/rc_libretro.c +915 -0
  208. data/vendor/rcheevos/src/rc_libretro.h +98 -0
  209. data/vendor/rcheevos/src/rc_util.c +199 -0
  210. data/vendor/rcheevos/src/rc_version.c +11 -0
  211. data/vendor/rcheevos/src/rc_version.h +32 -0
  212. data/vendor/rcheevos/src/rcheevos/alloc.c +312 -0
  213. data/vendor/rcheevos/src/rcheevos/condition.c +754 -0
  214. data/vendor/rcheevos/src/rcheevos/condset.c +777 -0
  215. data/vendor/rcheevos/src/rcheevos/consoleinfo.c +1215 -0
  216. data/vendor/rcheevos/src/rcheevos/format.c +330 -0
  217. data/vendor/rcheevos/src/rcheevos/lboard.c +287 -0
  218. data/vendor/rcheevos/src/rcheevos/memref.c +805 -0
  219. data/vendor/rcheevos/src/rcheevos/operand.c +607 -0
  220. data/vendor/rcheevos/src/rcheevos/rc_internal.h +390 -0
  221. data/vendor/rcheevos/src/rcheevos/rc_runtime_types.natvis +541 -0
  222. data/vendor/rcheevos/src/rcheevos/rc_validate.c +1406 -0
  223. data/vendor/rcheevos/src/rcheevos/rc_validate.h +18 -0
  224. data/vendor/rcheevos/src/rcheevos/richpresence.c +922 -0
  225. data/vendor/rcheevos/src/rcheevos/runtime.c +852 -0
  226. data/vendor/rcheevos/src/rcheevos/runtime_progress.c +1073 -0
  227. data/vendor/rcheevos/src/rcheevos/trigger.c +344 -0
  228. data/vendor/rcheevos/src/rcheevos/value.c +935 -0
  229. data/vendor/rcheevos/src/rhash/aes.c +480 -0
  230. data/vendor/rcheevos/src/rhash/aes.h +49 -0
  231. data/vendor/rcheevos/src/rhash/cdreader.c +838 -0
  232. data/vendor/rcheevos/src/rhash/hash.c +1402 -0
  233. data/vendor/rcheevos/src/rhash/hash_disc.c +1340 -0
  234. data/vendor/rcheevos/src/rhash/hash_encrypted.c +566 -0
  235. data/vendor/rcheevos/src/rhash/hash_rom.c +426 -0
  236. data/vendor/rcheevos/src/rhash/hash_zip.c +460 -0
  237. data/vendor/rcheevos/src/rhash/md5.c +382 -0
  238. data/vendor/rcheevos/src/rhash/md5.h +91 -0
  239. data/vendor/rcheevos/src/rhash/rc_hash_internal.h +116 -0
  240. data/vendor/rcheevos/test/libretro.h +205 -0
  241. data/vendor/rcheevos/test/rapi/test_rc_api_common.c +941 -0
  242. data/vendor/rcheevos/test/rapi/test_rc_api_editor.c +931 -0
  243. data/vendor/rcheevos/test/rapi/test_rc_api_info.c +545 -0
  244. data/vendor/rcheevos/test/rapi/test_rc_api_runtime.c +2213 -0
  245. data/vendor/rcheevos/test/rapi/test_rc_api_user.c +998 -0
  246. data/vendor/rcheevos/test/rcheevos/mock_memory.h +32 -0
  247. data/vendor/rcheevos/test/rcheevos/test_condition.c +570 -0
  248. data/vendor/rcheevos/test/rcheevos/test_condset.c +5170 -0
  249. data/vendor/rcheevos/test/rcheevos/test_consoleinfo.c +203 -0
  250. data/vendor/rcheevos/test/rcheevos/test_format.c +112 -0
  251. data/vendor/rcheevos/test/rcheevos/test_lboard.c +746 -0
  252. data/vendor/rcheevos/test/rcheevos/test_memref.c +520 -0
  253. data/vendor/rcheevos/test/rcheevos/test_operand.c +692 -0
  254. data/vendor/rcheevos/test/rcheevos/test_rc_validate.c +502 -0
  255. data/vendor/rcheevos/test/rcheevos/test_richpresence.c +1564 -0
  256. data/vendor/rcheevos/test/rcheevos/test_runtime.c +1667 -0
  257. data/vendor/rcheevos/test/rcheevos/test_runtime_progress.c +1821 -0
  258. data/vendor/rcheevos/test/rcheevos/test_timing.c +166 -0
  259. data/vendor/rcheevos/test/rcheevos/test_trigger.c +2521 -0
  260. data/vendor/rcheevos/test/rcheevos/test_value.c +870 -0
  261. data/vendor/rcheevos/test/rcheevos-test.sln +46 -0
  262. data/vendor/rcheevos/test/rcheevos-test.vcxproj +239 -0
  263. data/vendor/rcheevos/test/rcheevos-test.vcxproj.filters +335 -0
  264. data/vendor/rcheevos/test/rhash/data.c +657 -0
  265. data/vendor/rcheevos/test/rhash/data.h +32 -0
  266. data/vendor/rcheevos/test/rhash/mock_filereader.c +236 -0
  267. data/vendor/rcheevos/test/rhash/mock_filereader.h +31 -0
  268. data/vendor/rcheevos/test/rhash/test_cdreader.c +920 -0
  269. data/vendor/rcheevos/test/rhash/test_hash.c +310 -0
  270. data/vendor/rcheevos/test/rhash/test_hash_disc.c +1450 -0
  271. data/vendor/rcheevos/test/rhash/test_hash_rom.c +899 -0
  272. data/vendor/rcheevos/test/rhash/test_hash_zip.c +551 -0
  273. data/vendor/rcheevos/test/test.c +113 -0
  274. data/vendor/rcheevos/test/test_framework.h +205 -0
  275. data/vendor/rcheevos/test/test_rc_client.c +10509 -0
  276. data/vendor/rcheevos/test/test_rc_client_external.c +2197 -0
  277. data/vendor/rcheevos/test/test_rc_client_raintegration.c +441 -0
  278. data/vendor/rcheevos/test/test_rc_libretro.c +952 -0
  279. data/vendor/rcheevos/test/test_types.natvis +9 -0
  280. data/vendor/rcheevos/validator/validator.c +658 -0
  281. data/vendor/rcheevos/validator/validator.vcxproj +152 -0
  282. data/vendor/rcheevos/validator/validator.vcxproj.filters +82 -0
  283. metadata +274 -11
  284. data/lib/gemba/input_mappings.rb +0 -214
  285. data/lib/gemba/player.rb +0 -1515
@@ -0,0 +1,460 @@
1
+ #include "rc_hash_internal.h"
2
+
3
+ #include "../rc_compat.h"
4
+
5
+ struct rc_hash_zip_idx
6
+ {
7
+ size_t length;
8
+ uint8_t* data;
9
+ };
10
+
11
+ static int rc_hash_zip_idx_sort(const void* a, const void* b)
12
+ {
13
+ struct rc_hash_zip_idx* A = (struct rc_hash_zip_idx*)a, * B = (struct rc_hash_zip_idx*)b;
14
+ size_t len = (A->length < B->length ? A->length : B->length);
15
+ return memcmp(A->data, B->data, len);
16
+ }
17
+
18
+ typedef int (RC_CCONV* rc_hash_zip_filter_t)(const char* filename, uint32_t filename_len, uint64_t decomp_size, void* userdata);
19
+
20
+ static int rc_hash_zip_file(md5_state_t* md5, void* file_handle,
21
+ const rc_hash_iterator_t* iterator,
22
+ rc_hash_zip_filter_t filter_func, void* filter_userdata)
23
+ {
24
+ uint8_t buf[2048], *alloc_buf, *cdir_start, *cdir_max, *cdir, *hashdata, eocdirhdr_size, cdirhdr_size, nparents;
25
+ uint32_t cdir_entry_len;
26
+ size_t sizeof_idx, indices_offset, alloc_size;
27
+ int64_t i_file, archive_size, ecdh_ofs, total_files, cdir_size, cdir_ofs;
28
+ struct rc_hash_zip_idx* hashindices, *hashindex;
29
+
30
+ rc_file_seek(iterator, file_handle, 0, SEEK_END);
31
+ archive_size = rc_file_tell(iterator, file_handle);
32
+
33
+ /* Basic sanity checks - reject files which are too small */
34
+ eocdirhdr_size = 22; /* the 'end of central directory header' is 22 bytes */
35
+ if (archive_size < eocdirhdr_size)
36
+ return rc_hash_iterator_error(iterator, "ZIP is too small");
37
+
38
+ /* Macros used for reading ZIP and writing to a buffer for hashing (undefined again at the end of the function) */
39
+ #define RC_ZIP_READ_LE16(p) ((uint16_t)(((const uint8_t*)(p))[0]) | ((uint16_t)(((const uint8_t*)(p))[1]) << 8U))
40
+ #define RC_ZIP_READ_LE32(p) ((uint32_t)(((const uint8_t*)(p))[0]) | ((uint32_t)(((const uint8_t*)(p))[1]) << 8U) | ((uint32_t)(((const uint8_t*)(p))[2]) << 16U) | ((uint32_t)(((const uint8_t*)(p))[3]) << 24U))
41
+ #define RC_ZIP_READ_LE64(p) ((uint64_t)(((const uint8_t*)(p))[0]) | ((uint64_t)(((const uint8_t*)(p))[1]) << 8U) | ((uint64_t)(((const uint8_t*)(p))[2]) << 16U) | ((uint64_t)(((const uint8_t*)(p))[3]) << 24U) | ((uint64_t)(((const uint8_t*)(p))[4]) << 32U) | ((uint64_t)(((const uint8_t*)(p))[5]) << 40U) | ((uint64_t)(((const uint8_t*)(p))[6]) << 48U) | ((uint64_t)(((const uint8_t*)(p))[7]) << 56U))
42
+ #define RC_ZIP_WRITE_LE32(p,v) { ((uint8_t*)(p))[0] = (uint8_t)((uint32_t)(v) & 0xFF); ((uint8_t*)(p))[1] = (uint8_t)(((uint32_t)(v) >> 8) & 0xFF); ((uint8_t*)(p))[2] = (uint8_t)(((uint32_t)(v) >> 16) & 0xFF); ((uint8_t*)(p))[3] = (uint8_t)((uint32_t)(v) >> 24); }
43
+ #define RC_ZIP_WRITE_LE64(p,v) { ((uint8_t*)(p))[0] = (uint8_t)((uint64_t)(v) & 0xFF); ((uint8_t*)(p))[1] = (uint8_t)(((uint64_t)(v) >> 8) & 0xFF); ((uint8_t*)(p))[2] = (uint8_t)(((uint64_t)(v) >> 16) & 0xFF); ((uint8_t*)(p))[3] = (uint8_t)(((uint64_t)(v) >> 24) & 0xFF); ((uint8_t*)(p))[4] = (uint8_t)(((uint64_t)(v) >> 32) & 0xFF); ((uint8_t*)(p))[5] = (uint8_t)(((uint64_t)(v) >> 40) & 0xFF); ((uint8_t*)(p))[6] = (uint8_t)(((uint64_t)(v) >> 48) & 0xFF); ((uint8_t*)(p))[7] = (uint8_t)((uint64_t)(v) >> 56); }
44
+
45
+ /* Find the end of central directory record by scanning the file from the end towards the beginning */
46
+ for (ecdh_ofs = archive_size - sizeof(buf); ; ecdh_ofs -= (sizeof(buf) - 3)) {
47
+ int i, n = sizeof(buf);
48
+ if (ecdh_ofs < 0)
49
+ ecdh_ofs = 0;
50
+ if (n > archive_size)
51
+ n = (int)archive_size;
52
+
53
+ rc_file_seek(iterator, file_handle, ecdh_ofs, SEEK_SET);
54
+ if (rc_file_read(iterator, file_handle, buf, n) != (size_t)n)
55
+ return rc_hash_iterator_error(iterator, "ZIP read error");
56
+
57
+ for (i = n - 4; i >= 0; --i) {
58
+ if (buf[i] == 'P' && RC_ZIP_READ_LE32(buf + i) == 0x06054b50) /* end of central directory header signature */
59
+ break;
60
+ }
61
+
62
+ if (i >= 0) {
63
+ ecdh_ofs += i;
64
+ break;
65
+ }
66
+
67
+ if (!ecdh_ofs || (archive_size - ecdh_ofs) >= (0xFFFF + eocdirhdr_size))
68
+ return rc_hash_iterator_error(iterator, "Failed to find ZIP central directory");
69
+ }
70
+
71
+ /* Read and verify the end of central directory record. */
72
+ rc_file_seek(iterator, file_handle, ecdh_ofs, SEEK_SET);
73
+ if (rc_file_read(iterator, file_handle, buf, eocdirhdr_size) != eocdirhdr_size)
74
+ return rc_hash_iterator_error(iterator, "Failed to read ZIP central directory");
75
+
76
+ /* Read central dir information from end of central directory header */
77
+ total_files = RC_ZIP_READ_LE16(buf + 0x0A);
78
+ cdir_size = RC_ZIP_READ_LE32(buf + 0x0C);
79
+ cdir_ofs = RC_ZIP_READ_LE32(buf + 0x10);
80
+
81
+ /* Check if this is a Zip64 file. In the block of code below:
82
+ * - 20 is the size of the ZIP64 end of central directory locator
83
+ * - 56 is the size of the ZIP64 end of central directory header
84
+ */
85
+ if ((cdir_ofs == 0xFFFFFFFF || cdir_size == 0xFFFFFFFF || total_files == 0xFFFF) && ecdh_ofs >= (20 + 56)) {
86
+ /* Read the ZIP64 end of central directory locator if it actually exists */
87
+ rc_file_seek(iterator, file_handle, ecdh_ofs - 20, SEEK_SET);
88
+ if (rc_file_read(iterator, file_handle, buf, 20) == 20 && RC_ZIP_READ_LE32(buf) == 0x07064b50) { /* locator signature */
89
+ /* Found the locator, now read the actual ZIP64 end of central directory header */
90
+ int64_t ecdh64_ofs = (int64_t)RC_ZIP_READ_LE64(buf + 0x08);
91
+ if (ecdh64_ofs <= (archive_size - 56)) {
92
+ rc_file_seek(iterator, file_handle, ecdh64_ofs, SEEK_SET);
93
+ if (rc_file_read(iterator, file_handle, buf, 56) == 56 && RC_ZIP_READ_LE32(buf) == 0x06064b50) { /* header signature */
94
+ total_files = RC_ZIP_READ_LE64(buf + 0x20);
95
+ cdir_size = RC_ZIP_READ_LE64(buf + 0x28);
96
+ cdir_ofs = RC_ZIP_READ_LE64(buf + 0x30);
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ /* Basic verificaton of central directory (limit to a 256MB content directory) */
103
+ cdirhdr_size = 46; /* the 'central directory header' is 46 bytes */
104
+ if ((cdir_size >= 0x10000000) || (cdir_size < total_files * cdirhdr_size) || ((cdir_ofs + cdir_size) > archive_size))
105
+ return rc_hash_iterator_error(iterator, "Central directory of ZIP file is invalid");
106
+
107
+ /* Allocate once for both directory and our temporary sort index (memory aligned to sizeof(rc_hash_zip_idx)) */
108
+ sizeof_idx = sizeof(struct rc_hash_zip_idx);
109
+ indices_offset = (size_t)((cdir_size + sizeof_idx - 1) / sizeof_idx * sizeof_idx);
110
+ alloc_size = (size_t)(indices_offset + total_files * sizeof_idx);
111
+ alloc_buf = (uint8_t*)malloc(alloc_size);
112
+
113
+ /* Read entire central directory to a buffer */
114
+ if (!alloc_buf)
115
+ return rc_hash_iterator_error(iterator, "Could not allocate temporary buffer");
116
+
117
+ rc_file_seek(iterator, file_handle, cdir_ofs, SEEK_SET);
118
+ if ((int64_t)rc_file_read(iterator, file_handle, alloc_buf, (int)cdir_size) != cdir_size) {
119
+ free(alloc_buf);
120
+ return rc_hash_iterator_error(iterator, "Failed to read central directory of ZIP file");
121
+ }
122
+
123
+ cdir_start = alloc_buf;
124
+ cdir_max = cdir_start + cdir_size - cdirhdr_size;
125
+ cdir = cdir_start;
126
+
127
+ /* Write our temporary hash data to the same buffer we read the central directory from.
128
+ * We can do that because the amount of data we keep for each file is guaranteed to be less than the file record.
129
+ */
130
+ hashdata = alloc_buf;
131
+ hashindices = (struct rc_hash_zip_idx*)(alloc_buf + indices_offset);
132
+ hashindex = hashindices;
133
+
134
+ /* Now process the central directory file records */
135
+ for (i_file = nparents = 0, cdir = cdir_start; i_file < total_files && cdir >= cdir_start && cdir <= cdir_max; i_file++, cdir += cdir_entry_len) {
136
+ const uint8_t* name, * name_end;
137
+ uint32_t signature = RC_ZIP_READ_LE32(cdir + 0x00);
138
+ uint32_t method = RC_ZIP_READ_LE16(cdir + 0x0A);
139
+ uint32_t crc32 = RC_ZIP_READ_LE32(cdir + 0x10);
140
+ uint64_t comp_size = RC_ZIP_READ_LE32(cdir + 0x14);
141
+ uint64_t decomp_size = RC_ZIP_READ_LE32(cdir + 0x18);
142
+ uint32_t filename_len = RC_ZIP_READ_LE16(cdir + 0x1C);
143
+ int32_t extra_len = RC_ZIP_READ_LE16(cdir + 0x1E);
144
+ int32_t comment_len = RC_ZIP_READ_LE16(cdir + 0x20);
145
+ int32_t external_attr = RC_ZIP_READ_LE16(cdir + 0x26);
146
+ uint64_t local_hdr_ofs = RC_ZIP_READ_LE32(cdir + 0x2A);
147
+ cdir_entry_len = cdirhdr_size + filename_len + extra_len + comment_len;
148
+
149
+ if (signature != 0x02014b50) /* expected central directory entry signature */
150
+ break;
151
+
152
+ /* Ignore records describing a directory (we only hash file records) */
153
+ name = (cdir + cdirhdr_size);
154
+ if (name[filename_len - 1] == '/' || name[filename_len - 1] == '\\' || (external_attr & 0x10))
155
+ continue;
156
+
157
+ /* Handle Zip64 fields */
158
+ if (decomp_size == 0xFFFFFFFF || comp_size == 0xFFFFFFFF || local_hdr_ofs == 0xFFFFFFFF) {
159
+ int invalid = 0;
160
+ const uint8_t* x = cdir + cdirhdr_size + filename_len, * xEnd, * field, * fieldEnd;
161
+ for (xEnd = x + extra_len; (x + (sizeof(uint16_t) * 2)) < xEnd; x = fieldEnd) {
162
+ field = x + (sizeof(uint16_t) * 2);
163
+ fieldEnd = field + RC_ZIP_READ_LE16(x + 2);
164
+ if (RC_ZIP_READ_LE16(x) != 0x0001 || fieldEnd > xEnd)
165
+ continue; /* Not the Zip64 extended information extra field */
166
+
167
+ if (decomp_size == 0xFFFFFFFF) {
168
+ if ((unsigned)(fieldEnd - field) < sizeof(uint64_t)) {
169
+ invalid = 1;
170
+ break;
171
+ }
172
+
173
+ decomp_size = RC_ZIP_READ_LE64(field);
174
+ field += sizeof(uint64_t);
175
+ }
176
+
177
+ if (comp_size == 0xFFFFFFFF) {
178
+ if ((unsigned)(fieldEnd - field) < sizeof(uint64_t)) {
179
+ invalid = 1;
180
+ break;
181
+ }
182
+
183
+ comp_size = RC_ZIP_READ_LE64(field);
184
+ field += sizeof(uint64_t);
185
+ }
186
+
187
+ if (local_hdr_ofs == 0xFFFFFFFF) {
188
+ if ((unsigned)(fieldEnd - field) < sizeof(uint64_t)) {
189
+ invalid = 1;
190
+ break;
191
+ }
192
+
193
+ local_hdr_ofs = RC_ZIP_READ_LE64(field);
194
+ field += sizeof(uint64_t);
195
+ }
196
+
197
+ break;
198
+ }
199
+
200
+ if (invalid) {
201
+ free(alloc_buf);
202
+ return rc_hash_iterator_error(iterator, "Encountered invalid Zip64 file");
203
+ }
204
+ }
205
+
206
+ /* Basic sanity check on file record */
207
+ /* 30 is the length of the local directory header preceeding the compressed data */
208
+ if ((!method && decomp_size != comp_size) || (decomp_size && !comp_size) ||
209
+ ((local_hdr_ofs + 30 + comp_size) > (uint64_t)archive_size)) {
210
+ free(alloc_buf);
211
+ return rc_hash_iterator_error(iterator, "Encountered invalid entry in ZIP central directory");
212
+ }
213
+
214
+ if (filter_func) {
215
+ int filtered = filter_func((const char*)name, filename_len, decomp_size, filter_userdata);
216
+ if (filtered < 0) {
217
+ free(alloc_buf);
218
+ return 0;
219
+ }
220
+
221
+ if (filtered) /* this file shouldn't be hashed */
222
+ continue;
223
+ }
224
+
225
+ /* Write the pointer and length of the data we record about this file */
226
+ hashindex->data = hashdata;
227
+ hashindex->length = filename_len + 1 + 4 + 8;
228
+ hashindex++;
229
+
230
+ rc_hash_iterator_verbose_formatted(iterator, "File in ZIP: %.*s (%u bytes, CRC32 = %08X)", filename_len, (const char*)name, (unsigned)decomp_size, crc32);
231
+
232
+ /* Convert and store the file name in the hash data buffer */
233
+ for (name_end = name + filename_len; name != name_end; name++) {
234
+ *(hashdata++) =
235
+ (*name == '\\' ? '/' : /* convert back-slashes to regular slashes */
236
+ (*name >= 'A' && *name <= 'Z') ? (*name | 0x20) : /* convert upper case letters to lower case */
237
+ *name); /* else use the byte as-is */
238
+ }
239
+
240
+ /* Add zero terminator, CRC32 and decompressed size to the hash data buffer */
241
+ *(hashdata++) = '\0';
242
+ RC_ZIP_WRITE_LE32(hashdata, crc32);
243
+ hashdata += 4;
244
+ RC_ZIP_WRITE_LE64(hashdata, decomp_size);
245
+ hashdata += 8;
246
+ }
247
+
248
+ rc_hash_iterator_verbose_formatted(iterator, "Hashing %u files in ZIP archive", (unsigned)(hashindex - hashindices));
249
+
250
+ /* Sort the file list indices */
251
+ qsort(hashindices, (hashindex - hashindices), sizeof(struct rc_hash_zip_idx), rc_hash_zip_idx_sort);
252
+
253
+ /* Hash the data in the order of the now sorted indices */
254
+ for (; hashindices != hashindex; hashindices++)
255
+ md5_append(md5, hashindices->data, (int)hashindices->length);
256
+
257
+ free(alloc_buf);
258
+
259
+ return 1;
260
+
261
+ #undef RC_ZIP_READ_LE16
262
+ #undef RC_ZIP_READ_LE32
263
+ #undef RC_ZIP_READ_LE64
264
+ #undef RC_ZIP_WRITE_LE32
265
+ #undef RC_ZIP_WRITE_LE64
266
+ }
267
+
268
+ /* ===================================================== */
269
+
270
+ static int rc_hash_arduboyfx_filter(const char* filename, uint32_t filename_len, uint64_t decomp_size, void* userdata)
271
+ {
272
+ (void)decomp_size;
273
+ (void)userdata;
274
+
275
+ /* An .arduboy file is a zip file containing an info.json pointing at one or more bin
276
+ * and hex files. It can also contain a bunch of screenshots, but we don't care about
277
+ * those. As they're also referenced in the info.json, we have to ignore that too.
278
+ * Instead of ignoring the info.json and all image files, only process any bin/hex files */
279
+ if (filename_len > 4) {
280
+ const char* ext = &filename[filename_len - 4];
281
+ if (strncasecmp(ext, ".hex", 4) == 0 || strncasecmp(ext, ".bin", 4) == 0)
282
+ return 0; /* keep hex and bin */
283
+ }
284
+
285
+ return 1; /* filter everything else */
286
+ }
287
+
288
+ int rc_hash_arduboyfx(char hash[33], const rc_hash_iterator_t* iterator)
289
+ {
290
+ md5_state_t md5;
291
+ int res;
292
+
293
+ void* file_handle = rc_file_open(iterator, iterator->path);
294
+ if (!file_handle)
295
+ return rc_hash_iterator_error(iterator, "Could not open file");
296
+
297
+ md5_init(&md5);
298
+ res = rc_hash_zip_file(&md5, file_handle, iterator, rc_hash_arduboyfx_filter, NULL);
299
+ rc_file_close(iterator, file_handle);
300
+
301
+ if (!res)
302
+ return 0;
303
+
304
+ return rc_hash_finalize(iterator, &md5, hash);
305
+ }
306
+
307
+ /* ===================================================== */
308
+
309
+ struct rc_hash_ms_dos_dosz_state {
310
+ const char* path;
311
+ const struct rc_hash_ms_dos_dosz_state* child;
312
+
313
+ md5_state_t* md5;
314
+ const rc_hash_iterator_t* iterator;
315
+ void* file_handle;
316
+ uint32_t nparents;
317
+ };
318
+
319
+ static int rc_hash_dosz(struct rc_hash_ms_dos_dosz_state* dosz);
320
+
321
+ static int rc_hash_ms_dos_parent(const struct rc_hash_ms_dos_dosz_state* child,
322
+ const char* parentname, uint32_t parentname_len)
323
+ {
324
+ const char* lastfslash = strrchr(child->path, '/');
325
+ const char* lastbslash = strrchr(child->path, '\\');
326
+ const char* lastslash = (lastbslash > lastfslash ? lastbslash : lastfslash);
327
+ size_t dir_len = (lastslash ? (lastslash + 1 - child->path) : 0);
328
+ char* parent_path = (char*)malloc(dir_len + parentname_len + 1);
329
+ struct rc_hash_ms_dos_dosz_state parent;
330
+ const struct rc_hash_ms_dos_dosz_state* check;
331
+ void* parent_handle;
332
+ int parent_res;
333
+
334
+ /* Build the path of the parent by combining the directory of the current file with the name */
335
+ if (!parent_path)
336
+ return rc_hash_iterator_error(child->iterator, "Could not allocate temporary buffer");
337
+
338
+ memcpy(parent_path, child->path, dir_len);
339
+ memcpy(parent_path + dir_len, parentname, parentname_len);
340
+ parent_path[dir_len + parentname_len] = '\0';
341
+
342
+ /* Make sure there is no recursion where a parent DOSZ is an already seen child DOSZ */
343
+ for (check = child->child; check; check = check->child) {
344
+ if (!strcmp(check->path, parent_path)) {
345
+ free(parent_path);
346
+ return rc_hash_iterator_error(child->iterator, "Invalid DOSZ file with recursive parents");
347
+ }
348
+ }
349
+
350
+ /* Try to open the parent DOSZ file */
351
+ parent_handle = rc_file_open(child->iterator, parent_path);
352
+ if (!parent_handle) {
353
+ rc_hash_iterator_error_formatted(child->iterator, "DOSZ parent file '%s' does not exist", parent_path);
354
+ free(parent_path);
355
+ return 0;
356
+ }
357
+
358
+ /* Fully hash the parent DOSZ ahead of the child */
359
+ memcpy(&parent, child, sizeof(parent));
360
+ parent.path = parent_path;
361
+ parent.child = child;
362
+ parent.file_handle = parent_handle;
363
+ parent_res = rc_hash_dosz(&parent);
364
+ rc_file_close(child->iterator, parent_handle);
365
+ free(parent_path);
366
+ return parent_res;
367
+ }
368
+
369
+ static int rc_hash_ms_dos_dosc(const struct rc_hash_ms_dos_dosz_state* dosz)
370
+ {
371
+ size_t path_len = strlen(dosz->path);
372
+ if (dosz->path[path_len - 1] == 'z' || dosz->path[path_len - 1] == 'Z') {
373
+ void* file_handle;
374
+ char* dosc_path = strdup(dosz->path);
375
+ if (!dosc_path)
376
+ return rc_hash_iterator_error(dosz->iterator, "Could not allocate temporary buffer");
377
+
378
+ /* Swap the z to c and use the same capitalization, hash the file if it exists */
379
+ dosc_path[path_len - 1] = (dosz->path[path_len - 1] == 'z' ? 'c' : 'C');
380
+ file_handle = rc_file_open(dosz->iterator, dosc_path);
381
+ free(dosc_path);
382
+
383
+ if (file_handle) {
384
+ /* Hash the entire contents of the DOSC file */
385
+ int res = rc_hash_zip_file(dosz->md5, file_handle, dosz->iterator, NULL, NULL);
386
+ rc_file_close(dosz->iterator, file_handle);
387
+ if (!res)
388
+ return 0;
389
+ }
390
+ }
391
+
392
+ return 1;
393
+ }
394
+
395
+ static int rc_hash_dosz_filter(const char* filename, uint32_t filename_len, uint64_t decomp_size, void* userdata)
396
+ {
397
+ struct rc_hash_ms_dos_dosz_state* dosz = (struct rc_hash_ms_dos_dosz_state*)userdata;
398
+
399
+ /* A DOSZ file can contain a special empty <base>.dosz.parent file in its root which means a parent dosz file is used */
400
+ if (decomp_size == 0 && filename_len > 7 &&
401
+ strncasecmp(&filename[filename_len - 7], ".parent", 7) == 0 &&
402
+ !memchr(filename, '/', filename_len) &&
403
+ !memchr(filename, '\\', filename_len))
404
+ {
405
+ /* A DOSZ file can only have one parent file */
406
+ if (dosz->nparents++)
407
+ return -1;
408
+
409
+ /* process the parent. if it fails, stop */
410
+ if (!rc_hash_ms_dos_parent(dosz, filename, (filename_len - 7)))
411
+ return -1;
412
+
413
+ /* We don't hash this meta file so a user is free to rename it and the parent file */
414
+ return 1;
415
+ }
416
+
417
+ return 0;
418
+ }
419
+
420
+ static int rc_hash_dosz(struct rc_hash_ms_dos_dosz_state* dosz)
421
+ {
422
+ if (!rc_hash_zip_file(dosz->md5, dosz->file_handle, dosz->iterator, rc_hash_dosz_filter, dosz))
423
+ return 0;
424
+
425
+ /* A DOSZ file can only have one parent file */
426
+ if (dosz->nparents > 1)
427
+ return rc_hash_iterator_error(dosz->iterator, "Invalid DOSZ file with multiple parents");
428
+
429
+ /* Check if an associated .dosc file exists */
430
+ if (!rc_hash_ms_dos_dosc(dosz))
431
+ return 0;
432
+
433
+ return 1;
434
+ }
435
+
436
+ int rc_hash_ms_dos(char hash[33], const rc_hash_iterator_t* iterator)
437
+ {
438
+ struct rc_hash_ms_dos_dosz_state dosz;
439
+ md5_state_t md5;
440
+ int res;
441
+
442
+ void* file_handle = rc_file_open(iterator, iterator->path);
443
+ if (!file_handle)
444
+ return rc_hash_iterator_error(iterator, "Could not open file");
445
+
446
+ memset(&dosz, 0, sizeof(dosz));
447
+ dosz.path = iterator->path;
448
+ dosz.file_handle = file_handle;
449
+ dosz.iterator = iterator;
450
+ dosz.md5 = &md5;
451
+
452
+ md5_init(&md5);
453
+ res = rc_hash_dosz(&dosz);
454
+ rc_file_close(iterator, file_handle);
455
+
456
+ if (!res)
457
+ return 0;
458
+
459
+ return rc_hash_finalize(iterator, &md5, hash);
460
+ }