From 62d1592cedff0b23c2a25808a06fd1846e654232 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 23:31:45 +0000 Subject: [PATCH 1/2] Replace docparse with a tree-sitter based parser The SourcePawn docparse tool is deprecated and its C++ build no longer works with current toolchains. It also can't parse several current SourceMod includes (handles.inc, float.inc, entitylump.inc, tf2_stocks.inc), and its Rust post-processing panics on sourcemod.inc (`typedef Address = int64`), so the core bundle can't be regenerated. libalternator now parses includes itself: - a small preprocessor evaluates #if/#elseif/#else, records #defines and honours #endinput, blanking inactive code without shifting offsets - function bodies are blanked before parsing, since documentation never needs statements and unusual statement syntax could otherwise derail the parser - tree-sitter-sourcepawn parses declarations - doc comments attach to declarations by the same front/tail comment rules docparse used, and are parsed with spdcp - types, defaults and enum values are rendered in docparse's format so existing bundles don't churn Syntax errors inside declarations fail the file, like before, so history walks don't mistake a failed parse for removed symbols. spdcp is pinned to the commit from rumblefrog/sp-dcp#1, which upstreams the doc comment fixes (e.g. `@note` on its own line). Switch to the 0.5 release once published. Upstream SourceMod and third-party sources are vendored as a snapshot regression corpus (221 files). On the 189 of them docparse could handle, the output matches docparse apart from intended doc comment fixes. CI no longer builds SourcePawn; it tests the parser and builds chumbucket. Refs #29 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011dNCUV71HWmyb9MwBD4xhC --- .github/workflows/workflow.yml | 146 +- .gitignore | 1 - Cargo.lock | 388 +- README.md | 29 +- libalternator/Cargo.toml | 12 +- libalternator/binding/AMBuilder | 21 - libalternator/binding/configure.py | 31 - libalternator/binding/dll_exports.cpp | 0 libalternator/binding/docparse.cpp | 705 --- libalternator/binding/docparse.h | 6 - libalternator/binding/wrapper.h | 1 - libalternator/build.rs | 49 - libalternator/examples/dump.rs | 29 + libalternator/src/comments.rs | 176 + libalternator/src/error.rs | 17 +- libalternator/src/expr.rs | 731 +++ libalternator/src/extract.rs | 851 +++ libalternator/src/lib.rs | 365 +- libalternator/src/preprocess.rs | 775 +++ libalternator/tests/corpus.rs | 134 + libalternator/tests/corpus/SOURCES.md | 19 + .../corpus/autoexecconfig/autoexecconfig.inc | 768 +++ .../tests/corpus/bytebuffer/bytebuffer.inc | 246 + .../tests/corpus/bytebuffer/bytebuffer_es.inc | 167 + .../corpus/includelibrary/include/cURL.inc | 565 ++ .../includelibrary/include/cURL_header.inc | 1206 ++++ .../corpus/includelibrary/include/ccc.inc | 186 + .../corpus/includelibrary/include/cccm.inc | 28 + .../includelibrary/include/chat-processor.inc | 79 + .../includelibrary/include/morecolors.inc | 995 ++++ .../includelibrary/include/steamtools.inc | 352 ++ .../includelibrary/include/steamworks.inc | 378 ++ .../includelibrary/include/tf2attributes.inc | 306 + .../includelibrary/include/tf2items.inc | 248 + .../include/tf2items_stocks.inc | 117 + .../sourcemod/scripting/include/json.inc | 805 +++ .../scripting/include/json/array.inc | 955 ++++ .../scripting/include/json/definitions.inc | 180 + .../scripting/include/json/helpers/decode.inc | 567 ++ .../scripting/include/json/helpers/errors.inc | 64 + .../include/json/helpers/metastringmap.inc | 237 + .../scripting/include/json/helpers/string.inc | 247 + .../include/json/helpers/typedstringmap.inc | 222 + .../include/json/helpers/unicode.inc | 185 + .../scripting/include/json/object.inc | 724 +++ .../addons/sourcemod/scripting/json_test.sp | 1400 +++++ .../scripting/include/multicolors.inc | 515 ++ .../scripting/include/multicolors/colors.inc | 905 +++ .../include/multicolors/morecolors.inc | 1140 ++++ .../ripext/pawn/scripting/include/ripext.inc | 26 + .../pawn/scripting/include/ripext/http.inc | 359 ++ .../pawn/scripting/include/ripext/json.inc | 400 ++ .../ripext/pawn/scripting/ripext-test.sp | 141 + .../scripting/include/sbpp_server_ip.inc | 73 + .../scripting/include/sbpp_version.inc | 23 + .../scripting/include/sourcebanschecker.inc | 85 + .../scripting/include/sourcebanspp.inc | 175 + .../scripting/include/sourcecomms.inc | 143 + .../addons/sourcemod/scripting/sbpp_admcfg.sp | 187 + .../sourcemod/scripting/sbpp_checker.sp | 741 +++ .../addons/sourcemod/scripting/sbpp_comms.sp | 3523 ++++++++++++ .../addons/sourcemod/scripting/sbpp_main.sp | 3225 +++++++++++ .../addons/sourcemod/scripting/sbpp_report.sp | 200 + .../addons/sourcemod/scripting/sbpp_sleuth.sp | 369 ++ .../socket/scripting/include/socket.inc | 668 +++ .../client/Source-Chat-Relay.sp | 991 ++++ .../client/include/Source-Chat-Relay.inc | 93 + .../sourcemod/plugins/admin-flatfile.sp | 93 + .../plugins/admin-flatfile/admin-groups.sp | 249 + .../plugins/admin-flatfile/admin-overrides.sp | 213 + .../plugins/admin-flatfile/admin-simple.sp | 224 + .../plugins/admin-flatfile/admin-users.sp | 252 + .../sourcemod/plugins/admin-sql-prefetch.sp | 379 ++ .../sourcemod/plugins/admin-sql-threaded.sp | 874 +++ .../corpus/sourcemod/plugins/adminhelp.sp | 169 + .../corpus/sourcemod/plugins/adminmenu.sp | 269 + .../plugins/adminmenu/dynamicmenu.sp | 718 +++ .../corpus/sourcemod/plugins/antiflood.sp | 123 + .../corpus/sourcemod/plugins/basebans.sp | 413 ++ .../corpus/sourcemod/plugins/basebans/ban.sp | 333 ++ .../corpus/sourcemod/plugins/basechat.sp | 469 ++ .../corpus/sourcemod/plugins/basecomm.sp | 248 + .../sourcemod/plugins/basecomm/forwards.sp | 62 + .../corpus/sourcemod/plugins/basecomm/gag.sp | 653 +++ .../sourcemod/plugins/basecomm/natives.sp | 138 + .../corpus/sourcemod/plugins/basecommands.sp | 438 ++ .../plugins/basecommands/cancelvote.sp | 76 + .../sourcemod/plugins/basecommands/execcfg.sp | 170 + .../sourcemod/plugins/basecommands/kick.sp | 221 + .../sourcemod/plugins/basecommands/map.sp | 171 + .../plugins/basecommands/reloadadmins.sp | 67 + .../sourcemod/plugins/basecommands/who.sp | 264 + .../corpus/sourcemod/plugins/basetriggers.sp | 603 ++ .../corpus/sourcemod/plugins/basevotes.sp | 445 ++ .../sourcemod/plugins/basevotes/voteban.sp | 184 + .../sourcemod/plugins/basevotes/votekick.sp | 170 + .../sourcemod/plugins/basevotes/votemap.sp | 313 + .../corpus/sourcemod/plugins/clientprefs.sp | 174 + .../corpus/sourcemod/plugins/funcommands.sp | 350 ++ .../sourcemod/plugins/funcommands/beacon.sp | 243 + .../sourcemod/plugins/funcommands/blind.sp | 292 + .../sourcemod/plugins/funcommands/drug.sp | 344 ++ .../sourcemod/plugins/funcommands/fire.sp | 474 ++ .../sourcemod/plugins/funcommands/gravity.sp | 245 + .../sourcemod/plugins/funcommands/ice.sp | 584 ++ .../sourcemod/plugins/funcommands/noclip.sp | 173 + .../sourcemod/plugins/funcommands/timebomb.sp | 342 ++ .../corpus/sourcemod/plugins/funvotes.sp | 359 ++ .../sourcemod/plugins/funvotes/votealltalk.sp | 104 + .../sourcemod/plugins/funvotes/voteburn.sp | 182 + .../sourcemod/plugins/funvotes/voteff.sp | 103 + .../sourcemod/plugins/funvotes/votegravity.sp | 138 + .../sourcemod/plugins/funvotes/voteslay.sp | 182 + .../sourcemod/plugins/include/admin.inc | 832 +++ .../sourcemod/plugins/include/adminmenu.inc | 152 + .../corpus/sourcemod/plugins/include/adt.inc | 40 + .../sourcemod/plugins/include/adt_array.inc | 500 ++ .../sourcemod/plugins/include/adt_stack.inc | 281 + .../sourcemod/plugins/include/adt_trie.inc | 451 ++ .../sourcemod/plugins/include/banning.inc | 156 + .../sourcemod/plugins/include/basecomm.inc | 109 + .../sourcemod/plugins/include/bitbuffer.inc | 470 ++ .../sourcemod/plugins/include/clientprefs.inc | 433 ++ .../sourcemod/plugins/include/clients.inc | 846 +++ .../plugins/include/commandfilters.inc | 171 + .../sourcemod/plugins/include/commandline.inc | 86 + .../sourcemod/plugins/include/console.inc | 819 +++ .../sourcemod/plugins/include/convars.inc | 525 ++ .../corpus/sourcemod/plugins/include/core.inc | 320 ++ .../sourcemod/plugins/include/cstrike.inc | 498 ++ .../sourcemod/plugins/include/datapack.inc | 255 + .../corpus/sourcemod/plugins/include/dbi.inc | 1077 ++++ .../sourcemod/plugins/include/dhooks.inc | 1100 ++++ .../sourcemod/plugins/include/entity.inc | 785 +++ .../plugins/include/entity_prop_stocks.inc | 590 ++ .../sourcemod/plugins/include/entitylump.inc | 157 + .../sourcemod/plugins/include/events.inc | 343 ++ .../sourcemod/plugins/include/files.inc | 887 +++ .../sourcemod/plugins/include/float.inc | 477 ++ .../sourcemod/plugins/include/functions.inc | 621 ++ .../sourcemod/plugins/include/geoip.inc | 241 + .../sourcemod/plugins/include/halflife.inc | 734 +++ .../sourcemod/plugins/include/handles.inc | 109 + .../sourcemod/plugins/include/helpers.inc | 287 + .../sourcemod/plugins/include/keyvalues.inc | 731 +++ .../corpus/sourcemod/plugins/include/lang.inc | 143 + .../sourcemod/plugins/include/logging.inc | 135 + .../sourcemod/plugins/include/mapchooser.inc | 164 + .../sourcemod/plugins/include/menus.inc | 1161 ++++ .../sourcemod/plugins/include/nextmap.inc | 82 + .../sourcemod/plugins/include/profiler.inc | 123 + .../sourcemod/plugins/include/protobuf.inc | 602 ++ .../sourcemod/plugins/include/regex.inc | 291 + .../sourcemod/plugins/include/sdkhooks.inc | 461 ++ .../sourcemod/plugins/include/sdktools.inc | 236 + .../plugins/include/sdktools_client.inc | 52 + .../plugins/include/sdktools_engine.inc | 66 + .../plugins/include/sdktools_entinput.inc | 51 + .../plugins/include/sdktools_entoutput.inc | 108 + .../plugins/include/sdktools_functions.inc | 419 ++ .../plugins/include/sdktools_gamerules.inc | 199 + .../plugins/include/sdktools_hooks.inc | 112 + .../plugins/include/sdktools_sound.inc | 722 +++ .../plugins/include/sdktools_stocks.inc | 75 + .../plugins/include/sdktools_stringtables.inc | 180 + .../plugins/include/sdktools_tempents.inc | 250 + .../include/sdktools_tempents_stocks.inc | 443 ++ .../plugins/include/sdktools_trace.inc | 732 +++ .../plugins/include/sdktools_variant_t.inc | 93 + .../plugins/include/sdktools_voice.inc | 143 + .../sourcemod/plugins/include/sorting.inc | 198 + .../sourcemod/plugins/include/sourcemod.inc | 815 +++ .../sourcemod/plugins/include/string.inc | 632 +++ .../sourcemod/plugins/include/testing.inc | 157 + .../sourcemod/plugins/include/textparse.inc | 263 + .../corpus/sourcemod/plugins/include/tf2.inc | 502 ++ .../sourcemod/plugins/include/tf2_stocks.inc | 647 +++ .../sourcemod/plugins/include/timers.inc | 220 + .../sourcemod/plugins/include/topmenus.inc | 446 ++ .../plugins/include/usermessages.inc | 270 + .../sourcemod/plugins/include/vector.inc | 179 + .../sourcemod/plugins/include/version.inc | 49 + .../corpus/sourcemod/plugins/mapchooser.sp | 1317 +++++ .../tests/corpus/sourcemod/plugins/nextmap.sp | 250 + .../corpus/sourcemod/plugins/nominations.sp | 505 ++ .../sourcemod/plugins/playercommands.sp | 98 + .../plugins/playercommands/rename.sp | 204 + .../sourcemod/plugins/playercommands/slap.sp | 222 + .../sourcemod/plugins/playercommands/slay.sp | 162 + .../corpus/sourcemod/plugins/randomcycle.sp | 111 + .../corpus/sourcemod/plugins/reservedslots.sp | 339 ++ .../corpus/sourcemod/plugins/rockthevote.sp | 281 + .../tests/corpus/sourcemod/plugins/sounds.sp | 127 + .../sourcemod/plugins/sql-admin-manager.sp | 947 +++ .../sourcemod/plugins/testsuite/benchmark.sp | 157 + .../sourcemod/plugins/testsuite/bug4059.sp | 31 + .../plugins/testsuite/callfunctest.sp | 169 + .../sourcemod/plugins/testsuite/capstest.sp | 33 + .../plugins/testsuite/clientprefstest.sp | 63 + .../plugins/testsuite/cstrike-test.sp | 142 + .../plugins/testsuite/entitylumptest.sp | 125 + .../plugins/testsuite/entpropelements.sp | 161 + .../plugins/testsuite/fakenative1.sp | 85 + .../plugins/testsuite/fakenative2.sp | 87 + .../sourcemod/plugins/testsuite/filetest.sp | 73 + .../sourcemod/plugins/testsuite/findmap.sp | 31 + .../sourcemod/plugins/testsuite/floats.sp | 81 + .../sourcemod/plugins/testsuite/fwdtest1.sp | 161 + .../sourcemod/plugins/testsuite/fwdtest2.sp | 42 + .../plugins/testsuite/gamerules-props.sp | 129 + .../sourcemod/plugins/testsuite/goto_test.sp | 28 + .../sourcemod/plugins/testsuite/keyvalues.sp | 58 + .../plugins/testsuite/mapdisplayname.sp | 25 + .../sourcemod/plugins/testsuite/outputtest.sp | 102 + .../sourcemod/plugins/testsuite/ptstest.sp | 26 + .../sourcemod/plugins/testsuite/sorttest.sp | 331 ++ .../sourcemod/plugins/testsuite/sqltest.sp | 318 ++ .../sourcemod/plugins/testsuite/stacktest.sp | 51 + .../sourcemod/plugins/testsuite/structtest.sp | 156 + .../sourcemod/plugins/testsuite/tf2-test.sp | 234 + .../sourcemod/plugins/testsuite/tries.sp | 350 ++ .../sourcemod/scripting/include/ttt.inc | 1320 +++++ .../scripting/include/ttt_bodies.inc | 170 + .../scripting/include/ttt_detective_bans.inc | 68 + .../scripting/include/ttt_discord.inc | 24 + .../sourcemod/scripting/include/ttt_glow.inc | 90 + .../scripting/include/ttt_glow_light.inc | 90 + .../scripting/include/ttt_grabbermod.inc | 70 + .../sourcemod/scripting/include/ttt_haste.inc | 31 + .../sourcemod/scripting/include/ttt_icon.inc | 27 + .../scripting/include/ttt_inventory.inc | 66 + .../scripting/include/ttt_ioncannon.inc | 91 + .../scripting/include/ttt_knockout.inc | 24 + .../scripting/include/ttt_overlay.inc | 36 + .../scripting/include/ttt_playerhud.inc | 25 + .../scripting/include/ttt_ragdoll.inc | 29 + .../sourcemod/scripting/include/ttt_shop.inc | 339 ++ .../sourcemod/scripting/include/ttt_sql.inc | 10 + .../sourcemod/scripting/include/ttt_stock.inc | 34 + .../scripting/include/ttt_talk_override.inc | 20 + .../sourcemod/scripting/include/ttt_taser.inc | 21 + .../scripting/include/ttt_weapons.inc | 44 + .../autoexecconfig/autoexecconfig.inc.json | 873 +++ .../expected/bytebuffer/bytebuffer.inc.json | 374 ++ .../bytebuffer/bytebuffer_es.inc.json | 349 ++ .../includelibrary/include/cURL.inc.json | 2401 ++++++++ .../include/cURL_header.inc.json | 3 + .../includelibrary/include/ccc.inc.json | 641 +++ .../includelibrary/include/cccm.inc.json | 66 + .../include/chat-processor.inc.json | 277 + .../include/morecolors.inc.json | 975 ++++ .../include/steamtools.inc.json | 1987 +++++++ .../include/steamworks.inc.json | 4254 ++++++++++++++ .../include/tf2attributes.inc.json | 889 +++ .../includelibrary/include/tf2items.inc.json | 923 +++ .../include/tf2items_stocks.inc.json | 276 + .../sourcemod/scripting/include/json.inc.json | 439 ++ .../scripting/include/json/array.inc.json | 1541 +++++ .../include/json/definitions.inc.json | 350 ++ .../include/json/helpers/decode.inc.json | 551 ++ .../include/json/helpers/errors.inc.json | 105 + .../json/helpers/metastringmap.inc.json | 663 +++ .../include/json/helpers/string.inc.json | 219 + .../json/helpers/typedstringmap.inc.json | 515 ++ .../include/json/helpers/unicode.inc.json | 159 + .../scripting/include/json/object.inc.json | 1180 ++++ .../sourcemod/scripting/json_test.sp.json | 893 +++ .../scripting/include/multicolors.inc.json | 876 +++ .../include/multicolors/colors.inc.json | 1079 ++++ .../include/multicolors/morecolors.inc.json | 1110 ++++ .../pawn/scripting/include/ripext.inc.json | 20 + .../scripting/include/ripext/http.inc.json | 1828 ++++++ .../scripting/include/ripext/json.inc.json | 2307 ++++++++ .../ripext/pawn/scripting/ripext-test.sp.json | 91 + .../scripting/include/sbpp_server_ip.inc.json | 84 + .../scripting/include/sbpp_version.inc.json | 92 + .../include/sourcebanschecker.inc.json | 168 + .../scripting/include/sourcebanspp.inc.json | 422 ++ .../scripting/include/sourcecomms.inc.json | 654 +++ .../sourcemod/scripting/sbpp_admcfg.sp.json | 171 + .../sourcemod/scripting/sbpp_checker.sp.json | 748 +++ .../sourcemod/scripting/sbpp_comms.sp.json | 3154 ++++++++++ .../sourcemod/scripting/sbpp_main.sp.json | 2341 ++++++++ .../sourcemod/scripting/sbpp_report.sp.json | 321 ++ .../sourcemod/scripting/sbpp_sleuth.sp.json | 402 ++ .../socket/scripting/include/socket.inc.json | 2386 ++++++++ .../client/Source-Chat-Relay.sp.json | 1279 +++++ .../client/include/Source-Chat-Relay.inc.json | 397 ++ .../sourcemod/plugins/admin-flatfile.sp.json | 64 + .../admin-flatfile/admin-groups.sp.json | 261 + .../admin-flatfile/admin-overrides.sp.json | 263 + .../admin-flatfile/admin-simple.sp.json | 70 + .../admin-flatfile/admin-users.sp.json | 191 + .../plugins/admin-sql-prefetch.sp.json | 83 + .../plugins/admin-sql-threaded.sp.json | 499 ++ .../sourcemod/plugins/adminhelp.sp.json | 56 + .../sourcemod/plugins/adminmenu.sp.json | 301 + .../plugins/adminmenu/dynamicmenu.sp.json | 748 +++ .../sourcemod/plugins/antiflood.sp.json | 128 + .../sourcemod/plugins/basebans.sp.json | 254 + .../sourcemod/plugins/basebans/ban.sp.json | 281 + .../sourcemod/plugins/basechat.sp.json | 470 ++ .../sourcemod/plugins/basecomm.sp.json | 311 + .../plugins/basecomm/forwards.sp.json | 59 + .../sourcemod/plugins/basecomm/gag.sp.json | 580 ++ .../plugins/basecomm/natives.sp.json | 107 + .../sourcemod/plugins/basecommands.sp.json | 306 + .../plugins/basecommands/cancelvote.sp.json | 101 + .../plugins/basecommands/execcfg.sp.json | 244 + .../plugins/basecommands/kick.sp.json | 167 + .../plugins/basecommands/map.sp.json | 161 + .../plugins/basecommands/reloadadmins.sp.json | 101 + .../plugins/basecommands/who.sp.json | 173 + .../sourcemod/plugins/basetriggers.sp.json | 488 ++ .../sourcemod/plugins/basevotes.sp.json | 264 + .../plugins/basevotes/voteban.sp.json | 161 + .../plugins/basevotes/votekick.sp.json | 161 + .../plugins/basevotes/votemap.sp.json | 242 + .../sourcemod/plugins/clientprefs.sp.json | 70 + .../sourcemod/plugins/funcommands.sp.json | 189 + .../plugins/funcommands/beacon.sp.json | 232 + .../plugins/funcommands/blind.sp.json | 221 + .../plugins/funcommands/drug.sp.json | 256 + .../plugins/funcommands/fire.sp.json | 388 ++ .../plugins/funcommands/gravity.sp.json | 221 + .../sourcemod/plugins/funcommands/ice.sp.json | 454 ++ .../plugins/funcommands/noclip.sp.json | 161 + .../plugins/funcommands/timebomb.sp.json | 232 + .../sourcemod/plugins/funvotes.sp.json | 196 + .../plugins/funvotes/votealltalk.sp.json | 101 + .../plugins/funvotes/voteburn.sp.json | 167 + .../sourcemod/plugins/funvotes/voteff.sp.json | 101 + .../plugins/funvotes/votegravity.sp.json | 113 + .../plugins/funvotes/voteslay.sp.json | 167 + .../sourcemod/plugins/include/admin.inc.json | 4105 +++++++++++++ .../plugins/include/adminmenu.inc.json | 304 + .../sourcemod/plugins/include/adt.inc.json | 20 + .../plugins/include/adt_array.inc.json | 2173 +++++++ .../plugins/include/adt_stack.inc.json | 1032 ++++ .../plugins/include/adt_trie.inc.json | 2055 +++++++ .../plugins/include/banning.inc.json | 577 ++ .../plugins/include/basecomm.inc.json | 268 + .../plugins/include/bitbuffer.inc.json | 2151 +++++++ .../plugins/include/clientprefs.inc.json | 1551 +++++ .../plugins/include/clients.inc.json | 3206 +++++++++++ .../plugins/include/commandfilters.inc.json | 672 +++ .../plugins/include/commandline.inc.json | 267 + .../plugins/include/console.inc.json | 3281 +++++++++++ .../plugins/include/convars.inc.json | 2320 ++++++++ .../sourcemod/plugins/include/core.inc.json | 796 +++ .../plugins/include/cstrike.inc.json | 2727 +++++++++ .../plugins/include/datapack.inc.json | 1149 ++++ .../sourcemod/plugins/include/dbi.inc.json | 4419 ++++++++++++++ .../sourcemod/plugins/include/dhooks.inc.json | 5056 +++++++++++++++++ .../sourcemod/plugins/include/entity.inc.json | 2987 ++++++++++ .../include/entity_prop_stocks.inc.json | 2225 ++++++++ .../plugins/include/entitylump.inc.json | 601 ++ .../sourcemod/plugins/include/events.inc.json | 1570 +++++ .../sourcemod/plugins/include/files.inc.json | 4057 +++++++++++++ .../sourcemod/plugins/include/float.inc.json | 1344 +++++ .../plugins/include/functions.inc.json | 3007 ++++++++++ .../sourcemod/plugins/include/geoip.inc.json | 831 +++ .../plugins/include/halflife.inc.json | 3156 ++++++++++ .../plugins/include/handles.inc.json | 232 + .../plugins/include/helpers.inc.json | 209 + .../plugins/include/keyvalues.inc.json | 3594 ++++++++++++ .../sourcemod/plugins/include/lang.inc.json | 484 ++ .../plugins/include/logging.inc.json | 440 ++ .../plugins/include/mapchooser.inc.json | 541 ++ .../sourcemod/plugins/include/menus.inc.json | 5041 ++++++++++++++++ .../plugins/include/nextmap.inc.json | 246 + .../plugins/include/profiler.inc.json | 330 ++ .../plugins/include/protobuf.inc.json | 3355 +++++++++++ .../sourcemod/plugins/include/regex.inc.json | 1487 +++++ .../plugins/include/sdkhooks.inc.json | 2679 +++++++++ .../plugins/include/sdktools.inc.json | 1040 ++++ .../plugins/include/sdktools_client.inc.json | 89 + .../plugins/include/sdktools_engine.inc.json | 162 + .../include/sdktools_entinput.inc.json | 99 + .../include/sdktools_entoutput.inc.json | 432 ++ .../include/sdktools_functions.inc.json | 1828 ++++++ .../include/sdktools_gamerules.inc.json | 856 +++ .../plugins/include/sdktools_hooks.inc.json | 516 ++ .../plugins/include/sdktools_sound.inc.json | 3185 +++++++++++ .../plugins/include/sdktools_stocks.inc.json | 55 + .../include/sdktools_stringtables.inc.json | 670 +++ .../include/sdktools_tempents.inc.json | 914 +++ .../include/sdktools_tempents_stocks.inc.json | 1678 ++++++ .../plugins/include/sdktools_trace.inc.json | 3623 ++++++++++++ .../include/sdktools_variant_t.inc.json | 265 + .../plugins/include/sdktools_voice.inc.json | 535 ++ .../plugins/include/sorting.inc.json | 728 +++ .../plugins/include/sourcemod.inc.json | 2853 ++++++++++ .../sourcemod/plugins/include/string.inc.json | 2095 +++++++ .../plugins/include/testing.inc.json | 281 + .../plugins/include/textparse.inc.json | 1280 +++++ .../sourcemod/plugins/include/tf2.inc.json | 3744 ++++++++++++ .../plugins/include/tf2_stocks.inc.json | 2695 +++++++++ .../sourcemod/plugins/include/timers.inc.json | 691 +++ .../plugins/include/topmenus.inc.json | 1542 +++++ .../plugins/include/usermessages.inc.json | 944 +++ .../sourcemod/plugins/include/vector.inc.json | 627 ++ .../plugins/include/version.inc.json | 115 + .../sourcemod/plugins/mapchooser.sp.json | 1049 ++++ .../sourcemod/plugins/nextmap.sp.json | 206 + .../sourcemod/plugins/nominations.sp.json | 306 + .../sourcemod/plugins/playercommands.sp.json | 40 + .../plugins/playercommands/rename.sp.json | 179 + .../plugins/playercommands/slap.sp.json | 221 + .../plugins/playercommands/slay.sp.json | 161 + .../sourcemod/plugins/randomcycle.sp.json | 51 + .../sourcemod/plugins/reservedslots.sp.json | 306 + .../sourcemod/plugins/rockthevote.sp.json | 210 + .../expected/sourcemod/plugins/sounds.sp.json | 46 + .../plugins/sql-admin-manager.sp.json | 490 ++ .../plugins/testsuite/benchmark.sp.json | 171 + .../plugins/testsuite/bug4059.sp.json | 100 + .../plugins/testsuite/callfunctest.sp.json | 172 + .../plugins/testsuite/capstest.sp.json | 76 + .../plugins/testsuite/clientprefstest.sp.json | 112 + .../plugins/testsuite/cstrike-test.sp.json | 262 + .../plugins/testsuite/entitylumptest.sp.json | 156 + .../plugins/testsuite/entpropelements.sp.json | 118 + .../plugins/testsuite/fakenative1.sp.json | 167 + .../plugins/testsuite/fakenative2.sp.json | 250 + .../plugins/testsuite/filetest.sp.json | 64 + .../plugins/testsuite/findmap.sp.json | 40 + .../plugins/testsuite/floats.sp.json | 70 + .../plugins/testsuite/fwdtest1.sp.json | 135 + .../plugins/testsuite/fwdtest2.sp.json | 89 + .../plugins/testsuite/gamerules-props.sp.json | 336 ++ .../plugins/testsuite/goto_test.sp.json | 40 + .../plugins/testsuite/keyvalues.sp.json | 40 + .../plugins/testsuite/mapdisplayname.sp.json | 40 + .../plugins/testsuite/outputtest.sp.json | 69 + .../plugins/testsuite/ptstest.sp.json | 46 + .../plugins/testsuite/sorttest.sp.json | 418 ++ .../plugins/testsuite/sqltest.sp.json | 538 ++ .../plugins/testsuite/stacktest.sp.json | 40 + .../plugins/testsuite/structtest.sp.json | 130 + .../plugins/testsuite/tf2-test.sp.json | 358 ++ .../sourcemod/plugins/testsuite/tries.sp.json | 58 + .../sourcemod/scripting/include/ttt.inc.json | 3618 ++++++++++++ .../scripting/include/ttt_bodies.inc.json | 678 +++ .../include/ttt_detective_bans.inc.json | 246 + .../scripting/include/ttt_discord.inc.json | 56 + .../scripting/include/ttt_glow.inc.json | 356 ++ .../scripting/include/ttt_glow_light.inc.json | 356 ++ .../scripting/include/ttt_grabbermod.inc.json | 200 + .../scripting/include/ttt_haste.inc.json | 97 + .../scripting/include/ttt_icon.inc.json | 116 + .../scripting/include/ttt_inventory.inc.json | 302 + .../scripting/include/ttt_ioncannon.inc.json | 279 + .../scripting/include/ttt_knockout.inc.json | 50 + .../scripting/include/ttt_overlay.inc.json | 174 + .../scripting/include/ttt_playerhud.inc.json | 181 + .../scripting/include/ttt_ragdoll.inc.json | 221 + .../scripting/include/ttt_shop.inc.json | 1459 +++++ .../scripting/include/ttt_sql.inc.json | 74 + .../scripting/include/ttt_stock.inc.json | 112 + .../include/ttt_talk_override.inc.json | 99 + .../scripting/include/ttt_taser.inc.json | 111 + .../scripting/include/ttt_weapons.inc.json | 326 ++ libalternator/tests/extract.rs | 297 + libalternator/tests/update-corpus.sh | 69 + libschema/Cargo.toml | 3 +- libschema/src/symbol/mod.rs | 1 + libschema/src/symbol/type_def.rs | 46 +- 468 files changed, 256843 insertions(+), 1458 deletions(-) delete mode 100644 libalternator/binding/AMBuilder delete mode 100644 libalternator/binding/configure.py delete mode 100644 libalternator/binding/dll_exports.cpp delete mode 100644 libalternator/binding/docparse.cpp delete mode 100644 libalternator/binding/docparse.h delete mode 100644 libalternator/binding/wrapper.h delete mode 100644 libalternator/build.rs create mode 100644 libalternator/examples/dump.rs create mode 100644 libalternator/src/comments.rs create mode 100644 libalternator/src/expr.rs create mode 100644 libalternator/src/extract.rs create mode 100644 libalternator/src/preprocess.rs create mode 100644 libalternator/tests/corpus.rs create mode 100644 libalternator/tests/corpus/SOURCES.md create mode 100644 libalternator/tests/corpus/autoexecconfig/autoexecconfig.inc create mode 100644 libalternator/tests/corpus/bytebuffer/bytebuffer.inc create mode 100644 libalternator/tests/corpus/bytebuffer/bytebuffer_es.inc create mode 100644 libalternator/tests/corpus/includelibrary/include/cURL.inc create mode 100644 libalternator/tests/corpus/includelibrary/include/cURL_header.inc create mode 100644 libalternator/tests/corpus/includelibrary/include/ccc.inc create mode 100644 libalternator/tests/corpus/includelibrary/include/cccm.inc create mode 100644 libalternator/tests/corpus/includelibrary/include/chat-processor.inc create mode 100644 libalternator/tests/corpus/includelibrary/include/morecolors.inc create mode 100644 libalternator/tests/corpus/includelibrary/include/steamtools.inc create mode 100644 libalternator/tests/corpus/includelibrary/include/steamworks.inc create mode 100644 libalternator/tests/corpus/includelibrary/include/tf2attributes.inc create mode 100644 libalternator/tests/corpus/includelibrary/include/tf2items.inc create mode 100644 libalternator/tests/corpus/includelibrary/include/tf2items_stocks.inc create mode 100644 libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json.inc create mode 100644 libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/array.inc create mode 100644 libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/definitions.inc create mode 100644 libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/helpers/decode.inc create mode 100644 libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/helpers/errors.inc create mode 100644 libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/helpers/metastringmap.inc create mode 100644 libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/helpers/string.inc create mode 100644 libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/helpers/typedstringmap.inc create mode 100644 libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/helpers/unicode.inc create mode 100644 libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/object.inc create mode 100644 libalternator/tests/corpus/json/addons/sourcemod/scripting/json_test.sp create mode 100644 libalternator/tests/corpus/multicolors/addons/sourcemod/scripting/include/multicolors.inc create mode 100644 libalternator/tests/corpus/multicolors/addons/sourcemod/scripting/include/multicolors/colors.inc create mode 100644 libalternator/tests/corpus/multicolors/addons/sourcemod/scripting/include/multicolors/morecolors.inc create mode 100644 libalternator/tests/corpus/ripext/pawn/scripting/include/ripext.inc create mode 100644 libalternator/tests/corpus/ripext/pawn/scripting/include/ripext/http.inc create mode 100644 libalternator/tests/corpus/ripext/pawn/scripting/include/ripext/json.inc create mode 100644 libalternator/tests/corpus/ripext/pawn/scripting/ripext-test.sp create mode 100644 libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/include/sbpp_server_ip.inc create mode 100644 libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/include/sbpp_version.inc create mode 100644 libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/include/sourcebanschecker.inc create mode 100644 libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/include/sourcebanspp.inc create mode 100644 libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/include/sourcecomms.inc create mode 100644 libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/sbpp_admcfg.sp create mode 100644 libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/sbpp_checker.sp create mode 100644 libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/sbpp_comms.sp create mode 100644 libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/sbpp_main.sp create mode 100644 libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/sbpp_report.sp create mode 100644 libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/sbpp_sleuth.sp create mode 100644 libalternator/tests/corpus/socket/scripting/include/socket.inc create mode 100644 libalternator/tests/corpus/source-chat-relay/client/Source-Chat-Relay.sp create mode 100644 libalternator/tests/corpus/source-chat-relay/client/include/Source-Chat-Relay.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/admin-flatfile.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/admin-flatfile/admin-groups.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/admin-flatfile/admin-overrides.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/admin-flatfile/admin-simple.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/admin-flatfile/admin-users.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/admin-sql-prefetch.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/admin-sql-threaded.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/adminhelp.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/adminmenu.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/adminmenu/dynamicmenu.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/antiflood.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/basebans.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/basebans/ban.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/basechat.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/basecomm.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/basecomm/forwards.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/basecomm/gag.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/basecomm/natives.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/basecommands.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/basecommands/cancelvote.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/basecommands/execcfg.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/basecommands/kick.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/basecommands/map.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/basecommands/reloadadmins.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/basecommands/who.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/basetriggers.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/basevotes.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/basevotes/voteban.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/basevotes/votekick.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/basevotes/votemap.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/clientprefs.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/funcommands.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/funcommands/beacon.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/funcommands/blind.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/funcommands/drug.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/funcommands/fire.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/funcommands/gravity.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/funcommands/ice.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/funcommands/noclip.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/funcommands/timebomb.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/funvotes.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/funvotes/votealltalk.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/funvotes/voteburn.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/funvotes/voteff.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/funvotes/votegravity.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/funvotes/voteslay.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/admin.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/adminmenu.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/adt.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/adt_array.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/adt_stack.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/adt_trie.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/banning.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/basecomm.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/bitbuffer.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/clientprefs.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/clients.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/commandfilters.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/commandline.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/console.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/convars.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/core.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/cstrike.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/datapack.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/dbi.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/dhooks.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/entity.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/entity_prop_stocks.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/entitylump.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/events.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/files.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/float.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/functions.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/geoip.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/halflife.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/handles.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/helpers.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/keyvalues.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/lang.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/logging.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/mapchooser.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/menus.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/nextmap.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/profiler.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/protobuf.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/regex.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/sdkhooks.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/sdktools.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/sdktools_client.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/sdktools_engine.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/sdktools_entinput.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/sdktools_entoutput.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/sdktools_functions.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/sdktools_gamerules.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/sdktools_hooks.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/sdktools_sound.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/sdktools_stocks.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/sdktools_stringtables.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/sdktools_tempents.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/sdktools_tempents_stocks.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/sdktools_trace.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/sdktools_variant_t.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/sdktools_voice.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/sorting.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/sourcemod.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/string.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/testing.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/textparse.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/tf2.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/tf2_stocks.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/timers.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/topmenus.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/usermessages.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/vector.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/include/version.inc create mode 100644 libalternator/tests/corpus/sourcemod/plugins/mapchooser.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/nextmap.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/nominations.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/playercommands.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/playercommands/rename.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/playercommands/slap.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/playercommands/slay.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/randomcycle.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/reservedslots.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/rockthevote.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/sounds.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/sql-admin-manager.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/benchmark.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/bug4059.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/callfunctest.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/capstest.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/clientprefstest.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/cstrike-test.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/entitylumptest.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/entpropelements.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/fakenative1.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/fakenative2.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/filetest.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/findmap.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/floats.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/fwdtest1.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/fwdtest2.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/gamerules-props.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/goto_test.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/keyvalues.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/mapdisplayname.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/outputtest.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/ptstest.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/sorttest.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/sqltest.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/stacktest.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/structtest.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/tf2-test.sp create mode 100644 libalternator/tests/corpus/sourcemod/plugins/testsuite/tries.sp create mode 100644 libalternator/tests/corpus/ttt/addons/sourcemod/scripting/include/ttt.inc create mode 100644 libalternator/tests/corpus/ttt/addons/sourcemod/scripting/include/ttt_bodies.inc create mode 100644 libalternator/tests/corpus/ttt/addons/sourcemod/scripting/include/ttt_detective_bans.inc create mode 100644 libalternator/tests/corpus/ttt/addons/sourcemod/scripting/include/ttt_discord.inc create mode 100644 libalternator/tests/corpus/ttt/addons/sourcemod/scripting/include/ttt_glow.inc create mode 100644 libalternator/tests/corpus/ttt/addons/sourcemod/scripting/include/ttt_glow_light.inc create mode 100644 libalternator/tests/corpus/ttt/addons/sourcemod/scripting/include/ttt_grabbermod.inc create mode 100644 libalternator/tests/corpus/ttt/addons/sourcemod/scripting/include/ttt_haste.inc create mode 100644 libalternator/tests/corpus/ttt/addons/sourcemod/scripting/include/ttt_icon.inc create mode 100644 libalternator/tests/corpus/ttt/addons/sourcemod/scripting/include/ttt_inventory.inc create mode 100644 libalternator/tests/corpus/ttt/addons/sourcemod/scripting/include/ttt_ioncannon.inc create mode 100644 libalternator/tests/corpus/ttt/addons/sourcemod/scripting/include/ttt_knockout.inc create mode 100644 libalternator/tests/corpus/ttt/addons/sourcemod/scripting/include/ttt_overlay.inc create mode 100644 libalternator/tests/corpus/ttt/addons/sourcemod/scripting/include/ttt_playerhud.inc create mode 100644 libalternator/tests/corpus/ttt/addons/sourcemod/scripting/include/ttt_ragdoll.inc create mode 100644 libalternator/tests/corpus/ttt/addons/sourcemod/scripting/include/ttt_shop.inc create mode 100644 libalternator/tests/corpus/ttt/addons/sourcemod/scripting/include/ttt_sql.inc create mode 100644 libalternator/tests/corpus/ttt/addons/sourcemod/scripting/include/ttt_stock.inc create mode 100644 libalternator/tests/corpus/ttt/addons/sourcemod/scripting/include/ttt_talk_override.inc create mode 100644 libalternator/tests/corpus/ttt/addons/sourcemod/scripting/include/ttt_taser.inc create mode 100644 libalternator/tests/corpus/ttt/addons/sourcemod/scripting/include/ttt_weapons.inc create mode 100644 libalternator/tests/expected/autoexecconfig/autoexecconfig.inc.json create mode 100644 libalternator/tests/expected/bytebuffer/bytebuffer.inc.json create mode 100644 libalternator/tests/expected/bytebuffer/bytebuffer_es.inc.json create mode 100644 libalternator/tests/expected/includelibrary/include/cURL.inc.json create mode 100644 libalternator/tests/expected/includelibrary/include/cURL_header.inc.json create mode 100644 libalternator/tests/expected/includelibrary/include/ccc.inc.json create mode 100644 libalternator/tests/expected/includelibrary/include/cccm.inc.json create mode 100644 libalternator/tests/expected/includelibrary/include/chat-processor.inc.json create mode 100644 libalternator/tests/expected/includelibrary/include/morecolors.inc.json create mode 100644 libalternator/tests/expected/includelibrary/include/steamtools.inc.json create mode 100644 libalternator/tests/expected/includelibrary/include/steamworks.inc.json create mode 100644 libalternator/tests/expected/includelibrary/include/tf2attributes.inc.json create mode 100644 libalternator/tests/expected/includelibrary/include/tf2items.inc.json create mode 100644 libalternator/tests/expected/includelibrary/include/tf2items_stocks.inc.json create mode 100644 libalternator/tests/expected/json/addons/sourcemod/scripting/include/json.inc.json create mode 100644 libalternator/tests/expected/json/addons/sourcemod/scripting/include/json/array.inc.json create mode 100644 libalternator/tests/expected/json/addons/sourcemod/scripting/include/json/definitions.inc.json create mode 100644 libalternator/tests/expected/json/addons/sourcemod/scripting/include/json/helpers/decode.inc.json create mode 100644 libalternator/tests/expected/json/addons/sourcemod/scripting/include/json/helpers/errors.inc.json create mode 100644 libalternator/tests/expected/json/addons/sourcemod/scripting/include/json/helpers/metastringmap.inc.json create mode 100644 libalternator/tests/expected/json/addons/sourcemod/scripting/include/json/helpers/string.inc.json create mode 100644 libalternator/tests/expected/json/addons/sourcemod/scripting/include/json/helpers/typedstringmap.inc.json create mode 100644 libalternator/tests/expected/json/addons/sourcemod/scripting/include/json/helpers/unicode.inc.json create mode 100644 libalternator/tests/expected/json/addons/sourcemod/scripting/include/json/object.inc.json create mode 100644 libalternator/tests/expected/json/addons/sourcemod/scripting/json_test.sp.json create mode 100644 libalternator/tests/expected/multicolors/addons/sourcemod/scripting/include/multicolors.inc.json create mode 100644 libalternator/tests/expected/multicolors/addons/sourcemod/scripting/include/multicolors/colors.inc.json create mode 100644 libalternator/tests/expected/multicolors/addons/sourcemod/scripting/include/multicolors/morecolors.inc.json create mode 100644 libalternator/tests/expected/ripext/pawn/scripting/include/ripext.inc.json create mode 100644 libalternator/tests/expected/ripext/pawn/scripting/include/ripext/http.inc.json create mode 100644 libalternator/tests/expected/ripext/pawn/scripting/include/ripext/json.inc.json create mode 100644 libalternator/tests/expected/ripext/pawn/scripting/ripext-test.sp.json create mode 100644 libalternator/tests/expected/sbpp/game/addons/sourcemod/scripting/include/sbpp_server_ip.inc.json create mode 100644 libalternator/tests/expected/sbpp/game/addons/sourcemod/scripting/include/sbpp_version.inc.json create mode 100644 libalternator/tests/expected/sbpp/game/addons/sourcemod/scripting/include/sourcebanschecker.inc.json create mode 100644 libalternator/tests/expected/sbpp/game/addons/sourcemod/scripting/include/sourcebanspp.inc.json create mode 100644 libalternator/tests/expected/sbpp/game/addons/sourcemod/scripting/include/sourcecomms.inc.json create mode 100644 libalternator/tests/expected/sbpp/game/addons/sourcemod/scripting/sbpp_admcfg.sp.json create mode 100644 libalternator/tests/expected/sbpp/game/addons/sourcemod/scripting/sbpp_checker.sp.json create mode 100644 libalternator/tests/expected/sbpp/game/addons/sourcemod/scripting/sbpp_comms.sp.json create mode 100644 libalternator/tests/expected/sbpp/game/addons/sourcemod/scripting/sbpp_main.sp.json create mode 100644 libalternator/tests/expected/sbpp/game/addons/sourcemod/scripting/sbpp_report.sp.json create mode 100644 libalternator/tests/expected/sbpp/game/addons/sourcemod/scripting/sbpp_sleuth.sp.json create mode 100644 libalternator/tests/expected/socket/scripting/include/socket.inc.json create mode 100644 libalternator/tests/expected/source-chat-relay/client/Source-Chat-Relay.sp.json create mode 100644 libalternator/tests/expected/source-chat-relay/client/include/Source-Chat-Relay.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/admin-flatfile.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/admin-flatfile/admin-groups.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/admin-flatfile/admin-overrides.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/admin-flatfile/admin-simple.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/admin-flatfile/admin-users.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/admin-sql-prefetch.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/admin-sql-threaded.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/adminhelp.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/adminmenu.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/adminmenu/dynamicmenu.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/antiflood.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/basebans.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/basebans/ban.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/basechat.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/basecomm.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/basecomm/forwards.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/basecomm/gag.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/basecomm/natives.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/basecommands.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/basecommands/cancelvote.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/basecommands/execcfg.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/basecommands/kick.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/basecommands/map.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/basecommands/reloadadmins.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/basecommands/who.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/basetriggers.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/basevotes.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/basevotes/voteban.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/basevotes/votekick.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/basevotes/votemap.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/clientprefs.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/funcommands.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/funcommands/beacon.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/funcommands/blind.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/funcommands/drug.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/funcommands/fire.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/funcommands/gravity.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/funcommands/ice.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/funcommands/noclip.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/funcommands/timebomb.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/funvotes.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/funvotes/votealltalk.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/funvotes/voteburn.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/funvotes/voteff.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/funvotes/votegravity.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/funvotes/voteslay.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/admin.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/adminmenu.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/adt.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/adt_array.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/adt_stack.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/adt_trie.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/banning.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/basecomm.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/bitbuffer.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/clientprefs.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/clients.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/commandfilters.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/commandline.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/console.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/convars.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/core.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/cstrike.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/datapack.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/dbi.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/dhooks.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/entity.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/entity_prop_stocks.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/entitylump.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/events.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/files.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/float.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/functions.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/geoip.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/halflife.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/handles.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/helpers.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/keyvalues.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/lang.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/logging.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/mapchooser.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/menus.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/nextmap.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/profiler.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/protobuf.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/regex.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/sdkhooks.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/sdktools.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/sdktools_client.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/sdktools_engine.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/sdktools_entinput.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/sdktools_entoutput.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/sdktools_functions.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/sdktools_gamerules.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/sdktools_hooks.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/sdktools_sound.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/sdktools_stocks.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/sdktools_stringtables.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/sdktools_tempents.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/sdktools_tempents_stocks.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/sdktools_trace.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/sdktools_variant_t.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/sdktools_voice.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/sorting.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/sourcemod.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/string.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/testing.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/textparse.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/tf2.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/tf2_stocks.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/timers.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/topmenus.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/usermessages.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/vector.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/include/version.inc.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/mapchooser.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/nextmap.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/nominations.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/playercommands.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/playercommands/rename.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/playercommands/slap.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/playercommands/slay.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/randomcycle.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/reservedslots.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/rockthevote.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/sounds.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/sql-admin-manager.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/benchmark.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/bug4059.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/callfunctest.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/capstest.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/clientprefstest.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/cstrike-test.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/entitylumptest.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/entpropelements.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/fakenative1.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/fakenative2.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/filetest.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/findmap.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/floats.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/fwdtest1.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/fwdtest2.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/gamerules-props.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/goto_test.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/keyvalues.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/mapdisplayname.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/outputtest.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/ptstest.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/sorttest.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/sqltest.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/stacktest.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/structtest.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/tf2-test.sp.json create mode 100644 libalternator/tests/expected/sourcemod/plugins/testsuite/tries.sp.json create mode 100644 libalternator/tests/expected/ttt/addons/sourcemod/scripting/include/ttt.inc.json create mode 100644 libalternator/tests/expected/ttt/addons/sourcemod/scripting/include/ttt_bodies.inc.json create mode 100644 libalternator/tests/expected/ttt/addons/sourcemod/scripting/include/ttt_detective_bans.inc.json create mode 100644 libalternator/tests/expected/ttt/addons/sourcemod/scripting/include/ttt_discord.inc.json create mode 100644 libalternator/tests/expected/ttt/addons/sourcemod/scripting/include/ttt_glow.inc.json create mode 100644 libalternator/tests/expected/ttt/addons/sourcemod/scripting/include/ttt_glow_light.inc.json create mode 100644 libalternator/tests/expected/ttt/addons/sourcemod/scripting/include/ttt_grabbermod.inc.json create mode 100644 libalternator/tests/expected/ttt/addons/sourcemod/scripting/include/ttt_haste.inc.json create mode 100644 libalternator/tests/expected/ttt/addons/sourcemod/scripting/include/ttt_icon.inc.json create mode 100644 libalternator/tests/expected/ttt/addons/sourcemod/scripting/include/ttt_inventory.inc.json create mode 100644 libalternator/tests/expected/ttt/addons/sourcemod/scripting/include/ttt_ioncannon.inc.json create mode 100644 libalternator/tests/expected/ttt/addons/sourcemod/scripting/include/ttt_knockout.inc.json create mode 100644 libalternator/tests/expected/ttt/addons/sourcemod/scripting/include/ttt_overlay.inc.json create mode 100644 libalternator/tests/expected/ttt/addons/sourcemod/scripting/include/ttt_playerhud.inc.json create mode 100644 libalternator/tests/expected/ttt/addons/sourcemod/scripting/include/ttt_ragdoll.inc.json create mode 100644 libalternator/tests/expected/ttt/addons/sourcemod/scripting/include/ttt_shop.inc.json create mode 100644 libalternator/tests/expected/ttt/addons/sourcemod/scripting/include/ttt_sql.inc.json create mode 100644 libalternator/tests/expected/ttt/addons/sourcemod/scripting/include/ttt_stock.inc.json create mode 100644 libalternator/tests/expected/ttt/addons/sourcemod/scripting/include/ttt_talk_override.inc.json create mode 100644 libalternator/tests/expected/ttt/addons/sourcemod/scripting/include/ttt_taser.inc.json create mode 100644 libalternator/tests/expected/ttt/addons/sourcemod/scripting/include/ttt_weapons.inc.json create mode 100644 libalternator/tests/extract.rs create mode 100755 libalternator/tests/update-corpus.sh diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 66c8142..9530ee4 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -1,95 +1,29 @@ -name: Build libraries & docgen +name: Build docgen -on: [push] +on: [push, pull_request] jobs: - libraries: - name: SP Libraries + test: + name: Test - strategy: - matrix: - os: [windows-latest, ubuntu-latest] - - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest steps: - - name: Setup Python environment - uses: actions/setup-python@v2 - - name: Checkout generator - uses: actions/checkout@v2 - with: - path: docgen - - - name: Checkout SourcePawn - uses: actions/checkout@v2 - with: - path: sourcepawn - repository: SlidyBat/sourcepawn - ref: enum-struct-docgen - - - name: Checkout SP Submodules - shell: bash - working-directory: sourcepawn - run: | - auth_header="$(git config --local --get http.https://github.com/.extraheader)" - git submodule sync --recursive - git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1 - - - name: Checkout AMbuild - uses: actions/checkout@v2 - with: - path: ambuild - repository: alliedmodders/ambuild - - - name: Install AMBuild (Win/Mac) - working-directory: ambuild - if: ${{ matrix.os }} == windows-latest || ${{ matrix.os }} == macos-latest - run: | - python setup.py install - - - name: Install AMBuild (Linux) - working-directory: ambuild - if: ${{ matrix.os }} == ubuntu-latest - run: | - python setup.py install --user - - - name: Copy binding helpers - run: | - cp docgen/libalternator/binding/configure.py sourcepawn/ - cp docgen/libalternator/binding/AMBuilder sourcepawn/exp/tools/docparse/ - cp docgen/libalternator/binding/docparse.cpp sourcepawn/exp/tools/docparse/ - cp docgen/libalternator/binding/docparse.h sourcepawn/exp/tools/docparse/ - cp docgen/libalternator/binding/dll_exports.cpp sourcepawn/exp/compiler/ + uses: actions/checkout@v4 - - name: Create build directory - working-directory: sourcepawn - run: | - mkdir build + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable - - name: Build libraries - working-directory: sourcepawn/build - # env: - # CC: gcc - # CXX: g++ - run: | - python ../configure.py --build exp --enable-optimize - ambuild + - name: Cache cargo + uses: Swatinem/rust-cache@v2 - - name: Package libraries - working-directory: sourcepawn - run: cd build/ && tar -zcvf ../build.tar.gz * && cd - + - name: Test parser and schema + run: cargo test -p alternator -p schema - - name: Upload libraries - uses: actions/upload-artifact@v2 - with: - name: libraries-${{ matrix.os }} - path: sourcepawn/build.tar.gz generator: name: DocGen - needs: libraries - strategy: matrix: os: [windows-latest, ubuntu-latest] @@ -98,56 +32,22 @@ jobs: steps: - name: Checkout generator - uses: actions/checkout@v2 - with: - path: docgen + uses: actions/checkout@v4 - name: Install Rust stable - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - - - name: Download libraries - uses: actions/download-artifact@v1 - with: - name: libraries-${{ matrix.os }} - path: libraries + uses: dtolnay/rust-toolchain@stable - - name: Extract libraries - run: | - mkdir docgen/libalternator/libraries - tar -xzf libraries/build.tar.gz -C docgen/libalternator/libraries - - - name: Install LLVM and Clang # required for bindgen to work, see https://github.com/rust-lang/rust-bindgen/issues/1797 - uses: KyleMayes/install-llvm-action@v1.5.0 - if: matrix.os == 'windows-latest' - with: - version: "11.0" - directory: ${{ runner.temp }}/llvm - - - name: Set LIBCLANG_PATH - run: echo "LIBCLANG_PATH=$((gcm clang).source -replace "clang.exe")" >> $env:GITHUB_ENV - if: matrix.os == 'windows-latest' - - - name: Cache cargo registry - uses: actions/cache@v1 - with: - path: ~/.cargo/registry - key: ${{ runner.os }}-cargo-registry-${{ hashFiles('docgen/Cargo.lock') }} - - - name: Cache cargo build - uses: actions/cache@v1 - with: - path: docgen/target - key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('docgen/Cargo.lock') }} + - name: Cache cargo + uses: Swatinem/rust-cache@v2 - - name: Build crates - working-directory: docgen - run: cargo build --release + - name: Build chumbucket + run: cargo build --release -p chumbucket - name: Upload docgen - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: docgen-${{ matrix.os }} - path: docgen/target/release/chumbucket* + path: | + target/release/chumbucket + target/release/chumbucket.exe + if-no-files-found: error diff --git a/.gitignore b/.gitignore index 7682df7..89bd426 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ target .vscode **/node_modules -libalternator/libraries libwalker/tests/contents/* edge_worker/bin/ diff --git a/Cargo.lock b/Cargo.lock index 73e6451..9ef7ab5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,35 +1,27 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aho-corasick" -version = "0.7.18" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] [[package]] name = "alternator" -version = "0.2.0" +version = "0.3.0" dependencies = [ - "bindgen", "schema", "serde", "serde_json", "spdcp", "thiserror", -] - -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", + "tree-sitter", + "tree-sitter-sourcepawn", ] [[package]] @@ -55,29 +47,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" -[[package]] -name = "bindgen" -version = "0.59.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "clap 2.34.0", - "env_logger", - "lazy_static", - "lazycell", - "log", - "peeking_take_while", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "which", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -98,20 +67,14 @@ checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cc" -version = "1.0.72" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" +checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" dependencies = [ + "find-msvc-tools", "jobserver", -] - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", + "libc", + "shlex", ] [[package]] @@ -132,7 +95,7 @@ version = "0.4.2" dependencies = [ "alternator", "anyhow", - "clap 3.0.10", + "clap", "git2", "schema", "serde", @@ -142,32 +105,6 @@ dependencies = [ "walker", ] -[[package]] -name = "clang-sys" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa66045b9cb23c2e9c1520732030608b02ee07e5cfaa5a521ec15ded7fa24c90" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "clap" -version = "2.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" -dependencies = [ - "ansi_term", - "atty", - "bitflags", - "strsim 0.8.0", - "textwrap 0.11.0", - "unicode-width", - "vec_map", -] - [[package]] name = "clap" version = "3.0.10" @@ -176,12 +113,12 @@ checksum = "7a30c3bf9ff12dfe5dae53f0a96e0febcd18420d1c0e7fad77796d9d5c4b5375" dependencies = [ "atty", "bitflags", - "indexmap", + "indexmap 1.8.0", "lazy_static", "os_str_bytes", - "strsim 0.10.0", + "strsim", "termcolor", - "textwrap 0.14.2", + "textwrap", ] [[package]] @@ -206,23 +143,16 @@ dependencies = [ ] [[package]] -name = "either" -version = "1.6.1" +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] -name = "env_logger" -version = "0.9.0" +name = "find-msvc-tools" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] +checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" [[package]] name = "form_urlencoded" @@ -234,6 +164,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "r-efi", +] + [[package]] name = "git2" version = "0.13.25" @@ -250,16 +191,16 @@ dependencies = [ ] [[package]] -name = "glob" -version = "0.3.0" +name = "hashbrown" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hermit-abi" @@ -270,12 +211,6 @@ dependencies = [ "libc", ] -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "idna" version = "0.2.3" @@ -294,7 +229,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.11.2", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", ] [[package]] @@ -314,10 +259,11 @@ checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" [[package]] name = "jobserver" -version = "0.1.24" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ + "getrandom", "libc", ] @@ -336,17 +282,11 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "libc" -version = "0.2.113" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eef78b64d87775463c549fbd80e19249ef436ea3bf1de2a1eb7e717ec7fab1e9" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libgit2-sys" @@ -362,16 +302,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "libloading" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" -dependencies = [ - "cfg-if 1.0.0", - "winapi", -] - [[package]] name = "libssh2-sys" version = "0.2.23" @@ -424,9 +354,9 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "memchr" -version = "2.4.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memory_units" @@ -434,12 +364,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "mio" version = "0.7.14" @@ -462,17 +386,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "nom" -version = "7.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109" -dependencies = [ - "memchr", - "minimal-lexical", - "version_check", -] - [[package]] name = "ntapi" version = "0.3.6" @@ -551,12 +464,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - [[package]] name = "percent-encoding" version = "2.1.0" @@ -577,22 +484,28 @@ checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" [[package]] name = "proc-macro2" -version = "1.0.36" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.14" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "redox_syscall" version = "0.2.10" @@ -604,32 +517,32 @@ dependencies = [ [[package]] name = "regex" -version = "1.5.4" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", + "regex-automata", "regex-syntax", ] [[package]] -name = "regex-syntax" -version = "0.6.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" - -[[package]] -name = "rustc-hash" -version = "1.1.0" +name = "regex-automata" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] [[package]] -name = "ryu" -version = "1.0.9" +name = "regex-syntax" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "schema" @@ -653,40 +566,53 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.133" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.133" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.6", ] [[package]] name = "serde_json" -version = "1.0.75" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c059c05b48c5c0067d4b4b2b4f0732dd65feb52daf7e0ea09cd87e7dadc1af79" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ + "indexmap 2.14.2", "itoa", - "ryu", + "memchr", "serde", + "serde_core", + "zmij", ] [[package]] name = "shlex" -version = "1.1.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook-registry" @@ -705,18 +631,17 @@ checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" [[package]] name = "spdcp" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25fe37403addc7482e82377ffa07e2d4a23efc47a6e2dc30c49a48218500f775" +version = "0.5.0" +source = "git+https://github.com/rumblefrog/sp-dcp?rev=584897c42a6e011d3c92e3e0db2f78f24de2f5f8#584897c42a6e011d3c92e3e0db2f78f24de2f5f8" dependencies = [ "serde", ] [[package]] -name = "strsim" -version = "0.8.0" +name = "streaming-iterator" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" [[package]] name = "strsim" @@ -736,21 +661,23 @@ dependencies = [ ] [[package]] -name = "termcolor" -version = "1.1.2" +name = "syn" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" dependencies = [ - "winapi-util", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] -name = "textwrap" -version = "0.11.0" +name = "termcolor" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" dependencies = [ - "unicode-width", + "winapi-util", ] [[package]] @@ -776,7 +703,7 @@ checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.86", ] [[package]] @@ -821,7 +748,7 @@ checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.86", ] [[package]] @@ -833,12 +760,48 @@ dependencies = [ "serde", ] +[[package]] +name = "tree-sitter" +version = "0.26.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ebdd3a5a7e28a1890b876fdbd0c3c0fe0a6336cffaa104f11b9f720c9daa29" +dependencies = [ + "cc", + "regex", + "regex-syntax", + "serde_json", + "streaming-iterator", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0d1bf6fdd806e43ae5198f82f527056d359def39e54e67a0f478ac09dac081" + +[[package]] +name = "tree-sitter-sourcepawn" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ead5d013a58791643c156b8b081e52262dbd30cd50297c14554852f41348bd49" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "unicode-bidi" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + [[package]] name = "unicode-normalization" version = "0.1.19" @@ -848,12 +811,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-width" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" - [[package]] name = "unicode-xid" version = "0.2.2" @@ -878,18 +835,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - [[package]] name = "walker" version = "0.1.1" @@ -919,7 +864,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 1.0.86", "wasm-bindgen-shared", ] @@ -953,7 +898,7 @@ checksum = "841a6d1c35c6f596ccea1f82504a192a60378f64b3bb0261904ad8f2f5657556" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.86", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1010,17 +955,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "which" -version = "4.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea187a8ef279bc014ec368c27a920da2024d2a711109bfbe3440585d5cf27ad9" -dependencies = [ - "either", - "lazy_static", - "libc", -] - [[package]] name = "winapi" version = "0.3.9" @@ -1051,3 +985,9 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/README.md b/README.md index d2dba11..d261038 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A documentation generator for SourcePawn includes - `chumbucket` - Chums through manifest and include files - `edge_worker` - CloudFlare edge server worker for modifying content title and description -- `libalternator` - Parser and binder to SourcePawn v2 compiler +- `libalternator` - SourcePawn include parser built on [tree-sitter-sourcepawn](https://github.com/nilshelmig/tree-sitter-sourcepawn) - `libschema` Shared types between documentation components - `libwalker` Git history traversal/walker for sources - `schema` - Typescript library of mostly symbol classes/interfaces for UI @@ -14,3 +14,30 @@ A documentation generator for SourcePawn includes ## Download You could download the latest Linux & Windows chumbucket from the artifacts of each commit. Mac builds are currently not being automatically built. + +## Parser + +`libalternator` parses one include (or plugin) file at a time into a strand of +documented symbols. It runs a small preprocessor (conditional compilation, +`#define`s, `#endinput`) and then walks the tree-sitter syntax tree. Doc +comments are attached to declarations the same way the old SourcePawn +`docparse` tool did, and parsed with [spdcp](https://github.com/rumblefrog/sp-dcp). + +Dump the strand of a file: + +```sh +cargo run -p alternator --example dump -- path/to/file.inc +``` + +### Tests + +```sh +cargo test -p alternator -p schema +``` + +`libalternator/tests/corpus` holds upstream SourceMod and third-party sources +(see its `SOURCES.md`), and `libalternator/tests/expected` their expected +output. After an intended output change, update the snapshots with +`ALTERNATOR_BLESS=1 cargo test -p alternator --test corpus` and review the +diff. To pull newer upstream sources, bump the commits in +`libalternator/tests/update-corpus.sh` and re-run it. diff --git a/libalternator/Cargo.toml b/libalternator/Cargo.toml index a5e2489..36aaf12 100644 --- a/libalternator/Cargo.toml +++ b/libalternator/Cargo.toml @@ -1,17 +1,15 @@ [package] name = "alternator" -version = "0.2.0" +version = "0.3.0" authors = ["rumblefrog "] edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +description = "SourcePawn include parser producing documentation strands" [dependencies] schema = { path = "../libschema" } -spdcp = { version = "0.4.4", features = ["serde"]} serde = { version = "1", features = ["derive"] } serde_json = "1" +spdcp = { git = "https://github.com/rumblefrog/sp-dcp", rev = "584897c42a6e011d3c92e3e0db2f78f24de2f5f8", features = ["serde"] } thiserror = "1" - -[build-dependencies] -bindgen = "0.59" +tree-sitter = "0.26" +tree-sitter-sourcepawn = "=0.8.0" diff --git a/libalternator/binding/AMBuilder b/libalternator/binding/AMBuilder deleted file mode 100644 index f54f1e3..0000000 --- a/libalternator/binding/AMBuilder +++ /dev/null @@ -1,21 +0,0 @@ -import os - -lib = Root.StaticLibrary(builder, 'docparse', arch) -lib.compiler.cxxincludes += [ - os.path.join(builder.sourcePath), - os.path.join(builder.sourcePath, 'exp'), - os.path.join(builder.sourcePath, 'exp/compiler'), -] - -if lib.compiler.like('msvc'): - lib.compiler.postlink += ['/SUBSYSTEM:CONSOLE'] - -lib.sources += [ - 'docparse.cpp', -] -lib.compiler.linkflags += [ - SP.libspcomp2[arch].binary, - SP.libsmx[arch].binary, - SP.libamtl[arch], -] -builder.Add(lib) diff --git a/libalternator/binding/configure.py b/libalternator/binding/configure.py deleted file mode 100644 index c763e6e..0000000 --- a/libalternator/binding/configure.py +++ /dev/null @@ -1,31 +0,0 @@ -import sys -try: - from ambuild2 import run, util -except: - try: - import ambuild - sys.stderr.write('It looks like you have AMBuild 1 installed, but this project uses AMBuild 2.\n') - sys.stderr.write('Upgrade to the latest version of AMBuild to continue.\n') - except: - sys.stderr.write('AMBuild must be installed to build this project.\n') - sys.stderr.write('http://www.alliedmods.net/ambuild\n') - sys.exit(1) - -def make_objdir_name(p): - return 'obj-' + util.Platform() + '-' + p.target_arch - -parser = run.BuildParser(sourcePath=sys.path[0], api='2.1') -parser.default_arch = 'x64' -parser.default_build_folder = make_objdir_name -parser.options.add_option('--enable-debug', action='store_const', const='1', dest='debug', - help='Enable debugging symbols') -parser.options.add_option('--enable-optimize', action='store_const', const='1', dest='opt', - help='Enable optimization') -parser.options.add_option('--amtl', type='string', dest='amtl', default=None, help='Custom AMTL path') -parser.options.add_option('--build', type='string', dest='build', default='all', - help='Build which components (all, spcomp, vm, exp, test, core)') -parser.options.add_option('--enable-spew', action='store_true', default=False, dest='enable_spew', - help='Enable debug spew') -parser.options.add_option("--enable-coverage", action='store_true', default=False, - dest='enable_coverage', help='Enable code coverage support.') -parser.Configure() \ No newline at end of file diff --git a/libalternator/binding/dll_exports.cpp b/libalternator/binding/dll_exports.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/libalternator/binding/docparse.cpp b/libalternator/binding/docparse.cpp deleted file mode 100644 index e7d57b7..0000000 --- a/libalternator/binding/docparse.cpp +++ /dev/null @@ -1,705 +0,0 @@ -// vim: set ts=2 sw=2 tw=99 et: -// -// Copyright (C) 2012-2014 David Anderson -// -// This file is part of SourcePawn. -// -// SourcePawn is free software: you can redistribute it and/or modify it under -// the terms of the GNU General Public License as published by the Free -// Software Foundation, either version 3 of the License, or (at your option) -// any later version. -// -// SourcePawn is distributed in the hope that it will be useful, but WITHOUT ANY -// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along with -// SourcePawn. If not, see http://www.gnu.org/licenses/. -#include -#include -#include "shared/string-pool.h" -#include "compiler/reporting.h" -#include "compiler/source-manager.h" -#include "compiler/compile-context.h" -#include "compiler/parser/preprocessor.h" -#include "compiler/parser/parser.h" -#include "compiler/parser/json-tools.h" -#include "compiler/sema/name-resolver.h" -#include -#include -#include "docparse.h" - -using namespace ke; -using namespace sp; - -// Comment analyzer. This is designed to only work when we're ignoring -// #include directives. -class Comments : public CommentDelegate -{ - struct CommentLoc { - unsigned line; - unsigned offset; - - CommentLoc() - : line(0), - offset(0) - {} - CommentLoc(unsigned line, unsigned offset) - : line(line), - offset(offset) - {} - }; - struct CommentRange { - CommentLoc start; - CommentLoc end; - - CommentRange() - {} - CommentRange(const CommentLoc &start, const CommentLoc &end) - : start(start), - end(end) - {} - }; - - public: - Comments(CompileContext &cc) - : cc_(cc) - {} - - void HandleComment(CommentPos aWhere, const SourceRange &aRange) - { - Vector &where = (aWhere == CommentPos::Front) - ? lead_comments_ - : tail_comments_; - - FullSourceRef start = cc_.source().decode(aRange.start); - FullSourceRef end = cc_.source().decode(aRange.end); - assert(start.file && end.file && start.file == end.file); - - CommentRange range( - CommentLoc(start.line, start.offset), - CommentLoc(end.line, end.offset)); - - if (!where.empty()) { - // Comments must be in order. - assert(range.start.offset >= where.back().end.offset); - } - where.append(range); - } - - static int cmp_ends_at_line(const void *aItem1, const void *aItem2) { - unsigned line = reinterpret_cast(aItem1); - const CommentRange *item2 = (const CommentRange *)aItem2; - if (line < item2->end.line) - return -1; - if (line > item2->end.line) - return 1; - return 0; - } - - static int cmp_starts_at_line(const void *aItem1, const void *aItem2) { - unsigned line = reinterpret_cast(aItem1); - const CommentRange *item2 = (const CommentRange *)aItem2; - if (line < item2->start.line) - return -1; - if (line > item2->start.line) - return 1; - return 0; - } - - bool findCommentFor(const SourceLocation &loc, unsigned *start, unsigned *end, unsigned *line) { - // Find a comment that ends one line above loc. - FullSourceRef ref = cc_.source().decode(loc); - assert(ref.file); - - *line = ref.line; - - void *found = - bsearch(reinterpret_cast(ref.line - 1), - lead_comments_.buffer(), - lead_comments_.length(), - sizeof(CommentRange), - cmp_ends_at_line); - if (!found) { - found = bsearch(reinterpret_cast(ref.line), - tail_comments_.buffer(), - tail_comments_.length(), - sizeof(CommentRange), - cmp_starts_at_line); - if (!found) - return false; - } - - CommentRange *range = reinterpret_cast(found); - *start = range->start.offset; - *end = range->end.offset; - return true; - } - - private: - CompileContext &cc_; - Vector lead_comments_; - Vector tail_comments_; -}; - -class ExprToStr : public StrictAstVisitor -{ - public: - static ke::AString Convert(Expression *expr) { - ExprToStr converter(expr); - return converter.result_; - } - - private: - ExprToStr(Expression *expr) { - expr->accept(this); - } - - void visitIntegerLiteral(IntegerLiteral *node) override { - result_.format("%" KE_FMT_I64, node->value()); - } - void visitFloatLiteral(FloatLiteral *node) override { - result_.format("%f", node->value()); - } - void visitStringLiteral(StringLiteral *node) override { - result_.format("\"%s\"", node->literal()->chars()); - } - void visitCharLiteral(CharLiteral *node) override { - result_.format("'%c'", (char)node->value()); - } - void visitBinaryExpression(BinaryExpression *node) override { - ke::AString left = Convert(node->left()); - ke::AString right = Convert(node->right()); - result_.format("%s %s %s", left.chars(), TokenNames[node->token()], right.chars()); - } - void visitAssignment(Assignment *node) override { - ke::AString lvalue = Convert(node->lvalue()); - ke::AString rvalue = Convert(node->expression()); - result_.format("%s %s %s", lvalue.chars(), TokenNames[node->token()], rvalue.chars()); - } - void visitNameProxy(NameProxy *node) override { - result_ = ke::AString(node->name()->chars(), node->name()->length()); - } - void visitCallExpression(CallExpression *node) override { - ke::AString callee = Convert(node->callee()); - result_.format("%s(", callee.chars()); - ExpressionList *args = node->arguments(); - for (size_t i = 0; i < args->length(); i++) { - ke::AString arg = Convert(args->at(i)); - if (i != args->length() - 1) - result_.format("%s%s, ", result_.chars(), arg.chars()); - else - result_.format("%s%s", result_.chars(), arg.chars()); - } - result_.format("%s)", result_.chars()); - } - void visitFieldExpression(FieldExpression *node) override { - ke::AString base = Convert(node->base()); - result_.format("%s.%s", base.chars(), node->field()->chars()); - } - void visitIndexExpression(IndexExpression *node) override { - ke::AString left = Convert(node->left()); - ke::AString right = Convert(node->right()); - result_.format("%s[%s]", left.chars(), right.chars()); - } - void visitIncDecExpression(IncDecExpression *node) override { - ke::AString expr = Convert(node->expression()); - if (node->postfix()) - result_.format("%s%s", expr.chars(), TokenNames[node->token()]); - else - result_.format("%s%s", TokenNames[node->token()], expr.chars()); - } - void visitUnaryExpression(UnaryExpression *node) override { - ke::AString expr = Convert(node->expression()); - result_.format("%s%s", TokenNames[node->token()], expr.chars()); - } - void visitViewAsExpression(ViewAsExpression *node) override { - ke::AString type = BuildTypeName(node->te(), nullptr); - ke::AString expr = Convert(node->expr()); - result_.format("view_as<%s>(%s)", type.chars(), expr.chars()); - } - void visitSizeofExpression(SizeofExpression *node) override { - Atom *atom_name = node->proxy()->name(); - result_.format("sizeof(%s", atom_name->chars()); - AbstractAccessorExpression* accessor = node->accessorExpression(); - while (accessor != nullptr) { - if (AbstractArrayMemberExpression* arrayExpr = accessor->asAbstractArrayMemberExpression()) { - for (size_t i = 0; i < arrayExpr->level(); i++) { - result_.format("%s[]", result_.chars()); - } - } else if (AbstractFieldExpression* fieldExpr = accessor->asAbstractFieldExpression()) { - Atom *field_name = fieldExpr->field(); - TokenKind token = fieldExpr->token(); - result_.format("%s%s%s", result_.chars(), TokenNames[token], field_name->chars()); - } - accessor = accessor->child(); - } - - result_.format("%s)", result_.chars()); - } - void visitTernaryExpression(TernaryExpression *node) override { - ke::AString condition = Convert(node->condition()); - ke::AString left = Convert(node->left()); - ke::AString right = Convert(node->right()); - result_.format("%s ? %s : %s", condition.chars(), left.chars(), right.chars()); - } - void visitTokenLiteral(TokenLiteral *node) override { - result_ = TokenNames[node->token()]; - } - void visitArrayLiteral(ArrayLiteral *node) override { - if (node->arrayLength() == 0) { - result_ = "{}"; - return; - } - - ExpressionList* exprs = node->expressions(); - - result_ = "{ "; - for (size_t i = 0; i < exprs->length(); i++) { - ke::AString element = Convert(exprs->at(i)); - if (i == exprs->length() - 1 && !node->repeatLastElement()) - result_.format("%s%s", result_.chars(), element.chars()); - else - result_.format("%s%s, ", result_.chars(), element.chars()); - } - if (node->repeatLastElement()) - result_.format("%s...", result_.chars()); - result_.format("%s }", result_.chars()); - } - void visitThisExpression(ThisExpression* node) override { - result_ = "this"; - } - - private: - ke::AString result_; -}; - -class Analyzer : public PartialAstVisitor -{ - public: - Analyzer(CompileContext &cc, Comments &comments) - : cc_(cc), - pool_(cc.pool()), - comments_(comments) - { - atom_name_ = cc_.add("name"); - atom_kind_ = cc_.add("kind"); - atom_returnType_ = cc_.add("returnType"); - atom_type_ = cc_.add("type"); - atom_parameters_ = cc_.add("arguments"); - atom_ref_line_ = cc_.add("refLine"); - atom_doc_start_ = cc_.add("docStart"); - atom_doc_end_ = cc_.add("docEnd"); - atom_properties_ = cc_.add("properties"); - atom_methods_ = cc_.add("methods"); - atom_fields_ = cc_.add("fields"); - atom_getter_ = cc_.add("getter"); - atom_setter_ = cc_.add("setter"); - atom_entries_ = cc_.add("entries"); - atom_constants_ = cc_.add("constants"); - atom_decl_ = cc_.add("decl"); - atom_default_ = cc_.add("default"); - atom_value_ = cc_.add("value"); - atom_parent_ = cc_.add("parent"); - } - - JsonObject *analyze(ParseTree *tree, Preprocessor& pp) { - functions_ = new (pool_) JsonList(); - methodmaps_ = new (pool_) JsonList(); - enums_ = new (pool_) JsonList(); - constants_ = new (pool_) JsonList(); - typesets_ = new (pool_) JsonList(); - typedefs_ = new (pool_) JsonList(); - enum_structs_ = new (pool_) JsonList(); - defines_ = new (pool_) JsonList(); - - for (auto iter = pp.macros(); !iter.empty(); iter.next()) { - visitMacro(iter->key, iter->value); - } - - for (size_t i = 0; i < tree->statements()->length(); i++) { - Statement *stmt = tree->statements()->at(i); - stmt->accept(this); - } - - JsonObject *obj = new (pool_) JsonObject(); - obj->add(cc_.add("functions"), functions_); - obj->add(cc_.add("methodmaps"), methodmaps_); - obj->add(cc_.add("enums"), enums_); - obj->add(cc_.add("constants"), constants_); - obj->add(cc_.add("typesets"), typesets_); - obj->add(cc_.add("typedefs"), typedefs_); - obj->add(cc_.add("enumstructs"), enum_structs_); - obj->add(cc_.add("defines"), defines_); - return obj; - } - - void visitRecordDecl(RecordDecl* node) override { - JsonObject *obj = new (pool_) JsonObject(); - obj->add(atom_name_, toJson(node->name())); - startDoc(obj, "enum struct", node->name(), node->loc()); - - SaveAndSet new_props(&props_, new (pool_) JsonList()); - SaveAndSet new_methods(&methods_, new (pool_) JsonList()); - for (size_t i = 0; i < node->body()->length(); i++) - node->body()->at(i)->accept(this); - - obj->add(atom_methods_, methods_); - obj->add(atom_fields_, props_); - enum_structs_->add(obj); - } - - void visitMethodmapDecl(MethodmapDecl *node) override { - JsonObject *obj = new (pool_) JsonObject(); - obj->add(atom_name_, toJson(node->name())); - startDoc(obj, "class", node->name(), node->loc()); - - SaveAndSet new_props(&props_, new (pool_) JsonList()); - SaveAndSet new_methods(&methods_, new (pool_) JsonList()); - for (size_t i = 0; i < node->body()->length(); i++) - node->body()->at(i)->accept(this); - - obj->add(atom_methods_, methods_); - obj->add(atom_properties_, props_); - if (NameProxy* parent = node->parent()) { - Atom* parent_name = parent->name(); - obj->add(atom_parent_, toJson(parent_name->chars())); - } - methodmaps_->add(obj); - } - - void visitMethodDecl(MethodDecl *node) override { - JsonObject *obj = new (pool_) JsonObject(); - obj->add(atom_name_, toJson(node->name())); - startDoc(obj, "method", node->name(), node->loc()); - - FunctionNode *fun = node->method(); - if (fun->signature()->native()) - obj->add(atom_kind_, toJson("native")); - else - obj->add(atom_kind_, toJson("stock")); - obj->add(atom_returnType_, toJson(fun->signature()->returnType())); - obj->add(atom_parameters_, toJson(fun->signature()->parameters())); - methods_->add(obj); - } - void visitPropertyDecl(PropertyDecl *node) override { - JsonObject *obj = new (pool_) JsonObject(); - obj->add(atom_name_, toJson(node->name())); - startDoc(obj, "property", node->name(), node->loc()); - - obj->add(atom_type_, toJson(node->te())); - obj->add(atom_getter_, new (pool_) JsonBool(!!node->getter())); - obj->add(atom_setter_, new (pool_) JsonBool(!!node->setter())); - props_->add(obj); - } - void visitFieldDecl(FieldDecl* node) override { - JsonObject *obj = new (pool_) JsonObject(); - obj->add(atom_name_, toJson(node->name())); - startDoc(obj, "field", node->name(), node->loc()); - - obj->add(atom_type_, toJson(node->te())); - props_->add(obj); - } - - void visitTypesetDecl(TypesetDecl *decl) override { - JsonObject *obj = new (pool_) JsonObject(); - obj->add(atom_name_, toJson(decl->name())); - startDoc(obj, "typeset", decl->name(), decl->loc()); - - JsonList *list = new (pool_) JsonList(); - for (size_t i = 0; i < decl->types()->length(); i++) { - const TypesetDecl::Entry &entry = decl->types()->at(i); - JsonObject *te = new (pool_) JsonObject(); - te->add(atom_type_, toJson(entry.te)); - unsigned start, end, line; - if (comments_.findCommentFor(entry.loc, &start, &end, &line)) { - te->add(atom_ref_line_, new (pool_) JsonInt(line)); - te->add(atom_doc_start_, new (pool_) JsonInt(start)); - te->add(atom_doc_end_, new (pool_) JsonInt(end)); - } - list->add(te); - } - obj->add(cc_.add("types"), list); - - typesets_->add(obj); - } - - void visitTypedefDecl(TypedefDecl *decl) override { - JsonObject *obj = new (pool_) JsonObject(); - obj->add(atom_name_, toJson(decl->name())); - startDoc(obj, "typedef", decl->name(), decl->loc()); - - obj->add(atom_type_, toJson(decl->te())); - - typedefs_->add(obj); - } - - void visitEnumStatement(EnumStatement *node) override { - if (!node->name()) { - for (size_t i = 0; i < node->entries()->length(); i++) { - EnumConstant *cs = node->entries()->at(i); - - JsonObject *val = new (pool_) JsonObject(); - val->add(atom_name_, toJson(cs->name())); - if (cs->expression()) { - ke::AString expr = ExprToStr::Convert(cs->expression()); - val->add(atom_value_, toJson(expr.chars())); - } - startDoc(val, "enum value", cs->name(), cs->loc()); - - constants_->add(val); - } - return; - } - - JsonObject *obj = new (pool_) JsonObject(); - obj->add(atom_name_, toJson(node->name())); - startDoc(obj, "enum", node->name(), node->loc()); - - JsonList *list = new (pool_) JsonList(); - for (size_t i = 0; i < node->entries()->length(); i++) { - EnumConstant *cs = node->entries()->at(i); - - JsonObject *val = new (pool_) JsonObject(); - val->add(atom_name_, toJson(cs->name())); - if (cs->expression()) { - ke::AString expr = ExprToStr::Convert(cs->expression()); - val->add(atom_value_, toJson(expr.chars())); - } - startDoc(val, "enum value", cs->name(), cs->loc()); - - list->add(val); - } - obj->add(atom_entries_, list); - - enums_->add(obj); - } - - void visitFunctionStatement(FunctionStatement *node) override { - JsonObject *obj = new (pool_) JsonObject(); - obj->add(atom_name_, toJson(node->name())); - startDoc(obj, "function", node->name(), node->loc()); - - obj->add(atom_kind_, toJson(node->decoration())); - obj->add(atom_returnType_, toJson(node->signature()->returnType())); - obj->add(atom_parameters_, toJson(node->signature()->parameters())); - - functions_->add(obj); - } - - void visitMacro(Atom* key, Macro *node) { - // Ignore macros that aren't backed by file (e.g. __sourcepawn__) - FullSourceRef ref = cc_.source().decode(node->start()); - if (!ref.file) - return; - - JsonObject *obj = new (pool_) JsonObject(); - obj->add(atom_name_, toJson(key)); - startDoc(obj, "define", key, node->start()); - - std::string value; - if (node->tokens->length()) { - ref = cc_.source().decode(node->start()); - if (ref.file) { - const char* file = ref.file->chars(); - - value = std::string{ - file + ref.offset, - file + ref.offset + node->length(), - }; - } - } - - obj->add(atom_value_, toJson(value.c_str())); - - defines_->add(obj); - } - - private: - void startDoc(JsonObject *obj, const char *type, Atom *name, const SourceLocation &loc) { - unsigned start, end, line; - if (!comments_.findCommentFor(loc, &start, &end, &line)) { - cc_.report(loc, rmsg::missing_comment) - << type << name; - return; - } - - assert(start < INT_MAX); - assert(end < INT_MAX); - - obj->add(atom_ref_line_, new (pool_) JsonInt(line)); - obj->add(atom_doc_start_, new (pool_) JsonInt(start)); - obj->add(atom_doc_end_, new (pool_) JsonInt(end)); - } - - JsonString *toJson(const TypeSpecifier *spec, Atom *name = nullptr) { - return toJson(BuildTypeName(spec, name, TypeDiagFlags::Names).chars()); - } - JsonString *toJson(Type *type, Atom *name = nullptr) { - return toJson(BuildTypeName(type, name, TypeDiagFlags::Names).chars()); - } - - JsonString *toJson(const TypeExpr &te, Atom *name = nullptr) { - if (te.spec()) - return toJson(te.spec(), name); - return toJson(te.resolved(), name); - } - - static inline bool isByRef(const TypeExpr& te) { - return te.resolved() - ? te.resolved()->isReference() - : te.spec()->isByRef(); - } - static inline bool isConst(const TypeExpr& te) { - return te.resolved() - ? te.resolved()->isConst() - : te.spec()->isConst(); - } - - JsonString *toJson(VarDecl *decl, bool named) { - // :TODO: add a BuildTypeName(VarDecl) helper. - TypeDiagFlags flags = TypeDiagFlags::Names; - if (isByRef(decl->te())) - flags |= TypeDiagFlags::IsByRef; - if (isConst(decl->te())) - flags |= TypeDiagFlags::IsConst; - return toJson(BuildTypeName( - decl->te(), - named ? decl->name() : nullptr, - flags).chars()); - } - - JsonList *toJson(const ParameterList *params) { - JsonList *list = new (pool_) JsonList(); - for (size_t i = 0; i < params->length(); i++) { - VarDecl *decl = params->at(i); - JsonObject *obj = new (pool_) JsonObject(); - - obj->add(atom_type_, toJson(decl, false)); - - if (decl->name()) { - obj->add(atom_name_, toJson(decl->name())); - obj->add(atom_decl_, toJson(decl, true)); - if (decl->initialization()) { - ke::AString def = ExprToStr::Convert(decl->initialization()); - obj->add(atom_default_, toJson(def.chars())); - } - } else { - obj->add(atom_name_, toJson("...")); - - AutoString builder = BuildTypeName(decl->te(), nullptr, TypeDiagFlags::Names); - obj->add(atom_decl_, toJson(builder.ptr())); - } - list->add(obj); - } - return list; - } - - JsonString *toJson(Atom *name) { - return new (pool_) JsonString(name); - } - JsonString *toJson(const char *str) { - return new (pool_) JsonString(cc_.add(str)); - } - - private: - CompileContext &cc_; - PoolAllocator &pool_; - Comments &comments_; - - Atom *atom_name_; - Atom *atom_kind_; - Atom *atom_returnType_; - Atom *atom_type_; - Atom *atom_parameters_; - Atom *atom_ref_line_; - Atom *atom_doc_start_; - Atom *atom_doc_end_; - Atom *atom_properties_; - Atom *atom_methods_; - Atom *atom_fields_; - Atom *atom_getter_; - Atom *atom_setter_; - Atom *atom_entries_; - Atom *atom_constants_; - Atom *atom_decl_; - Atom *atom_default_; - Atom *atom_value_; - Atom *atom_parent_; - JsonList *functions_; - JsonList *methodmaps_; - JsonList *enums_; - JsonList *constants_; - JsonList *typesets_; - JsonList *typedefs_; - JsonList *enum_structs_; - JsonList *defines_; - - JsonList *props_; - JsonList *methods_; -}; - -static JsonObject * -Run(CompileContext &cc, const char* buffer, const char* path) -{ - Comments comments(cc); - ParseTree *tree = nullptr; - - Preprocessor pp(cc); - - pp.disableIncludes(); - pp.setCommentDelegate(&comments); - - UniquePtr o_ptr = MakeUnique(strlen(buffer) + 10); - strcpy(o_ptr.get(), buffer); - - { - RefPtr file = cc.source().createFromBuffer(std::move(o_ptr), strlen(buffer), path); - - if (!file) - return nullptr; - if (!pp.enter(file)) - return nullptr; - } - - NameResolver nr(cc); - Parser parser(cc, pp, nr); - - tree = parser.parse(); - if (!tree || !cc.phasePassed()) - return nullptr; - - Analyzer analyzer(cc, comments); - return analyzer.analyze(tree, pp); -} - -const char* parse(const char* input, const char* path) { - StringPool strings; - ReportManager reports; - SourceManager source(strings, reports); - - PoolAllocator pool; - { - PoolScope scope(pool); - - CompileContext cc(pool, strings, reports, source); - - cc.SkipResolution(); - - JsonObject *obj = Run(cc, input, path); - - if (!obj) { - return nullptr; - } - - std::stringstream out; - - JsonRenderer renderer(out); - renderer.Render(obj); - - char *optr = new char[out.str().size()+1]; - strcpy(optr, out.str().c_str()); - - return optr; - } -} diff --git a/libalternator/binding/docparse.h b/libalternator/binding/docparse.h deleted file mode 100644 index fd6c680..0000000 --- a/libalternator/binding/docparse.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef _DOCPARSE_H -#define _DOCPARSE_H - -const char* parse(const char* input, const char* path); - -#endif diff --git a/libalternator/binding/wrapper.h b/libalternator/binding/wrapper.h deleted file mode 100644 index 46efd44..0000000 --- a/libalternator/binding/wrapper.h +++ /dev/null @@ -1 +0,0 @@ -#include "docparse.h" \ No newline at end of file diff --git a/libalternator/build.rs b/libalternator/build.rs deleted file mode 100644 index 7581c69..0000000 --- a/libalternator/build.rs +++ /dev/null @@ -1,49 +0,0 @@ -extern crate bindgen; - -use std::env; -use std::env::var; -use std::path::PathBuf; - -fn main() { - let manifest_dir = var("CARGO_MANIFEST_DIR").unwrap(); - - println!( - "cargo:rustc-link-search={}/libraries/libsmx/smx.x64", - manifest_dir - ); - println!( - "cargo:rustc-link-search={}/libraries/exp/compiler/spcomp2.x64", - manifest_dir - ); - println!( - "cargo:rustc-link-search={}/libraries/exp/tools/docparse/docparse.x64", - manifest_dir - ); - - println!("cargo:rustc-link-lib=static=docparse"); - println!("cargo:rustc-link-lib=static=spcomp2"); - println!("cargo:rustc-link-lib=static=smx"); - - // Required for CI - if cfg!(unix) { - println!("cargo:rustc-link-lib=dylib=stdc++"); - } - - println!("cargo:rerun-if-changed=binding/wrapper.h"); - - let bindings = bindgen::Builder::default() - .header("binding/wrapper.h") - .rust_target(bindgen::RustTarget::Nightly) - .clang_arg("-x") - .clang_arg("c++") - .clang_arg("-std=c++14") - .parse_callbacks(Box::new(bindgen::CargoCallbacks)) - .generate() - .expect("Unable to generate bindings"); - - // Write the bindings to the $OUT_DIR/bindings.rs file. - let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); - bindings - .write_to_file(out_path.join("bindings.rs")) - .expect("Couldn't write bindings!"); -} diff --git a/libalternator/examples/dump.rs b/libalternator/examples/dump.rs new file mode 100644 index 0000000..edd7179 --- /dev/null +++ b/libalternator/examples/dump.rs @@ -0,0 +1,29 @@ +//! Prints the strand of one or more SourcePawn files as JSON. +//! +//! Usage: `cargo run -p alternator --example dump -- [out.json]` + +fn main() { + let args: Vec = std::env::args().skip(1).collect(); + if args.is_empty() { + eprintln!("usage: dump [out.json]"); + std::process::exit(2); + } + + let content = std::fs::read(&args[0]).expect("unable to read file"); + + match alternator::parse_bytes(&content) { + Ok(strand) => { + // Round trip through Value for stable key order + let value = serde_json::to_value(&strand).unwrap(); + let json = serde_json::to_string_pretty(&value).unwrap(); + match args.get(1) { + Some(out) => std::fs::write(out, json + "\n").expect("unable to write output"), + None => println!("{}", json), + } + } + Err(e) => { + eprintln!("{}: {}", args[0], e); + std::process::exit(1); + } + } +} diff --git a/libalternator/src/comments.rs b/libalternator/src/comments.rs new file mode 100644 index 0000000..a25dc03 --- /dev/null +++ b/libalternator/src/comments.rs @@ -0,0 +1,176 @@ +//! Associates comments with declarations. +//! +//! This follows the rules of the SourcePawn lexer that the old docparse tool +//! relied on: +//! - A *tail* comment block starts with a single line comment that follows a +//! token on the same line and extends over directly following comments +//! (no blank line). +//! - A *front* comment block is a sequence of comments with at most one line +//! between them, followed by a token on its own line. +//! +//! A declaration on line `L` is documented by the front block ending on line +//! `L - 1`, or otherwise by a tail block starting on line `L`. + +use crate::preprocess::{Item, RawComment}; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct CommentRange { + pub start: usize, + pub end: usize, + pub start_line: usize, + pub end_line: usize, +} + +#[derive(Default)] +pub struct Comments { + front: Vec, + tail: Vec, +} + +fn range(a: &RawComment, b: &RawComment) -> CommentRange { + CommentRange { + start: a.start, + end: b.end, + start_line: a.start_line, + end_line: b.end_line, + } +} + +impl Comments { + pub fn new(items: &[Item]) -> Self { + let mut c = Comments::default(); + let mut i = 0; + let mut last_token_line = 0; + + while i < items.len() { + let first = match items[i] { + Item::Token { line, .. } => { + last_token_line = line; + i += 1; + continue; + } + Item::Comment(first) => first, + }; + + // Tail block. Scanning a multi-line comment resets the lexer's + // "token seen on this line" state, so those are never tails. + if last_token_line == first.start_line && first.start_line == first.end_line { + let mut last = first; + i += 1; + while let Some(Item::Comment(next)) = items.get(i) { + if next.start_line > last.end_line + 1 { + break; + } + last = *next; + i += 1; + } + c.tail.push(range(&first, &last)); + continue; + } + + // Front block(s) + let start = first; + let mut committed: Option = None; + let mut last = first; + i += 1; + loop { + match items.get(i) { + Some(Item::Comment(next)) => { + committed = Some(last); + if next.start_line > last.end_line + 1 { + // Block finished, the next comment starts a new one + break; + } + last = *next; + i += 1; + } + Some(Item::Token { line, directive }) => { + if start.start_line == *line || *directive { + // `/* ... */ token` is not a front comment, and + // comments before a directive belong to nothing + // (typically `@section` headers above `#define`s) + committed = None; + } else if *line != last.end_line { + committed = Some(last); + } + break; + } + None => { + committed = Some(last); + break; + } + } + } + + if let Some(end) = committed { + c.front.push(range(&start, &end)); + } + } + + c + } + + /// Finds the documentation comment for a declaration on `line` + pub fn find(&self, line: usize) -> Option { + if line == 0 { + return None; + } + self.front + .iter() + .find(|r| r.end_line == line - 1) + .or_else(|| self.tail.iter().find(|r| r.start_line == line)) + .copied() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::preprocess::preprocess; + + fn find(src: &str, line: usize) -> Option { + let p = preprocess(src); + let c = Comments::new(&p.items); + c.find(line).map(|r| src[r.start..r.end].to_string()) + } + + #[test] + fn front_comment() { + let src = "/**\n * Doc\n */\nnative void A();\n"; + assert_eq!(find(src, 4).unwrap(), "/**\n * Doc\n */"); + } + + #[test] + fn blank_line_breaks_association() { + let src = "/** Doc */\n\nnative void A();\n"; + assert_eq!(find(src, 3), None); + } + + #[test] + fn tail_comment() { + let src = "enum X {\n A, // first\n B /**< second */\n};\n"; + assert_eq!(find(src, 2).unwrap(), "// first"); + assert_eq!(find(src, 3).unwrap(), "/**< second */"); + } + + #[test] + fn defines_use_tail_comments() { + let src = "/** @section Flags */\n#define A 1 /**< Tail */\n// Label\n#define B 2\n"; + assert_eq!(find(src, 2).unwrap(), "/**< Tail */"); + assert_eq!(find(src, 4), None); + } + + #[test] + fn multi_line_trailing_comment_documents_next_line() { + // Mirrors docparse: a multi-line comment after a token is a front comment + let src = "enum X {\n A, /**< one\n two */\n B\n};\n"; + assert_eq!(find(src, 2), None); + assert!(find(src, 4).unwrap().starts_with("/**< one")); + } + + #[test] + fn consecutive_line_comments() { + let src = "// a\n// b\nnative void A();\n"; + assert_eq!(find(src, 3).unwrap(), "// a\n// b"); + } +} diff --git a/libalternator/src/error.rs b/libalternator/src/error.rs index a7b266b..23dfdb1 100644 --- a/libalternator/src/error.rs +++ b/libalternator/src/error.rs @@ -4,15 +4,16 @@ pub type Result = std::result::Result; #[derive(Error, Debug)] pub enum AlternatorError { - #[error("FFI Null {0}")] - FFINul(#[from] std::ffi::NulError), + #[error("Unable to load the SourcePawn grammar: {0}")] + Language(#[from] tree_sitter::LanguageError), - #[error("Docparse unable to process content")] + #[error("Unable to parse content")] ParseFail, - #[error("Schema mismatch from alternator's strand")] - SchemaMismatch(#[from] serde_json::error::Error), - - #[error("Encoding non utf-8")] - Encoding(#[from] std::str::Utf8Error), + #[error("Syntax error at {line}:{column} near `{snippet}`")] + Syntax { + line: usize, + column: usize, + snippet: String, + }, } diff --git a/libalternator/src/expr.rs b/libalternator/src/expr.rs new file mode 100644 index 0000000..eb2f41d --- /dev/null +++ b/libalternator/src/expr.rs @@ -0,0 +1,731 @@ +//! A tiny SourcePawn constant expression parser. +//! +//! It is used for three things: +//! - evaluating `#if`/`#elseif` conditions, +//! - rendering default argument values and enum values, +//! - rendering array dimensions. +//! +//! Rendering intentionally mirrors the output of the old SourcePawn `docparse` +//! tool so that existing bundles don't churn: parentheses are dropped, binary +//! operators are separated by single spaces, integer literals are printed in +//! decimal and float literals with six decimal places. + +use std::iter::Peekable; +use std::str::CharIndices; + +#[derive(Debug, Clone, PartialEq)] +pub enum Token { + Int(i64), + Float(f64), + /// String literal, raw source text including quotes. + Str(String), + /// Character literal, raw source text including quotes. + Char(String, i64), + Ident(String), + Punct(&'static str), +} + +const PUNCTS: &[&str] = &[ + ">>>=", "<<=", ">>=", ">>>", "...", "::", "<<", ">>", "<=", ">=", "==", "!=", "&&", "||", "++", + "--", "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "~=", "(", ")", "[", "]", "{", "}", "<", + ">", "+", "-", "*", "/", "%", "&", "|", "^", "!", "~", "?", ":", ",", ".", "=", ";", +]; + +/// Tokenizes an expression. Comments are skipped. Returns `None` on +/// characters that can't appear in an expression. +pub fn tokenize(src: &str) -> Option> { + let mut out = Vec::new(); + let mut it: Peekable = src.char_indices().peekable(); + + while let Some(&(i, c)) = it.peek() { + if c.is_whitespace() || c == '\\' { + it.next(); + continue; + } + + let rest = &src[i..]; + + if rest.starts_with("//") { + break; + } + + if rest.starts_with("/*") { + let end = rest[2..].find("*/").map(|e| i + 2 + e + 2).unwrap_or(src.len()); + while let Some(&(j, _)) = it.peek() { + if j >= end { + break; + } + it.next(); + } + continue; + } + + if c.is_ascii_digit() { + let mut end = i; + let mut is_float = false; + let bytes = src.as_bytes(); + let hex = rest.starts_with("0x") || rest.starts_with("0X"); + while end < src.len() { + let b = bytes[end]; + let ok = b.is_ascii_alphanumeric() + || b == b'_' + || (b == b'.' && !hex && !is_float && !rest[end - i..].starts_with("...")) + || ((b == b'-' || b == b'+') + && is_float + && matches!(bytes[end - 1], b'e' | b'E')); + if !ok { + break; + } + if b == b'.' { + is_float = true; + } + end += 1; + } + let text = &src[i..end]; + while let Some(&(j, _)) = it.peek() { + if j >= end { + break; + } + it.next(); + } + out.push(parse_number(text, is_float)?); + continue; + } + + if c.is_alphabetic() || c == '_' || c == '@' { + let mut end = i; + while let Some(&(j, ch)) = it.peek() { + if ch.is_alphanumeric() || ch == '_' || (ch == '@' && j == i) { + end = j + ch.len_utf8(); + it.next(); + } else { + break; + } + } + out.push(Token::Ident(src[i..end].to_string())); + continue; + } + + if c == '"' || c == '\'' { + it.next(); + let mut end = src.len(); + let mut escaped = false; + for (j, ch) in it.by_ref() { + if escaped { + escaped = false; + continue; + } + if ch == '\\' { + escaped = true; + continue; + } + if ch == c { + end = j + 1; + break; + } + } + let raw = src[i..end].to_string(); + if c == '"' { + out.push(Token::Str(raw)); + } else { + let value = char_value(&raw); + out.push(Token::Char(raw, value)); + } + continue; + } + + let p = PUNCTS.iter().find(|p| rest.starts_with(**p))?; + for _ in 0..p.len() { + it.next(); + } + out.push(Token::Punct(p)); + } + + Some(out) +} + +fn parse_number(text: &str, is_float: bool) -> Option { + let clean: String = text.chars().filter(|c| *c != '_').collect(); + + if is_float { + return clean.parse::().ok().map(Token::Float); + } + + let lower = clean.to_ascii_lowercase(); + let (digits, radix) = if let Some(d) = lower.strip_prefix("0x") { + (d, 16) + } else if let Some(d) = lower.strip_prefix("0b") { + (d, 2) + } else if let Some(d) = lower.strip_prefix("0o") { + (d, 8) + } else { + (lower.as_str(), 10) + }; + + u64::from_str_radix(digits, radix).ok().map(|v| Token::Int(v as i64)) +} + +fn char_value(raw: &str) -> i64 { + let inner = raw.trim_start_matches('\'').trim_end_matches('\''); + let mut chars = inner.chars(); + match chars.next() { + Some('\\') => match chars.next() { + Some('n') => 10, + Some('r') => 13, + Some('t') => 9, + Some('a') => 7, + Some('b') => 8, + Some('e') => 27, + Some('f') => 12, + Some('v') => 11, + Some('0') => 0, + Some(c) => c as i64, + None => '\\' as i64, + }, + Some(c) => c as i64, + None => 0, + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Expr { + Int(i64), + Float(f64), + Str(String), + Char(String, i64), + Name(String), + Bool(bool), + Null, + This, + Unary(&'static str, Box), + Postfix(&'static str, Box), + Binary(&'static str, Box, Box), + Ternary(Box, Box, Box), + Call(Box, Vec), + Index(Box, Box), + Field(Box, &'static str, String), + ViewAs(String, Box), + Sizeof(String), + Defined(String), + Array(Vec, bool), +} + +struct Parser<'t> { + tokens: &'t [Token], + pos: usize, +} + +fn binary_prec(p: &str) -> Option { + Some(match p { + "||" => 1, + "&&" => 2, + "|" => 3, + "^" => 4, + "&" => 5, + "==" | "!=" => 6, + "<" | "<=" | ">" | ">=" => 7, + "<<" | ">>" | ">>>" => 8, + "+" | "-" => 9, + "*" | "/" | "%" => 10, + "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "&=" | "|=" | "^=" | "<<=" | ">>=" | ">>>=" => 0, + _ => return None, + }) +} + +impl<'t> Parser<'t> { + fn peek(&self) -> Option<&'t Token> { + self.tokens.get(self.pos) + } + + fn peek_punct(&self, p: &str) -> bool { + matches!(self.peek(), Some(Token::Punct(x)) if *x == p) + } + + fn eat(&mut self, p: &str) -> bool { + if self.peek_punct(p) { + self.pos += 1; + true + } else { + false + } + } + + fn expect(&mut self, p: &str) -> Option<()> { + self.eat(p).then_some(()) + } + + fn expr(&mut self) -> Option { + self.ternary() + } + + fn ternary(&mut self) -> Option { + let cond = self.binary(0)?; + if self.eat("?") { + let a = self.ternary()?; + self.expect(":")?; + let b = self.ternary()?; + return Some(Expr::Ternary(Box::new(cond), Box::new(a), Box::new(b))); + } + Some(cond) + } + + fn binary(&mut self, min: u8) -> Option { + let mut lhs = self.unary()?; + loop { + let op = match self.peek() { + Some(Token::Punct(p)) => *p, + _ => break, + }; + let prec = match binary_prec(op) { + Some(p) if p >= min => p, + _ => break, + }; + self.pos += 1; + // Assignments are right associative, everything else left. + let rhs = if prec == 0 { + self.binary(prec)? + } else { + self.binary(prec + 1)? + }; + lhs = Expr::Binary(op, Box::new(lhs), Box::new(rhs)); + } + Some(lhs) + } + + fn unary(&mut self) -> Option { + if let Some(Token::Punct(p)) = self.peek() { + if matches!(*p, "!" | "~" | "-" | "+" | "++" | "--" | "&") { + self.pos += 1; + let e = self.unary()?; + return Some(Expr::Unary(p, Box::new(e))); + } + } + + // Old style tag cast `Float:5`, `_:x` + if let (Some(Token::Ident(_)), Some(Token::Punct(":"))) = + (self.peek(), self.tokens.get(self.pos + 1)) + { + if !matches!(self.tokens.get(self.pos + 2), Some(Token::Punct(":")) | None) { + let tag = match self.peek() { + Some(Token::Ident(t)) => t.clone(), + _ => unreachable!(), + }; + self.pos += 2; + let e = self.unary()?; + return Some(Expr::ViewAs(old_tag_to_type(&tag).to_string(), Box::new(e))); + } + } + + self.postfix() + } + + fn postfix(&mut self) -> Option { + let mut e = self.primary()?; + loop { + if self.eat("(") { + let mut args = Vec::new(); + if !self.eat(")") { + loop { + args.push(self.expr()?); + if self.eat(")") { + break; + } + self.expect(",")?; + } + } + e = Expr::Call(Box::new(e), args); + } else if self.eat("[") { + let idx = self.expr()?; + self.expect("]")?; + e = Expr::Index(Box::new(e), Box::new(idx)); + } else if self.peek_punct(".") || self.peek_punct("::") { + let op = if self.eat(".") { "." } else { + self.pos += 1; + "::" + }; + match self.peek() { + Some(Token::Ident(n)) => { + self.pos += 1; + e = Expr::Field(Box::new(e), op, n.clone()); + } + _ => return None, + } + } else if self.peek_punct("++") || self.peek_punct("--") { + let op = if self.eat("++") { "++" } else { + self.pos += 1; + "--" + }; + e = Expr::Postfix(op, Box::new(e)); + } else { + break; + } + } + Some(e) + } + + fn primary(&mut self) -> Option { + let tok = self.peek()?.clone(); + self.pos += 1; + Some(match tok { + Token::Int(v) => Expr::Int(v), + Token::Float(v) => Expr::Float(v), + Token::Str(s) => { + // Adjacent string literals are concatenated + let mut s = s; + while let Some(Token::Str(next)) = self.peek() { + s = format!("{}{}", &s[..s.len() - 1], &next[1..]); + self.pos += 1; + } + Expr::Str(s) + } + Token::Char(raw, v) => Expr::Char(raw, v), + Token::Ident(name) => match name.as_str() { + "true" => Expr::Bool(true), + "false" => Expr::Bool(false), + "null" => Expr::Null, + "this" => Expr::This, + "defined" => { + let paren = self.eat("("); + let n = match self.peek()? { + Token::Ident(n) => n.clone(), + _ => return None, + }; + self.pos += 1; + if paren { + self.expect(")")?; + } + Expr::Defined(n) + } + "sizeof" => { + let paren = self.eat("("); + let start = self.pos; + let mut depth = 0i32; + // Consume the sizeof operand verbatim + while let Some(t) = self.peek() { + match t { + Token::Punct("(") | Token::Punct("[") => depth += 1, + Token::Punct(")") | Token::Punct("]") => { + if depth == 0 { + break; + } + depth -= 1; + } + Token::Ident(_) | Token::Punct(".") | Token::Punct("::") => {} + _ if depth > 0 || paren => {} + _ => break, + } + self.pos += 1; + } + let operand = render_tokens(&self.tokens[start..self.pos]); + if paren { + self.expect(")")?; + } + Expr::Sizeof(operand) + } + "view_as" => { + self.expect("<")?; + let mut ty = String::new(); + while let Some(t) = self.peek() { + match t { + Token::Punct(">") => break, + Token::Ident(n) => ty.push_str(n), + Token::Punct(p) => ty.push_str(p), + _ => return None, + } + self.pos += 1; + } + self.expect(">")?; + self.expect("(")?; + let e = self.expr()?; + self.expect(")")?; + Expr::ViewAs(ty, Box::new(e)) + } + _ => Expr::Name(name), + }, + Token::Punct("(") => { + let e = self.expr()?; + self.expect(")")?; + e + } + Token::Punct("{") => { + let mut items = Vec::new(); + let mut repeat = false; + if !self.eat("}") { + loop { + if self.eat("...") { + repeat = true; + self.expect("}")?; + break; + } + items.push(self.expr()?); + if self.eat("}") { + break; + } + self.expect(",")?; + if self.eat("}") { + break; + } + } + } + Expr::Array(items, repeat) + } + _ => return None, + }) + } +} + +fn render_tokens(tokens: &[Token]) -> String { + let mut s = String::new(); + for t in tokens { + match t { + Token::Int(v) => s += &v.to_string(), + Token::Float(v) => s += &format!("{:.6}", v), + Token::Str(r) | Token::Char(r, _) => s += r, + Token::Ident(n) => s += n, + Token::Punct(p) => s += p, + } + } + s +} + +/// Maps old style tags (`Float:`, `String:`, `_:`) to new style types +pub fn old_tag_to_type(tag: &str) -> &str { + match tag { + "Float" => "float", + "String" => "char", + "_" => "int", + "bool" => "bool", + other => other, + } +} + +pub fn parse(src: &str) -> Option { + let tokens = tokenize(src)?; + if tokens.is_empty() { + return None; + } + let mut p = Parser { + tokens: &tokens, + pos: 0, + }; + let e = p.expr()?; + if p.pos != tokens.len() { + return None; + } + Some(e) +} + +/// Access to macro definitions needed while rendering or evaluating +pub trait MacroLookup { + /// Returns the replacement text of an object-like macro visible at `offset` + fn lookup(&self, name: &str, offset: usize) -> Option<&str>; +} + +#[cfg(test)] +pub struct NoMacros; + +#[cfg(test)] +impl MacroLookup for NoMacros { + fn lookup(&self, _: &str, _: usize) -> Option<&str> { + None + } +} + +pub struct Renderer<'m, M: MacroLookup> { + pub macros: &'m M, + /// Byte offset where the expression appears (macros defined later aren't visible) + pub offset: usize, + depth: usize, +} + +impl<'m, M: MacroLookup> Renderer<'m, M> { + pub fn new(macros: &'m M, offset: usize) -> Self { + Self { + macros, + offset, + depth: 0, + } + } + + /// Renders an expression's source text. Falls back to the whitespace + /// normalized source when it can't be parsed. + pub fn render_src(&mut self, src: &str) -> String { + match parse(src) { + Some(e) => self.render(&e), + None => src.split_whitespace().collect::>().join(" "), + } + } + + pub fn render(&mut self, e: &Expr) -> String { + match e { + Expr::Int(v) => v.to_string(), + Expr::Float(v) => format!("{:.6}", v), + Expr::Str(s) => s.clone(), + Expr::Char(raw, _) => raw.clone(), + Expr::Bool(b) => b.to_string(), + Expr::Null => "null".into(), + Expr::This => "this".into(), + Expr::Name(n) => { + if self.depth < 16 { + if let Some(value) = self.macros.lookup(n, self.offset) { + let value = value.to_string(); + self.depth += 1; + let r = self.render_src(&value); + self.depth -= 1; + return r; + } + } + n.clone() + } + Expr::Unary(op, e) => format!("{}{}", op, self.render(e)), + Expr::Postfix(op, e) => format!("{}{}", self.render(e), op), + Expr::Binary(op, l, r) => { + format!("{} {} {}", self.render(l), op, self.render(r)) + } + Expr::Ternary(c, a, b) => format!( + "{} ? {} : {}", + self.render(c), + self.render(a), + self.render(b) + ), + Expr::Call(f, args) => { + let args: Vec = args.iter().map(|a| self.render(a)).collect(); + format!("{}({})", self.render(f), args.join(", ")) + } + Expr::Index(a, i) => format!("{}[{}]", self.render(a), self.render(i)), + Expr::Field(b, op, n) => format!("{}{}{}", self.render(b), op, n), + Expr::ViewAs(t, e) => format!("view_as<{}>({})", t, self.render(e)), + Expr::Sizeof(s) => format!("sizeof({})", s), + Expr::Defined(n) => format!("defined {}", n), + Expr::Array(items, repeat) => { + if items.is_empty() { + return "{}".into(); + } + let mut s = "{ ".to_string(); + for (i, item) in items.iter().enumerate() { + s += &self.render(item); + if i != items.len() - 1 || *repeat { + s += ", "; + } + } + if *repeat { + s += "..."; + } + s + " }" + } + } + } +} + +/// Evaluates a preprocessor condition. +pub fn eval(e: &Expr, defined: &F, value_of: &dyn Fn(&str) -> Option, depth: usize) -> Option +where + F: Fn(&str) -> bool, +{ + if depth > 32 { + return None; + } + let ev = |e: &Expr| eval(e, defined, value_of, depth + 1); + Some(match e { + Expr::Int(v) => *v, + Expr::Char(_, v) => *v, + Expr::Bool(b) => *b as i64, + Expr::Float(f) => *f as i64, + Expr::Defined(n) => defined(n) as i64, + Expr::Name(n) => match value_of(n) { + Some(v) => match parse(&v) { + Some(inner) => eval(&inner, defined, value_of, depth + 1)?, + None => 0, + }, + None => 0, + }, + Expr::Unary(op, e) => { + let v = ev(e)?; + match *op { + "!" => (v == 0) as i64, + "~" => !v, + "-" => v.wrapping_neg(), + "+" => v, + _ => return None, + } + } + Expr::Binary(op, l, r) => { + let a = ev(l)?; + // Short circuit + match *op { + "&&" => return Some((a != 0 && ev(r)? != 0) as i64), + "||" => return Some((a != 0 || ev(r)? != 0) as i64), + _ => {} + } + let b = ev(r)?; + match *op { + "|" => a | b, + "^" => a ^ b, + "&" => a & b, + "==" => (a == b) as i64, + "!=" => (a != b) as i64, + "<" => (a < b) as i64, + "<=" => (a <= b) as i64, + ">" => (a > b) as i64, + ">=" => (a >= b) as i64, + "<<" => a.wrapping_shl(b as u32), + ">>" => a.wrapping_shr(b as u32), + ">>>" => ((a as u64).wrapping_shr(b as u32)) as i64, + "+" => a.wrapping_add(b), + "-" => a.wrapping_sub(b), + "*" => a.wrapping_mul(b), + "/" => a.checked_div(b)?, + "%" => a.checked_rem(b)?, + _ => return None, + } + } + Expr::Ternary(c, a, b) => { + if ev(c)? != 0 { + ev(a)? + } else { + ev(b)? + } + } + Expr::ViewAs(_, e) => ev(e)?, + _ => return None, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn r(s: &str) -> String { + Renderer::new(&NoMacros, 0).render_src(s) + } + + #[test] + fn renders_like_docparse() { + assert_eq!(r("(1<<0)"), "1 << 0"); + assert_eq!(r("(3<<1)|(1<<0)"), "3 << 1 | 1 << 0"); + assert_eq!(r("0x0100"), "256"); + assert_eq!(r("-1"), "-1"); + assert_eq!(r("1.0"), "1.000000"); + assert_eq!(r("{0.0, 0.0, 1.0}"), "{ 0.000000, 0.000000, 1.000000 }"); + assert_eq!(r("{}"), "{}"); + assert_eq!(r("view_as(0)"), "view_as(0)"); + assert_eq!(r("\"GAME\""), "\"GAME\""); + assert_eq!(r("'C'"), "'C'"); + assert_eq!(r("MenuAction_Select|MenuAction_Cancel"), "MenuAction_Select | MenuAction_Cancel"); + assert_eq!(r("sizeof(buffer)"), "sizeof(buffer)"); + assert_eq!(r("a ? b : c"), "a ? b : c"); + assert_eq!(r("Foo(1, 2)"), "Foo(1, 2)"); + } + + #[test] + fn evaluates_conditions() { + let defined = |n: &str| n == "FOO"; + let value = |n: &str| if n == "FOO" { Some("2".to_string()) } else { None }; + let e = |s: &str| eval(&parse(s).unwrap(), &defined, &value, 0).unwrap(); + assert_eq!(e("defined FOO"), 1); + assert_eq!(e("!defined BAR"), 1); + assert_eq!(e("defined(FOO) && FOO >= 2"), 1); + assert_eq!(e("BAR"), 0); + assert_eq!(e("FOO * 2 == 4"), 1); + } +} diff --git a/libalternator/src/extract.rs b/libalternator/src/extract.rs new file mode 100644 index 0000000..7a00db1 --- /dev/null +++ b/libalternator/src/extract.rs @@ -0,0 +1,851 @@ +//! Walks the tree-sitter syntax tree and extracts documented symbols. + +use std::collections::HashMap; + +use tree_sitter::Node; + +use schema::bundle::Strand; +use schema::symbol::{ + parse_type_signature, Argument, Constant, Declaration, Define, DocLocation, Documentation, + EnumStruct, Entry, Enumeration, Field, Function, MethodMap, Property, Type, TypeDefinition, + TypeSet, +}; + +use crate::comments::Comments; +use crate::expr::{old_tag_to_type, Renderer}; +use crate::preprocess::Preprocessed; + +pub struct Extractor<'a> { + /// Original source, used for comment text and define values + pub source: &'a str, + /// Preprocessed source that was handed to tree-sitter + pub text: &'a str, + pub pp: &'a Preprocessed, + pub comments: Comments, + pub line_starts: Vec, + pub strand: Strand, +} + +fn line_of(node: &Node) -> usize { + node.start_position().row + 1 +} + +fn children<'t>(node: &Node<'t>) -> Vec> { + let mut c = node.walk(); + node.children(&mut c).collect() +} + +/// Children along with the field name they're assigned to +fn children_with_fields<'t>(node: &Node<'t>) -> Vec<(Option<&'static str>, Node<'t>)> { + let mut out = Vec::new(); + let mut c = node.walk(); + if c.goto_first_child() { + loop { + out.push((c.field_name(), c.node())); + if !c.goto_next_sibling() { + break; + } + } + } + out +} + +fn field_nodes<'t>(node: &Node<'t>, name: &str) -> Vec> { + let mut c = node.walk(); + node.children_by_field_name(name, &mut c).collect() +} + +/// Structured type of a parameter, field or return value +#[derive(Default, Debug)] +struct TypeParts { + is_const: bool, + base: String, + prefix_dims: Vec, + postfix_dims: Vec, + by_ref: bool, +} + +impl TypeParts { + fn dims(&self) -> String { + let mut s = String::new(); + for d in self.prefix_dims.iter().chain(self.postfix_dims.iter()) { + s += d; + } + s + } + + /// `const char[]`, `int&`, `float[3]` + fn type_string(&self) -> String { + let mut s = String::new(); + if self.is_const { + s += "const "; + } + s += &self.base; + s += &self.dims(); + if self.by_ref { + s += "&"; + } + s + } + + /// `const char[] name`, `float vec[3]`, `int& value` + fn decl_string(&self, name: &str) -> String { + let mut s = String::new(); + if self.is_const { + s += "const "; + } + s += &self.base; + if self.postfix_dims.is_empty() { + for d in &self.prefix_dims { + s += d; + } + if self.by_ref { + s += "&"; + } + s += " "; + s += name; + } else { + for d in &self.prefix_dims { + s += d; + } + if self.by_ref { + s += "&"; + } + s += " "; + s += name; + for d in &self.postfix_dims { + s += d; + } + } + s + } +} + +impl<'a> Extractor<'a> { + pub fn new(source: &'a str, pp: &'a Preprocessed) -> Self { + let mut line_starts = vec![0]; + for (i, b) in source.bytes().enumerate() { + if b == b'\n' { + line_starts.push(i + 1); + } + } + Extractor { + source, + text: &pp.text, + pp, + comments: Comments::new(&pp.items), + line_starts, + strand: Strand::default(), + } + } + + fn line_of_offset(&self, offset: usize) -> usize { + match self.line_starts.binary_search(&offset) { + Ok(i) => i + 1, + Err(i) => i, + } + } + + fn text(&self, node: &Node) -> &'a str { + &self.text[node.start_byte()..node.end_byte()] + } + + fn documentation(&self, line: usize) -> Documentation { + match self.comments.find(line) { + Some(r) => Documentation { + ref_line: line as u64, + doc_start: DocLocation::from(r.start), + doc_end: DocLocation::from(r.end), + docs: Some(spdcp::Comment::parse(&self.source[r.start..r.end])), + metadata: None, + }, + None => Documentation { + ref_line: line as u64, + doc_start: DocLocation::default(), + doc_end: DocLocation::default(), + docs: None, + metadata: None, + }, + } + } + + fn declaration(&self, name: &str, line: usize) -> Declaration { + Declaration { + name: name.to_string(), + documentation: self.documentation(line), + } + } + + fn render_expr(&self, node: &Node) -> String { + Renderer::new(self.pp, node.start_byte()).render_src(self.text(node)) + } + + fn dim(&self, node: &Node) -> String { + match node.kind() { + "fixed_dimension" => { + // `[` expr (char)? `]` + let expr = children(node) + .into_iter() + .find(|c| c.is_named() && c.kind() != "dimension_packing" && c.kind() != "comment"); + match expr { + // Like docparse, only simple sizes are shown + Some(e) => match self.render_expr(&e) { + size if size.contains(' ') => "[]".to_string(), + size => format!("[{}]", size), + }, + None => "[]".to_string(), + } + } + _ => "[]".to_string(), + } + } + + /// Base type name from a `type`, `old_type` or `builtin_type` node + fn base_type(&self, node: &Node) -> String { + match node.kind() { + "old_type" => { + let inner = children(node).into_iter().find(|c| c.kind() != ":"); + match inner { + Some(n) if n.kind() == "multi_tag" => "any".to_string(), + Some(n) => old_tag_to_type(self.text(&n).trim()).to_string(), + None => "int".to_string(), + } + } + "array_type" => children(node) + .into_iter() + .find(|c| c.kind() == "type") + .map(|t| self.base_type(&t)) + .unwrap_or_default(), + _ => match self.text(node).trim() { + // Untagged old style parameters like `...` + "" => "int".to_string(), + t => t.to_string(), + }, + } + } + + /// Collects type information from a node's `type`/`returnType` field + /// and any dimensions written after it. + fn type_parts(&self, nodes: &[Node]) -> TypeParts { + let mut parts = TypeParts { + base: "int".to_string(), + ..Default::default() + }; + for n in nodes { + match n.kind() { + "type" | "old_type" | "builtin_type" => parts.base = self.base_type(n), + "array_type" => { + parts.base = self.base_type(n); + for c in children(n) { + if matches!(c.kind(), "dimension" | "fixed_dimension") { + parts.prefix_dims.push(self.dim(&c)); + } + } + } + "dimension" | "fixed_dimension" => parts.prefix_dims.push(self.dim(n)), + "&" => parts.by_ref = true, + _ => {} + } + } + parts + } + + fn parameter(&self, node: &Node) -> Option { + match node.kind() { + "parameter_declaration" => { + let mut parts = self.type_parts(&field_nodes(node, "type")); + let mut name = String::new(); + let mut default = None; + let mut seen_name = false; + for (field, c) in children_with_fields(node) { + match c.kind() { + "variable_storage_class" => parts.is_const = true, + "&" => parts.by_ref = true, + "dimension" | "fixed_dimension" if seen_name => { + parts.postfix_dims.push(self.dim(&c)) + } + _ => {} + } + if field == Some("name") { + name = self.text(&c).to_string(); + seen_name = true; + } + } + if let Some(d) = node.child_by_field_name("defaultValue") { + default = Some(self.render_expr(&d)); + } + // Old style `String:name[]` is printed as `char[] name`, sized arrays keep + // their dimensions after the name + let old_style = field_nodes(node, "type").is_empty() + || field_nodes(node, "type").iter().any(|t| t.kind() == "old_type"); + if old_style && parts.postfix_dims.iter().all(|d| d == "[]") { + let postfix = std::mem::take(&mut parts.postfix_dims); + parts.prefix_dims.extend(postfix); + } + Some(Argument { + r#type: parts.type_string(), + decl: parts.decl_string(&name), + name, + default, + }) + } + "rest_parameter" => { + let mut parts = self.type_parts(&field_nodes(node, "type")); + for c in children(node) { + if c.kind() == "variable_storage_class" { + parts.is_const = true; + } + } + let ty = format!("{}...", parts.type_string()); + Some(Argument { + r#type: ty.clone(), + name: "...".to_string(), + decl: ty, + default: None, + }) + } + _ => None, + } + } + + fn parameters(&self, node: Option) -> Vec { + match node { + Some(n) => children(&n) + .iter() + .filter_map(|c| self.parameter(c)) + .collect(), + None => Vec::new(), + } + } + + fn return_type(&self, node: &Node) -> String { + let nodes = field_nodes(node, "returnType"); + if nodes.is_empty() { + // Old style declarations without a tag. docparse inferred the + // return type of definitions from their body. + let returns_value = node.kind() == "function_definition" + && node + .child_by_field_name("body") + .map_or(false, |b| returns_value(&self.source[b.start_byte()..b.end_byte()])); + return match node.kind() { + "function_definition" if !returns_value => "void", + _ => "int", + } + .to_string(); + } + self.type_parts(&nodes).type_string() + } + + fn name(&self, node: &Node) -> Option { + field_nodes(node, "name") + .into_iter() + .find(|n| n.kind() == "identifier") + .map(|n| self.text(&n).to_string()) + } + + fn name_line(&self, node: &Node) -> usize { + field_nodes(node, "name") + .first() + .map(line_of) + .unwrap_or_else(|| line_of(node)) + } + + /// `function void(int client)` + fn signature(&self, return_type: &str, params: &[Argument]) -> String { + let args: Vec<&str> = params.iter().map(|a| a.decl.as_str()).collect(); + format!("function {}({})", return_type, args.join(", ")) + } + + pub fn run(&mut self, root: &Node) { + for node in children(root) { + match node.kind() { + "function_declaration" | "function_definition" => self.function(&node), + "enum" => self.enumeration(&node), + "enum_struct" => self.enum_struct(&node), + "methodmap" => self.methodmap(&node), + "typedef" => self.typedef(&node), + "typeset" => self.typeset(&node), + "functag" => self.functag(&node), + "funcenum" => self.funcenum(&node), + "struct" => self.structure(&node), + _ => {} + } + } + + self.defines(); + self.space_foreign_rest_types(); + } + + /// docparse printed rest parameters of types it couldn't resolve (types + /// declared in other files) as `Type ...` and all others as `Type...` + fn space_foreign_rest_types(&mut self) { + const BUILTIN: &[&str] = &["int", "bool", "float", "char", "any", "void", "int64"]; + let s = &mut self.strand; + let local: std::collections::HashSet = s + .enums + .keys() + .chain(s.methodmaps.keys()) + .chain(s.enumstructs.keys()) + .chain(s.typedefs.keys()) + .chain(s.typesets.keys()) + .cloned() + .collect(); + + let is_local = |t: &str| { + let name = t + .trim_start_matches("const ") + .split(|c| c == '[' || c == '&' || c == ' ' || c == '.') + .next() + .unwrap_or(""); + BUILTIN.contains(&name) || local.contains(name) + }; + + let fix = |f: &mut Function| { + // Unresolved by-ref types were printed as `Handle &name` + for a in f.arguments.iter_mut() { + if a.name != "..." && a.r#type.ends_with('&') && !is_local(&a.r#type) { + a.decl = format!("{} &{}", a.r#type.trim_end_matches('&'), a.name); + } + } + for a in f.arguments.iter_mut().filter(|a| a.name == "...") { + let base = a.r#type.trim_end_matches("...").trim_end(); + let t = if is_local(base) { + format!("{}...", base) + } else { + format!("{} ...", base) + }; + a.r#type = t.clone(); + a.decl = t; + } + }; + + s.functions.values_mut().for_each(fix); + s.methodmaps + .values_mut() + .flat_map(|m| m.methods.values_mut()) + .for_each(fix); + s.enumstructs + .values_mut() + .flat_map(|m| m.methods.values_mut()) + .for_each(fix); + } + + /// Legacy `struct`s such as `Plugin` and `Extension` are documented as enum structs + fn structure(&mut self, node: &Node) { + let name = match self.name(node) { + Some(n) => n, + None => return, + }; + let mut es = EnumStruct { + declaration: self.declaration(&name, line_of(node)), + methods: HashMap::new(), + fields: HashMap::new(), + }; + for member in children(node) { + if !matches!(member.kind(), "struct_field" | "old_struct_field") { + continue; + } + let fname = match self.name(&member) { + Some(n) => n, + None => continue, + }; + let mut parts = self.type_parts(&field_nodes(&member, "type")); + let mut seen_name = false; + for (field, c) in children_with_fields(&member) { + match c.kind() { + "const" => parts.is_const = true, + "dimension" | "fixed_dimension" if seen_name => parts.postfix_dims.push(self.dim(&c)), + _ => {} + } + if field == Some("name") { + seen_name = true; + } + } + es.fields.insert( + fname.clone(), + Field { + declaration: self.declaration(&fname, line_of(&member)), + r#type: parts.type_string(), + }, + ); + } + self.strand.enumstructs.insert(name, es); + } + + fn function_kind(&self, node: &Node) -> String { + if let Some(k) = node.child_by_field_name("kind") { + return self.text(&k).to_string(); + } + match node.child_by_field_name("visibility") { + Some(v) => { + // Normalized order, `stock static` is `static stock` + let words: Vec<&str> = self.text(&v).split_whitespace().collect(); + ["public", "static", "stock"] + .iter() + .filter(|w| words.contains(w)) + .copied() + .collect::>() + .join(" ") + } + None => "function".to_string(), + } + } + + fn function(&mut self, node: &Node) { + let name = match self.name(node) { + Some(n) => n, + None => return, + }; + let f = Function { + declaration: self.declaration(&name, self.name_line(node)), + kind: self.function_kind(node), + return_type: self.return_type(node), + arguments: self.parameters(node.child_by_field_name("parameters")), + }; + self.strand.functions.insert(name, f); + } + + fn enumeration(&mut self, node: &Node) { + let mut entries = Vec::new(); + if let Some(list) = node.child_by_field_name("entries") { + for e in children(&list) { + if e.kind() != "enum_entry" { + continue; + } + let name = match self.name(&e) { + Some(n) => n, + None => continue, + }; + let value = field_nodes(&e, "value") + .into_iter() + .filter(|v| v.is_named()) + .last() + .map(|v| self.render_expr(&v)); + let line = self.name_line(&e); + entries.push(Entry { + declaration: self.declaration(&name, line), + value, + }); + } + } + + match self.name(node) { + None => { + for e in entries { + self.strand.constants.insert( + e.declaration.name.clone(), + Constant { + declaration: e.declaration, + }, + ); + } + } + Some(name) => { + let mut map = HashMap::new(); + for e in entries { + map.insert(e.declaration.name.clone(), e); + } + self.strand.enums.insert( + name.clone(), + Enumeration { + declaration: self.declaration(&name, line_of(node)), + entries: map, + }, + ); + } + } + } + + fn enum_struct(&mut self, node: &Node) { + let name = match self.name(node) { + Some(n) => n, + None => return, + }; + let mut es = EnumStruct { + declaration: self.declaration(&name, line_of(node)), + methods: HashMap::new(), + fields: HashMap::new(), + }; + for member in children(node) { + match member.kind() { + "enum_struct_field" => { + let fname = match self.name(&member) { + Some(n) => n, + None => continue, + }; + let mut parts = self.type_parts(&field_nodes(&member, "type")); + for c in children(&member) { + if c.kind() == "fixed_dimension" { + parts.postfix_dims.push(self.dim(&c)); + } + } + es.fields.insert( + fname.clone(), + Field { + declaration: self.declaration(&fname, line_of(&member)), + r#type: parts.type_string(), + }, + ); + } + "enum_struct_method" => { + let mname = match self.name(&member) { + Some(n) => n, + None => continue, + }; + es.methods.insert( + mname.clone(), + Function { + declaration: self.declaration(&mname, line_of(&member)), + kind: "stock".to_string(), + return_type: self.return_type(&member), + arguments: self.parameters(member.child_by_field_name("parameters")), + }, + ); + } + _ => {} + } + } + self.strand.enumstructs.insert(name, es); + } + + fn methodmap(&mut self, node: &Node) { + let name = match self.name(node) { + Some(n) => n, + None => return, + }; + let parent = node + .child_by_field_name("inherits") + .map(|n| self.text(&n).to_string()); + + let mut mm = MethodMap { + declaration: self.declaration(&name, line_of(node)), + parent, + methods: HashMap::new(), + properties: HashMap::new(), + }; + + for member in children(node) { + let kind = member.kind(); + if kind == "methodmap_property" { + if let Some(p) = self.property(&member) { + mm.properties.insert(p.declaration.name.clone(), p); + } + continue; + } + + let mname = match self.name(&member) { + Some(n) => n, + None => continue, + }; + let (method_kind, return_type, arguments) = match kind { + "methodmap_native" => ( + "native", + self.return_type(&member), + self.parameters(member.child_by_field_name("parameters")), + ), + "methodmap_method" => ( + "stock", + self.return_type(&member), + self.parameters(member.child_by_field_name("parameters")), + ), + "methodmap_native_constructor" => ( + "native", + name.clone(), + self.parameters(member.child_by_field_name("parameters")), + ), + "methodmap_method_constructor" => ( + "stock", + name.clone(), + self.parameters(member.child_by_field_name("parameters")), + ), + "methodmap_native_destructor" => ("native", "void".to_string(), Vec::new()), + "methodmap_method_destructor" => ("stock", "void".to_string(), Vec::new()), + "methodmap_alias" => { + // `public Close() = CloseHandle;` borrows the aliased + // function's signature, minus the `this` argument + let target = member + .child_by_field_name("function") + .map(|f| self.text(&f).to_string()); + match target.and_then(|t| self.strand.functions.get(&t)) { + Some(f) => ( + "native", + f.return_type.clone(), + f.arguments.iter().skip(1).cloned().collect(), + ), + None => ("native", "void".to_string(), Vec::new()), + } + } + _ => continue, + }; + let is_destructor = kind.ends_with("destructor"); + let mname = if is_destructor { + format!("~{}", mname) + } else { + mname + }; + mm.methods.insert( + mname.clone(), + Function { + declaration: self.declaration(&mname, line_of(&member)), + kind: method_kind.to_string(), + return_type, + arguments, + }, + ); + } + + self.strand.methodmaps.insert(name, mm); + } + + fn property(&self, node: &Node) -> Option { + let name = self.name(node)?; + let r#type = self.type_parts(&field_nodes(node, "type")).type_string(); + let mut getter = false; + let mut setter = false; + for accessor in children(node) { + let mut stack = vec![accessor]; + while let Some(n) = stack.pop() { + match n.kind() { + "methodmap_property_getter" | "get" => getter = true, + "methodmap_property_setter" | "set" => setter = true, + "block" => continue, + _ => stack.extend(children(&n)), + } + } + } + Some(Property { + declaration: self.declaration(&name, line_of(node)), + r#type, + getter, + setter, + }) + } + + /// Renders a `typedef_expression` into `function ()` + fn typedef_expression(&self, node: &Node) -> String { + let ret = self.return_type(node); + let params = self.parameters(node.child_by_field_name("parameters")); + self.signature(&ret, ¶ms) + } + + fn typedef(&mut self, node: &Node) { + let name = match self.name(node) { + Some(n) => n, + None => return, + }; + let r#type = match children(node) + .into_iter() + .find(|c| c.kind() == "typedef_expression") + { + Some(e) => self.typedef_expression(&e), + None => { + let mut parts = self.type_parts(&field_nodes(node, "type")); + // Dimensions after the aliased type are part of it + parts.postfix_dims.clear(); + parts.type_string() + } + }; + self.insert_typedef(name, line_of(node), r#type); + } + + fn insert_typedef(&mut self, name: String, line: usize, r#type: String) { + let parsed_signature = parse_type_signature(&r#type); + let td = TypeDefinition { + declaration: self.declaration(&name, line), + r#type, + parsed_signature, + }; + self.strand.typedefs.insert(name, td); + } + + fn insert_typeset(&mut self, name: String, line: usize, types: Vec<(String, usize)>) { + let mut ts = TypeSet { + declaration: self.declaration(&name, line), + types: HashMap::new(), + }; + for (ty, line) in types { + ts.types.insert( + ty.clone(), + Type { + documentation: self.documentation(line), + parsed_signature: parse_type_signature(&ty), + r#type: ty, + }, + ); + } + self.strand.typesets.insert(name, ts); + } + + fn typeset(&mut self, node: &Node) { + let name = match self.name(node) { + Some(n) => n, + None => return, + }; + let types = children(node) + .iter() + .filter(|c| c.kind() == "typedef_expression") + .map(|c| (self.typedef_expression(c), line_of(c))) + .collect(); + self.insert_typeset(name, line_of(node), types); + } + + fn functag(&mut self, node: &Node) { + let name = match self.name(node) { + Some(n) => n, + None => return, + }; + let ret = self.return_type(node); + let params = self.parameters(node.child_by_field_name("parameters")); + let sig = self.signature(&ret, ¶ms); + self.insert_typedef(name, line_of(node), sig); + } + + fn funcenum(&mut self, node: &Node) { + let name = match self.name(node) { + Some(n) => n, + None => return, + }; + let types = children(node) + .iter() + .filter(|c| c.kind() == "funcenum_member") + .map(|c| { + let ret = self.return_type(c); + let params = self.parameters(c.child_by_field_name("parameters")); + (self.signature(&ret, ¶ms), line_of(c)) + }) + .collect(); + self.insert_typeset(name, line_of(node), types); + } + + fn defines(&mut self) { + for m in self.pp.macros.iter().filter(|m| m.undefined_at.is_none()) { + let line = self.line_of_offset(m.offset); + let define = Define { + declaration: self.declaration(&m.name, line), + value: m.value.clone(), + }; + self.strand.defines.insert(m.name.clone(), define); + } + } +} + +/// Whether a function body contains `return ;` +fn returns_value(body: &str) -> bool { + let bytes = body.as_bytes(); + let mut i = 0; + while let Some(p) = body[i..].find("return") { + let start = i + p; + let end = start + "return".len(); + i = end; + let bounded_left = start == 0 || !(bytes[start - 1].is_ascii_alphanumeric() || bytes[start - 1] == b'_'); + let bounded_right = end >= bytes.len() || !(bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_'); + if !bounded_left || !bounded_right { + continue; + } + match body[end..].trim_start().chars().next() { + Some(';') | Some('}') | None => {} + Some(_) => return true, + } + } + false +} diff --git a/libalternator/src/lib.rs b/libalternator/src/lib.rs index f1f7811..dd408a7 100644 --- a/libalternator/src/lib.rs +++ b/libalternator/src/lib.rs @@ -1,290 +1,111 @@ -use std::collections::HashMap; -use std::ffi::{CStr, CString}; -use std::os::raw::c_char; -use std::str::from_utf8; +//! SourcePawn include parser. +//! +//! Parses a single include (or plugin) file into a documentation [`Strand`] +//! using [tree-sitter-sourcepawn](https://github.com/nilshelmig/tree-sitter-sourcepawn) +//! together with a small preprocessor for conditional compilation and +//! `#define`s. `#include` directives are not followed; every file is +//! documented on its own. -use serde::Deserialize; - -use spdcp::Comment; +use tree_sitter::{Node, Parser}; use schema::bundle::Strand; -use schema::symbol::{ - parse_type_signature, Constant, DPEnumStruct, DPEnumeration, DPMethodMap, DPTypeSet, Define, - DocLocation, Documentation, EnumStruct, Enumeration, Field, Function, MethodMap, Property, - Type, TypeDefinition, TypeSet, -}; +mod comments; mod error; +mod expr; +mod extract; +mod preprocess; + +pub use error::{AlternatorError, Result}; + +/// Declaration keywords. A top level syntax error containing one of these +/// most likely swallowed a declaration, so the file can't be trusted. +const DECLARATION_KEYWORDS: &[&str] = &[ + "native", "forward", "methodmap", "enum", "typedef", "typeset", "functag", "funcenum", + "property", +]; + +/// Parses SourcePawn source into a documentation strand. +/// +/// Syntax errors inside function bodies are tolerated. Errors that may have +/// swallowed declarations fail the whole file, so that callers walking +/// history don't mistake a failed parse for removed symbols. +pub fn parse(source: &str) -> Result { + let pp = preprocess::preprocess(source); + + let mut parser = Parser::new(); + parser.set_language(&tree_sitter_sourcepawn::language())?; + + let tree = parser + .parse(pp.text.as_bytes(), None) + .ok_or(AlternatorError::ParseFail)?; + let root = tree.root_node(); + + if let Some(err) = find_fatal_error(&root, &pp.text) { + return Err(err); + } -use error::{AlternatorError, Result}; - -include!(concat!(env!("OUT_DIR"), "/bindings.rs")); - -#[derive(Deserialize, Default)] -pub struct DPStrand { - pub functions: Vec, - - pub methodmaps: Vec, - - pub enumstructs: Vec, - - pub constants: Vec, - - pub defines: Vec, - - pub enums: Vec, - - pub typesets: Vec, + let mut extractor = extract::Extractor::new(source, &pp); + extractor.run(&root); - pub typedefs: Vec, + Ok(extractor.strand) } -pub async fn consume>>(atom: T, content: Vec) -> Result { - let dp_ptr: *const c_char = unsafe { - parse( - CString::new(content.clone())?.as_ptr(), - CString::new(atom.into())?.as_ptr(), - ) - }; - - let parsed = unsafe { - CStr::from_ptr(dp_ptr.as_ref().ok_or(AlternatorError::ParseFail)?).to_string_lossy() - }; - - Ok(DPStrand::parse(&parsed, from_utf8(&content)?).await?) +/// Parses raw file content. Invalid UTF-8 is replaced. +pub fn parse_bytes(content: &[u8]) -> Result { + match std::str::from_utf8(content) { + Ok(s) => parse(s), + Err(_) => parse(&String::from_utf8_lossy(content)), + } } -impl DPStrand { - pub async fn parse(parsed: &str, content: &str) -> Result { - let mut dp_strand: Self = serde_json::from_str(parsed)?; - - let mut alternator_strand = Strand::default(); - - for m in &mut dp_strand.methodmaps { - Self::process_methodmap(m, content).await; - - let mut t = MethodMap { - declaration: m.declaration.clone(), - parent: m.parent.clone(), - methods: HashMap::new(), - properties: HashMap::new(), - }; - - for v in &m.methods { - t.methods.insert(v.declaration.name.clone(), v.clone()); - } - - for v in &m.properties { - t.properties.insert(v.declaration.name.clone(), v.clone()); - } - - alternator_strand - .methodmaps - .insert(m.declaration.name.clone(), t); - } - - for e in &mut dp_strand.enumstructs { - Self::process_enumstruct(e, content).await; - - let mut t = EnumStruct { - declaration: e.declaration.clone(), - methods: HashMap::new(), - fields: HashMap::new(), - }; - - for v in &e.methods { - t.methods.insert(v.declaration.name.clone(), v.clone()); - } - - for v in &e.fields { - t.fields.insert(v.declaration.name.clone(), v.clone()); - } - - alternator_strand - .enumstructs - .insert(e.declaration.name.clone(), t); - } - - for func in &mut dp_strand.functions { - Self::process_function(func, content).await; - - alternator_strand - .functions - .insert(func.declaration.name.clone(), func.clone()); - } - - for constant in &mut dp_strand.constants { - Self::process_constant(constant, content).await; - - alternator_strand - .constants - .insert(constant.declaration.name.clone(), constant.clone()); - } - - for define in &mut dp_strand.defines { - Self::process_define(define, content).await; - - alternator_strand - .defines - .insert(define.declaration.name.clone(), define.clone()); - } - - for r#enum in &mut dp_strand.enums { - Self::process_enum(r#enum, content).await; - - let mut t = Enumeration { - declaration: r#enum.declaration.clone(), - entries: HashMap::new(), - }; - - for v in &r#enum.entries { - t.entries.insert(v.declaration.name.clone(), v.clone()); - } - - alternator_strand - .enums - .insert(r#enum.declaration.name.clone(), t); - } - - for typeset in &mut dp_strand.typesets { - Self::process_typeset(typeset, content).await; - - let mut t = TypeSet { - declaration: typeset.declaration.clone(), - types: HashMap::new(), - }; +/// Parses an include file into a strand. +/// +/// Kept for compatibility with the previous docparse based implementation; +/// `atom` (the file name) is unused. +pub async fn consume>>(_atom: T, content: Vec) -> Result { + parse_bytes(&content) +} - for v in &typeset.types { - t.types.insert(v.r#type.clone(), v.clone()); +fn find_fatal_error(root: &Node, text: &str) -> Option { + let mut stack = vec![*root]; + + while let Some(node) = stack.pop() { + // Errors in function bodies don't affect declarations + if node.kind() == "block" || !node.has_error() && !node.is_missing() { + continue; + } + + // Missing tokens (such as the type of an untyped `...`) are + // recovered by tree-sitter without losing declarations + if node.is_missing() { + continue; + } + + if node.is_error() { + let snippet = &text[node.start_byte()..node.end_byte()]; + let top_level = node.parent().map_or(true, |p| p.kind() == "source_file"); + let has_keyword = snippet + .split(|c: char| !(c.is_alphanumeric() || c == '_')) + .any(|w| DECLARATION_KEYWORDS.contains(&w)); + + // Top level errors without declaration keywords are typically + // legacy global variables, which aren't documented anyway. + // Anything inside a declaration may have corrupted it. + if !top_level || has_keyword { + let pos = node.start_position(); + return Some(AlternatorError::Syntax { + line: pos.row + 1, + column: pos.column + 1, + snippet: snippet.chars().take(80).collect(), + }); } - - alternator_strand - .typesets - .insert(typeset.declaration.name.clone(), t); + continue; } - for typedef in &mut dp_strand.typedefs { - Self::process_typedef(typedef, content).await; - - alternator_strand - .typedefs - .insert(typedef.declaration.name.clone(), typedef.clone()); - } - - Ok(alternator_strand) + let mut c = node.walk(); + stack.extend(node.children(&mut c)); } - async fn process_methodmap(m: &mut DPMethodMap, section: &str) { - Self::process_section(&mut m.declaration.documentation, section).await; - - for method in &mut m.methods { - Self::process_function(method, section).await; - } - - for property in &mut m.properties { - Self::process_property(property, section).await; - } - } - - async fn process_enumstruct(e: &mut DPEnumStruct, section: &str) { - Self::process_section(&mut e.declaration.documentation, section).await; - - for method in &mut e.methods { - Self::process_function(method, section).await; - } - - for field in &mut e.fields { - Self::process_field(field, section).await; - } - } - - async fn process_typeset(t: &mut DPTypeSet, section: &str) { - Self::process_section(&mut t.declaration.documentation, section).await; - - for type_t in &mut t.types { - Self::process_type(type_t, section).await; - } - } - - async fn process_enum(e: &mut DPEnumeration, section: &str) { - Self::process_section(&mut e.declaration.documentation, section).await; - - for entry in &mut e.entries { - Self::process_section(&mut entry.declaration.documentation, section).await; - } - } - - async fn process_function(f: &mut Function, section: &str) { - Self::process_section(&mut f.declaration.documentation, section).await; - - // For array types, the array couples the type in the - // `type` prop, but in practice, it should couple name - // Instead of parsing type and extracting any dimension out of type - // we extract it directly from decl which already is correct - // for arg in &mut f.arguments { - // if arg.decl.contains("...") { - // arg.r#type = arg.decl.replace("...", ""); - // arg.name = "...".to_string(); - // } else { - // let split = arg.decl.split(" ").collect::>(); - - // if split.len() == 2 { - // arg.r#type = split[0].to_string(); - // arg.name = split[1].to_string(); - // } else if split.len() > 2 { - // arg.r#type = split[0..2].join(" "); - // arg.name = split[2].to_string(); - // } - // } - // } - } - - async fn process_property(p: &mut Property, section: &str) { - Self::process_section(&mut p.declaration.documentation, section).await; - } - - async fn process_field(f: &mut Field, section: &str) { - Self::process_section(&mut f.declaration.documentation, section).await; - } - - async fn process_constant(c: &mut Constant, section: &str) { - Self::process_section(&mut c.declaration.documentation, section).await; - } - - async fn process_define(d: &mut Define, section: &str) { - Self::process_section(&mut d.declaration.documentation, section).await; - } - - async fn process_type(t: &mut Type, section: &str) { - Self::process_section(&mut t.documentation, section).await; - - t.parsed_signature = Some(parse_type_signature(&t.r#type)); - } - - async fn process_typedef(t: &mut TypeDefinition, section: &str) { - Self::process_section(&mut t.declaration.documentation, section).await; - - t.parsed_signature = Some(parse_type_signature(&t.r#type)); - } - - async fn process_section(doc: &mut Documentation, section: &str) { - if doc.docs != None { - return; - } - - if doc.doc_start == DocLocation::from(0) || doc.doc_end == DocLocation::from(0) { - return; - } - - let bytes = section.as_bytes(); - - let start: usize = doc.doc_start.into(); - let end: usize = doc.doc_end.into(); - - let snippet = &bytes[start..end]; - - let section: String = from_utf8(snippet).unwrap().to_owned(); - - doc.docs = Some(Comment::parse(section)); - } + None } diff --git a/libalternator/src/preprocess.rs b/libalternator/src/preprocess.rs new file mode 100644 index 0000000..b0804b8 --- /dev/null +++ b/libalternator/src/preprocess.rs @@ -0,0 +1,775 @@ +//! Minimal SourcePawn preprocessor. +//! +//! Include files are documented one at a time without resolving `#include`s, +//! so this only has to: +//! - evaluate `#if`/`#elseif`/`#else`/`#endif` and drop inactive branches, +//! - record `#define`s (they're documented symbols and are expanded when +//! rendering constant expressions), +//! - honour `#endinput`, +//! - collect comments and a coarse token stream used for attaching +//! documentation comments to declarations. +//! +//! The source handed to tree-sitter has every directive line and every +//! inactive region blanked out with spaces, so byte offsets and line numbers +//! stay identical to the original file. + +use crate::expr::{self, MacroLookup}; + +/// Macros predefined by the SourcePawn compiler +const PREDEFINED: &[(&str, &str)] = &[("__sourcepawn__", "2"), ("__sourcepawn2__", "1")]; + +#[derive(Debug, Clone)] +pub struct Macro { + pub name: String, + /// Raw replacement text as written in the source + pub value: String, + /// Replacement text with comments and line continuations removed + pub clean_value: String, + pub function_like: bool, + /// Byte offset of the start of the replacement text (or the name when empty) + pub offset: usize, + /// Byte offset of the `#define` directive + pub defined_at: usize, + /// Byte offset of the `#undef` (or redefinition) removing this macro + pub undefined_at: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct RawComment { + pub start: usize, + pub end: usize, + pub start_line: usize, + pub end_line: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Item { + /// Any non comment token. Only its line matters, and whether it starts + /// a preprocessor directive. + Token { line: usize, directive: bool }, + Comment(RawComment), +} + +pub struct Preprocessed { + /// Source with directives and inactive regions blanked out + pub text: String, + pub items: Vec, + pub macros: Vec, +} + +impl MacroLookup for Preprocessed { + fn lookup(&self, name: &str, offset: usize) -> Option<&str> { + macro_at(&self.macros, name, offset).map(|m| m.clean_value.as_str()) + } +} + +fn macro_at<'a>(macros: &'a [Macro], name: &str, offset: usize) -> Option<&'a Macro> { + macros.iter().rev().find(|m| { + m.name == name + && !m.function_like + && m.defined_at <= offset + && m.undefined_at.map_or(true, |u| u > offset) + }) +} + +struct Frame { + parent_active: bool, + taken: bool, + active: bool, +} + +struct Scanner<'s> { + src: &'s [u8], + pos: usize, + line: usize, + stack: Vec, + items: Vec, + macros: Vec, + /// Byte ranges to blank out for tree-sitter + masked: Vec<(usize, usize)>, + inactive_since: Option, +} + +fn is_ident_start(b: u8) -> bool { + b.is_ascii_alphabetic() || b == b'_' || b >= 0x80 +} + +fn is_ident(b: u8) -> bool { + b.is_ascii_alphanumeric() || b == b'_' || b >= 0x80 +} + +impl<'s> Scanner<'s> { + fn active(&self) -> bool { + self.stack.last().map_or(true, |f| f.active) + } + + fn peek(&self, n: usize) -> u8 { + *self.src.get(self.pos + n).unwrap_or(&0) + } + + fn push_token(&mut self, line: usize, directive: bool) { + if let Some(Item::Token { line: l, .. }) = self.items.last() { + if *l == line && !directive { + return; + } + } + self.items.push(Item::Token { line, directive }); + } + + /// Consumes a newline sequence at the current position if there is one + fn newline(&mut self) -> bool { + match self.peek(0) { + b'\n' => { + self.pos += 1; + self.line += 1; + true + } + b'\r' => { + self.pos += 1; + if self.peek(0) == b'\n' { + self.pos += 1; + } + self.line += 1; + true + } + _ => false, + } + } + + /// Scans a comment starting at the current position (which must be `//` or `/*`) + fn comment(&mut self, record: bool) { + let start = self.pos; + let start_line = self.line; + if self.peek(1) == b'/' { + while self.pos < self.src.len() && !matches!(self.peek(0), b'\n' | b'\r') { + self.pos += 1; + } + } else { + self.pos += 2; + loop { + if self.pos >= self.src.len() { + break; + } + if self.peek(0) == b'*' && self.peek(1) == b'/' { + self.pos += 2; + break; + } + if !self.newline() { + self.pos += 1; + } + } + } + if record { + self.items.push(Item::Comment(RawComment { + start, + end: self.pos, + start_line, + end_line: self.line, + })); + } + } + + /// Skips a string or character literal + fn literal(&mut self) { + let quote = self.peek(0); + self.pos += 1; + while self.pos < self.src.len() { + match self.peek(0) { + b'\\' => { + self.pos += 1; + if !self.newline() { + self.pos += 1; + } + } + b'\n' | b'\r' => break, + c => { + self.pos += 1; + if c == quote { + break; + } + } + } + } + } + + fn run(&mut self) { + let mut at_line_start = true; + + while self.pos < self.src.len() { + let c = self.peek(0); + + if self.newline() { + at_line_start = true; + continue; + } + + if c == b' ' || c == b'\t' || c == 0x0B || c == 0x0C { + self.pos += 1; + continue; + } + + // Line continuation outside of a directive + if c == b'\\' && matches!(self.peek(1), b'\n' | b'\r') { + self.pos += 1; + self.newline(); + continue; + } + + let active = self.active(); + + if c == b'/' && (self.peek(1) == b'/' || self.peek(1) == b'*') { + self.comment(active); + continue; + } + + if at_line_start && c == b'#' { + if !self.directive() { + // #endinput + return; + } + at_line_start = true; + continue; + } + + at_line_start = false; + + if active { + self.push_token(self.line, false); + } + + if c == b'"' || c == b'\'' { + self.literal(); + } else { + self.pos += 1; + } + } + } + + /// Handles a directive at the current position. Returns false on `#endinput`. + fn directive(&mut self) -> bool { + let start = self.pos; + let start_line = self.line; + let was_active = self.active(); + + if was_active { + self.push_token(start_line, true); + } + + // Collect the logical line: strip comments, join continuations + self.pos += 1; + let mut clean = String::new(); + // Byte ranges of the raw text that make up the directive body (for define values) + let mut body_start: Option = None; + let mut body_end = self.pos; + + loop { + if self.pos >= self.src.len() { + break; + } + let c = self.peek(0); + if c == b'\n' || c == b'\r' { + break; + } + if c == b'\\' && matches!(self.peek(1), b'\n' | b'\r') { + self.pos += 1; + self.newline(); + clean.push(' '); + continue; + } + if c == b'/' && (self.peek(1) == b'/' || self.peek(1) == b'*') { + self.comment(was_active); + clean.push(' '); + continue; + } + if c == b'"' || c == b'\'' { + let s = self.pos; + self.literal(); + clean.push_str(&String::from_utf8_lossy(&self.src[s..self.pos])); + body_start.get_or_insert(s); + body_end = self.pos; + continue; + } + if c != b' ' && c != b'\t' { + body_start.get_or_insert(self.pos); + body_end = self.pos + 1; + } + // Keep multi-byte characters intact + let len = utf8_len(c); + let end = (self.pos + len).min(self.src.len()); + clean.push_str(&String::from_utf8_lossy(&self.src[self.pos..end])); + self.pos = end; + } + + let end_line = self.line; + let end = self.pos; + self.masked.push((start, end)); + + if was_active { + // Directives end with an end of line token which terminates tail comments + self.push_token(end_line, false); + } + + let trimmed = clean.trim_start(); + let name_len = trimmed + .bytes() + .take_while(|b| is_ident(*b)) + .count(); + let name = &trimmed[..name_len]; + let rest = trimmed[name_len..].trim(); + + match name { + "if" => { + let parent = self.active(); + let cond = parent && self.evaluate(rest, start); + self.push_frame(Frame { + parent_active: parent, + taken: cond || !parent, + active: cond, + }, end); + } + "elseif" | "elif" => { + if let Some(f) = self.stack.last() { + let (parent, taken) = (f.parent_active, f.taken); + let cond = !taken && parent && self.evaluate(rest, start); + self.set_top(cond, taken || cond, start, end); + } + } + "else" => { + if let Some(f) = self.stack.last() { + let (parent, taken) = (f.parent_active, f.taken); + self.set_top(parent && !taken, true, start, end); + } + } + "endif" => { + if self.stack.pop().is_some() { + self.update_inactive(start, end); + } + } + _ if !was_active => {} + "define" => self.define(start, body_start, body_end), + "undef" => { + let n: String = rest.bytes().take_while(|b| is_ident(*b)).map(|b| b as char).collect(); + self.undefine(&n, start); + } + "endinput" => { + self.masked.push((end, self.src.len())); + return false; + } + _ => {} + } + + true + } + + fn push_frame(&mut self, frame: Frame, directive_end: usize) { + let before = self.active(); + self.stack.push(frame); + let after = self.active(); + if before && !after { + self.inactive_since = Some(directive_end); + } + } + + fn set_top(&mut self, active: bool, taken: bool, start: usize, end: usize) { + if let Some(f) = self.stack.last_mut() { + f.active = active; + f.taken = taken; + } + self.update_inactive(start, end); + } + + /// Opens or closes the current masked inactive range after a + /// conditional directive spanning `start..end` was processed. + fn update_inactive(&mut self, start: usize, end: usize) { + let active = self.active(); + match (self.inactive_since, active) { + (Some(since), true) => { + self.masked.push((since, start)); + self.inactive_since = None; + } + (None, false) => self.inactive_since = Some(end), + _ => {} + } + } + + fn evaluate(&self, cond: &str, offset: usize) -> bool { + let e = match expr::parse(cond) { + Some(e) => e, + None => return false, + }; + let defined = |n: &str| self.is_defined(n, offset); + let value_of = |n: &str| -> Option { + if let Some((_, v)) = PREDEFINED.iter().find(|(p, _)| *p == n) { + return Some(v.to_string()); + } + macro_at(&self.macros, n, offset).map(|m| m.clean_value.clone()) + }; + expr::eval(&e, &defined, &value_of, 0).map_or(false, |v| v != 0) + } + + fn is_defined(&self, name: &str, offset: usize) -> bool { + PREDEFINED.iter().any(|(p, _)| *p == name) + || self.macros.iter().any(|m| { + m.name == name && m.defined_at <= offset && m.undefined_at.map_or(true, |u| u > offset) + }) + } + + fn undefine(&mut self, name: &str, at: usize) { + for m in self.macros.iter_mut() { + if m.name == name && m.undefined_at.is_none() { + m.undefined_at = Some(at); + } + } + } + + fn define(&mut self, directive: usize, body_start: Option, body_end: usize) { + let body_start = match body_start { + Some(b) => b, + None => return, + }; + let src = self.src; + // Skip `#`, spaces and `define` + let mut p = directive + 1; + while p < body_end && (src[p] == b' ' || src[p] == b'\t') { + p += 1; + } + p += "define".len(); + while p < body_end && matches!(src[p], b' ' | b'\t' | b'\\' | b'\r' | b'\n') { + p += 1; + } + let _ = body_start; + if p >= body_end || !is_ident_start(src[p]) { + return; + } + let name_start = p; + while p < body_end && is_ident(src[p]) { + p += 1; + } + let name = String::from_utf8_lossy(&src[name_start..p]).to_string(); + + let mut function_like = false; + if p < body_end && src[p] == b'(' { + function_like = true; + while p < body_end && src[p] != b')' { + p += 1; + } + p += 1; + } + + // Skip whitespace and comments before the replacement + loop { + while p < body_end && matches!(src[p], b' ' | b'\t' | b'\r' | b'\n') { + p += 1; + } + if p + 1 < body_end && src[p] == b'\\' && matches!(src[p + 1], b'\r' | b'\n') { + p += 1; + continue; + } + if p + 1 < body_end && src[p] == b'/' && src[p + 1] == b'*' { + match find(src, p + 2, b"*/") { + Some(e) => { + p = e + 2; + continue; + } + None => break, + } + } + break; + } + + let (value, offset) = if p < body_end { + (String::from_utf8_lossy(&src[p..body_end]).to_string(), p) + } else { + (String::new(), name_start) + }; + + let clean_value = strip_comments(&value); + + self.undefine(&name, directive); + self.macros.push(Macro { + name, + value, + clean_value, + function_like, + offset, + defined_at: directive, + undefined_at: None, + }); + } +} + +fn utf8_len(first: u8) -> usize { + match first { + 0xF0..=0xFF => 4, + 0xE0..=0xEF => 3, + 0xC0..=0xDF => 2, + _ => 1, + } +} + +fn find(src: &[u8], from: usize, needle: &[u8]) -> Option { + if from >= src.len() { + return None; + } + src[from..] + .windows(needle.len()) + .position(|w| w == needle) + .map(|p| p + from) +} + +/// Removes comments and line continuations from a macro body +fn strip_comments(s: &str) -> String { + let mut out = String::new(); + let mut rest = s; + while !rest.is_empty() { + if rest.starts_with("/*") { + match rest[2..].find("*/") { + Some(e) => rest = &rest[2 + e + 2..], + None => break, + } + out.push(' '); + } else if rest.starts_with("//") { + break; + } else if rest.starts_with('"') || rest.starts_with('\'') { + let q = rest.as_bytes()[0]; + let bytes = rest.as_bytes(); + let mut i = 1; + while i < bytes.len() { + if bytes[i] == b'\\' { + i += 2; + continue; + } + if bytes[i] == q { + i += 1; + break; + } + i += 1; + } + let i = i.min(rest.len()); + out.push_str(&rest[..i]); + rest = &rest[i..]; + } else if rest.starts_with("\\\r\n") { + out.push(' '); + rest = &rest[3..]; + } else if rest.starts_with("\\\n") || rest.starts_with("\\\r") { + out.push(' '); + rest = &rest[2..]; + } else { + let c = rest.chars().next().unwrap(); + out.push(c); + rest = &rest[c.len_utf8()..]; + } + } + out.trim().to_string() +} + +pub fn preprocess(source: &str) -> Preprocessed { + let mut s = Scanner { + src: source.as_bytes(), + pos: 0, + line: 1, + stack: Vec::new(), + items: Vec::new(), + macros: Vec::new(), + masked: Vec::new(), + inactive_since: None, + }; + + s.run(); + + if let Some(since) = s.inactive_since { + s.masked.push((since, source.len())); + } + + let mut bytes = source.as_bytes().to_vec(); + for (a, b) in &s.masked { + for byte in &mut bytes[*a..(*b).min(source.len())] { + if *byte != b'\n' && *byte != b'\r' { + *byte = b' '; + } + } + } + + blank_bodies(&mut bytes); + + Preprocessed { + // Only ASCII bytes and whole characters were replaced + text: String::from_utf8(bytes).expect("masking preserves utf-8"), + items: s.items, + macros: s.macros, + } +} + +/// Blanks out the contents of function bodies. +/// +/// Documentation never needs statements, and blanking them keeps unusual +/// statement syntax from confusing the parser about the surrounding +/// declarations. A body is a `{` directly following a parameter list `)`, +/// which at declaration level only happens for functions, methods and +/// property accessors. `enum E (+= 1) {` is excluded. +fn blank_bodies(bytes: &mut [u8]) { + let len = bytes.len(); + let mut i = 0; + // Stack of the first significant byte inside each open paren + let mut parens: Vec = Vec::new(); + // Last significant byte, and for `)` whether its group looked like an enum increment + let mut prev: u8 = 0; + let mut prev_enum_increment = false; + + while i < len { + let c = bytes[i]; + match c { + b'/' if i + 1 < len && bytes[i + 1] == b'/' => { + while i < len && bytes[i] != b'\n' { + i += 1; + } + continue; + } + b'/' if i + 1 < len && bytes[i + 1] == b'*' => { + i += 2; + while i + 1 < len && !(bytes[i] == b'*' && bytes[i + 1] == b'/') { + i += 1; + } + i += 2; + continue; + } + b'"' | b'\'' => { + i = skip_literal(bytes, i); + prev = c; + continue; + } + b'(' => parens.push(i + 1), + b')' => { + prev_enum_increment = parens.pop().map_or(false, |start| { + let inner = &bytes[start..i]; + let first = inner.iter().position(|b| !b.is_ascii_whitespace()); + first.map_or(false, |f| { + let rest = &inner[f..]; + ["+=", "-=", "*=", "/=", "|=", "&=", "^=", "~=", "<<=", ">>=", "="] + .iter() + .any(|op| rest.starts_with(op.as_bytes())) + }) + }); + } + b'{' if prev == b')' && !prev_enum_increment => { + if let Some(end) = matching_brace(bytes, i) { + for b in &mut bytes[i + 1..end] { + if *b != b'\n' && *b != b'\r' { + *b = b' '; + } + } + i = end; + prev = b'}'; + i += 1; + continue; + } + } + _ => {} + } + if !c.is_ascii_whitespace() { + prev = c; + } + i += 1; + } +} + +fn skip_literal(bytes: &[u8], start: usize) -> usize { + let quote = bytes[start]; + let mut i = start + 1; + while i < bytes.len() { + match bytes[i] { + b'\\' => i += 2, + b'\n' => return i, + c if c == quote => return i + 1, + _ => i += 1, + } + } + bytes.len() +} + +/// Finds the `}` closing the `{` at `open`, skipping comments and literals +fn matching_brace(bytes: &[u8], open: usize) -> Option { + let len = bytes.len(); + let mut depth = 0usize; + let mut i = open; + while i < len { + match bytes[i] { + b'/' if i + 1 < len && bytes[i + 1] == b'/' => { + while i < len && bytes[i] != b'\n' { + i += 1; + } + continue; + } + b'/' if i + 1 < len && bytes[i + 1] == b'*' => { + i += 2; + while i + 1 < len && !(bytes[i] == b'*' && bytes[i + 1] == b'/') { + i += 1; + } + i += 2; + continue; + } + b'"' | b'\'' => { + i = skip_literal(bytes, i); + continue; + } + b'{' => depth += 1, + b'}' => { + depth -= 1; + if depth == 0 { + return Some(i); + } + } + _ => {} + } + i += 1; + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn masks_inactive_branches() { + let src = "#if defined FOO\nnative A();\n#else\nnative B();\n#endif\n"; + let p = preprocess(src); + assert!(!p.text.contains("A()")); + assert!(p.text.contains("native B();")); + assert_eq!(p.text.len(), src.len()); + } + + #[test] + fn tracks_defines() { + let src = "#define FOO 1\n#if FOO\nnative A();\n#endif\n#undef FOO\n#if defined FOO\nnative B();\n#endif"; + let p = preprocess(src); + assert!(p.text.contains("native A();")); + assert!(!p.text.contains("native B();")); + assert_eq!(p.macros.len(), 1); + assert_eq!(p.macros[0].value, "1"); + } + + #[test] + fn define_values() { + let p = preprocess("#define A (1<<0) /* doc */\n#define B\n#define C(%1) %1+1\n"); + assert_eq!(p.macros[0].value, "(1<<0)"); + assert_eq!(p.macros[1].value, ""); + assert!(p.macros[2].function_like); + assert_eq!(p.macros[2].value, "%1+1"); + } + + #[test] + fn blanks_function_bodies() { + let src = "stock int Foo(int a)\n{\n switch (a) { case 1: return 2; }\n}\nenum E (<<= 1) { A = 1, B }\n"; + let p = preprocess(src); + assert!(p.text.contains("stock int Foo(int a)\n{\n")); + assert!(!p.text.contains("switch")); + assert!(p.text.contains("A = 1, B")); + } + + #[test] + fn endinput_stops() { + let p = preprocess("native A();\n#endinput\nnative B();"); + assert!(!p.text.contains("B")); + } +} diff --git a/libalternator/tests/corpus.rs b/libalternator/tests/corpus.rs new file mode 100644 index 0000000..b454576 --- /dev/null +++ b/libalternator/tests/corpus.rs @@ -0,0 +1,134 @@ +//! Snapshot tests over vendored upstream SourcePawn sources. +//! +//! Every file under `tests/corpus` is parsed and compared against +//! `tests/expected/.json`. Run with `ALTERNATOR_BLESS=1` to write the +//! current output as the new expectation, then review the diff. Files that +//! fail to parse are recorded as `{ "error": ... }`. + +use std::fs; +use std::path::{Path, PathBuf}; + +use serde_json::Value; + +fn root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests") +} + +fn collect(dir: &Path, out: &mut Vec) { + let mut entries: Vec<_> = fs::read_dir(dir) + .unwrap_or_else(|e| panic!("unable to read {}: {}", dir.display(), e)) + .map(|e| e.unwrap().path()) + .collect(); + entries.sort(); + for path in entries { + if path.is_dir() { + collect(&path, out); + } else if matches!( + path.extension().and_then(|e| e.to_str()), + Some("inc") | Some("sp") + ) { + out.push(path); + } + } +} + +/// First JSON path where two values differ +fn first_difference(expected: &Value, actual: &Value, path: String) -> Option { + match (expected, actual) { + (Value::Object(a), Value::Object(b)) => { + let mut keys: Vec<&String> = a.keys().chain(b.keys()).collect(); + keys.sort(); + keys.dedup(); + keys.into_iter().find_map(|k| match (a.get(k), b.get(k)) { + (Some(x), Some(y)) => first_difference(x, y, format!("{}/{}", path, k)), + (Some(_), None) => Some(format!("{}/{} (missing)", path, k)), + (None, _) => Some(format!("{}/{} (unexpected)", path, k)), + }) + } + (Value::Array(a), Value::Array(b)) if a.len() == b.len() => a + .iter() + .zip(b) + .enumerate() + .find_map(|(i, (x, y))| first_difference(x, y, format!("{}/{}", path, i))), + _ if expected == actual => None, + _ => Some(format!("{}: expected {} got {}", path, expected, actual)), + } +} + +#[test] +fn corpus_matches_expected_output() { + let corpus = root().join("corpus"); + let expected_dir = root().join("expected"); + let bless = std::env::var_os("ALTERNATOR_BLESS").is_some(); + + let mut files = Vec::new(); + collect(&corpus, &mut files); + assert!(files.len() > 100, "corpus is missing, run tests/update-corpus.sh"); + + let mut failures = Vec::new(); + + for file in &files { + let rel = file.strip_prefix(&corpus).unwrap(); + let expected_path = expected_dir.join(format!("{}.json", rel.display())); + + let content = fs::read(file).unwrap(); + + // Files that can't be parsed are snapshotted as their error + let actual = match alternator::parse_bytes(&content) { + // Round trip through Value for a stable key order + Ok(strand) => serde_json::to_value(&strand).unwrap(), + Err(e) => serde_json::json!({ "error": e.to_string() }), + }; + let rendered = serde_json::to_string_pretty(&actual).unwrap() + "\n"; + + if bless { + fs::create_dir_all(expected_path.parent().unwrap()).unwrap(); + fs::write(&expected_path, rendered).unwrap(); + continue; + } + + let expected: Value = match fs::read_to_string(&expected_path) { + Ok(s) => serde_json::from_str(&s).unwrap(), + Err(_) => { + failures.push(format!("{}: no expected output", rel.display())); + continue; + } + }; + + if let Some(diff) = first_difference(&expected, &actual, String::new()) { + failures.push(format!("{}: {}", rel.display(), diff)); + } + } + + // Expectations without a corpus file are stale + let mut expected_files = Vec::new(); + if expected_dir.exists() && !bless { + collect_json(&expected_dir, &mut expected_files); + } + for e in expected_files { + let rel = e.strip_prefix(&expected_dir).unwrap().to_string_lossy().to_string(); + let source = corpus.join(rel.trim_end_matches(".json")); + if !source.exists() { + failures.push(format!("{}: stale expectation", rel)); + } + } + + assert!( + failures.is_empty(), + "{} of {} corpus files differ (run with ALTERNATOR_BLESS=1 to update):\n{}", + failures.len(), + files.len(), + failures.join("\n") + ); +} + +fn collect_json(dir: &Path, out: &mut Vec) { + for entry in fs::read_dir(dir).unwrap() { + let path = entry.unwrap().path(); + if path.is_dir() { + collect_json(&path, out); + } else if path.extension().and_then(|e| e.to_str()) == Some("json") { + out.push(path); + } + } +} diff --git a/libalternator/tests/corpus/SOURCES.md b/libalternator/tests/corpus/SOURCES.md new file mode 100644 index 0000000..73a6ac8 --- /dev/null +++ b/libalternator/tests/corpus/SOURCES.md @@ -0,0 +1,19 @@ +# Parser regression corpus + +Unmodified upstream SourcePawn sources used as regression tests for the +parser. Generated by `../update-corpus.sh`; each file keeps its original +license, listed below. + +| Suite | Repository | Commit | License | +| --- | --- | --- | --- | +| `sourcemod` | [alliedmodders/sourcemod](https://github.com/alliedmodders/sourcemod) | `6eb5f8fa38` | GPL-3.0 (with SourceMod exception) | +| `autoexecconfig` | [Impact123/AutoExecConfig](https://github.com/Impact123/AutoExecConfig) | `ccd723616a` | GPL-3.0 | +| `bytebuffer` | [rumblefrog/SM-ByteBuffer-Inc](https://github.com/rumblefrog/SM-ByteBuffer-Inc) | `57d580ab21` | GPL-3.0 | +| `includelibrary` | [JoinedSenses/SourceMod-IncludeLibrary](https://github.com/JoinedSenses/SourceMod-IncludeLibrary) | `9d4e4263b7` | LGPL-3.0 | +| `json` | [clugg/sm-json](https://github.com/clugg/sm-json) | `fd63b4458c` | GPL-3.0 | +| `multicolors` | [Bara/Multi-Colors](https://github.com/Bara/Multi-Colors) | `d2f2dc9126` | GPL-3.0 | +| `ripext` | [ErikMinekus/sm-ripext](https://github.com/ErikMinekus/sm-ripext) | `a607852767` | GPL-3.0 | +| `sbpp` | [sbpp/sourcebans-pp](https://github.com/sbpp/sourcebans-pp) | `0bf2a772e0` | GPL-3.0 | +| `socket` | [JoinedSenses/sm-ext-socket](https://github.com/JoinedSenses/sm-ext-socket) | `f699399524` | GPL-2.0 | +| `source-chat-relay` | [rumblefrog/source-chat-relay](https://github.com/rumblefrog/source-chat-relay) | `7077e906c8` | GPL-3.0 | +| `ttt` | [TroubleInTerroristTown/Public](https://github.com/TroubleInTerroristTown/Public) | `aa9699cbc4` | GPL-3.0 | diff --git a/libalternator/tests/corpus/autoexecconfig/autoexecconfig.inc b/libalternator/tests/corpus/autoexecconfig/autoexecconfig.inc new file mode 100644 index 0000000..ac85cfe --- /dev/null +++ b/libalternator/tests/corpus/autoexecconfig/autoexecconfig.inc @@ -0,0 +1,768 @@ +/** + * AutoExecConfig + * + * Copyright (C) 2013-2023 Impact + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +#if defined _autoexecconfig_included + #endinput +#endif +#define _autoexecconfig_included + + +#include + +#pragma semicolon 1 +#pragma newdecls required + +#define AUTOEXECCONFIG_VERSION "0.1.6" +#define AUTOEXECCONFIG_URL "https://forums.alliedmods.net/showthread.php?t=204254" + +// Append +#define AUTOEXEC_APPEND_BAD_FILENAME 0 +#define AUTOEXEC_APPEND_FILE_NOT_FOUND 1 +#define AUTOEXEC_APPEND_BAD_HANDLE 2 +#define AUTOEXEC_APPEND_SUCCESS 3 + + + +// Find +#define AUTOEXEC_FIND_BAD_FILENAME 10 +#define AUTOEXEC_FIND_FILE_NOT_FOUND 11 +#define AUTOEXEC_FIND_BAD_HANDLE 12 +#define AUTOEXEC_FIND_NOT_FOUND 13 +#define AUTOEXEC_FIND_SUCCESS 14 + + + +// Clean +#define AUTOEXEC_CLEAN_FILE_NOT_FOUND 20 +#define AUTOEXEC_CLEAN_BAD_HANDLE 21 +#define AUTOEXEC_CLEAN_SUCCESS 22 + + + +// General +#define AUTOEXEC_NO_CONFIG 30 + + + +// Formatter +#define AUTOEXEC_FORMAT_BAD_FILENAME 40 +#define AUTOEXEC_FORMAT_SUCCESS 41 + + + +// Global variables +static char g_sConfigFile[PLATFORM_MAX_PATH]; +static char g_sRawFileName[PLATFORM_MAX_PATH]; +static char g_sFolderPath[PLATFORM_MAX_PATH]; + +static bool g_bCreateFile = false; +static Handle g_hPluginHandle = null; + +static bool g_bCreateDirectory = false; +static int g_bCreateDirectoryMode = FPERM_U_READ|FPERM_U_WRITE|FPERM_U_EXEC|FPERM_G_READ|FPERM_G_EXEC|FPERM_O_READ|FPERM_O_EXEC; + + +// Workaround for now +static int g_iLastFindResult; +static int g_iLastAppendResult; + + + + +/** + * Returns the last result from the parser. + * + * @return Returns one of the AUTOEXEC_FIND values or -1 if not set. +*/ +stock int AutoExecConfig_GetFindResult() +{ + return g_iLastFindResult; +} + + + + + +/** + * Returns the last result from the appender. + * + * @return Returns one of the AUTOEXEC_APPEND values or -1 if not set. +*/ +stock int AutoExecConfig_GetAppendResult() +{ + return g_iLastAppendResult; +} + + +/** + * Set if the config file should be created by the autoexecconfig include itself if it doesn't exist. + * + * @param create True if config file should be created, false otherwise. + * @noreturn + */ +stock void AutoExecConfig_SetCreateFile(bool create) +{ + g_bCreateFile = create; +} + + +/** + * Set if the config file's folder should be created by the autoexecconfig include itself if it doesn't exist. + * Note: Must be used before AutoExecConfig_SetFile as the potential creation of it happens there + * + * @param create True if config file should be created, false otherwise. + * @param mode Folder permission mode, default is u=rwx,g=rx,o=rx. + * @noreturn + */ +stock void AutoExecConfig_SetCreateDirectory(bool create, int mode=FPERM_U_READ|FPERM_U_WRITE|FPERM_U_EXEC|FPERM_G_READ|FPERM_G_EXEC|FPERM_O_READ|FPERM_O_EXEC) +{ + g_bCreateDirectory = create; + g_bCreateDirectoryMode = mode; +} + + +/** + * Returns if the config file should be created if it doesn't exist. + * + * @return Returns true, if the config file should be created or false if it should not. + */ +stock bool AutoExecConfig_GetCreateFile() +{ + return g_bCreateFile; +} + + +/** + * Set the plugin for which the config file should be created. + * Set to null to use the calling plugin. + * Used to print the correct filename in the top comment when creating the file. + * + * @param plugin The plugin to create convars for or null to use the calling plugin. + * @noreturn + */ +stock void AutoExecConfig_SetPlugin(Handle plugin) +{ + g_hPluginHandle = plugin; +} + + +/** + * Returns the plugin's handle for which the config file is created. + * + * @return The plugin handle + */ +stock Handle AutoExecConfig_GetPlugin() +{ + return g_hPluginHandle; +} + + +/** + * Set the global autoconfigfile used by functions of this file. + * Note: does not support subfolders like folder1/folder2 + * + * @param file Name of the config file, path and .cfg extension is being added if not given. + * @param folder Folder under cfg/ to use. By default this is "sourcemod." + * @return True if formatter returned success, false otherwise. +*/ +stock bool AutoExecConfig_SetFile(char[] file, char[] folder="sourcemod") +{ + Format(g_sConfigFile, sizeof(g_sConfigFile), "%s", file); + + // Global buffers for cfg execution + strcopy(g_sRawFileName, sizeof(g_sRawFileName), file); + strcopy(g_sFolderPath, sizeof(g_sFolderPath), folder); + + + // Format the filename + return AutoExecConfig_FormatFileName(g_sConfigFile, sizeof(g_sConfigFile), folder) == AUTOEXEC_FORMAT_SUCCESS; +} + + + + + + +/** + * Get the formatted autoconfigfile used by functions of this file. + * + * @param buffer String to format. + * @param size Maximum size of buffer + * @return True if filename was set, false otherwise. +*/ +stock bool AutoExecConfig_GetFile(char[] buffer,int size) +{ + if (strlen(g_sConfigFile) > 0) + { + strcopy(buffer, size, g_sConfigFile); + + return true; + } + + // Security for decl users + buffer[0] = '\0'; + + return false; +} + + + + + + +/** + * Creates a convar and appends it to the autoconfigfile if not found. + * FCVAR_DONTRECORD will be skipped. + * + * @param name Name of new convar. + * @param defaultValue String containing the default value of new convar. + * @param description Optional description of the convar. + * @param flags Optional bitstring of flags determining how the convar should be handled. See FCVAR_* constants for more details. + * @param hasMin Optional boolean that determines if the convar has a minimum value. + * @param min Minimum floating point value that the convar can have if hasMin is true. + * @param hasMax Optional boolean that determines if the convar has a maximum value. + * @param max Maximum floating point value that the convar can have if hasMax is true. + * @return A handle to the newly created convar. If the convar already exists, a handle to it will still be returned. + * @error Convar name is blank or is the same as an existing console command. +*/ +stock ConVar AutoExecConfig_CreateConVar(const char[] name, const char[] defaultValue, const char[] description="", int flags=0, bool hasMin=false, float min=0.0, bool hasMax=false, float max=0.0) +{ + // If configfile was set and convar has no dontrecord flag + if (!(flags & FCVAR_DONTRECORD) && strlen(g_sConfigFile) > 0) + { + // Reset the results + g_iLastFindResult = -1; + g_iLastAppendResult = -1; + + + // Add it if not found + char buffer[64]; + + g_iLastFindResult = AutoExecConfig_FindValue(name, buffer, sizeof(buffer), true); + + // We only add this convar if it doesn't exist, or the file doesn't exist and it should be auto-generated + if (g_iLastFindResult == AUTOEXEC_FIND_NOT_FOUND || (g_iLastFindResult == AUTOEXEC_FIND_FILE_NOT_FOUND && g_bCreateFile)) + { + g_iLastAppendResult = AutoExecConfig_AppendValue(name, defaultValue, description, flags, hasMin, min, hasMax, max); + } + } + + + // Create the convar + return CreateConVar(name, defaultValue, description, flags, hasMin, min, hasMax, max); +} + + + + +/** + * Executes the autoconfigfile and adds it to the OnConfigsExecuted forward. + * If we didn't create it ourselves we let SourceMod create it. + * + * @noreturn +*/ +stock void AutoExecConfig_ExecuteFile() +{ + // Only let sourcemod create the file, if we didn't do that already. + AutoExecConfig(!g_bCreateFile, g_sRawFileName, g_sFolderPath); +} + + + + + +/** + * Formats a autoconfigfile, prefixes path and adds .cfg extension if missing. + * + * @param buffer String to format. + * @param size Maximum size of buffer. + * @return Returns one of the AUTOEXEC_FORMAT values.. +*/ +stock static int AutoExecConfig_FormatFileName(char[] buffer, int size, char[] folder="sourcemod") +{ + // No config set + if (strlen(g_sConfigFile) < 1) + { + return AUTOEXEC_NO_CONFIG; + } + + + // Can't be an cfgfile + if (StrContains(g_sConfigFile, ".cfg") != -1 && strlen(g_sConfigFile) < 4) + { + return AUTOEXEC_FORMAT_BAD_FILENAME; + } + + + // Pathprefix + char pathprefixbuffer[PLATFORM_MAX_PATH]; + if (strlen(folder) > 0) + { + Format(pathprefixbuffer, sizeof(pathprefixbuffer), "cfg/%s/", folder); + + if (g_bCreateDirectory && !DirExists(pathprefixbuffer)) + { + CreateDirectory(pathprefixbuffer, g_bCreateDirectoryMode); + } + } + else + { + Format(pathprefixbuffer, sizeof(pathprefixbuffer), "cfg/"); + } + + + char filebuffer[PLATFORM_MAX_PATH]; + filebuffer[0] = '\0'; + + // Add path if file doesn't begin with it + if (StrContains(buffer, pathprefixbuffer) != 0) + { + StrCat(filebuffer, sizeof(filebuffer), pathprefixbuffer); + } + + StrCat(filebuffer, sizeof(filebuffer), g_sConfigFile); + + + // Add .cfg extension if file doesn't end with it + if (StrContains(filebuffer[strlen(filebuffer) - 4], ".cfg") != 0) + { + StrCat(filebuffer, sizeof(filebuffer), ".cfg"); + } + + strcopy(buffer, size, filebuffer); + + return AUTOEXEC_FORMAT_SUCCESS; +} + + + + + + +/** + * Appends a convar to the global autoconfigfile + * + * @param name Name of new convar. + * @param defaultValue String containing the default value of new convar. + * @param description Optional description of the convar. + * @param flags Optional bitstring of flags determining how the convar should be handled. See FCVAR_* constants for more details. + * @param hasMin Optional boolean that determines if the convar has a minimum value. + * @param min Minimum floating point value that the convar can have if hasMin is true. + * @param hasMax Optional boolean that determines if the convar has a maximum value. + * @param max Maximum floating point value that the convar can have if hasMax is true. + * @return Returns one of the AUTOEXEC_APPEND values +*/ +stock int AutoExecConfig_AppendValue(const char[] name, const char[] defaultValue, const char[] description, int flags, bool hasMin, float min, bool hasMax, float max) +{ + // No config set + if (strlen(g_sConfigFile) < 1) + { + return AUTOEXEC_NO_CONFIG; + } + + + char filebuffer[PLATFORM_MAX_PATH]; + strcopy(filebuffer, sizeof(filebuffer), g_sConfigFile); + + + //PrintToServer("pathbuffer: %s", filebuffer); + + bool bFileExists = FileExists(filebuffer); + + if (g_bCreateFile || bFileExists) + { + // If the file already exists we open it in append mode, otherwise we use a write mode which creates the file + File fFile = OpenFile(filebuffer, (bFileExists ? "a" : "w")); + char writebuffer[2048]; + + + if (fFile == null) + { + return AUTOEXEC_APPEND_BAD_HANDLE; + } + + // We just created the file, so add some header about version and stuff + if (g_bCreateFile && !bFileExists) + { + fFile.WriteLine( "// This file was auto-generated by AutoExecConfig v%s (%s)", AUTOEXECCONFIG_VERSION, AUTOEXECCONFIG_URL); + + GetPluginFilename(g_hPluginHandle, writebuffer, sizeof(writebuffer)); + Format(writebuffer, sizeof(writebuffer), "// ConVars for plugin \"%s\"", writebuffer); + fFile.WriteLine("%s", writebuffer); + } + + // Spacer + fFile.WriteLine("\n"); + + + // This is used for multiline comments + int newlines = GetCharCountInStr('\n', description); + if (newlines == 0) + { + // We have no newlines, we can write the description to the file as is + Format(writebuffer, sizeof(writebuffer), "// %s", description); + fFile.WriteLine("%s", writebuffer); + } + else + { + char[][] newlineBuf = new char[newlines +1][2048]; + ExplodeString(description, "\n", newlineBuf, newlines +1, 2048, false); + + // Each newline gets a commented newline + for (int i; i <= newlines; i++) + { + if (strlen(newlineBuf[i]) > 0) + { + fFile.WriteLine("// %s", newlineBuf[i]); + } + } + } + + + // Descspacer + fFile.WriteLine("// -"); + + + // Default + Format(writebuffer, sizeof(writebuffer), "// Default: \"%s\"", defaultValue); + fFile.WriteLine("%s", writebuffer); + + + // Minimum + if (hasMin) + { + Format(writebuffer, sizeof(writebuffer), "// Minimum: \"%f\"", min); + fFile.WriteLine("%s", writebuffer); + } + + + // Maximum + if (hasMax) + { + Format(writebuffer, sizeof(writebuffer), "// Maximum: \"%f\"", max); + fFile.WriteLine("%s", writebuffer); + } + + + // Write end and defaultvalue + Format(writebuffer, sizeof(writebuffer), "%s \"%s\"", name, defaultValue); + fFile.WriteLine("%s", writebuffer); + + + fFile.Close(); + + return AUTOEXEC_APPEND_SUCCESS; + } + + return AUTOEXEC_APPEND_FILE_NOT_FOUND; +} + + + + + + +/** + * Returns a convar's value from the global autoconfigfile + * + * @param cvar Cvar to search for. + * @param value Buffer to store result into. + * @param size Maximum size of buffer. + * @param caseSensitive Whether or not the search should be case sensitive. + * @return Returns one of the AUTOEXEC_FIND values +*/ +stock int AutoExecConfig_FindValue(const char[] cvar, char[] value, int size, bool caseSensitive=false) +{ + // Security for decl users + value[0] = '\0'; + + + // No config set + if (strlen(g_sConfigFile) < 1) + { + return AUTOEXEC_NO_CONFIG; + } + + + char filebuffer[PLATFORM_MAX_PATH]; + strcopy(filebuffer, sizeof(filebuffer), g_sConfigFile); + + + + //PrintToServer("pathbuffer: %s", filebuffer); + + bool bFileExists = FileExists(filebuffer); + + // We want to create the config file and it doesn't exist yet. + if (g_bCreateFile && !bFileExists) + { + return AUTOEXEC_FIND_FILE_NOT_FOUND; + } + + + if (bFileExists) + { + File fFile = OpenFile(filebuffer, "r"); + int valuestart; + int valueend; + int cvarend; + + // Just a reminder to self, leave the values that high + char sConvar[64]; + char sValue[64]; + char readbuffer[2048]; + char copybuffer[2048]; + + if (fFile == null) + { + return AUTOEXEC_FIND_BAD_HANDLE; + } + + + while (!fFile.EndOfFile() && fFile.ReadLine(readbuffer, sizeof(readbuffer))) + { + // Is a comment or not valid + if (IsCharSpace(readbuffer[0]) || readbuffer[0] == '/') + { + continue; + } + + + // Has not enough spaces, must have at least 1 + if (GetCharCountInStr(' ', readbuffer) < 1) + { + continue; + } + + + // Ignore cvars which aren't quoted + if (GetCharCountInStr('"', readbuffer) != 2) + { + continue; + } + + + + // Get the start of the value + if ( (valuestart = StrContains(readbuffer, "\"")) == -1 ) + { + continue; + } + + + // Get the end of the value + if ( (valueend = StrContains(readbuffer[valuestart+1], "\"")) == -1 ) + { + continue; + } + + + // Get the start of the cvar, + if ( (cvarend = StrContains(readbuffer, " ")) == -1 || cvarend >= valuestart) + { + continue; + } + + + // Skip if cvarendindex is before valuestartindex + if (cvarend >= valuestart) + { + continue; + } + + + // Convar + // Tempcopy for security + strcopy(copybuffer, sizeof(copybuffer), readbuffer); + copybuffer[cvarend] = '\0'; + + strcopy(sConvar, sizeof(sConvar), copybuffer); + + + // Value + // Tempcopy for security + strcopy(copybuffer, sizeof(copybuffer), readbuffer[valuestart+1]); + copybuffer[valueend] = '\0'; + + strcopy(sValue, sizeof(sValue), copybuffer); + + + //PrintToServer("Cvar %s has a value of %s", sConvar, sValue); + + if (StrEqual(sConvar, cvar, caseSensitive)) + { + Format(value, size, "%s", sConvar); + + fFile.Close(); + return AUTOEXEC_FIND_SUCCESS; + } + } + + fFile.Close(); + return AUTOEXEC_FIND_NOT_FOUND; + } + + + return AUTOEXEC_FIND_FILE_NOT_FOUND; +} + + + + + + +/** + * Cleans the global autoconfigfile from too much spaces + * + * @return One of the AUTOEXEC_CLEAN values. +*/ +stock int AutoExecConfig_CleanFile() +{ + // No config set + if (strlen(g_sConfigFile) < 1) + { + return AUTOEXEC_NO_CONFIG; + } + + + char sfile[PLATFORM_MAX_PATH]; + strcopy(sfile, sizeof(sfile), g_sConfigFile); + + + // Security + if (!FileExists(sfile)) + { + return AUTOEXEC_CLEAN_FILE_NOT_FOUND; + } + + + + char sfile2[PLATFORM_MAX_PATH]; + Format(sfile2, sizeof(sfile2), "%s_tempcopy", sfile); + + + char readbuffer[2048]; + int count; + bool firstreached; + + + // Open files + File fFile1 = OpenFile(sfile, "r"); + File fFile2 = OpenFile(sfile2, "w"); + + + + // Check filehandles + if (fFile1 == null || fFile2 == null) + { + if (fFile1 != null) + { + //PrintToServer("Handle1 invalid"); + fFile1.Close(); + } + + if (fFile2 != null) + { + //PrintToServer("Handle2 invalid"); + fFile2.Close(); + } + + return AUTOEXEC_CLEAN_BAD_HANDLE; + } + + + + while (!fFile1.EndOfFile() && fFile1.ReadLine(readbuffer, sizeof(readbuffer))) + { + // Is space + if (IsCharSpace(readbuffer[0])) + { + count++; + } + // No space, count from start + else + { + count = 0; + } + + + // Don't write more than 1 space if seperation after informations have been reached + if (count < 2 || !firstreached) + { + ReplaceString(readbuffer, sizeof(readbuffer), "\n", ""); + fFile2.WriteLine("%s", readbuffer); + } + + + // First bigger seperation after informations has been reached + if (count == 2) + { + firstreached = true; + } + } + + + fFile1.Close(); + fFile2.Close(); + + + // This might be a risk, for now it works + DeleteFile(sfile); + RenameFile(sfile, sfile2); + + return AUTOEXEC_CLEAN_SUCCESS; +} + + + + + + +/** + * Returns how many times the given char occures in the given string. + * + * @param str String to search for in. + * @return Occurences of the given char found in string. +*/ +stock static int GetCharCountInStr(int character, const char[] str) +{ + int len = strlen(str); + int count; + + for (int i; i < len; i++) + { + if (str[i] == character) + { + count++; + } + } + + return count; +} + + + + + + +#pragma deprecated +stock bool AutoExecConfig_CacheConvars() +{ + return false; +} diff --git a/libalternator/tests/corpus/bytebuffer/bytebuffer.inc b/libalternator/tests/corpus/bytebuffer/bytebuffer.inc new file mode 100644 index 0000000..18183ba --- /dev/null +++ b/libalternator/tests/corpus/bytebuffer/bytebuffer.inc @@ -0,0 +1,246 @@ +#if defined _bytebuffer_included + #endinput +#endif +#define _bytebuffer_included + +#define BYTEBUFFER_VERSION 2 + +#if !defined MAX_BUFFERS + #define MAX_BUFFERS 32 +#endif + +#if !defined MAX_BUFFER_LENGTH + #define MAX_BUFFER_LENGTH 1024 +#endif + +enum ByteBuffer { + INVALID_BUFFER = -1 +}; + +static char g_Buffer[MAX_BUFFERS][MAX_BUFFER_LENGTH]; +static bool g_InUse[MAX_BUFFERS]; +static bool g_IsLE[MAX_BUFFERS]; +static int g_Cursor[MAX_BUFFERS]; +static int g_Length[MAX_BUFFERS]; + +methodmap ByteBuffer { + property int Cursor { + public get() { + CheckBuffer(this); + return g_Cursor[view_as(this)]; + } + + public set(int value) { + CheckBuffer(this); + g_Cursor[view_as(this)] = value; + } + } + + property bool LittleEndian { + public get() { + CheckBuffer(this); + return g_IsLE[view_as(this)]; + } + + public set(bool value) { + CheckBuffer(this); + g_IsLE[view_as(this)] = value; + } + } + + property bool BigEndian { + public get() { + CheckBuffer(this); + return !g_IsLE[view_as(this)]; + } + + public set(bool value) { + CheckBuffer(this); + g_IsLE[view_as(this)] = !value; + } + } + + public void WriteByte(int data) { + CheckBuffer(this); + g_Buffer[view_as(this)][this.Cursor] = data & 0x000000FF; + this.Cursor++; + } + + public void WriteShort(int data) { + CheckBuffer(this); + if(this.LittleEndian) { + this.WriteByte(data & 0x000000FF); + this.WriteByte((data & 0x0000FF00) >> 8); + } else { + this.WriteByte((data & 0x0000FF00) >> 8); + this.WriteByte(data & 0x000000FF); + } + } + + public void WriteInt(int data) { + CheckBuffer(this); + if(this.LittleEndian) { + this.WriteByte(data & 0x000000FF); + this.WriteByte((data & 0x0000FF00) >> 8); + this.WriteByte((data & 0x00FF0000) >> 16); + this.WriteByte((data & 0xFF000000) >> 24); + } else { + this.WriteByte((data & 0xFF000000) >> 24); + this.WriteByte((data & 0x00FF0000) >> 16); + this.WriteByte((data & 0x0000FF00) >> 8); + this.WriteByte(data & 0x000000FF); + } + } + + public void WriteString(const char[] input) { + CheckBuffer(this); + int length = strlen(input); + for(int i = 0; i < length; i++) { + this.WriteByte(view_as(input[i])); + } + + this.WriteByte(0); + } + + public int ReadByte() { + CheckBuffer(this); + int value = g_Buffer[view_as(this)][this.Cursor] & 0xFF; + this.Cursor++; + return value; + } + + public int ReadShort() { + CheckBuffer(this); + int a, b; + + if(this.LittleEndian) { + a = this.ReadByte(); + b = (this.ReadByte() << 8) & 0x0000FF00; + } else { + a = (this.ReadByte() << 8) & 0x0000FF00; + b = this.ReadByte(); + } + + return (a | b); + } + + public int ReadInt() { + CheckBuffer(this); + int a, b, c, d; + + if(this.LittleEndian) { + a = this.ReadByte(); + b = (this.ReadByte() << 8) & 0x0000FF00; + c = (this.ReadByte() << 16) & 0x00FF0000; + d = (this.ReadByte() << 24) & 0xFF000000; + } else { + a = (this.ReadByte() << 24) & 0xFF000000; + b = (this.ReadByte() << 16) & 0x00FF0000; + c = (this.ReadByte() << 8) & 0x0000FF00; + d = this.ReadByte(); + } + + return (a | b | c | d); + } + + public int ReadString(char[] output, int maxlen) { + CheckBuffer(this); + for(int i = 0; i < maxlen; i++) { + output[i] = this.ReadByte(); + + if(output[i] == '\0') { + return i + 1; + } + } + + return maxlen; + } + + public int Dump(char[] output, int maxlen) { + CheckBuffer(this); + // Dump from start to cursor + if(maxlen > this.Cursor) { + maxlen = this.Cursor; + } + + int i; + for(i = 0; i < maxlen; i++) { + output[i] = g_Buffer[view_as(this)][i]; + } + + return i; + } + + public int Append(ByteBuffer buffer) { + CheckBuffer(this); + int length = buffer.Cursor; + char[] dumped = new char[length]; + buffer.Dump(dumped, length); + + for(int i = 0; i < length; i++) { + this.WriteByte(dumped[i]); + } + + return length; + } + + public ByteBuffer Extract(int length) { + CheckBuffer(this); + ByteBuffer output = CreateByteBuffer(this.LittleEndian); + + for(int i = 0; i < length; i++) { + output.WriteByte(this.ReadByte()); + } + + output.Cursor = 0; + return output; + } + + public void Reset() { + CheckBuffer(this); + this.Cursor = 0; + } + + public void Close() { + CheckBuffer(this); + g_InUse[view_as(this)] = false; + } +} + +ByteBuffer CreateByteBuffer(bool littleEndian = true, const char[] data = "", int dataSize = 0) { + int id = -1; + for(int i = 0; i < MAX_BUFFERS; i++) { + if(!g_InUse[i]) { + id = i; + break; + } + } + + if(id == -1) { + ThrowError("Cannot allocate a new buffer: all %d buffers in use", MAX_BUFFERS); + } + + g_InUse[id] = true; + g_IsLE[id] = littleEndian; + g_Cursor[id] = 0; + g_Length[id] = 0; + + if(dataSize > 0) { + // Write starting data + if(dataSize >= MAX_BUFFER_LENGTH) { + dataSize = MAX_BUFFER_LENGTH; + } + + for(int i = 0; i < dataSize; i++) { + g_Buffer[id][i] = data[i]; + } + } + + return view_as(id); +} + +static void CheckBuffer(ByteBuffer index) { + if(!g_InUse[view_as(index)]) { + ThrowError("Cannot read/write from closed buffer %d", index); + } +} diff --git a/libalternator/tests/corpus/bytebuffer/bytebuffer_es.inc b/libalternator/tests/corpus/bytebuffer/bytebuffer_es.inc new file mode 100644 index 0000000..eb1f519 --- /dev/null +++ b/libalternator/tests/corpus/bytebuffer/bytebuffer_es.inc @@ -0,0 +1,167 @@ +#if defined _bytebuffer_included + #endinput +#endif +#define _bytebuffer_included + +#define BYTEBUFFER_VERSION 3 + +#if !defined MAX_BUFFER_LENGTH + #define MAX_BUFFER_LENGTH 1024 +#endif + +enum Endianness { + LittleEndian, + BigEndian, +} + +enum struct ByteBuffer { + // Current cursor position + int iCursor; + + // Byte order + Endianness eEndian; + + // Data buffer + char cBuffer[MAX_BUFFER_LENGTH]; + + void WriteByte(int data) { + this.cBuffer[this.iCursor] = data & 0x000000FF; + this.iCursor++; + } + + void WriteShort(int data) { + switch (this.eEndian) { + case LittleEndian: + this.WriteByte(data & 0x000000FF); + this.WriteByte((data & 0x0000FF00) >> 8); + case BigEndian: + this.WriteByte((data & 0x0000FF00) >> 8); + this.WriteByte(data & 0x000000FF); + } + } + + void WriteInt(int data) { + switch (this.eEndian) { + case LittleEndian: + this.WriteByte(data & 0x000000FF); + this.WriteByte((data & 0x0000FF00) >> 8); + this.WriteByte((data & 0x00FF0000) >> 16); + this.WriteByte((data & 0xFF000000) >> 24); + case BigEndian: + this.WriteByte((data & 0xFF000000) >> 24); + this.WriteByte((data & 0x00FF0000) >> 16); + this.WriteByte((data & 0x0000FF00) >> 8); + this.WriteByte(data & 0x000000FF); + } + } + + void WriteString(const char[] input) { + int length = strlen(input); + + for(int i = 0; i < length; i++) { + this.WriteByte(view_as(input[i])); + } + } + + void WriteCString(const char[] input) { + this.WriteString(input); + this.WriteByte(0); + } + + int ReadByte() { + int value = this.cBuffer[this.iCursor] & 0xFF; + this.iCursor++; + return value; + } + + int ReadShort() { + int a, b; + + switch (this.eEndian) { + case LittleEndian: + a = this.ReadByte(); + b = (this.ReadByte() << 8) & 0x0000FF00; + case BigEndian: + a = (this.ReadByte() << 8) & 0x0000FF00; + b = this.ReadByte(); + } + + return (a | b); + } + + int ReadInt() { + int a, b, c, d; + + switch (this.eEndian) { + case LittleEndian: + a = this.ReadByte(); + b = (this.ReadByte() << 8) & 0x0000FF00; + c = (this.ReadByte() << 16) & 0x00FF0000; + d = (this.ReadByte() << 24) & 0xFF000000; + case BigEndian: + a = (this.ReadByte() << 24) & 0xFF000000; + b = (this.ReadByte() << 16) & 0x00FF0000; + c = (this.ReadByte() << 8) & 0x0000FF00; + d = this.ReadByte(); + } + + return (a | b | c | d); + } + + int ReadCString(char[] output, int maxlen) { + for(int i = 0; i < maxlen; i++) { + output[i] = this.ReadByte(); + + if(output[i] == '\0') { + return i + 1; + } + } + + return maxlen; + } + + int Dump(char[] output, int maxlen) { + if(maxlen > this.Cursor) { + maxlen = this.Cursor; + } + + int i; + + for(i = 0; i < maxlen; i++) { + output[i] = g_Buffer[this][i]; + } + + return i; + } + + int Append(ByteBuffer buffer) { + int length = buffer.iCursor; + char[] dumped = new char[length]; + + buffer.Dump(dumped, length); + + for(int i = 0; i < length; i++) { + this.WriteByte(dumped[i]); + } + + return length; + } + + ByteBuffer Extract(int length) { + ByteBuffer output; + + output.eEndian = this.eEndian; + + for(int i = 0; i < length; i++) { + output.WriteByte(this.ReadByte()); + } + + output.iCursor = 0; + + return output; + } + + void Reset() { + this.iCursor = 0; + } +} \ No newline at end of file diff --git a/libalternator/tests/corpus/includelibrary/include/cURL.inc b/libalternator/tests/corpus/includelibrary/include/cURL.inc new file mode 100644 index 0000000..1d79093 --- /dev/null +++ b/libalternator/tests/corpus/includelibrary/include/cURL.inc @@ -0,0 +1,565 @@ + +#if defined _cURL_included + #endinput +#endif +#define _cURL_included + +#include + + +/* +======================================== +The Following CURLOPT_* NOT support +ERRORBUFFER // use curl_get_error_buffer +WRITEINFO // ??? +PROGRESSFUNCTION // unused +PROGRESSDATA // same +HEADERFUNCTION // unused +DEBUGFUNCTION // unused +DEBUGDATA // same +SHARE // unsupport +PRIVATE // unsupport +SSL_CTX_FUNC // unused +SSL_CTX_DATA // same +IOCTLFUNCTION // unused +IOCTLDATA // same +CONV_FROM_NETWORK_FUNC // unused +CONV_TO_NETWORK_FUNC // unused +CONV_FROM_UTF8_FUNC // unused +SOCKOPTFUNCTION // unused +SOCKOPTDATA // unused +OPENSOCKETFUNCTION // used +OPENSOCKETDATA // used +COPYPOSTFIELDS // unsupport +SEEKFUNCTION // unused +SEEKDATA // unused +SOCKS5_GSSAPI_SERVICE // unsupport +SOCKS5_GSSAPI_NEC // unsupport +SSH_KEYFUNCTION // unsupport +SSH_KEYDATA // unsupport +INTERLEAVEFUNCTION // unsupport +CHUNK_BGN_FUNC // unsupport +CHUNK_END_FUNC // unsupport +FNMATCH_FUNC // unsupport +CHUNK_DATA // unsupport +FNMATCH_DATA // unsupport +TLSAUTH_USERNAME // unsupport, require tls-srp +TLSAUTH_PASSWORD // unsupport, require tls-srp +TLSAUTH_TYPE // unsupport, require tls-srp +CLOSESOCKETFUNCTION // unsupport +CLOSESOCKETDATA // unsupport +========================================*/ + +/* +======================================== +The Following CURLOPT_* supports the "file //" notation. +COOKIEFILE +COOKIEJAR +RANDOM_FILE +EGDSOCKET +SSLKEY +CAPATH +NETRC_FILE +SSH_PUBLIC_KEYFILE +SSH_PRIVATE_KEYFILE +_CRLFILE +ISSUERCERT +SSH_KNOWNHOSTS + +========================================*/ + +/* +======================================== +The Following CURLINFO_* NOT support +CURLINFO_SLIST + +========================================*/ + +/* +======================================== +The Following CURLFORM_* NOT support +CURLFORM_PTRNAME +CURLFORM_PTRCONTENTS +CURLFORM_ARRAY +CURLFORM_BUFFER +CURLFORM_BUFFERPTR +CURLFORM_BUFFERLENGTH +CURLFORM_STREAM + +========================================*/ + + + + +/*************************************************************************************************/ +/******************************************** OPTIONS ********************************************/ +/*************************************************************************************************/ + + +/** + * The Send & Receive Action + * Using on CURL_OnSend, CURL_OnReceive + * SendRecv_Act_GOTO_SEND = go to send + * SendRecv_Act_GOTO_RECV = go to receive + * SendRecv_Act_GOTO_WAIT = go to wait + * SendRecv_Act_GOTO_END = end the connection + * SendRecv_Act_GOTO_SEND_NO_WAIT = go to send but no select + * SendRecv_Act_GOTO_RECV_NO_WAIT = go to receive but no select + * To see how it work? see curl_echo_test.sp & curl_rcon_test.sp examples + */ +enum SendRecv_Act { + SendRecv_Act_NOTHING = 0, + + SendRecv_Act_GOTO_SEND, + SendRecv_Act_GOTO_RECV, + SendRecv_Act_GOTO_WAIT, + SendRecv_Act_GOTO_END, + SendRecv_Act_GOTO_SEND_NO_WAIT, + SendRecv_Act_GOTO_RECV_NO_WAIT, + + SendRecv_Act_LAST, +}; + +/** + * Hash type for curl_hash_file, curl_hash_string + */ +enum Openssl_Hash { + Openssl_Hash_MD5 = 0, + Openssl_Hash_MD4, + Openssl_Hash_MD2, + Openssl_Hash_SHA, + Openssl_Hash_SHA1, + Openssl_Hash_SHA224, + Openssl_Hash_SHA256, + Openssl_Hash_SHA384, + Openssl_Hash_SHA512, + Openssl_Hash_RIPEMD160, +}; + + +/*************************************************************************************************/ +/******************************************* CALLBACKS *******************************************/ +/*************************************************************************************************/ + + +/** + * called if curl_easy_perform_thread() or curl_easy_send_recv() Complete + * @ param Handle hndl The curl handle + * @ param CURLcode code The CURLcode code, see cURL_header.inc + * @ param any data Data passed to curl_easy_perform_thread() + * @ noreturn + */ +typeset CURL_OnComplete +{ + function void (Handle hndl, CURLcode code); + function void (Handle hndl, CURLcode code , any data); +}; + +/** + * called if curl_easy_send_recv() before sending data + * @ param Handle hndl The curl handle + * @ param CURLcode code The last CURLcode code, see cURL_header.inc + * @ param cell_t last_sent_dataSize The last sent datasize + * @ param any data Data passed to curl_easy_send_recv() + * @ return SendRecv_Act + */ +typeset CURL_OnSend +{ + function SendRecv_Act (Handle hndl, CURLcode code, const int last_sent_dataSize); + function SendRecv_Act (Handle hndl, CURLcode code, const int last_sent_dataSize, any data); +} + +/** + * called if curl_easy_send_recv() after received data + * @ param Handle hndl The curl handle + * @ param CURLcode code The CURLcode code, see cURL_header.inc + * @ param String dataSize The received datasize + * @ param any data Data passed to curl_easy_send_recv() + * @ return SendRecv_Act + */ +typeset CURL_OnReceive +{ + function SendRecv_Act (Handle hndl, CURLcode code, const char[] receiveData, const int dataSize); + function SendRecv_Act (Handle hndl, CURLcode code, const char[] receiveData, const int dataSize, any data); +} + +/** + * called if Openssl_Hash_file() after hashed the file + * @ param bool success True on success, false if hash file fail + * @ param String buffer The hash string + * @ param any data Data passed to Openssl_Hash_file() + * @ noreturn + */ +typeset Openssl_Hash_Complete +{ + function void (const bool success, const char[] buffer); + function void (const bool success, const char[] buffer, any data); +} + + +typeset CURL_Function_CB +{ + // CURLOPT_WRITEFUNCTION + function void (Handle hndl, const char[] buffer, const int bytes, const int nmemb); + function void (Handle hndl, const char[] buffer, const int bytes, const int nmemb, any data); + + // CURLOPT_READFUNCTION + function void (Handle hndl, const int bytes, const int nmemb); + function void (Handle hndl, const int bytes, const int nmemb, any data); +} + +/*************************************************************************************************/ +/******************************************** NATIVES ********************************************/ +/*************************************************************************************************/ + + +/** + * Create a curl handle + * @ return Handle The curl handle. Returns INVALID_HANDLE on failure + */ +native Handle curl_easy_init(); + +/** + * Set a curl option for CURLOPTTYPE_OBJECTPOINT type + * + * @ param Handle hndl The handle of the curl to be used. May be INVALID_HANDLE if not essential. + * @ param CURLoption opt The option to add (see enum CURLoption for details). + * @ param String buffer The value to set the option to. + * @ return bool 1 on success. 0 = The option not accept string or unsupport. + */ +native bool curl_easy_setopt_string(Handle hndl, CURLoption opt, const char[] buffer); + +/** + * Set a curl option for CURLOPTTYPE_LONG type + * + * @ param Handle hndl The handle of the curl to be used. May be INVALID_HANDLE if not essential. + * @ param CURLoption opt The option to add (see enum CURLoption for details). + * @ param cell_t value The value to set the option to. + * @ return bool 1 on success. 0 = The option not accept integer or unsupport. + */ +native bool curl_easy_setopt_int(Handle hndl, CURLoption opt, int value); + +/** + * Set a curl option for CURLOPTTYPE_LONG type + * @ example" + new opt[][2] = { + {_ CURLOPT_NOPROGRESS,1}, + {_ CURLOPT_VERBOSE,0} + }; + * + * @ param Handle hndl The handle of the curl to be used. May be INVALID_HANDLE if not essential. + * @ param cell_t array The option array to add (see enum CURLoption for details). + * @ param cell_t array_size The array size. + * @ return bool 1 on success. 0 = The option not accept integer or unsupport. + */ +native bool curl_easy_setopt_int_array(Handle hndl, int[][] array, int array_size); // int array[][2] + +/** + * Set a curl option for CURLOPTTYPE_OFF_T type + * + * @ param Handle hndl The handle of the curl to be used. May be INVALID_HANDLE if not essential. + * @ param CURLoption opt The option to add (see enum CURLoption for details). + * @ param String buffer The value to set the option to. + * @ return bool 1 on success. 0 = The option not accept string or unsupport. + */ +native bool curl_easy_setopt_int64(Handle hndl, CURLoption opt, const char buffer); + +/** + * Set a curl option for CURLOPTTYPE_OBJECTPOINT type + * @ note only accept the following handle type + curl_OpenFile() + curl_httppost() + curl_slist() + * + * @ param Handle hndl The handle of the curl to be used. May be INVALID_HANDLE if not essential. + * @ param CURLoption opt The option to add (see enum CURLoption for details). + * @ param Handle other_hndl The other handle to set the option to. + * @ return bool 1 on success. 0 = The option not accept string or unsupport. + */ +native bool curl_easy_setopt_handle(Handle hndl, CURLoption opt, Handle other_hndl); + +/** + * Set a curl option for CURLOPTTYPE_FUNCTIONPOINT type + * + * @ param Handle hndl The handle of the curl to be used. May be INVALID_HANDLE if not essential. + * @ param CURLoption opt The option to add (see enum CURLoption for details). + * @ param CURL_Function_CB callback The value to set the option to. + * @ param cell_t value Value to set. + * @ return bool 1 on success. 0 = The option unsupport or invalid callback function. + */ +native bool curl_easy_setopt_function(Handle hndl, CURLoption opt, CURL_Function_CB callback, any value=0); + +/** + * Load all CURLoption to curl Handle + * @ note + * Using curl_easy_perform_thread() will load option in thread + * Use this on curl_easy_perform or check all CURLoption are valid or not + * Only can use one time for each curl handle + * @ return The CURLcode code, see cURL_header.inc + */ +native CURLcode curl_load_opt(Handle hndl); + +/** + * Perform a file transfer + * @ return The CURLcode code, see cURL_header.inc + */ +native CURLcode curl_easy_perform(Handle hndl); + +/** + * Perform a file transfer, using thread + * @ param Handle hndl The handle of the curl to be used. May be INVALID_HANDLE if not essential. + * @ param CURL_OnComplete perform_callback The complete callback. + * @ param cell_t value Value to set. + * @ noreturn + */ +native void curl_easy_perform_thread(Handle hndl, CURL_OnComplete perform_callback, any value=0); + +/** + * Create a send & receive function for a connected curl handle + * @ param Handle hndl The handle of the curl to be used. + * @ param CURL_OnSend send_callback The send callback. + * @ param CURL_OnReceive receive_callback The receive callback. + * @ param CURL_OnComplete complete_callback The complete callback. + * @ param SendRecv_Act act The first SendRecv_Act action + * @ param cell_t send_timeout Send timeout value in milliseconds. + * @ param cell_t recv_timeout Receive timeout value in milliseconds. + * @ param cenn_t recv_buffer_Size Receive buffer size. + * @ param cell_t value Value to set. + * @ noreturn + */ +native void curl_easy_send_recv(Handle hndl, CURL_OnSend send_callback, CURL_OnReceive receive_callback, CURL_OnComplete complete_callback, SendRecv_Act act, int send_timeout, int recv_timeout, int recv_buffer_Size = 1024, any value=0); + +/** + * Send a signal to a send & receive curl handle + * @ param Handle hndl The handle of the send & receive curl to be used. + * @ param SendRecv_Act act The SendRecv_Act action after the singal + * @ return bool 1 on success. 0 = not a curl_easy_send_recv() curl, or not running/waiting + */ +native bool curl_send_recv_Signal(Handle hndl, SendRecv_Act act); + +/** + * Check send & receive curl handle is Waiting or not + * @ param Handle hndl The handle of the send & receive curl to be used. + * @ return bool 1 = is waiting. 0 = not a curl_easy_send_recv() curl, or not running/waiting + */ +native bool curl_send_recv_IsWaiting(Handle hndl); + +/** + * Send the send buffer for send & receive curl handle + * @ param Handle hndl The handle of the send & receive curl to be used. + * @ param cell_t data The data to send + * @ param cell_t size if specified the \0 terminator will not be included + * @ noreturn + */ +native void curl_set_send_buffer(Handle hndl, const char[] data, int size=-1); + +/** + * Send the receive data size for send & receive curl handle + * @ param Handle hndl The handle of the send & receive curl to be used. + * @ param cell_t size The receive size + * @ noreturn + */ +native void curl_set_receive_size(Handle hndl, int size); + +/** + * Set send timeout for curl_easy_send_recv() + * @ param Handle hndl The handle of the send & receive curl to be used. + * @ param cell_t timeout How long will try to send data before it timeout (milliseconds). + * @ noreturn + */ +native void curl_set_send_timeout(Handle hndl, int timeout); + +/** + * Set receive timeout for curl_easy_send_recv() + * @ param Handle hndl The handle of the send & receive curl to be used. + * @ param cell_t timeout How long will try to receive data before it timeout (milliseconds). + * @ noreturn + */ +native void curl_set_recv_timeout(Handle hndl, int timeout); + +/** + * Get CURLOPT_ERRORBUFFER error string in curl handle + * @ param Handle hndl The handle of the curl to be used. + * @ param String buffer Destination string buffer to copy to. + * @ param cell_t maxlen Destination buffer length (includes null terminator). + * @ noreturn + */ +native void curl_get_error_buffer(Handle hndl, char[] buffer, int maxlen); + +/** + * Extract information from a curl handle. (CURLINFO_STRING only) + * @ param Handle hndl The handle of the curl to be used. + * @ param CURLINFO info The enum CURLINFO, see cURL_header.inc + * @ param String buffer Destination string buffer to copy to. + * @ param cell_t maxlen Destination buffer length (includes null terminator). + * @ return The CURLcode code, see cURL_header.inc + */ +native CURLcode curl_easy_getinfo_string(Handle hndl, CURLINFO info, char[] buffer, int maxlen); + +/** + * Extract information from a curl handle. (CURLINFO_LONG, CURLINFO_DOUBLE only) + * @ param Handle hndl The handle of the curl to be used. + * @ param CURLINFO info The enum CURLINFO, see cURL_header.inc + * @ param value Variable to store the value. + * @ return The CURLcode code, see cURL_header.inc + */ +native CURLcode curl_easy_getinfo_int(Handle hndl, CURLINFO info, any &value); + +/** + * URL encodes the given string + * @ param Handle hndl The handle of the curl to be used. + * @ param String url The string to encodes. + * @ param String buffer Destination string buffer to copy to. + * @ param cell_t maxlen Destination buffer length (includes null terminator). + * @ return 1 on success. + */ +native bool curl_easy_escape(Handle hndl, const char[] url, char[] buffer, int maxlen); + +/** + * URL decodes the given string + * @ param Handle hndl The handle of the curl to be used. + * @ param String url The string to dencodes. + * @ param String buffer Destination string buffer to copy to. + * @ param cell_t maxlen Destination buffer length (includes null terminator). + * @ return The output length. + */ +native int curl_easy_unescape(Handle hndl, const char[] url, char[] buffer, int maxlen); + +/** + * Return string describing error code + * @ param CURLcode code The CURLcode code, see cURL_header.inc + * @ param String buffer Destination string buffer to copy to. + * @ param cell_t maxlen Destination buffer length (includes null terminator). + * @ noreturn + */ +native void curl_easy_strerror(CURLcode code, char[] buffer, int maxlen); + +/** + * Returns the libcurl version string + * @ param String buffer Destination string buffer to copy to. + * @ param cell_t maxlen Destination buffer length (includes null terminator). + * @ noreturn + */ +native void curl_version(char[] buffer, int maxlen); + +/** + * Returns the libcurl supported protocols string + * @ param String buffer Destination string buffer to copy to. + * @ param cell_t maxlen Destination buffer length (includes null terminator). + * @ noreturn + */ +native void curl_protocols(char[] buffer, int maxlen); + +/** + * Returns the libcurl supported features + * @ return The currently features bits. see CURL_VERSION_* + */ +native int curl_features(); + +/** + * This funcitopn same as Sourcemod OpenFile() + * For the following CUROPT_* only + * CURLOPT_WRITEDATA + * CURLOPT_HEADERDATA + * CURLOPT_READDATA + * CURLOPT_STDERR + * CURLOPT_INTERLEAVEDATA + * + * @ note + * Should not share to another threaded curl handle. + * + * @ param file File to open. + * @ param mode Open mode. + * @ return A Handle to the file, INVALID_HANDLE on open error. + */ +native Handle curl_OpenFile(const char[] file, const char[] mode); + + +/** + * Create a curl_httppost struct + * For the following CUROPT_* only + * CURLOPT_HTTPPOST + * @ note + * Should not share to another threaded curl handle. + * + * @ return A Handle to the curl_httppost, INVALID_HANDLE on error. + */ +native Handle curl_httppost(); + +/** + * Add a section to a multipart/formdata HTTP POST + * @ note + * Check enum CURLformoption (cURL_head.inc) to see which option supported + * + * @ param Handle hndl The handle of the curl_httppost to be used. + * @ param ... Variable number of format parameters. + * @ return The CURLFORMcode code, see cURL_header.inc + */ +native CURLFORMcode curl_formadd(Handle handl, any ...); + +/** + * Create a curl_slist struct + * For the following CUROPT_* only + * CURLOPT_QUOTE + * CURLOPT_HTTPHEADER + * CURLOPT_POSTQUOTE + * CURLOPT_TELNETOPTIONS + * CURLOPT_PREQUOTE + * CURLOPT_HTTP200ALIASES + * CURLOPT_MAIL_RCPT + * CURLOPT_RESOLVE + * + * @ note + * Should not share to another threaded curl handle. + * + * @ return A Handle to the curl_slist, INVALID_HANDLE on error. + */ +native Handle curl_slist(); + +/** + * Add a string to an slist + * @ param Handle hndl The handle of the curl_slist to be used. + * @ param String buffer The string to add + * @ noreturn + */ +native void curl_slist_append(Handle hndl, const char[] buffer); + +/** + * Hash a file + * @ parma String file The file path. supports the "file //" notation. + * @ param Openssl_Hash algorithm Hash Algorithm. + * @ param Openssl_Hash_Complete complete_callback The complete callback. + * @ param cell_t value Value to set. + * @ noreturn + */ +native void curl_hash_file(const char[] file, Openssl_Hash algorithm, Openssl_Hash_Complete complete_callback, any value=0); + +/** + * Hash a string + * @ parma String input The string to hash. + * @ param cell_t dataSize The input string size. + * @ param Openssl_Hash algorithm Hash Algorithm. + * @ param String buffer Destination string buffer to copy to. + * @ param cell_t maxlen Destination buffer length (includes null terminator). + * @ return 1 on success + */ +native bool curl_hash_string(const char[] input, int dataSize, Openssl_Hash algorithm, char[] buffer, int maxlength); + + +/** + * Do not edit below this line! + */ +public Extension __ext_curl = { + name = "curl", + file = "curl.ext", +#if defined AUTOLOAD_EXTENSIONS + autoload = 1, +#else + autoload = 0, +#endif +#if defined REQUIRE_EXTENSIONS + required = 1, +#else + required = 0, +#endif +}; diff --git a/libalternator/tests/corpus/includelibrary/include/cURL_header.inc b/libalternator/tests/corpus/includelibrary/include/cURL_header.inc new file mode 100644 index 0000000..cf51cf1 --- /dev/null +++ b/libalternator/tests/corpus/includelibrary/include/cURL_header.inc @@ -0,0 +1,1206 @@ +#if defined _cURL_header_included + #endinput +#endif +#define _cURL_header_included + + +/* SourceMod */ +#define LONG CURLOPTTYPE_LONG +#define OBJECTPOINT CURLOPTTYPE_OBJECTPOINT +#define FUNCTIONPOINT CURLOPTTYPE_FUNCTIONPOINT +#define OFF_T CURLOPTTYPE_OFF_T + + +#define CURL_MAX_WRITE_SIZE 16384 + +#define CURL_VERSION_IPV6 (1<<0) /* IPv6-enabled */ +#define CURL_VERSION_KERBEROS4 (1<<1) /* kerberos auth is supported */ +#define CURL_VERSION_SSL (1<<2) /* SSL options are present */ +#define CURL_VERSION_LIBZ (1<<3) /* libz features are present */ +#define CURL_VERSION_NTLM (1<<4) /* NTLM auth is supported */ +#define CURL_VERSION_GSSNEGOTIATE (1<<5) /* Negotiate auth support */ +#define CURL_VERSION_DEBUG (1<<6) /* built with debug capabilities */ +#define CURL_VERSION_ASYNCHDNS (1<<7) /* asynchronous dns resolves */ +#define CURL_VERSION_SPNEGO (1<<8) /* SPNEGO auth */ +#define CURL_VERSION_LARGEFILE (1<<9) /* supports files bigger than 2GB */ +#define CURL_VERSION_IDN (1<<10) /* International Domain Names support */ +#define CURL_VERSION_SSPI (1<<11) /* SSPI is supported */ +#define CURL_VERSION_CONV (1<<12) /* character conversions supported */ +#define CURL_VERSION_CURLDEBUG (1<<13) /* debug memory tracking supported */ +#define CURL_VERSION_TLSAUTH_SRP (1<<14) /* TLS-SRP auth is supported */ +#define CURL_VERSION_NTLM_WB (1<<15) /* NTLM delegating to winbind helper */ + + +#define CURLOPTTYPE_LONG 0 +#define CURLOPTTYPE_OBJECTPOINT 10000 +#define CURLOPTTYPE_FUNCTIONPOINT 20000 +#define CURLOPTTYPE_OFF_T 30000 + +#define CINIT(%1,%2,%3) CURLOPT_%1 = %2 + %3 + + /* three convenient "aliases" that follow the name scheme better */ +#define CURLOPT_WRITEDATA CURLOPT_FILE +#define CURLOPT_READDATA CURLOPT_INFILE +#define CURLOPT_HEADERDATA CURLOPT_WRITEHEADER +#define CURLOPT_RTSPHEADER CURLOPT_HTTPHEADER + +/* This was added in version 7.19.1 */ +#define CURLOPT_POST301 CURLOPT_POSTREDIR + +/* The following were added in 7.17.0 */ +#define CURLOPT_SSLKEYPASSWD CURLOPT_KEYPASSWD +#define CURLOPT_FTPAPPEND CURLOPT_APPEND +#define CURLOPT_FTPLISTONLY CURLOPT_DIRLISTONLY +#define CURLOPT_FTP_SSL CURLOPT_USE_SSL + +/* The following were added earlier */ + +#define CURLOPT_SSLCERTPASSWD CURLOPT_KEYPASSWD +#define CURLOPT_KRB4LEVEL CURLOPT_KRBLEVEL + +enum CURLoption { + /* This is the FILE * or void * the regular output should be written to. */ + CINIT(FILE, OBJECTPOINT, 1), + + /* The full URL to get/put */ + CINIT(URL, OBJECTPOINT, 2), + + /* Port number to connect to, if other than default. */ + CINIT(PORT, LONG, 3), + + /* Name of proxy to use. */ + CINIT(PROXY, OBJECTPOINT, 4), + + /* "name:password" to use when fetching. */ + CINIT(USERPWD, OBJECTPOINT, 5), + + /* "name:password" to use with proxy. */ + CINIT(PROXYUSERPWD, OBJECTPOINT, 6), + + /* Range to get, specified as an ASCII string. */ + CINIT(RANGE, OBJECTPOINT, 7), + + /* not used */ + + /* Specified file stream to upload from (use as input): */ + CINIT(INFILE, OBJECTPOINT, 9), + + /* Buffer to receive error messages in, must be at least CURL_ERROR_SIZE + * bytes big. If this is not used, error messages go to stderr instead: */ + CINIT(ERRORBUFFER, OBJECTPOINT, 10), + + /* Function that will be called to store the output (instead of fwrite). The + * parameters will use fwrite() syntax, make sure to follow them. */ + CINIT(WRITEFUNCTION, FUNCTIONPOINT, 11), + + /* Function that will be called to read the input (instead of fread). The + * parameters will use fread() syntax, make sure to follow them. */ + CINIT(READFUNCTION, FUNCTIONPOINT, 12), + + /* Time-out the read operation after this amount of seconds */ + CINIT(TIMEOUT, LONG, 13), + + /* If the CURLOPT_INFILE is used, this can be used to inform libcurl about + * how large the file being sent really is. That allows better error + * checking and better verifies that the upload was successful. -1 means + * unknown size. + * + * For large file support, there is also a _LARGE version of the key + * which takes an OFF_T type, allowing platforms with larger OFF_T + * sizes to handle larger files. See below for INFILESIZE_LARGE. + */ + CINIT(INFILESIZE, LONG, 14), + + /* POST static input fields. */ + CINIT(POSTFIELDS, OBJECTPOINT, 15), + + /* Set the referrer page (needed by some CGIs) */ + CINIT(REFERER, OBJECTPOINT, 16), + + /* Set the FTP PORT string (interface name, named or numerical IP address) + Use i.e '-' to use default address. */ + CINIT(FTPPORT, OBJECTPOINT, 17), + + /* Set the User-Agent string (examined by some CGIs) */ + CINIT(USERAGENT, OBJECTPOINT, 18), + + /* If the download receives less than "low speed limit" bytes/second + * during "low speed time" seconds, the operations is aborted. + * You could i.e if you have a pretty high speed connection, abort if + * it is less than 2000 bytes/sec during 20 seconds. + */ + + /* Set the "low speed limit" */ + CINIT(LOW_SPEED_LIMIT, LONG, 19), + + /* Set the "low speed time" */ + CINIT(LOW_SPEED_TIME, LONG, 20), + + /* Set the continuation offset. + * + * Note there is also a _LARGE version of this key which uses + * OFF_T types, allowing for large file offsets on platforms which + * use larger-than-32-bit OFF_T's. Look below for RESUME_FROM_LARGE. + */ + CINIT(RESUME_FROM, LONG, 21), + + /* Set cookie in request: */ + CINIT(COOKIE, OBJECTPOINT, 22), + + /* This points to a linked list of headers, struct curl_slist kind */ + CINIT(HTTPHEADER, OBJECTPOINT, 23), + + /* This points to a linked list of post entries, struct curl_httppost */ + CINIT(HTTPPOST, OBJECTPOINT, 24), + + /* name of the file keeping your private SSL-certificate */ + CINIT(SSLCERT, OBJECTPOINT, 25), + + /* password for the SSL or SSH private key */ + CINIT(KEYPASSWD, OBJECTPOINT, 26), + + /* send TYPE parameter? */ + CINIT(CRLF, LONG, 27), + + /* send linked-list of QUOTE commands */ + CINIT(QUOTE, OBJECTPOINT, 28), + + /* send FILE * or void * to store headers to, if you use a callback it + is simply passed to the callback unmodified */ + CINIT(WRITEHEADER, OBJECTPOINT, 29), + + /* point to a file to read the initial cookies from, also enables + "cookie awareness" */ + CINIT(COOKIEFILE, OBJECTPOINT, 31), + + /* What version to specifically try to use. + See CURL_SSLVERSION defines below. */ + CINIT(SSLVERSION, LONG, 32), + + /* What kind of HTTP time condition to use, see defines */ + CINIT(TIMECONDITION, LONG, 33), + + /* Time to use with the above condition. Specified in number of seconds + since 1 Jan 1970 */ + CINIT(TIMEVALUE, LONG, 34), + + /* 35 = OBSOLETE */ + + /* Custom request, for customizing the get command like + HTTP: DELETE, TRACE and others + FTP: to use a different list command + */ + CINIT(CUSTOMREQUEST, OBJECTPOINT, 36), + + /* HTTP request, for odd commands like DELETE, TRACE and others */ + CINIT(STDERR, OBJECTPOINT, 37), + + /* 38 is not used */ + + /* send linked-list of post-transfer QUOTE commands */ + CINIT(POSTQUOTE, OBJECTPOINT, 39), + + /* Pass a pointer to string of the output using full variable-replacement + as described elsewhere. */ + CINIT(WRITEINFO, OBJECTPOINT, 40), /* DEPRECATED, do not use! */ + + CINIT(VERBOSE, LONG, 41), /* talk a lot */ + CINIT(HEADER, LONG, 42), /* throw the header out too */ + CINIT(NOPROGRESS, LONG, 43), /* shut off the progress meter */ + CINIT(NOBODY, LONG, 44), /* use HEAD to get http document */ + CINIT(FAILONERROR, LONG, 45), /* no output on http error codes >= 300 */ + CINIT(UPLOAD, LONG, 46), /* this is an upload */ + CINIT(POST, LONG, 47), /* HTTP POST method */ + CINIT(DIRLISTONLY, LONG, 48), /* bare names when listing directories */ + + CINIT(APPEND, LONG, 50), /* Append instead of overwrite on upload! */ + + /* Specify whether to read the user+password from the .netrc or the URL. + * This must be one of the CURL_NETRC_* enums below. */ + CINIT(NETRC, LONG, 51), + + CINIT(FOLLOWLOCATION, LONG, 52), /* use Location: Luke! */ + + CINIT(TRANSFERTEXT, LONG, 53), /* transfer data in text/ASCII format */ + CINIT(PUT, LONG, 54), /* HTTP PUT */ + + /* 55 = OBSOLETE */ + + /* Function that will be called instead of the internal progress display + * function. This function should be defined as the curl_progress_callback + * prototype defines. */ + CINIT(PROGRESSFUNCTION, FUNCTIONPOINT, 56), + + /* Data passed to the progress callback */ + CINIT(PROGRESSDATA, OBJECTPOINT, 57), + + /* We want the referrer field set automatically when following locations */ + CINIT(AUTOREFERER, LONG, 58), + + /* Port of the proxy, can be set in the proxy string as well with: + "[host]:[port]" */ + CINIT(PROXYPORT, LONG, 59), + + /* size of the POST input data, if strlen() is not good to use */ + CINIT(POSTFIELDSIZE, LONG, 60), + + /* tunnel non-http operations through a HTTP proxy */ + CINIT(HTTPPROXYTUNNEL, LONG, 61), + + /* Set the interface string to use as outgoing network interface */ + CINIT(INTERFACE, OBJECTPOINT, 62), + + /* Set the krb4/5 security level, this also enables krb4/5 awareness. This + * is a string, 'clear', 'safe', 'confidential' or 'private'. If the string + * is set but doesn't match one of these, 'private' will be used. */ + CINIT(KRBLEVEL, OBJECTPOINT, 63), + + /* Set if we should verify the peer in ssl handshake, set 1 to verify. */ + CINIT(SSL_VERIFYPEER, LONG, 64), + + /* The CApath or CAfile used to validate the peer certificate + this option is used only if SSL_VERIFYPEER is true */ + CINIT(CAINFO, OBJECTPOINT, 65), + + /* 66 = OBSOLETE */ + /* 67 = OBSOLETE */ + + /* Maximum number of http redirects to follow */ + CINIT(MAXREDIRS, LONG, 68), + + /* Pass a LONG set to 1 to get the date of the requested document (if + possible)! Pass a zero to shut it off. */ + CINIT(FILETIME, LONG, 69), + + /* This points to a linked list of telnet options */ + CINIT(TELNETOPTIONS, OBJECTPOINT, 70), + + /* Max amount of cached alive connections */ + CINIT(MAXCONNECTS, LONG, 71), + + CINIT(CLOSEPOLICY, LONG, 72), /* DEPRECATED, do not use! */ + + /* 73 = OBSOLETE */ + + /* Set to explicitly use a new connection for the upcoming transfer. + Do not use this unless you're absolutely sure of this, as it makes the + operation slower and is less friendly for the network. */ + CINIT(FRESH_CONNECT, LONG, 74), + + /* Set to explicitly forbid the upcoming transfer's connection to be re-used + when done. Do not use this unless you're absolutely sure of this, as it + makes the operation slower and is less friendly for the network. */ + CINIT(FORBID_REUSE, LONG, 75), + + /* Set to a file name that contains random data for libcurl to use to + seed the random engine when doing SSL connects. */ + CINIT(RANDOM_FILE, OBJECTPOINT, 76), + + /* Set to the Entropy Gathering Daemon socket pathname */ + CINIT(EGDSOCKET, OBJECTPOINT, 77), + + /* Time-out connect operations after this amount of seconds, if connects + are OK within this time, then fine... This only aborts the connect + phase. [Only works on unix-style/SIGALRM operating systems] */ + CINIT(CONNECTTIMEOUT, LONG, 78), + + /* Function that will be called to store headers (instead of fwrite). The + * parameters will use fwrite() syntax, make sure to follow them. */ + CINIT(HEADERFUNCTION, FUNCTIONPOINT, 79), + + /* Set this to force the HTTP request to get back to GET. Only really usable + if POST, PUT or a custom request have been used first. + */ + CINIT(HTTPGET, LONG, 80), + + /* Set if we should verify the Common name from the peer certificate in ssl + * handshake, set 1 to check existence, 2 to ensure that it matches the + * provided hostname. */ + CINIT(SSL_VERIFYHOST, LONG, 81), + + /* Specify which file name to write all known cookies in after completed + operation. Set file name to "-" (dash) to make it go to stdout. */ + CINIT(COOKIEJAR, OBJECTPOINT, 82), + + /* Specify which SSL ciphers to use */ + CINIT(SSL_CIPHER_LIST, OBJECTPOINT, 83), + + /* Specify which HTTP version to use! This must be set to one of the + CURL_HTTP_VERSION* enums set below. */ + CINIT(HTTP_VERSION, LONG, 84), + + /* Specifically switch on or off the FTP engine's use of the EPSV command. By + default, that one will always be attempted before the more traditional + PASV command. */ + CINIT(FTP_USE_EPSV, LONG, 85), + + /* type of the file keeping your SSL-certificate ("DER", "PEM", "ENG") */ + CINIT(SSLCERTTYPE, OBJECTPOINT, 86), + + /* name of the file keeping your private SSL-key */ + CINIT(SSLKEY, OBJECTPOINT, 87), + + /* type of the file keeping your private SSL-key ("DER", "PEM", "ENG") */ + CINIT(SSLKEYTYPE, OBJECTPOINT, 88), + + /* crypto engine for the SSL-sub system */ + CINIT(SSLENGINE, OBJECTPOINT, 89), + + /* set the crypto engine for the SSL-sub system as default + the param has no meaning... + */ + CINIT(SSLENGINE_DEFAULT, LONG, 90), + + /* Non-zero value means to use the global dns cache */ + CINIT(DNS_USE_GLOBAL_CACHE, LONG, 91), /* DEPRECATED, do not use! */ + + /* DNS cache timeout */ + CINIT(DNS_CACHE_TIMEOUT, LONG, 92), + + /* send linked-list of pre-transfer QUOTE commands */ + CINIT(PREQUOTE, OBJECTPOINT, 93), + + /* set the debug function */ + CINIT(DEBUGFUNCTION, FUNCTIONPOINT, 94), + + /* set the data for the debug function */ + CINIT(DEBUGDATA, OBJECTPOINT, 95), + + /* mark this as start of a cookie session */ + CINIT(COOKIESESSION, LONG, 96), + + /* The CApath directory used to validate the peer certificate + this option is used only if SSL_VERIFYPEER is true */ + CINIT(CAPATH, OBJECTPOINT, 97), + + /* Instruct libcurl to use a smaller receive buffer */ + CINIT(BUFFERSIZE, LONG, 98), + + /* Instruct libcurl to not use any signal/alarm handlers, even when using + timeouts. This option is useful for multi-threaded applications. + See libcurl-the-guide for more background information. */ + CINIT(NOSIGNAL, LONG, 99), + + /* Provide a CURLShare for mutexing non-ts data */ + CINIT(SHARE, OBJECTPOINT, 100), + + /* indicates type of proxy. accepted values are CURLPROXY_HTTP (default), + CURLPROXY_SOCKS4, CURLPROXY_SOCKS4A and CURLPROXY_SOCKS5. */ + CINIT(PROXYTYPE, LONG, 101), + + /* Set the Accept-Encoding string. Use this to tell a server you would like + the response to be compressed. Before 7.21.6, this was known as + CURLOPT_ENCODING */ + CINIT(ACCEPT_ENCODING, OBJECTPOINT, 102), + + /* Set pointer to private data */ + CINIT(PRIVATE, OBJECTPOINT, 103), + + /* Set aliases for HTTP 200 in the HTTP Response header */ + CINIT(HTTP200ALIASES, OBJECTPOINT, 104), + + /* Continue to send authentication (user+password) when following locations, + even when hostname changed. This can potentially send off the name + and password to whatever host the server decides. */ + CINIT(UNRESTRICTED_AUTH, LONG, 105), + + /* Specifically switch on or off the FTP engine's use of the EPRT command ( + it also disables the LPRT attempt). By default, those ones will always be + attempted before the good old traditional PORT command. */ + CINIT(FTP_USE_EPRT, LONG, 106), + + /* Set this to a bitmask value to enable the particular authentications + methods you like. Use this in combination with CURLOPT_USERPWD. + Note that setting multiple bits may cause extra network round-trips. */ + CINIT(HTTPAUTH, LONG, 107), + + /* Set the ssl context callback function, currently only for OpenSSL ssl_ctx + in second argument. The function must be matching the + curl_ssl_ctx_callback proto. */ + CINIT(SSL_CTX_FUNC, FUNCTIONPOINT, 108), + + /* Set the userdata for the ssl context callback function's third + argument */ + CINIT(SSL_CTX_DATA, OBJECTPOINT, 109), + + /* FTP Option that causes missing dirs to be created on the remote server. + In 7.19.4 we introduced the convenience enums for this option using the + CURLFTP_CREATE_DIR prefix. + */ + CINIT(FTP_CREATE_MISSING_DIRS, LONG, 110), + + /* Set this to a bitmask value to enable the particular authentications + methods you like. Use this in combination with CURLOPT_PROXYUSERPWD. + Note that setting multiple bits may cause extra network round-trips. */ + CINIT(PROXYAUTH, LONG, 111), + + /* FTP option that changes the timeout, in seconds, associated with + getting a response. This is different from transfer timeout time and + essentially places a demand on the FTP server to acknowledge commands + in a timely manner. */ + CINIT(FTP_RESPONSE_TIMEOUT, LONG, 112), +#define CURLOPT_SERVER_RESPONSE_TIMEOUT CURLOPT_FTP_RESPONSE_TIMEOUT + + /* Set this option to one of the CURL_IPRESOLVE_* defines (see below) to + tell libcurl to resolve names to those IP versions only. This only has + affect on systems with support for more than one, i.e IPv4 _and_ IPv6. */ + CINIT(IPRESOLVE, LONG, 113), + + /* Set this option to limit the size of a file that will be downloaded from + an HTTP or FTP server. + + Note there is also _LARGE version which adds large file support for + platforms which have larger OFF_T sizes. See MAXFILESIZE_LARGE below. */ + CINIT(MAXFILESIZE, LONG, 114), + + /* See the comment for INFILESIZE above, but in short, specifies + * the size of the file being uploaded. -1 means unknown. + */ + CINIT(INFILESIZE_LARGE, OFF_T, 115), + + /* Sets the continuation offset. There is also a LONG version of this; + * look above for RESUME_FROM. + */ + CINIT(RESUME_FROM_LARGE, OFF_T, 116), + + /* Sets the maximum size of data that will be downloaded from + * an HTTP or FTP server. See MAXFILESIZE above for the LONG version. + */ + CINIT(MAXFILESIZE_LARGE, OFF_T, 117), + + /* Set this option to the file name of your .netrc file you want libcurl + to parse (using the CURLOPT_NETRC option). If not set, libcurl will do + a poor attempt to find the user's home directory and check for a .netrc + file in there. */ + CINIT(NETRC_FILE, OBJECTPOINT, 118), + + /* Enable SSL/TLS for FTP, pick one of: + CURLFTPSSL_TRY - try using SSL, proceed anyway otherwise + CURLFTPSSL_CONTROL - SSL for the control connection or fail + CURLFTPSSL_ALL - SSL for all communication or fail + */ + CINIT(USE_SSL, LONG, 119), + + /* The _LARGE version of the standard POSTFIELDSIZE option */ + CINIT(POSTFIELDSIZE_LARGE, OFF_T, 120), + + /* Enable/disable the TCP Nagle algorithm */ + CINIT(TCP_NODELAY, LONG, 121), + + /* 122 OBSOLETE, used in 7.12.3. Gone in 7.13.0 */ + /* 123 OBSOLETE. Gone in 7.16.0 */ + /* 124 OBSOLETE, used in 7.12.3. Gone in 7.13.0 */ + /* 125 OBSOLETE, used in 7.12.3. Gone in 7.13.0 */ + /* 126 OBSOLETE, used in 7.12.3. Gone in 7.13.0 */ + /* 127 OBSOLETE. Gone in 7.16.0 */ + /* 128 OBSOLETE. Gone in 7.16.0 */ + + /* When FTP over SSL/TLS is selected (with CURLOPT_USE_SSL), this option + can be used to change libcurl's default action which is to first try + "AUTH SSL" and then "AUTH TLS" in this order, and proceed when a OK + response has been received. + + Available parameters are: + CURLFTPAUTH_DEFAULT - let libcurl decide + CURLFTPAUTH_SSL - try "AUTH SSL" first, then TLS + CURLFTPAUTH_TLS - try "AUTH TLS" first, then SSL + */ + CINIT(FTPSSLAUTH, LONG, 129), + + CINIT(IOCTLFUNCTION, FUNCTIONPOINT, 130), + CINIT(IOCTLDATA, OBJECTPOINT, 131), + + /* 132 OBSOLETE. Gone in 7.16.0 */ + /* 133 OBSOLETE. Gone in 7.16.0 */ + + /* zero terminated string for pass on to the FTP server when asked for + "account" info */ + CINIT(FTP_ACCOUNT, OBJECTPOINT, 134), + + /* feed cookies into cookie engine */ + CINIT(COOKIELIST, OBJECTPOINT, 135), + + /* ignore Content-Length */ + CINIT(IGNORE_CONTENT_LENGTH, LONG, 136), + + /* Set to non-zero to skip the IP address received in a 227 PASV FTP server + response. Typically used for FTP-SSL purposes but is not restricted to + that. libcurl will then instead use the same IP address it used for the + control connection. */ + CINIT(FTP_SKIP_PASV_IP, LONG, 137), + + /* Select "file method" to use when doing FTP, see the curl_ftpmethod + above. */ + CINIT(FTP_FILEMETHOD, LONG, 138), + + /* Local port number to bind the socket to */ + CINIT(LOCALPORT, LONG, 139), + + /* Number of ports to try, including the first one set with LOCALPORT. + Thus, setting it to 1 will make no additional attempts but the first. + */ + CINIT(LOCALPORTRANGE, LONG, 140), + + /* no transfer, set up connection and let application use the socket by + extracting it with CURLINFO_LASTSOCKET */ + CINIT(CONNECT_ONLY, LONG, 141), + + /* Function that will be called to convert from the + network encoding (instead of using the iconv calls in libcurl) */ + CINIT(CONV_FROM_NETWORK_FUNC, FUNCTIONPOINT, 142), + + /* Function that will be called to convert to the + network encoding (instead of using the iconv calls in libcurl) */ + CINIT(CONV_TO_NETWORK_FUNC, FUNCTIONPOINT, 143), + + /* Function that will be called to convert from UTF8 + (instead of using the iconv calls in libcurl) + Note that this is used only for SSL certificate processing */ + CINIT(CONV_FROM_UTF8_FUNC, FUNCTIONPOINT, 144), + + /* if the connection proceeds too quickly then need to slow it down */ + /* limit-rate: maximum number of bytes per second to send or receive */ + CINIT(MAX_SEND_SPEED_LARGE, OFF_T, 145), + CINIT(MAX_RECV_SPEED_LARGE, OFF_T, 146), + + /* Pointer to command string to send if USER/PASS fails. */ + CINIT(FTP_ALTERNATIVE_TO_USER, OBJECTPOINT, 147), + + /* callback function for setting socket options */ + CINIT(SOCKOPTFUNCTION, FUNCTIONPOINT, 148), + CINIT(SOCKOPTDATA, OBJECTPOINT, 149), + + /* set to 0 to disable session ID re-use for this transfer, default is + enabled (== 1) */ + CINIT(SSL_SESSIONID_CACHE, LONG, 150), + + /* allowed SSH authentication methods */ + CINIT(SSH_AUTH_TYPES, LONG, 151), + + /* Used by scp/sftp to do public/private key authentication */ + CINIT(SSH_PUBLIC_KEYFILE, OBJECTPOINT, 152), + CINIT(SSH_PRIVATE_KEYFILE, OBJECTPOINT, 153), + + /* Send CCC (Clear Command Channel) after authentication */ + CINIT(FTP_SSL_CCC, LONG, 154), + + /* Same as TIMEOUT and CONNECTTIMEOUT, but with ms resolution */ + CINIT(TIMEOUT_MS, LONG, 155), + CINIT(CONNECTTIMEOUT_MS, LONG, 156), + + /* set to zero to disable the libcurl's decoding and thus pass the raw body + data to the application even when it is encoded/compressed */ + CINIT(HTTP_TRANSFER_DECODING, LONG, 157), + CINIT(HTTP_CONTENT_DECODING, LONG, 158), + + /* Permission used when creating new files and directories on the remote + server for protocols that support it, SFTP/SCP/FILE */ + CINIT(NEW_FILE_PERMS, LONG, 159), + CINIT(NEW_DIRECTORY_PERMS, LONG, 160), + + /* Set the behaviour of POST when redirecting. Values must be set to one + of CURL_REDIR* defines below. This used to be called CURLOPT_POST301 */ + CINIT(POSTREDIR, LONG, 161), + + /* used by scp/sftp to verify the host's public key */ + CINIT(SSH_HOST_PUBLIC_KEY_MD5, OBJECTPOINT, 162), + + /* Callback function for opening socket (instead of socket(2)). Optionally, + callback is able change the address or refuse to connect returning + CURL_SOCKET_BAD. The callback should have type + curl_opensocket_callback */ + CINIT(OPENSOCKETFUNCTION, FUNCTIONPOINT, 163), + CINIT(OPENSOCKETDATA, OBJECTPOINT, 164), + + /* POST volatile input fields. */ + CINIT(COPYPOSTFIELDS, OBJECTPOINT, 165), + + /* set transfer mode (;type=) when doing FTP via an HTTP proxy */ + CINIT(PROXY_TRANSFER_MODE, LONG, 166), + + /* Callback function for seeking in the input stream */ + CINIT(SEEKFUNCTION, FUNCTIONPOINT, 167), + CINIT(SEEKDATA, OBJECTPOINT, 168), + + /* CRL file */ + CINIT(CRLFILE, OBJECTPOINT, 169), + + /* Issuer certificate */ + CINIT(ISSUERCERT, OBJECTPOINT, 170), + + /* (IPv6) Address scope */ + CINIT(ADDRESS_SCOPE, LONG, 171), + + /* Collect certificate chain info and allow it to get retrievable with + CURLINFO_CERTINFO after the transfer is complete. (Unfortunately) only + working with OpenSSL-powered builds. */ + CINIT(CERTINFO, LONG, 172), + + /* "name" and "pwd" to use when fetching. */ + CINIT(USERNAME, OBJECTPOINT, 173), + CINIT(PASSWORD, OBJECTPOINT, 174), + + /* "name" and "pwd" to use with Proxy when fetching. */ + CINIT(PROXYUSERNAME, OBJECTPOINT, 175), + CINIT(PROXYPASSWORD, OBJECTPOINT, 176), + + /* Comma separated list of hostnames defining no-proxy zones. These should + match both hostnames directly, and hostnames within a domain. For + example, local.com will match local.com and www.local.com, but NOT + notlocal.com or www.notlocal.com. For compatibility with other + implementations of this, .local.com will be considered to be the same as + local.com. A single * is the only valid wildcard, and effectively + disables the use of proxy. */ + CINIT(NOPROXY, OBJECTPOINT, 177), + + /* block size for TFTP transfers */ + CINIT(TFTP_BLKSIZE, LONG, 178), + + /* Socks Service */ + CINIT(SOCKS5_GSSAPI_SERVICE, OBJECTPOINT, 179), + + /* Socks Service */ + CINIT(SOCKS5_GSSAPI_NEC, LONG, 180), + + /* set the bitmask for the protocols that are allowed to be used for the + transfer, which thus helps the app which takes URLs from users or other + external inputs and want to restrict what protocol(s) to deal + with. Defaults to CURLPROTO_ALL. */ + CINIT(PROTOCOLS, LONG, 181), + + /* set the bitmask for the protocols that libcurl is allowed to follow to, + as a subset of the CURLOPT_PROTOCOLS ones. That means the protocol needs + to be set in both bitmasks to be allowed to get redirected to. Defaults + to all protocols except FILE and SCP. */ + CINIT(REDIR_PROTOCOLS, LONG, 182), + + /* set the SSH knownhost file name to use */ + CINIT(SSH_KNOWNHOSTS, OBJECTPOINT, 183), + + /* set the SSH host key callback, must point to a curl_sshkeycallback + function */ + CINIT(SSH_KEYFUNCTION, FUNCTIONPOINT, 184), + + /* set the SSH host key callback custom pointer */ + CINIT(SSH_KEYDATA, OBJECTPOINT, 185), + + /* set the SMTP mail originator */ + CINIT(MAIL_FROM, OBJECTPOINT, 186), + + /* set the SMTP mail receiver(s) */ + CINIT(MAIL_RCPT, OBJECTPOINT, 187), + + /* FTP: send PRET before PASV */ + CINIT(FTP_USE_PRET, LONG, 188), + + /* RTSP request method (OPTIONS, SETUP, PLAY, etc...) */ + CINIT(RTSP_REQUEST, LONG, 189), + + /* The RTSP session identifier */ + CINIT(RTSP_SESSION_ID, OBJECTPOINT, 190), + + /* The RTSP stream URI */ + CINIT(RTSP_STREAM_URI, OBJECTPOINT, 191), + + /* The Transport: header to use in RTSP requests */ + CINIT(RTSP_TRANSPORT, OBJECTPOINT, 192), + + /* Manually initialize the client RTSP CSeq for this handle */ + CINIT(RTSP_CLIENT_CSEQ, LONG, 193), + + /* Manually initialize the server RTSP CSeq for this handle */ + CINIT(RTSP_SERVER_CSEQ, LONG, 194), + + /* The stream to pass to INTERLEAVEFUNCTION. */ + CINIT(INTERLEAVEDATA, OBJECTPOINT, 195), + + /* Let the application define a custom write method for RTP data */ + CINIT(INTERLEAVEFUNCTION, FUNCTIONPOINT, 196), + + /* Turn on wildcard matching */ + CINIT(WILDCARDMATCH, LONG, 197), + + /* Directory matching callback called before downloading of an + individual file (chunk) started */ + CINIT(CHUNK_BGN_FUNC, FUNCTIONPOINT, 198), + + /* Directory matching callback called after the file (chunk) + was downloaded, or skipped */ + CINIT(CHUNK_END_FUNC, FUNCTIONPOINT, 199), + + /* Change match (fnmatch-like) callback for wildcard matching */ + CINIT(FNMATCH_FUNC, FUNCTIONPOINT, 200), + + /* Let the application define custom chunk data pointer */ + CINIT(CHUNK_DATA, OBJECTPOINT, 201), + + /* FNMATCH_FUNC user pointer */ + CINIT(FNMATCH_DATA, OBJECTPOINT, 202), + + /* send linked-list of name:port:address sets */ + CINIT(RESOLVE, OBJECTPOINT, 203), + + /* Set a username for authenticated TLS */ + CINIT(TLSAUTH_USERNAME, OBJECTPOINT, 204), + + /* Set a password for authenticated TLS */ + CINIT(TLSAUTH_PASSWORD, OBJECTPOINT, 205), + + /* Set authentication type for authenticated TLS */ + CINIT(TLSAUTH_TYPE, OBJECTPOINT, 206), + + /* Set to 1 to enable the "TE:" header in HTTP requests to ask for + compressed transfer-encoded responses. Set to 0 to disable the use of TE: + in outgoing requests. The current default is 0, but it might change in a + future libcurl release. + + libcurl will ask for the compressed methods it knows of, and if that + isn't any, it will not ask for transfer-encoding at all even if this + option is set to 1. + + */ + CINIT(TRANSFER_ENCODING, LONG, 207), + + /* Callback function for closing socket (instead of close(2)). The callback + should have type curl_closesocket_callback */ + CINIT(CLOSESOCKETFUNCTION, FUNCTIONPOINT, 208), + CINIT(CLOSESOCKETDATA, OBJECTPOINT, 209), + + /* allow GSSAPI credential delegation */ + CINIT(GSSAPI_DELEGATION, LONG, 210), + + CURLOPT_LASTENTRY /* the last unused */ +}; + + +enum CURLcode { + CURLE_OK = 0, + CURLE_UNSUPPORTED_PROTOCOL, /* 1 */ + CURLE_FAILED_INIT, /* 2 */ + CURLE_URL_MALFORMAT, /* 3 */ + CURLE_NOT_BUILT_IN, /* 4 - [was obsoleted in August 2007 for + 7.17.0, reused in April 2011 for 7.21.5] */ + CURLE_COULDNT_RESOLVE_PROXY, /* 5 */ + CURLE_COULDNT_RESOLVE_HOST, /* 6 */ + CURLE_COULDNT_CONNECT, /* 7 */ + CURLE_FTP_WEIRD_SERVER_REPLY, /* 8 */ + CURLE_REMOTE_ACCESS_DENIED, /* 9 a service was denied by the server + due to lack of access - when login fails + this is not returned. */ + CURLE_OBSOLETE10, /* 10 - NOT USED */ + CURLE_FTP_WEIRD_PASS_REPLY, /* 11 */ + CURLE_OBSOLETE12, /* 12 - NOT USED */ + CURLE_FTP_WEIRD_PASV_REPLY, /* 13 */ + CURLE_FTP_WEIRD_227_FORMAT, /* 14 */ + CURLE_FTP_CANT_GET_HOST, /* 15 */ + CURLE_OBSOLETE16, /* 16 - NOT USED */ + CURLE_FTP_COULDNT_SET_TYPE, /* 17 */ + CURLE_PARTIAL_FILE, /* 18 */ + CURLE_FTP_COULDNT_RETR_FILE, /* 19 */ + CURLE_OBSOLETE20, /* 20 - NOT USED */ + CURLE_QUOTE_ERROR, /* 21 - quote command failure */ + CURLE_HTTP_RETURNED_ERROR, /* 22 */ + CURLE_WRITE_ERROR, /* 23 */ + CURLE_OBSOLETE24, /* 24 - NOT USED */ + CURLE_UPLOAD_FAILED, /* 25 - failed upload "command" */ + CURLE_READ_ERROR, /* 26 - couldn't open/read from file */ + CURLE_OUT_OF_MEMORY, /* 27 */ + /* Note: CURLE_OUT_OF_MEMORY may sometimes indicate a conversion error + instead of a memory allocation error if CURL_DOES_CONVERSIONS + is defined + */ + CURLE_OPERATION_TIMEDOUT, /* 28 - the timeout time was reached */ + CURLE_OBSOLETE29, /* 29 - NOT USED */ + CURLE_FTP_PORT_FAILED, /* 30 - FTP PORT operation failed */ + CURLE_FTP_COULDNT_USE_REST, /* 31 - the REST command failed */ + CURLE_OBSOLETE32, /* 32 - NOT USED */ + CURLE_RANGE_ERROR, /* 33 - RANGE "command" didn't work */ + CURLE_HTTP_POST_ERROR, /* 34 */ + CURLE_SSL_CONNECT_ERROR, /* 35 - wrong when connecting with SSL */ + CURLE_BAD_DOWNLOAD_RESUME, /* 36 - couldn't resume download */ + CURLE_FILE_COULDNT_READ_FILE, /* 37 */ + CURLE_LDAP_CANNOT_BIND, /* 38 */ + CURLE_LDAP_SEARCH_FAILED, /* 39 */ + CURLE_OBSOLETE40, /* 40 - NOT USED */ + CURLE_FUNCTION_NOT_FOUND, /* 41 */ + CURLE_ABORTED_BY_CALLBACK, /* 42 */ + CURLE_BAD_FUNCTION_ARGUMENT, /* 43 */ + CURLE_OBSOLETE44, /* 44 - NOT USED */ + CURLE_INTERFACE_FAILED, /* 45 - CURLOPT_INTERFACE failed */ + CURLE_OBSOLETE46, /* 46 - NOT USED */ + CURLE_TOO_MANY_REDIRECTS , /* 47 - catch endless re-direct loops */ + CURLE_UNKNOWN_OPTION, /* 48 - User specified an unknown option */ + CURLE_TELNET_OPTION_SYNTAX , /* 49 - Malformed telnet option */ + CURLE_OBSOLETE50, /* 50 - NOT USED */ + CURLE_PEER_FAILED_VERIFICATION, /* 51 - peer's certificate or fingerprint + wasn't verified fine */ + CURLE_GOT_NOTHING, /* 52 - when this is a specific error */ + CURLE_SSL_ENGINE_NOTFOUND, /* 53 - SSL crypto engine not found */ + CURLE_SSL_ENGINE_SETFAILED, /* 54 - can not set SSL crypto engine as + default */ + CURLE_SEND_ERROR, /* 55 - failed sending network data */ + CURLE_RECV_ERROR, /* 56 - failure in receiving network data */ + CURLE_OBSOLETE57, /* 57 - NOT IN USE */ + CURLE_SSL_CERTPROBLEM, /* 58 - problem with the local certificate */ + CURLE_SSL_CIPHER, /* 59 - couldn't use specified cipher */ + CURLE_SSL_CACERT, /* 60 - problem with the CA cert (path?) */ + CURLE_BAD_CONTENT_ENCODING, /* 61 - Unrecognized/bad encoding */ + CURLE_LDAP_INVALID_URL, /* 62 - Invalid LDAP URL */ + CURLE_FILESIZE_EXCEEDED, /* 63 - Maximum file size exceeded */ + CURLE_USE_SSL_FAILED, /* 64 - Requested FTP SSL level failed */ + CURLE_SEND_FAIL_REWIND, /* 65 - Sending the data requires a rewind + that failed */ + CURLE_SSL_ENGINE_INITFAILED, /* 66 - failed to initialise ENGINE */ + CURLE_LOGIN_DENIED, /* 67 - user, password or similar was not + accepted and we failed to login */ + CURLE_TFTP_NOTFOUND, /* 68 - file not found on server */ + CURLE_TFTP_PERM, /* 69 - permission problem on server */ + CURLE_REMOTE_DISK_FULL, /* 70 - out of disk space on server */ + CURLE_TFTP_ILLEGAL, /* 71 - Illegal TFTP operation */ + CURLE_TFTP_UNKNOWNID, /* 72 - Unknown transfer ID */ + CURLE_REMOTE_FILE_EXISTS, /* 73 - File already exists */ + CURLE_TFTP_NOSUCHUSER, /* 74 - No such user */ + CURLE_CONV_FAILED, /* 75 - conversion failed */ + CURLE_CONV_REQD, /* 76 - caller must register conversion + callbacks using curl_easy_setopt options + CURLOPT_CONV_FROM_NETWORK_FUNCTION, + CURLOPT_CONV_TO_NETWORK_FUNCTION, and + CURLOPT_CONV_FROM_UTF8_FUNCTION */ + CURLE_SSL_CACERT_BADFILE, /* 77 - could not load CACERT file, missing + or wrong format */ + CURLE_REMOTE_FILE_NOT_FOUND, /* 78 - remote file not found */ + CURLE_SSH, /* 79 - error from the SSH layer, somewhat + generic so the error message will be of + interest when this has happened */ + + CURLE_SSL_SHUTDOWN_FAILED, /* 80 - Failed to shut down the SSL + connection */ + CURLE_AGAIN, /* 81 - socket is not ready for send/recv, + wait till it's ready and try again (Added + in 7.18.2) */ + CURLE_SSL_CRL_BADFILE, /* 82 - could not load CRL file, missing or + wrong format (Added in 7.19.0) */ + CURLE_SSL_ISSUER_ERROR, /* 83 - Issuer check failed. (Added in + 7.19.0) */ + CURLE_FTP_PRET_FAILED, /* 84 - a PRET command failed */ + CURLE_RTSP_CSEQ_ERROR, /* 85 - mismatch of RTSP CSeq numbers */ + CURLE_RTSP_SESSION_ERROR, /* 86 - mismatch of RTSP Session Ids */ + CURLE_FTP_BAD_FILE_LIST, /* 87 - unable to parse FTP file list */ + CURLE_CHUNK_FAILED, /* 88 - chunk callback reported error */ + + CURL_LAST /* never use! */ +}; + +/* compatibility with older names */ +#define CURLOPT_ENCODING CURLOPT_ACCEPT_ENCODING + +/* The following were added in 7.21.5, April 2011 */ +#define CURLE_UNKNOWN_TELNET_OPTION CURLE_UNKNOWN_OPTION + +enum curl_TimeCond { + CURL_TIMECOND_NONE, + + CURL_TIMECOND_IFMODSINCE, + CURL_TIMECOND_IFUNMODSINCE, + CURL_TIMECOND_LASTMOD, + + CURL_TIMECOND_LAST +}; + +/* These enums are for use with the CURLOPT_NETRC option. */ +enum CURL_NETRC_OPTION { + CURL_NETRC_IGNORED, /* The .netrc will never be read. + * This is the default. */ + CURL_NETRC_OPTIONAL, /* A user:password in the URL will be preferred + * to one in the .netrc. */ + CURL_NETRC_REQUIRED, /* A user:password in the URL will be ignored. + * Unless one is set programmatically, the .netrc + * will be queried. */ + CURL_NETRC_LAST +}; + +enum curl_closepolicy { + CURLCLOSEPOLICY_NONE, /* first, never use this */ + + CURLCLOSEPOLICY_OLDEST, + CURLCLOSEPOLICY_LEAST_RECENTLY, // CURLCLOSEPOLICY_LEAST_RECENTLY_USED + CURLCLOSEPOLICY_LEAST_TRAFFIC, + CURLCLOSEPOLICY_SLOWEST, + CURLCLOSEPOLICY_CALLBACK, + + CURLCLOSEPOLICY_LAST /* last, never use this */ +}; + +enum { + CURL_HTTP_VERSION_NONE, /* setting this means we don't care, and that we'd + like the library to choose the best possible + for us! */ + CURL_HTTP_VERSION_1_0, /* please use HTTP 1.0 in the request */ + CURL_HTTP_VERSION_1_1, /* please use HTTP 1.1 in the request */ + + CURL_HTTP_VERSION_LAST /* *ILLEGAL* http version */ +}; + + +#define CURLAUTH_NONE 0 /* nothing */ +#define CURLAUTH_BASIC (1<<0) /* Basic (default) */ +#define CURLAUTH_DIGEST (1<<1) /* Digest */ +#define CURLAUTH_GSSNEGOTIATE (1<<2) /* GSS-Negotiate */ +#define CURLAUTH_NTLM (1<<3) /* NTLM */ +#define CURLAUTH_DIGEST_IE (1<<4) /* Digest with IE flavour */ +#define CURLAUTH_NTLM_WB (1<<5) /* NTLM delegating to winbind helper */ +#define CURLAUTH_ONLY (1<<31) /* used together with a single other + type to force no auth or just that + single type */ +#define CURLAUTH_ANY (~CURLAUTH_DIGEST_IE) /* all fine types set */ +#define CURLAUTH_ANYSAFE (~(CURLAUTH_BASIC|CURLAUTH_DIGEST_IE)) + + +/* parameter for the CURLOPT_FTP_CREATE_MISSING_DIRS option */ +enum curl_ftpcreatedir { + CURLFTP_CREATE_DIR_NONE, /* do NOT create missing dirs! */ + CURLFTP_CREATE_DIR, /* (FTP/SFTP) if CWD fails, try MKD and then CWD + again if MKD succeeded, for SFTP this does + similar magic */ + CURLFTP_CREATE_DIR_RETRY, /* (FTP only) if CWD fails, try MKD and then CWD + again even if MKD failed! */ + CURLFTP_CREATE_DIR_LAST /* not an option, never use */ +}; + + + /* Below here follows defines for the CURLOPT_IPRESOLVE option. If a host + name resolves addresses using more than one IP protocol version, this + option might be handy to force libcurl to use a specific IP version. */ +#define CURL_IPRESOLVE_WHATEVER 0 /* default, resolves addresses to all IP + versions that your system allows */ +#define CURL_IPRESOLVE_V4 1 /* resolve to ipv4 addresses */ +#define CURL_IPRESOLVE_V6 2 /* resolve to ipv6 addresses */ + + +/* parameter for the CURLOPT_USE_SSL option */ +enum curl_usessl { + CURLUSESSL_NONE, /* do not attempt to use SSL */ + CURLUSESSL_TRY, /* try using SSL, proceed anyway otherwise */ + CURLUSESSL_CONTROL, /* SSL for the control connection or fail */ + CURLUSESSL_ALL, /* SSL for all communication or fail */ + CURLUSESSL_LAST /* not an option, never use */ +}; + + +enum { + CURL_SSLVERSION_DEFAULT, + CURL_SSLVERSION_TLSv1, + CURL_SSLVERSION_SSLv2, + CURL_SSLVERSION_SSLv3, + + CURL_SSLVERSION_LAST /* never use, keep last */ +}; + + +/* parameter for the CURLOPT_FTPSSLAUTH option */ +enum curl_ftpauth { + CURLFTPAUTH_DEFAULT, /* let libcurl decide */ + CURLFTPAUTH_SSL, /* use "AUTH SSL" */ + CURLFTPAUTH_TLS, /* use "AUTH TLS" */ + CURLFTPAUTH_LAST /* not an option, never use */ +}; + + +enum curl_ftpfile { + FTPFILE_MULTICWD = 1, /* as defined by RFC1738 */ + FTPFILE_NOCWD = 2, /* use SIZE / RETR / STOR on the full path */ + FTPFILE_SINGLECWD = 3 /* make one CWD, then SIZE / RETR / STOR on the file */ +}; + + +#define CURLSSH_AUTH_ANY ~0 /* all types supported by the server */ +#define CURLSSH_AUTH_NONE 0 /* none allowed, silly but complete */ +#define CURLSSH_AUTH_PUBLICKEY (1<<0) /* public/private key files */ +#define CURLSSH_AUTH_PASSWORD (1<<1) /* password */ +#define CURLSSH_AUTH_HOST (1<<2) /* host key files */ +#define CURLSSH_AUTH_KEYBOARD (1<<3) /* keyboard interactive */ +#define CURLSSH_AUTH_DEFAULT CURLSSH_AUTH_ANY + +#define CURLGSSAPI_DELEGATION_NONE 0 /* no delegation (default) */ +#define CURLGSSAPI_DELEGATION_POLICY_FLAG (1<<0) /* if permitted by policy */ +#define CURLGSSAPI_DELEGATION_FLAG (1<<1) /* delegate always */ + + +/* parameter for the CURLOPT_FTP_SSL_CCC option */ +enum curl_ftpccc { + CURLFTPSSL_CCC_NONE, /* do not send CCC */ + CURLFTPSSL_CCC_PASSIVE, /* Let the server initiate the shutdown */ + CURLFTPSSL_CCC_ACTIVE, /* Initiate the shutdown */ + CURLFTPSSL_CCC_LAST /* not an option, never use */ +}; + + +/* symbols to use with CURLOPT_POSTREDIR. + CURL_REDIR_POST_301 and CURL_REDIR_POST_302 can be bitwise ORed so that + CURL_REDIR_POST_301 | CURL_REDIR_POST_302 == CURL_REDIR_POST_ALL */ + +#define CURL_REDIR_GET_ALL 0 +#define CURL_REDIR_POST_301 1 +#define CURL_REDIR_POST_302 2 +#define CURL_REDIR_POST_ALL (CURL_REDIR_POST_301|CURL_REDIR_POST_302) + + +/* CURLPROTO_ defines are for the CURLOPT_*PROTOCOLS options */ +#define CURLPROTO_HTTP (1<<0) +#define CURLPROTO_HTTPS (1<<1) +#define CURLPROTO_FTP (1<<2) +#define CURLPROTO_FTPS (1<<3) +#define CURLPROTO_SCP (1<<4) +#define CURLPROTO_SFTP (1<<5) +#define CURLPROTO_TELNET (1<<6) +#define CURLPROTO_LDAP (1<<7) +#define CURLPROTO_LDAPS (1<<8) +#define CURLPROTO_DICT (1<<9) +#define CURLPROTO_FILE (1<<10) +#define CURLPROTO_TFTP (1<<11) +#define CURLPROTO_IMAP (1<<12) +#define CURLPROTO_IMAPS (1<<13) +#define CURLPROTO_POP3 (1<<14) +#define CURLPROTO_POP3S (1<<15) +#define CURLPROTO_SMTP (1<<16) +#define CURLPROTO_SMTPS (1<<17) +#define CURLPROTO_RTSP (1<<18) +#define CURLPROTO_RTMP (1<<19) +#define CURLPROTO_RTMPT (1<<20) +#define CURLPROTO_RTMPE (1<<21) +#define CURLPROTO_RTMPTE (1<<22) +#define CURLPROTO_RTMPS (1<<23) +#define CURLPROTO_RTMPTS (1<<24) +#define CURLPROTO_GOPHER (1<<25) +#define CURLPROTO_ALL (~0) /* enable everything */ + + +/* + * Public API enums for RTSP requests + */ +enum { + CURL_RTSPREQ_NONE, /* first in list */ + CURL_RTSPREQ_OPTIONS, + CURL_RTSPREQ_DESCRIBE, + CURL_RTSPREQ_ANNOUNCE, + CURL_RTSPREQ_SETUP, + CURL_RTSPREQ_PLAY, + CURL_RTSPREQ_PAUSE, + CURL_RTSPREQ_TEARDOWN, + CURL_RTSPREQ_GET_PARAMETER, + CURL_RTSPREQ_SET_PARAMETER, + CURL_RTSPREQ_RECORD, + CURL_RTSPREQ_RECEIVE, + CURL_RTSPREQ_LAST /* last in list */ +}; + + + +#define CURLINFO_STRING 0x100000 +#define CURLINFO_LONG 0x200000 +#define CURLINFO_DOUBLE 0x300000 +#define CURLINFO_SLIST 0x400000 +#define CURLINFO_MASK 0x0fffff +#define CURLINFO_TYPEMASK 0xf00000 + +enum CURLINFO { + CURLINFO_NONE, /* first, never use this */ + CURLINFO_EFFECTIVE_URL = CURLINFO_STRING + 1, + CURLINFO_RESPONSE_CODE = CURLINFO_LONG + 2, + CURLINFO_TOTAL_TIME = CURLINFO_DOUBLE + 3, + CURLINFO_NAMELOOKUP_TIME = CURLINFO_DOUBLE + 4, + CURLINFO_CONNECT_TIME = CURLINFO_DOUBLE + 5, + CURLINFO_PRETRANSFER_TIME = CURLINFO_DOUBLE + 6, + CURLINFO_SIZE_UPLOAD = CURLINFO_DOUBLE + 7, + CURLINFO_SIZE_DOWNLOAD = CURLINFO_DOUBLE + 8, + CURLINFO_SPEED_DOWNLOAD = CURLINFO_DOUBLE + 9, + CURLINFO_SPEED_UPLOAD = CURLINFO_DOUBLE + 10, + CURLINFO_HEADER_SIZE = CURLINFO_LONG + 11, + CURLINFO_REQUEST_SIZE = CURLINFO_LONG + 12, + CURLINFO_SSL_VERIFYRESULT = CURLINFO_LONG + 13, + CURLINFO_FILETIME = CURLINFO_LONG + 14, + CURLINFO_CONTENT_LEN_DOWNLOAD = CURLINFO_DOUBLE + 15, + CURLINFO_CONTENT_LEN_UPLOAD = CURLINFO_DOUBLE + 16, + CURLINFO_STARTTRANSFER_TIME = CURLINFO_DOUBLE + 17, + CURLINFO_CONTENT_TYPE = CURLINFO_STRING + 18, + CURLINFO_REDIRECT_TIME = CURLINFO_DOUBLE + 19, + CURLINFO_REDIRECT_COUNT = CURLINFO_LONG + 20, + CURLINFO_PRIVATE = CURLINFO_STRING + 21, + CURLINFO_HTTP_CONNECTCODE = CURLINFO_LONG + 22, + CURLINFO_HTTPAUTH_AVAIL = CURLINFO_LONG + 23, + CURLINFO_PROXYAUTH_AVAIL = CURLINFO_LONG + 24, + CURLINFO_OS_ERRNO = CURLINFO_LONG + 25, + CURLINFO_NUM_CONNECTS = CURLINFO_LONG + 26, + CURLINFO_SSL_ENGINES = CURLINFO_SLIST + 27, + CURLINFO_COOKIELIST = CURLINFO_SLIST + 28, + CURLINFO_LASTSOCKET = CURLINFO_LONG + 29, + CURLINFO_FTP_ENTRY_PATH = CURLINFO_STRING + 30, + CURLINFO_REDIRECT_URL = CURLINFO_STRING + 31, + CURLINFO_PRIMARY_IP = CURLINFO_STRING + 32, + CURLINFO_APPCONNECT_TIME = CURLINFO_DOUBLE + 33, + CURLINFO_CERTINFO = CURLINFO_SLIST + 34, + CURLINFO_CONDITION_UNMET = CURLINFO_LONG + 35, + CURLINFO_RTSP_SESSION_ID = CURLINFO_STRING + 36, + CURLINFO_RTSP_CLIENT_CSEQ = CURLINFO_LONG + 37, + CURLINFO_RTSP_SERVER_CSEQ = CURLINFO_LONG + 38, + CURLINFO_RTSP_CSEQ_RECV = CURLINFO_LONG + 39, + CURLINFO_PRIMARY_PORT = CURLINFO_LONG + 40, + CURLINFO_LOCAL_IP = CURLINFO_STRING + 41, + CURLINFO_LOCAL_PORT = CURLINFO_LONG + 42, + /* Fill in new entries below here! */ + + CURLINFO_LASTONE = 42 +}; + + +enum curl_proxytype { + CURLPROXY_HTTP = 0, /* added in 7.10, new in 7.19.4 default is to use + CONNECT HTTP/1.1 */ + CURLPROXY_HTTP_1_0 = 1, /* added in 7.19.4, force to use CONNECT + HTTP/1.0 */ + CURLPROXY_SOCKS4 = 4, /* support added in 7.15.2, enum existed already + in 7.10 */ + CURLPROXY_SOCKS5 = 5, /* added in 7.10 */ + CURLPROXY_SOCKS4A = 6, /* added in 7.18.0 */ + CURLPROXY_SOCKS5_HOSTNAME = 7 /* Use the SOCKS5 protocol but pass along the + host name rather than the IP address. added + in 7.18.0 */ +}; + + +enum CURLformoption { + CURLFORM_NOTHING, /********* the first one is unused ************/ + /* */ + CURLFORM_COPYNAME, // char + CURLFORM_PTRNAME, // not support + CURLFORM_NAMELENGTH, // long + CURLFORM_COPYCONTENTS, // char + CURLFORM_PTRCONTENTS, // not support + CURLFORM_CONTENTSLENGTH, // long + CURLFORM_FILECONTENT, // char + CURLFORM_ARRAY, // not support + CURLFORM_OBSOLETE, + CURLFORM_FILE, // char + CURLFORM_BUFFER, // not support + CURLFORM_BUFFERPTR, // not support + CURLFORM_BUFFERLENGTH, // not support + CURLFORM_CONTENTTYPE, // char + CURLFORM_CONTENTHEADER, // curl_slist + CURLFORM_FILENAME, // char + CURLFORM_END, // !! + CURLFORM_OBSOLETE2, + CURLFORM_STREAM, // not support + CURLFORM_LASTENTRY /* the last unused */ +}; + +enum CURLFORMcode { + CURL_FORMADD_OK, /* first, no error */ + CURL_FORMADD_MEMORY, + CURL_FORMADD_OPTION_TWICE, + CURL_FORMADD_NULL, + CURL_FORMADD_UNKNOWN_OPTION, + CURL_FORMADD_INCOMPLETE, + CURL_FORMADD_ILLEGAL_ARRAY, + CURL_FORMADD_DISABLED, /* libcurl was built with this disabled */ + CURL_FORMADD_LAST /* last */ +}; diff --git a/libalternator/tests/corpus/includelibrary/include/ccc.inc b/libalternator/tests/corpus/includelibrary/include/ccc.inc new file mode 100644 index 0000000..59712ec --- /dev/null +++ b/libalternator/tests/corpus/includelibrary/include/ccc.inc @@ -0,0 +1,186 @@ +/** + * This is the include file for Custom Chat Colors + * https://forums.alliedmods.net/showthread.php?t=186695 + * To check that Custom Chat Colors is installed and running, verify that the "ccc" library exists + */ + +#if defined _ccc_included + #endinput +#endif +#define _ccc_included + +enum CCC_ColorType { + CCC_TagColor, + CCC_NameColor, + CCC_ChatColor +}; + +#define COLOR_NONE -1 +#define COLOR_GREEN -2 +#define COLOR_OLIVE -3 +#define COLOR_TEAM -4 + +/** + * Gets a client's color as a hexadecimal integer. + * + * @param client Client index + * @param type Color type to retreive + * @param alpha Pass a boolean variable by reference here and it will be true if the color has alpha specified or false if it doesn't (or is a stock color) + * @return Color as a hexadecimal integer (use %X in formatting to get a hexadecimal string) + * + * On error/errors: Invalid client index or client is not in game + */ +native int CCC_GetColor(int client, CCC_ColorType type, bool &alpha = false); + +/** + * Sets a client's color as a hexadecimal integer. + * + * @param client Client index + * @param type Color type to set + * @param color Integer representation of the color (use StringToInt(input, 16) to convert a hexadecimal string) or one of the color defines + * @param alpha Are you specifying a color with alpha? + * @return True if the color is updated successfully, false otherwise + * + * On error/errors: Invalid client index or client is not in game + */ +native bool CCC_SetColor(int client, CCC_ColorType type, int color, bool alpha); + +/** + * Gets a client's tag + * + * @param client Client index + * @param buffer Buffer to store the tag in + * @param maxlen Maximum buffer length + * @noreturn + * + * On error/errors: Invalid client index or client is not in game + */ +native void CCC_GetTag(int client, char[] buffer, int maxlen); + +/** + * Sets a client's tag + * + * @param client Client index + * @param tag String containing the new tag + * @noreturn + * + * On error/errors: Invalid client index or client is not in game + */ +native void CCC_SetTag(int client, const char[] tag); + +/** + * Resets a client's color to the value in the config file. + * + * @param client Client index + * @param type Color type to restore + * @noreturn + * + * On error/errors: Invalid client index or client is not in game + */ +native void CCC_ResetColor(int client, CCC_ColorType type); + +/** + * Resets a client's tag to the value in the config file. + * + * @param client Client index + * @noreturn + * + * On error/errors: Invalid client index or client is not in game + */ +native void CCC_ResetTag(int client); + +/** + * Called when a cilent's name is about to be colored + * DO NOT START A NEW USERMESSAGE (i.e. PrintToChat, PrintToChatAll) WITHIN THIS FORWARD + * + * @param client Client index + * @return Plugin_Handled to prevent coloring, Plugin_Continue to allow coloring + */ +#pragma deprecated Use CCC_OnColor instead +forward Action CCC_OnNameColor(int client); + +/** + * Called when a client's chat is about to be colored + * DO NOT START A NEW USERMESSAGE (i.e. PrintToChat, PrintToChatAll) WITHIN THIS FORWARD + * + * @param client Client index + * @return Plugin_Handled to prevent coloring, Plugin_Continue to allow coloring + */ +#pragma deprecated Use CCC_OnColor instead +forward Action CCC_OnChatColor(int client); + +/** + * Called when a client's name is about to be tagged + * DO NOT START A NEW USERMESSAGE (i.e. PrintToChat, PrintToChatAll) WITHIN THIS FORWARD + * + * @param client Client index + * @return Plugin_Handled to prevent tagging, Plugin_Continue to allow tagging + */ +#pragma deprecated Use CCC_OnColor instead +forward Action CCC_OnTagApplied(int client); + +/** + * Called when a client's name is about to be tagged + * DO NOT START A NEW USERMESSAGE (i.e. PrintToChat, PrintToChatAll) WITHIN THIS FORWARD + * + * @param client Client index + * @param message Chat message that will be printed + * @param type What type of color will be applied. If this is CCC_TagColor, it controls whether the tag will be applied at all, not whether the tag will be colored. + * @return Plugin_Handled to prevent coloring, Plugin_Continue to allow coloring + */ +forward Action CCC_OnColor(int client, const char[] message, CCC_ColorType type); + +/** + * Called when a message has been fully colored and will be sent, unless further plugins modify it through Simple Chat Processor + * + * @param author Author client index + * @param message Message + * @param maxlen Maximum length of message buffer + * @noreturn + */ +forward void CCC_OnChatMessage(int author, char[] message, int maxlen); + +/** + * Called when a client's colors and tag are about to be loaded from the config file + * At this point, the client has NO COLORS + * + * @param client Client index + * @return Plugin_Handled or Plugin_Stop to prevent loading, Plugin_Continue or Plugin_Changed to allow + */ +forward Action CCC_OnUserConfigPreLoaded(int client); + +/** + * Called when a client's colors and tag have been loaded from the config file + * + * @param client Client index + * @noreturn + */ +forward void CCC_OnUserConfigLoaded(int client); + +/** + * Called when the configuration file is reloaded with the sm_reloadccc command + * + * @noreturn + */ +forward void CCC_OnConfigReloaded(); + +public SharedPlugin __pl_ccc = { + name = "ccc", + file = "custom-chatcolors.smx", +#if defined REQUIRE_PLUGIN + required = 1 +#else + required = 0 +#endif +}; + +#if !defined REQUIRE_PLUGIN +public void __pl_ccc_SetNTVOptional(){ + MarkNativeAsOptional("CCC_GetColor"); + MarkNativeAsOptional("CCC_SetColor"); + MarkNativeAsOptional("CCC_GetTag"); + MarkNativeAsOptional("CCC_ResetTag"); + MarkNativeAsOptional("CCC_ResetColor"); + MarkNativeAsOptional("CCC_ResetTag"); +} +#endif \ No newline at end of file diff --git a/libalternator/tests/corpus/includelibrary/include/cccm.inc b/libalternator/tests/corpus/includelibrary/include/cccm.inc new file mode 100644 index 0000000..c030f9c --- /dev/null +++ b/libalternator/tests/corpus/includelibrary/include/cccm.inc @@ -0,0 +1,28 @@ +#if defined _cccm_included + #endinput +#endif +#define _cccm_included + +/** + * Checks if client tag is hidden + * + * @param client Client index + * @return True if hidden, else false + */ +native bool CCCM_IsTagHidden(int client); + +public SharedPlugin __pl_cccm = { + name = "cccm", + file = "custom-chatcolors-menu.smx", +#if defined REQUIRE_PLUGIN + required = 1 +#else + required = 0 +#endif +}; + +#if !defined REQUIRE_PLUGIN +public void __pl_cccm_SetNTVOptional() { + MarkNativeAsOptional("CCCM_IsTagHidden"); +} +#endif \ No newline at end of file diff --git a/libalternator/tests/corpus/includelibrary/include/chat-processor.inc b/libalternator/tests/corpus/includelibrary/include/chat-processor.inc new file mode 100644 index 0000000..e832631 --- /dev/null +++ b/libalternator/tests/corpus/includelibrary/include/chat-processor.inc @@ -0,0 +1,79 @@ +#if defined _chat_processor_included + #endinput +#endif +#define _chat_processor_included + +//Globals +#define MAXLENGTH_INPUT 128 +#define MAXLENGTH_NAME 64 +#define MAXLENGTH_MESSAGE 256 + +enum eChatFlags { + ChatFlag_Invalid, + ChatFlag_All, + ChatFlag_Team, + ChatFlag_Spec, + ChatFlag_Dead +} + +//Natives +/** +* Retrieves the current format string assigned to a chat flag. +* Example: ChatFlag_Dead = ' *DEAD* {1} : {2} ' +* You can find the config formats in either the translations or the configs. +* +* param flag Flag to retrieve the format string for. +* param sFormatString String Buffer. +* param iSize Size of the String Buffer. +* +* noreturn +**/ +native void ChatProcessor_GetChatFlagString(eChatFlags flag, char[] sFormatString, int iSize); + +//Forwards +/** +* Called while sending a chat message before It's sent. +* Limits on the name and message strings can be found above. +* +* param author Author that created the message. +* param recipients Array of clients who will receive the message. +* param flag Determines which type of message is being sent. +* param name Name string of the author to be pushed. +* param message Message string from the author to be pushed. +* +* return types +* - Plugin_Continue Stops the message. +* - Plugin_Stop Stops the message. +* - Plugin_Changed Fires the post-forward below and prints out a message. +* - Plugin_Handled Fires the post-forward below but doesn't print a message. +**/ +forward Action OnChatMessage(int &author, ArrayList recipients, eChatFlags &flag, char[] name, char[] message, bool &bProcessColors, bool &bRemoveColors); + +/** +* Called after the chat message is sent to the designated clients by the author. +* +* param author Author that sent the message. +* param recipients Array of clients who received the message. +* param flag Determines which type of message was sent. +* param name Name string of the author. +* param message Message string from the author. +* +* noreturn +**/ +forward void OnChatMessagePost(int author, ArrayList recipients, eChatFlags flag, const char[] name, const char[] message, bool bProcessColors, bool bRemoveColors); + +#if !defined REQUIRE_PLUGIN +public void __pl_chat_processor_SetNTVOptional() { + MarkNativeAsOptional("ChatProcessor_GetChatFlagString"); +} +#endif + +public SharedPlugin _pl_chat_processor = { + name = "chat-processor", + file = "chat-processor.smx", +#if defined REQUIRE_PLUGIN + required = 1 +#else + required = 0 +#endif +}; diff --git a/libalternator/tests/corpus/includelibrary/include/morecolors.inc b/libalternator/tests/corpus/includelibrary/include/morecolors.inc new file mode 100644 index 0000000..ffd16a6 --- /dev/null +++ b/libalternator/tests/corpus/includelibrary/include/morecolors.inc @@ -0,0 +1,995 @@ +// MOAR COLORS +// By Dr. McKay +// Inspired by: https://forums.alliedmods.net/showthread.php?t=96831 + +#if defined _more_colors_included + #endinput +#endif +#define _more_colors_included + +#pragma newdecls optional +#include + +#define MORE_COLORS_VERSION "2.0.0-MC" +#define MC_MAX_MESSAGE_LENGTH 256 +#define MAX_BUFFER_LENGTH (MC_MAX_MESSAGE_LENGTH * 4) + +#define MCOLOR_RED 0xFF4040 +#define MCOLOR_BLUE 0x99CCFF +#define MCOLOR_GRAY 0xCCCCCC +#define MCOLOR_GREEN 0x3EFF3E + +#define MC_GAME_DODS 0 + +bool MC_SkipList[MAXPLAYERS + 1]; +StringMap MC_Trie; +int MC_TeamColors[][] = {{0xCCCCCC, 0x4D7942, 0xFF4040}}; // Multi-dimensional array for games that don't support SayText2. First index is the game index (as defined by the GAME_ defines), second index is team. 0 = spectator, 1 = team1, 2 = team2 + +static ConVar sm_show_activity = null; + +/** + * Prints a message to a specific client in the chat area. + * Supports color tags. + * + * @param client Client index. + * @param message Message (formatting rules). + * + * On error/Errors: If the client is not connected an error will be thrown. + */ +stock void MC_PrintToChat(int client, const char[] message, any ...) { + MC_CheckTrie(); + + if (client <= 0 || client > MaxClients) { + ThrowError("Invalid client index %i", client); + } + + if (!IsClientInGame(client)) { + ThrowError("Client %i is not in game", client); + } + + char buffer[MAX_BUFFER_LENGTH]; + char buffer2[MAX_BUFFER_LENGTH]; + + SetGlobalTransTarget(client); + Format(buffer, sizeof(buffer), "\x01%s", message); + VFormat(buffer2, sizeof(buffer2), buffer, 3); + + MC_ReplaceColorCodes(buffer2); + MC_SendMessage(client, buffer2); +} + +/** + * Prints a message to all clients in the chat area. + * Supports color tags. + * + * @param client Client index. + * @param message Message (formatting rules). + */ +stock void MC_PrintToChatAll(const char[] message, any ...) { + MC_CheckTrie(); + + char buffer[MAX_BUFFER_LENGTH], buffer2[MAX_BUFFER_LENGTH]; + + for (int i = 1; i <= MaxClients; ++i) { + if (!IsClientInGame(i) || MC_SkipList[i]) { + MC_SkipList[i] = false; + continue; + } + + SetGlobalTransTarget(i); + Format(buffer, sizeof(buffer), "\x01%s", message); + VFormat(buffer2, sizeof(buffer2), buffer, 2); + + MC_ReplaceColorCodes(buffer2); + MC_SendMessage(i, buffer2); + } +} + +/** + * Prints a message to a specific client in the chat area. + * Supports color tags and teamcolor tag. + * + * @param client Client index. + * @param author Author index whose color will be used for teamcolor tag. + * @param message Message (formatting rules). + * + * On error/Errors: If the client or author are not connected an error will be thrown + */ +stock void MC_PrintToChatEx(int client, int author, const char[] message, any ...) { + MC_CheckTrie(); + + if (client <= 0 || client > MaxClients) { + ThrowError("Invalid client index %i", client); + } + + if (!IsClientInGame(client)) { + ThrowError("Client %i is not in game", client); + } + + if (author <= 0 || author > MaxClients) { + ThrowError("Invalid client index %i", author); + } + + if (!IsClientInGame(author)) { + ThrowError("Client %i is not in game", author); + } + + char buffer[MAX_BUFFER_LENGTH], buffer2[MAX_BUFFER_LENGTH]; + SetGlobalTransTarget(client); + Format(buffer, sizeof(buffer), "\x01%s", message); + VFormat(buffer2, sizeof(buffer2), buffer, 4); + MC_ReplaceColorCodes(buffer2, author); + MC_SendMessage(client, buffer2, author); +} + +/** + * Prints a message to all clients in the chat area. + * Supports color tags and teamcolor tag. + * + * @param author Author index whose color will be used for teamcolor tag. + * @param message Message (formatting rules). + * + * On error/Errors: If the author is not connected an error will be thrown. + */ +stock void MC_PrintToChatAllEx(int author, const char[] message, any ...) { + MC_CheckTrie(); + + if (author <= 0 || author > MaxClients) { + ThrowError("Invalid client index %i", author); + } + + if (!IsClientInGame(author)) { + ThrowError("Client %i is not in game", author); + } + + char buffer[MAX_BUFFER_LENGTH]; + char buffer2[MAX_BUFFER_LENGTH]; + + for (int i = 1; i <= MaxClients; ++i) { + if (!IsClientInGame(i) || MC_SkipList[i]) { + MC_SkipList[i] = false; + continue; + } + + SetGlobalTransTarget(i); + Format(buffer, sizeof(buffer), "\x01%s", message); + VFormat(buffer2, sizeof(buffer2), buffer, 3); + + MC_ReplaceColorCodes(buffer2, author); + MC_SendMessage(i, buffer2, author); + } +} + +/** + * Sends a SayText2 usermessage + * + * @param client Client to send usermessage to + * @param message Message to send + */ +stock void MC_SendMessage(int client, const char[] message, int author = 0) { + if (author == 0) { + author = client; + } + + char buffer[MC_MAX_MESSAGE_LENGTH]; + strcopy(buffer, sizeof(buffer), message); + + UserMsg index = GetUserMessageId("SayText2"); + if (index == INVALID_MESSAGE_ID) { + if (GetEngineVersion() == Engine_DODS) { + int team = GetClientTeam(author); + if (team == 0) { + ReplaceString(buffer, sizeof(buffer), "\x03", "\x04", false); // Unassigned gets green + } + else { + char temp[16]; + Format(temp, sizeof(temp), "\x07%06X", MC_TeamColors[MC_GAME_DODS][team - 1]); + ReplaceString(buffer, sizeof(buffer), "\x03", temp, false); + } + } + + PrintToChat(client, "%s", buffer); + return; + } + + Handle buf = StartMessageOne("SayText2", client, USERMSG_RELIABLE|USERMSG_BLOCKHOOKS); + if (GetFeatureStatus(FeatureType_Native, "GetUserMessageType") == FeatureStatus_Available && GetUserMessageType() == UM_Protobuf) { + Protobuf pb = UserMessageToProtobuf(buf); + pb.SetInt("ent_idx", author); + pb.SetBool("chat", true); + pb.SetString("msg_name", buffer); + pb.AddString("params", ""); + pb.AddString("params", ""); + pb.AddString("params", ""); + pb.AddString("params", ""); + } + else { + BfWrite bf = UserMessageToBfWrite(buf); + bf.WriteByte(author); // Message author + bf.WriteByte(true); // Chat message + bf.WriteString(buffer); // Message text + } + + EndMessage(); +} + +/** + * This function should only be used right in front of + * MC_PrintToChatAll or MC_PrintToChatAllEx. It causes those functions + * to skip the specified client when printing the message. + * After printing the message, the client will no longer be skipped. + * + * @param client Client index + */ +stock void MC_SkipNextClient(int client) { + if (client <= 0 || client > MaxClients) { + ThrowError("Invalid client index %i", client); + } + + MC_SkipList[client] = true; +} + +/** + * Checks if the colors trie is initialized and initializes it if it's not (used internally) + * + * @return No return + */ +stock void MC_CheckTrie() { + if (MC_Trie == null) { + MC_Trie = MC_InitColorTrie(); + } +} + +/** + * Replaces color tags in a string with color codes (used internally by MC_PrintToChat, MC_PrintToChatAll, MC_PrintToChatEx, and MC_PrintToChatAllEx + * + * @param buffer String. + * @param author Optional client index to use for {teamcolor} tags, or 0 for none + * @param removeTags Optional boolean value to determine whether we're replacing tags with colors, or just removing tags, used by MC_RemoveTags + * @param maxlen Optional value for max buffer length, used by MC_RemoveTags + * + * On error/Errors: If the client index passed for author is invalid or not in game. + */ +stock void MC_ReplaceColorCodes(char[] buffer, int author = 0, bool removeTags = false, int maxlen = MAX_BUFFER_LENGTH) { + MC_CheckTrie(); + if (!removeTags) { + ReplaceString(buffer, maxlen, "{default}", "\x01", false); + } + else { + ReplaceString(buffer, maxlen, "{default}", "", false); + ReplaceString(buffer, maxlen, "{teamcolor}", "", false); + } + if (author != 0 && !removeTags) { + if (author < 0 || author > MaxClients) { + ThrowError("Invalid client index %i", author); + } + + if (!IsClientInGame(author)) { + ThrowError("Client %i is not in game", author); + } + + ReplaceString(buffer, maxlen, "{teamcolor}", "\x03", false); + } + + int cursor = 0; + int value; + char tag[32], buff[32]; + char[] output = new char[maxlen]; + + strcopy(output, maxlen, buffer); + // Since the string's size is going to be changing, output will hold the replaced string and we'll search buffer + + Regex regex = new Regex("{[#a-zA-Z0-9]+}"); + for(int i = 0; i < 1000; i++) { // The RegEx extension is quite flaky, so we have to loop here :/. This loop is supposed to be infinite and broken by return, but conditions have been added to be safe. + if (regex.Match(buffer[cursor]) < 1) { + delete regex; + strcopy(buffer, maxlen, output); + return; + } + regex.GetSubString(0, tag, sizeof(tag)); + MC_StrToLower(tag); + cursor = StrContains(buffer[cursor], tag, false) + cursor + 1; + strcopy(buff, sizeof(buff), tag); + ReplaceString(buff, sizeof(buff), "{", ""); + ReplaceString(buff, sizeof(buff), "}", ""); + + if (buff[0] == '#') { + if (strlen(buff) == 7) { + Format(buff, sizeof(buff), "\x07%s", buff[1]); + } + else if (strlen(buff) == 9) { + Format(buff, sizeof(buff), "\x08%s", buff[1]); + } + else { + continue; + } + + if (removeTags) { + ReplaceString(output, maxlen, tag, "", false); + } + else { + ReplaceString(output, maxlen, tag, buff, false); + } + } + else if (!MC_Trie.GetValue(buff, value)) { + continue; + } + + if (removeTags) { + ReplaceString(output, maxlen, tag, "", false); + } + else { + Format(buff, sizeof(buff), "\x07%06X", value); + ReplaceString(output, maxlen, tag, buff, false); + } + } + LogError("[MORE COLORS] Infinite loop broken."); +} + +/** + * Gets a part of a string + * + * @param input String to get the part from + * @param output Buffer to write to + * @param maxlen Max length of output buffer + * @param start Position to start at + * @param numChars Number of characters to return, or 0 for the end of the string + */ +stock void CSubString(const char[] input, char[] output, int maxlen, int start, int numChars = 0) { + int i = 0; + for(;;) { + if (i == maxlen - 1 || i >= numChars || input[start + i] == '\0') { + output[i] = '\0'; + return; + } + output[i] = input[start + i]; + i++; + } +} + +/** + * Converts a string to lowercase + * + * @param buffer String to convert + */ +stock void MC_StrToLower(char[] buffer) { + int len = strlen(buffer); + for(int i = 0; i < len; i++) { + buffer[i] = CharToLower(buffer[i]); + } +} + +/** + * Adds a color to the colors trie + * + * @param name Color name, without braces + * @param color Hexadecimal representation of the color (0xRRGGBB) + * @return True if color was added successfully, false if a color already exists with that name + */ +stock bool MC_AddColor(const char[] name, int color) { + MC_CheckTrie(); + + int value; + + if (MC_Trie.GetValue(name, value)) { + return false; + } + + char newName[64]; + strcopy(newName, sizeof(newName), name); + + MC_StrToLower(newName); + MC_Trie.SetValue(newName, color); + return true; +} + +/** + * Removes color tags from a message + * + * @param message Message to remove tags from + * @param maxlen Maximum buffer length + */ +stock void MC_RemoveTags(char[] message, int maxlen) { + MC_ReplaceColorCodes(message, 0, true, maxlen); +} + +/** + * Replies to a command with colors + * + * @param client Client to reply to + * @param message Message (formatting rules) + */ +stock void MC_ReplyToCommand(int client, const char[] message, any ...) { + char buffer[MAX_BUFFER_LENGTH]; + SetGlobalTransTarget(client); + VFormat(buffer, sizeof(buffer), message, 3); + + if (client == 0) { + MC_RemoveTags(buffer, sizeof(buffer)); + PrintToServer("%s", buffer); + } + + if (GetCmdReplySource() == SM_REPLY_TO_CONSOLE) { + MC_RemoveTags(buffer, sizeof(buffer)); + PrintToConsole(client, "%s", buffer); + } + else { + MC_PrintToChat(client, "%s", buffer); + } +} + +/** + * Replies to a command with colors + * + * @param client Client to reply to + * @param author Client to use for {teamcolor} + * @param message Message (formatting rules) + */ +stock void MC_ReplyToCommandEx(int client, int author, const char[] message, any ...) { + char buffer[MAX_BUFFER_LENGTH]; + SetGlobalTransTarget(client); + VFormat(buffer, sizeof(buffer), message, 4); + + if (client == 0) { + MC_RemoveTags(buffer, sizeof(buffer)); + PrintToServer("%s", buffer); + } + + if (GetCmdReplySource() == SM_REPLY_TO_CONSOLE) { + MC_RemoveTags(buffer, sizeof(buffer)); + PrintToConsole(client, "%s", buffer); + } + else { + MC_PrintToChatEx(client, author, "%s", buffer); + } +} + +/** + * Displays usage of an admin command to users depending on the + * setting of the sm_show_activity cvar. + * + * This version does not display a message to the originating client + * if used from chat triggers or menus. If manual replies are used + * for these cases, then this function will suffice. Otherwise, + * MC_ShowActivity2() is slightly more useful. + * Supports color tags. + * + * @param client Client index doing the action, or 0 for server. + * @param format Formatting rules. + * @param ... Variable number of format parameters. + * @error + */ +stock int MC_ShowActivity(int client, const char[] format, any ...) { + if (sm_show_activity == null) { + sm_show_activity = FindConVar("sm_show_activity"); + } + + char tag[] = "[SM] "; + + char szBuffer[MC_MAX_MESSAGE_LENGTH]; + //char szCMessage[MC_MAX_MESSAGE_LENGTH]; + int value = sm_show_activity.IntValue; + ReplySource replyto = GetCmdReplySource(); + + char name[MAX_NAME_LENGTH] = "Console"; + char sign[MAX_NAME_LENGTH] = "ADMIN"; + bool display_in_chat = false; + if (client != 0) { + if (client < 0 || client > MaxClients || !IsClientConnected(client)) + ThrowError("Client index %d is invalid", client); + + GetClientName(client, name, sizeof(name)); + AdminId id = GetUserAdmin(client); + if (id == INVALID_ADMIN_ID + || !id.HasFlag(Admin_Generic, Access_Effective)) + { + sign = "PLAYER"; + } + + /* Display the message to the client? */ + if (replyto == SM_REPLY_TO_CONSOLE) + { + SetGlobalTransTarget(client); + VFormat(szBuffer, sizeof(szBuffer), format, 3); + + MC_RemoveTags(szBuffer, sizeof(szBuffer)); + PrintToConsole(client, "%s%s\n", tag, szBuffer); + display_in_chat = true; + } + } + else { + SetGlobalTransTarget(LANG_SERVER); + VFormat(szBuffer, sizeof(szBuffer), format, 3); + + MC_RemoveTags(szBuffer, sizeof(szBuffer)); + PrintToServer("%s%s\n", tag, szBuffer); + } + + if (!value) { + return 1; + } + + for (int i = 1; i <= MaxClients; ++i) { + if (!IsClientInGame(i) + || IsFakeClient(i) + || (display_in_chat && i == client)) { + continue; + } + + AdminId id = GetUserAdmin(i); + SetGlobalTransTarget(i); + if (id == INVALID_ADMIN_ID + || !id.HasFlag(Admin_Generic, Access_Effective)) { + /* Treat this as a normal user. */ + if ((value & 1) | (value & 2)) { + char newsign[MAX_NAME_LENGTH]; + newsign = sign; + + if ((value & 2) || (i == client)) { + newsign = name; + } + + VFormat(szBuffer, sizeof(szBuffer), format, 3); + + MC_PrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + else { + /* Treat this as an admin user */ + bool is_root = id.HasFlag(Admin_Root, Access_Effective); + if ((value & 4) + || (value & 8) + || ((value & 16) && is_root)) { + char newsign[MAX_NAME_LENGTH]; + newsign = sign; + + if ((value & 8) || ((value & 16) && is_root) || (i == client)) { + newsign = name; + } + + VFormat(szBuffer, sizeof(szBuffer), format, 3); + + MC_PrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + } + + return 1; +} + +/** + * Same as MC_ShowActivity(), except the tag parameter is used instead of "[SM] " (note that you must supply any spacing). + * Supports color tags. + * + * @param client Client index doing the action, or 0 for server. + * @param tags Tag to display with. + * @param format Formatting rules. + * @param ... Variable number of format parameters. + * @error + */ +stock int MC_ShowActivityEx(int client, const char[] tag, const char[] format, any ...) { + if (sm_show_activity == null) + sm_show_activity = FindConVar("sm_show_activity"); + + char szBuffer[MC_MAX_MESSAGE_LENGTH]; + //char szCMessage[MC_MAX_MESSAGE_LENGTH]; + int value = sm_show_activity.IntValue; + ReplySource replyto = GetCmdReplySource(); + + char name[MAX_NAME_LENGTH] = "Console"; + char sign[MAX_NAME_LENGTH] = "ADMIN"; + bool display_in_chat = false; + if (client != 0) { + if (client < 0 || client > MaxClients || !IsClientConnected(client)) { + ThrowError("Client index %d is invalid", client); + } + + GetClientName(client, name, sizeof(name)); + AdminId id = GetUserAdmin(client); + if (id == INVALID_ADMIN_ID + || !id.HasFlag(Admin_Generic, Access_Effective)) { + sign = "PLAYER"; + } + + /* Display the message to the client? */ + if (replyto == SM_REPLY_TO_CONSOLE) { + SetGlobalTransTarget(client); + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + MC_RemoveTags(szBuffer, sizeof(szBuffer)); + PrintToConsole(client, "%s%s\n", tag, szBuffer); + display_in_chat = true; + } + } + else { + SetGlobalTransTarget(LANG_SERVER); + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + MC_RemoveTags(szBuffer, sizeof(szBuffer)); + PrintToServer("%s%s\n", tag, szBuffer); + } + + if (!value) { + return 1; + } + + for (int i = 1; i <= MaxClients; ++i) { + if (!IsClientInGame(i) + || IsFakeClient(i) + || (display_in_chat && i == client)) { + continue; + } + + AdminId id = GetUserAdmin(i); + SetGlobalTransTarget(i); + if (id == INVALID_ADMIN_ID + || !id.HasFlag(Admin_Generic, Access_Effective)) { + /* Treat this as a normal user. */ + if ((value & 1) | (value & 2)) { + char newsign[MAX_NAME_LENGTH]; + newsign = sign; + + if ((value & 2) || (i == client)) { + newsign = name; + } + + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + MC_PrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + else { + /* Treat this as an admin user */ + bool is_root = id.HasFlag(Admin_Root, Access_Effective); + if ((value & 4) + || (value & 8) + || ((value & 16) && is_root)) { + char newsign[MAX_NAME_LENGTH]; + newsign = sign; + + if ((value & 8) || ((value & 16) && is_root) || (i == client)) { + newsign = name; + } + + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + MC_PrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + } + + return 1; +} + +/** + * Displays usage of an admin command to users depending on the setting of the sm_show_activity cvar. + * All users receive a message in their chat text, except for the originating client, + * who receives the message based on the current ReplySource. + * Supports color tags. + * + * @param client Client index doing the action, or 0 for server. + * @param tags Tag to prepend to the message. + * @param format Formatting rules. + * @param ... Variable number of format parameters. + * @error + */ +stock int MC_ShowActivity2(int client, const char[] tag, const char[] format, any ...) { + if (sm_show_activity == null) { + sm_show_activity = FindConVar("sm_show_activity"); + } + + char szBuffer[MC_MAX_MESSAGE_LENGTH]; + //char szCMessage[MC_MAX_MESSAGE_LENGTH]; + int value = sm_show_activity.IntValue; + // ReplySource replyto = GetCmdReplySource(); + + char name[MAX_NAME_LENGTH] = "Console"; + char sign[MAX_NAME_LENGTH] = "ADMIN"; + + if (client != 0) { + if (client < 0 || client > MaxClients || !IsClientConnected(client)) { + ThrowError("Client index %d is invalid", client); + } + + GetClientName(client, name, sizeof(name)); + + AdminId id = GetUserAdmin(client); + if (id == INVALID_ADMIN_ID + || !id.HasFlag(Admin_Generic, Access_Effective)) { + sign = "PLAYER"; + } + + SetGlobalTransTarget(client); + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + /* We don't display directly to the console because the chat text + * simply gets added to the console, so we don't want it to print + * twice. + */ + MC_PrintToChatEx(client, client, "%s%s", tag, szBuffer); + } + else { + SetGlobalTransTarget(LANG_SERVER); + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + MC_RemoveTags(szBuffer, sizeof(szBuffer)); + PrintToServer("%s%s\n", tag, szBuffer); + } + + if (!value) { + return 1; + } + + for (int i = 1; i <= MaxClients; ++i) { + if (!IsClientInGame(i) + || IsFakeClient(i) + || i == client) { + continue; + } + + AdminId id = GetUserAdmin(i); + SetGlobalTransTarget(i); + if (id == INVALID_ADMIN_ID + || !id.HasFlag(Admin_Generic, Access_Effective)) { + /* Treat this as a normal user. */ + if ((value & 1) | (value & 2)) { + char newsign[MAX_NAME_LENGTH]; + newsign = sign; + + if ((value & 2)) { + newsign = name; + } + + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + MC_PrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + else { + /* Treat this as an admin user */ + bool is_root = id.HasFlag(Admin_Root, Access_Effective); + if ((value & 4) + || (value & 8) + || ((value & 16) && is_root)) { + char newsign[MAX_NAME_LENGTH]; + newsign = sign; + + if ((value & 8) || ((value & 16) && is_root)) { + newsign = name; + } + + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + MC_PrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + } + + return 1; +} + +/** + * Determines whether a color name exists + * + * @param color The color name to check + * @return True if the color exists, false otherwise + */ +stock bool CColorExists(const char[] color) { + MC_CheckTrie(); + int temp; + return MC_Trie.GetValue(color, temp); +} + +/** + * Returns the hexadecimal representation of a client's team color (will NOT initialize the trie) + * + * @param client Client to get the team color for + * @return Client's team color in hexadecimal, or green if unknown + * On error/Errors: If the client index passed is invalid or not in game. + */ +stock int CGetTeamColor(int client) { + if (client <= 0 || client > MaxClients) { + ThrowError("Invalid client index %i", client); + } + + if (!IsClientInGame(client)) { + ThrowError("Client %i is not in game", client); + } + + int value; + switch(GetClientTeam(client)) { + case 1: { + value = MCOLOR_GRAY; + } + case 2: { + value = MCOLOR_RED; + } + case 3: { + value = MCOLOR_BLUE; + } + default: { + value = MCOLOR_GREEN; + } + } + + return value; +} + +stock StringMap MC_InitColorTrie() { + StringMap hTrie = new StringMap(); + hTrie.SetValue("aliceblue", 0xF0F8FF); + hTrie.SetValue("allies", 0x4D7942); // same as Allies team in DoD:S + hTrie.SetValue("ancient", 0xEB4B4B); // same as Ancient item rarity in Dota 2 + hTrie.SetValue("antiquewhite", 0xFAEBD7); + hTrie.SetValue("aqua", 0x00FFFF); + hTrie.SetValue("aquamarine", 0x7FFFD4); + hTrie.SetValue("arcana", 0xADE55C); // same as Arcana item rarity in Dota 2 + hTrie.SetValue("axis", 0xFF4040); // same as Axis team in DoD:S + hTrie.SetValue("azure", 0x007FFF); + hTrie.SetValue("beige", 0xF5F5DC); + hTrie.SetValue("bisque", 0xFFE4C4); + hTrie.SetValue("black", 0x000000); + hTrie.SetValue("blanchedalmond", 0xFFEBCD); + hTrie.SetValue("blue", 0x99CCFF); // same as BLU/Counter-Terrorist team color + hTrie.SetValue("blueviolet", 0x8A2BE2); + hTrie.SetValue("brown", 0xA52A2A); + hTrie.SetValue("burlywood", 0xDEB887); + hTrie.SetValue("cadetblue", 0x5F9EA0); + hTrie.SetValue("chartreuse", 0x7FFF00); + hTrie.SetValue("chocolate", 0xD2691E); + hTrie.SetValue("collectors", 0xAA0000); // same as Collector's item quality in TF2 + hTrie.SetValue("common", 0xB0C3D9); // same as Common item rarity in Dota 2 + hTrie.SetValue("community", 0x70B04A); // same as Community item quality in TF2 + hTrie.SetValue("coral", 0xFF7F50); + hTrie.SetValue("cornflowerblue", 0x6495ED); + hTrie.SetValue("cornsilk", 0xFFF8DC); + hTrie.SetValue("corrupted", 0xA32C2E); // same as Corrupted item quality in Dota 2 + hTrie.SetValue("crimson", 0xDC143C); + hTrie.SetValue("cyan", 0x00FFFF); + hTrie.SetValue("darkblue", 0x00008B); + hTrie.SetValue("darkcyan", 0x008B8B); + hTrie.SetValue("darkgoldenrod", 0xB8860B); + hTrie.SetValue("darkgray", 0xA9A9A9); + hTrie.SetValue("darkgrey", 0xA9A9A9); + hTrie.SetValue("darkgreen", 0x006400); + hTrie.SetValue("darkkhaki", 0xBDB76B); + hTrie.SetValue("darkmagenta", 0x8B008B); + hTrie.SetValue("darkolivegreen", 0x556B2F); + hTrie.SetValue("darkorange", 0xFF8C00); + hTrie.SetValue("darkorchid", 0x9932CC); + hTrie.SetValue("darkred", 0x8B0000); + hTrie.SetValue("darksalmon", 0xE9967A); + hTrie.SetValue("darkseagreen", 0x8FBC8F); + hTrie.SetValue("darkslateblue", 0x483D8B); + hTrie.SetValue("darkslategray", 0x2F4F4F); + hTrie.SetValue("darkslategrey", 0x2F4F4F); + hTrie.SetValue("darkturquoise", 0x00CED1); + hTrie.SetValue("darkviolet", 0x9400D3); + hTrie.SetValue("deeppink", 0xFF1493); + hTrie.SetValue("deepskyblue", 0x00BFFF); + hTrie.SetValue("dimgray", 0x696969); + hTrie.SetValue("dimgrey", 0x696969); + hTrie.SetValue("dodgerblue", 0x1E90FF); + hTrie.SetValue("exalted", 0xCCCCCD); // same as Exalted item quality in Dota 2 + hTrie.SetValue("firebrick", 0xB22222); + hTrie.SetValue("floralwhite", 0xFFFAF0); + hTrie.SetValue("forestgreen", 0x228B22); + hTrie.SetValue("frozen", 0x4983B3); // same as Frozen item quality in Dota 2 + hTrie.SetValue("fuchsia", 0xFF00FF); + hTrie.SetValue("fullblue", 0x0000FF); + hTrie.SetValue("fullred", 0xFF0000); + hTrie.SetValue("gainsboro", 0xDCDCDC); + hTrie.SetValue("genuine", 0x4D7455); // same as Genuine item quality in TF2 + hTrie.SetValue("ghostwhite", 0xF8F8FF); + hTrie.SetValue("gold", 0xFFD700); + hTrie.SetValue("goldenrod", 0xDAA520); + hTrie.SetValue("gray", 0xCCCCCC); // same as spectator team color + hTrie.SetValue("grey", 0xCCCCCC); + hTrie.SetValue("green", 0x3EFF3E); + hTrie.SetValue("greenyellow", 0xADFF2F); + hTrie.SetValue("haunted", 0x38F3AB); // same as Haunted item quality in TF2 + hTrie.SetValue("honeydew", 0xF0FFF0); + hTrie.SetValue("hotpink", 0xFF69B4); + hTrie.SetValue("immortal", 0xE4AE33); // same as Immortal item rarity in Dota 2 + hTrie.SetValue("indianred", 0xCD5C5C); + hTrie.SetValue("indigo", 0x4B0082); + hTrie.SetValue("ivory", 0xFFFFF0); + hTrie.SetValue("khaki", 0xF0E68C); + hTrie.SetValue("lavender", 0xE6E6FA); + hTrie.SetValue("lavenderblush", 0xFFF0F5); + hTrie.SetValue("lawngreen", 0x7CFC00); + hTrie.SetValue("legendary", 0xD32CE6); // same as Legendary item rarity in Dota 2 + hTrie.SetValue("lemonchiffon", 0xFFFACD); + hTrie.SetValue("lightblue", 0xADD8E6); + hTrie.SetValue("lightcoral", 0xF08080); + hTrie.SetValue("lightcyan", 0xE0FFFF); + hTrie.SetValue("lightgoldenrodyellow", 0xFAFAD2); + hTrie.SetValue("lightgray", 0xD3D3D3); + hTrie.SetValue("lightgrey", 0xD3D3D3); + hTrie.SetValue("lightgreen", 0x99FF99); + hTrie.SetValue("lightpink", 0xFFB6C1); + hTrie.SetValue("lightsalmon", 0xFFA07A); + hTrie.SetValue("lightseagreen", 0x20B2AA); + hTrie.SetValue("lightskyblue", 0x87CEFA); + hTrie.SetValue("lightslategray", 0x778899); + hTrie.SetValue("lightslategrey", 0x778899); + hTrie.SetValue("lightsteelblue", 0xB0C4DE); + hTrie.SetValue("lightyellow", 0xFFFFE0); + hTrie.SetValue("lime", 0x00FF00); + hTrie.SetValue("limegreen", 0x32CD32); + hTrie.SetValue("linen", 0xFAF0E6); + hTrie.SetValue("magenta", 0xFF00FF); + hTrie.SetValue("maroon", 0x800000); + hTrie.SetValue("mediumaquamarine", 0x66CDAA); + hTrie.SetValue("mediumblue", 0x0000CD); + hTrie.SetValue("mediumorchid", 0xBA55D3); + hTrie.SetValue("mediumpurple", 0x9370D8); + hTrie.SetValue("mediumseagreen", 0x3CB371); + hTrie.SetValue("mediumslateblue", 0x7B68EE); + hTrie.SetValue("mediumspringgreen", 0x00FA9A); + hTrie.SetValue("mediumturquoise", 0x48D1CC); + hTrie.SetValue("mediumvioletred", 0xC71585); + hTrie.SetValue("midnightblue", 0x191970); + hTrie.SetValue("mintcream", 0xF5FFFA); + hTrie.SetValue("mistyrose", 0xFFE4E1); + hTrie.SetValue("moccasin", 0xFFE4B5); + hTrie.SetValue("mythical", 0x8847FF); // same as Mythical item rarity in Dota 2 + hTrie.SetValue("navajowhite", 0xFFDEAD); + hTrie.SetValue("navy", 0x000080); + hTrie.SetValue("normal", 0xB2B2B2); // same as Normal item quality in TF2 + hTrie.SetValue("oldlace", 0xFDF5E6); + hTrie.SetValue("olive", 0x9EC34F); + hTrie.SetValue("olivedrab", 0x6B8E23); + hTrie.SetValue("orange", 0xFFA500); + hTrie.SetValue("orangered", 0xFF4500); + hTrie.SetValue("orchid", 0xDA70D6); + hTrie.SetValue("palegoldenrod", 0xEEE8AA); + hTrie.SetValue("palegreen", 0x98FB98); + hTrie.SetValue("paleturquoise", 0xAFEEEE); + hTrie.SetValue("palevioletred", 0xD87093); + hTrie.SetValue("papayawhip", 0xFFEFD5); + hTrie.SetValue("peachpuff", 0xFFDAB9); + hTrie.SetValue("peru", 0xCD853F); + hTrie.SetValue("pink", 0xFFC0CB); + hTrie.SetValue("plum", 0xDDA0DD); + hTrie.SetValue("powderblue", 0xB0E0E6); + hTrie.SetValue("purple", 0x800080); + hTrie.SetValue("rare", 0x4B69FF); // same as Rare item rarity in Dota 2 + hTrie.SetValue("red", 0xFF4040); // same as RED/Terrorist team color + hTrie.SetValue("rosybrown", 0xBC8F8F); + hTrie.SetValue("royalblue", 0x4169E1); + hTrie.SetValue("saddlebrown", 0x8B4513); + hTrie.SetValue("salmon", 0xFA8072); + hTrie.SetValue("sandybrown", 0xF4A460); + hTrie.SetValue("seagreen", 0x2E8B57); + hTrie.SetValue("seashell", 0xFFF5EE); + hTrie.SetValue("selfmade", 0x70B04A); // same as Self-Made item quality in TF2 + hTrie.SetValue("sienna", 0xA0522D); + hTrie.SetValue("silver", 0xC0C0C0); + hTrie.SetValue("skyblue", 0x87CEEB); + hTrie.SetValue("slateblue", 0x6A5ACD); + hTrie.SetValue("slategray", 0x708090); + hTrie.SetValue("slategrey", 0x708090); + hTrie.SetValue("snow", 0xFFFAFA); + hTrie.SetValue("springgreen", 0x00FF7F); + hTrie.SetValue("steelblue", 0x4682B4); + hTrie.SetValue("strange", 0xCF6A32); // same as Strange item quality in TF2 + hTrie.SetValue("tan", 0xD2B48C); + hTrie.SetValue("teal", 0x008080); + hTrie.SetValue("thistle", 0xD8BFD8); + hTrie.SetValue("tomato", 0xFF6347); + hTrie.SetValue("turquoise", 0x40E0D0); + hTrie.SetValue("uncommon", 0xB0C3D9); // same as Uncommon item rarity in Dota 2 + hTrie.SetValue("unique", 0xFFD700); // same as Unique item quality in TF2 + hTrie.SetValue("unusual", 0x8650AC); // same as Unusual item quality in TF2 + hTrie.SetValue("valve", 0xA50F79); // same as Valve item quality in TF2 + hTrie.SetValue("vintage", 0x476291); // same as Vintage item quality in TF2 + hTrie.SetValue("violet", 0xEE82EE); + hTrie.SetValue("wheat", 0xF5DEB3); + hTrie.SetValue("white", 0xFFFFFF); + hTrie.SetValue("whitesmoke", 0xF5F5F5); + hTrie.SetValue("yellow", 0xFFFF00); + hTrie.SetValue("yellowgreen", 0x9ACD32); + + return hTrie; +} diff --git a/libalternator/tests/corpus/includelibrary/include/steamtools.inc b/libalternator/tests/corpus/includelibrary/include/steamtools.inc new file mode 100644 index 0000000..a3f243b --- /dev/null +++ b/libalternator/tests/corpus/includelibrary/include/steamtools.inc @@ -0,0 +1,352 @@ +#if defined _steamtools_included +#endinput +#endif +#define _steamtools_included + +#define USE_CUSTOM_STEAMID -1 + +/** + * Called after SteamTools has completely finished loading. + * No features are available before this point. + * + * @noreturn + */ +forward void Steam_FullyLoaded(); +forward void Steam_Shutdown(); + + +/** + * Gets the current status of VAC on the server. + * + * @return A bool representing the current VAC status. + */ +native bool Steam_IsVACEnabled(); + +/** + * Gets the server's external IP address, as reported by Steam. + * + * @param octets Reference to an array to be filled with the octets of + * the IP address. + * + * @noreturn + */ +native void Steam_GetPublicIP(int octets[4]); + + +/** + * Is fired when the Steam master servers report that your server is + * outdated + * + * @return Plugin_Continue to continue normal operation or Plugin_Handled + * to block the regular console message. + */ +forward Action Steam_RestartRequested(); + + +/** + * Requests a client's status in a Steam group. + * Response is returned in Steam_GroupStatusResult forward. + * + * @param client Client index. + * @param groupAccountID 32-bit account ID of group. + * + * @return A bool representing whether or not the request was sent to + * Steam. + */ +native bool Steam_RequestGroupStatus(int client, int groupAccountID); + +/** + * Called when a response to a group status request is recieved. + * This is called for all responses recieved, not just ones requested by + * your plugin. + * + * @param client Client index. + * @param groupAccountID 32-bit account ID of group. Make sure to check + * this agaist the ID you are expecting. + * @param groupMember Whether or not the client is a member in the + * specified group. + * @param groupMember Whether or not the client is an officer in the + * specified group. + * + * @noreturn + */ +forward void Steam_GroupStatusResult(int client, int groupAccountID, bool groupMember, bool groupOfficer); + +#pragma deprecated No longer operational +native void Steam_RequestGameplayStats(); +forward void Steam_GameplayStats(int rank, int totalConnects, int totalMinutesPlayed); + +#pragma deprecated No longer operational +native void Steam_RequestServerReputation(); +forward void Steam_Reputation(int reputationScore, bool banned, int bannedIP, int bannedPort, int bannedGameID, int banExpires); + + +/** + * Gets the current Steam connection state, the forwards below fire + * whenever this changes. + * + * @return Steam connection state. + */ +native bool Steam_IsConnected(); + +/** + * Fired upon a successfull connection to Steam. + * Is also fired for late-loaded plugins. + * + * @noreturn + */ +forward void Steam_SteamServersConnected(); + +/** + * Fired upon disconnection from Steam. + * Is also fired for late-loaded plugins. + * + * For plugins loaded with the server, this will normally be fired right + * after Steam_FullyLoaded, closly followed by Steam_SteamServersConnected + * if a successfull connection is established. + * + * @noreturn + */ +forward void Steam_SteamServersDisconnected(); + +/** + * Sets an entry in the server's list of rules. This list is used to + * build the response to the A2S_RULES query and is generally known as + * the list of public convars. + * + * @param key Name of the key to set, is created if it does not already + * exist. + * @param value Value of the key to set, the named key is removed if this + * is blank. + * + * @noreturn + */ +native void Steam_SetRule(const char[] key, const char[] value); + +/** + * Clears the server's list of rules. This list is used to build the + * response to the A2S_RULES query and is generally known as the list of + * public convars. + * + * @noreturn + */ +native void Steam_ClearRules(); + + +native void Steam_ForceHeartbeat(); + +#pragma deprecated No longer operational +native bool Steam_AddMasterServer(const char[] serverAddress); +#pragma deprecated No longer operational +native bool Steam_RemoveMasterServer(const char[] serverAddress); +#pragma deprecated No longer operational +native int Steam_GetNumMasterServers(); +#pragma deprecated No longer operational +native void Steam_GetMasterServerAddress(int server, char[] serverAddress, int maxlength); + +native void Steam_SetGameDescription(char[] gameDescription); + +native void Steam_RequestStats(int client); +forward void Steam_StatsReceived(int client); +forward void Steam_StatsUnloaded(int client); +native int Steam_GetStat(int client, const char[] statName); +native float Steam_GetStatFloat(int client, const char[] statName); +native bool Steam_IsAchieved(int client, const char[] achievementName); + + +native int Steam_GetNumClientSubscriptions(int client); +native int Steam_GetClientSubscription(int client, int index); + +native int Steam_GetNumClientDLCs(int client); +native int Steam_GetClientDLC(int client, int index); + +stock bool Steam_CheckClientSubscription(int client, int subid) +{ + int subCount = Steam_GetNumClientSubscriptions(client); + for (int x = 0; x < subCount; x++) + { + if (Steam_GetClientSubscription(client, x) == subid) + { + return true; + } + } + + return false; +} + +stock bool Steam_CheckClientDLC(int client, int appid) +{ + int subCount = Steam_GetNumClientDLCs(client); + for (int x = 0; x < subCount; x++) + { + if (Steam_GetClientDLC(client, x) == appid) + { + return true; + } + } + + return false; +} + +native void Steam_GetCSteamIDForClient(int client, char[] steamID, int maxlength); + +native bool Steam_SetCustomSteamID(const char[] renderedID); +native bool Steam_GetCustomSteamID(char[] renderedID, int maxlength); + +native void Steam_RenderedIDToCSteamID(const char[] renderedID, char[] steamID, int maxlength); +native void Steam_CSteamIDToRenderedID(const char[] steamID, char[] renderedID, int maxlength); + +native void Steam_GroupIDToCSteamID(int groupID, char[] steamID, int maxlength); +native void Steam_CSteamIDToGroupID(const char[] steamID); + +enum HTTPRequestHandle +{ + INVALID_HTTP_HANDLE = 0, +}; + +enum HTTPMethod +{ + HTTPMethod_Invalid = 0, + HTTPMethod_GET, + HTTPMethod_HEAD, + HTTPMethod_POST, +}; + +enum HTTPStatusCode +{ + HTTPStatusCode_Invalid = 0, + + // Informational codes + HTTPStatusCode_Continue = 100, + HTTPStatusCode_SwitchingProtocols = 101, + + // Success codes + HTTPStatusCode_OK = 200, + HTTPStatusCode_Created = 201, + HTTPStatusCode_Accepted = 202, + HTTPStatusCode_NonAuthoritative = 203, + HTTPStatusCode_NoContent = 204, + HTTPStatusCode_ResetContent = 205, + HTTPStatusCode_PartialContent = 206, + + // Redirection codes + HTTPStatusCode_MultipleChoices = 300, + HTTPStatusCode_MovedPermanently = 301, + HTTPStatusCode_Found = 302, + HTTPStatusCode_SeeOther = 303, + HTTPStatusCode_NotModified = 304, + HTTPStatusCode_UseProxy = 305, + HTTPStatusCode_TemporaryRedirect = 307, + + // Error codes + HTTPStatusCode_BadRequest = 400, + HTTPStatusCode_Unauthorized = 401, + HTTPStatusCode_PaymentRequired = 402, + HTTPStatusCode_Forbidden = 403, + HTTPStatusCode_NotFound = 404, + HTTPStatusCode_MethodNotAllowed = 405, + HTTPStatusCode_NotAcceptable = 406, + HTTPStatusCode_ProxyAuthRequired = 407, + HTTPStatusCode_RequestTimeout = 408, + HTTPStatusCode_Conflict = 409, + HTTPStatusCode_Gone = 410, + HTTPStatusCode_LengthRequired = 411, + HTTPStatusCode_PreconditionFailed = 412, + HTTPStatusCode_RequestEntityTooLarge = 413, + HTTPStatusCode_RequestURITooLong = 414, + HTTPStatusCode_UnsupportedMediaType = 415, + HTTPStatusCode_RequestedRangeNotSatisfiable = 416, + HTTPStatusCode_ExpectationFailed = 417, + + // Server error codes + HTTPStatusCode_InternalServerError = 500, + HTTPStatusCode_NotImplemented = 501, + HTTPStatusCode_BadGateway = 502, + HTTPStatusCode_ServiceUnavailable = 503, + HTTPStatusCode_GatewayTimeout = 504, + HTTPStatusCode_HTTPVersionNotSupported = 505, +}; + +typeset HTTPRequestComplete +{ + function void(HTTPRequestHandle HTTPRequest, bool requestSuccessful, HTTPStatusCode statusCode); + function void(HTTPRequestHandle HTTPRequest, bool requestSuccessful, HTTPStatusCode statusCode, any contextData); +}; + +native HTTPRequestHandle Steam_CreateHTTPRequest(HTTPMethod HTTPRequestMethod, const char[] absoluteURL); +native void Steam_SetHTTPRequestNetworkActivityTimeout(HTTPRequestHandle HTTPRequest, int timeoutSeconds); +native void Steam_SetHTTPRequestHeaderValue(HTTPRequestHandle HTTPRequest, const char[] headerName, const char[] headerValue); +native void Steam_SetHTTPRequestGetOrPostParameter(HTTPRequestHandle HTTPRequest, const char[] paramName, const char[] paramValue); +native bool Steam_SendHTTPRequest(HTTPRequestHandle HTTPRequest, HTTPRequestComplete callbackFunction, any contextData = 0); +native void Steam_DeferHTTPRequest(HTTPRequestHandle HTTPRequest); +native void Steam_PrioritizeHTTPRequest(HTTPRequestHandle HTTPRequest); +native int Steam_GetHTTPResponseHeaderSize(HTTPRequestHandle HTTPRequest, const char[] headerName); +native void Steam_GetHTTPResponseHeaderValue(HTTPRequestHandle HTTPRequest, const char[] headerName, char[] headerValueBuffer, int bufferSize); +native int Steam_GetHTTPResponseBodySize(HTTPRequestHandle HTTPRequest); +native void Steam_GetHTTPResponseBodyData(HTTPRequestHandle HTTPRequest, char[] bodyDataBuffer, int bufferSize); +native void Steam_WriteHTTPResponseBody(HTTPRequestHandle HTTPRequest, const char[] filePath); +native void Steam_ReleaseHTTPRequest(HTTPRequestHandle HTTPRequest); +native float Steam_GetHTTPDownloadProgressPercent(HTTPRequestHandle HTTPRequest); +native bool Steam_SetHTTPRequestRawPostBody(HTTPRequestHandle HTTPRequest, const char[] data, int dataLength, const char[] contentType="text/plain"); +native bool Steam_SetHTTPRequestRawPostBodyFile(HTTPRequestHandle HTTPRequest, const char[] filePath, const char[] contentType="text/plain"); + +public Extension __ext_SteamTools = +{ + name = "SteamTools", + file = "steamtools.ext", +#if defined AUTOLOAD_EXTENSIONS + autoload = 1, +#else + autoload = 0, +#endif +#if defined REQUIRE_EXTENSIONS + required = 1, +#else + required = 0, +#endif +} + +#if !defined REQUIRE_EXTENSIONS +public void __ext_SteamTools_SetNTVOptional() +{ + MarkNativeAsOptional("Steam_IsVACEnabled"); + MarkNativeAsOptional("Steam_GetPublicIP"); + MarkNativeAsOptional("Steam_RequestGroupStatus"); + MarkNativeAsOptional("Steam_IsConnected"); + MarkNativeAsOptional("Steam_SetRule"); + MarkNativeAsOptional("Steam_ClearRules"); + MarkNativeAsOptional("Steam_ForceHeartbeat"); + MarkNativeAsOptional("Steam_SetGameDescription"); + MarkNativeAsOptional("Steam_RequestStats"); + MarkNativeAsOptional("Steam_GetStat"); + MarkNativeAsOptional("Steam_GetStatFloat"); + MarkNativeAsOptional("Steam_IsAchieved"); + MarkNativeAsOptional("Steam_GetNumClientSubscriptions"); + MarkNativeAsOptional("Steam_GetClientSubscription"); + MarkNativeAsOptional("Steam_GetNumClientDLCs"); + MarkNativeAsOptional("Steam_GetClientDLC"); + MarkNativeAsOptional("Steam_GetCSteamIDForClient"); + MarkNativeAsOptional("Steam_SetCustomSteamID"); + MarkNativeAsOptional("Steam_GetCustomSteamID"); + MarkNativeAsOptional("Steam_RenderedIDToCSteamID"); + MarkNativeAsOptional("Steam_CSteamIDToRenderedID"); + MarkNativeAsOptional("Steam_GroupIDToCSteamID"); + MarkNativeAsOptional("Steam_CSteamIDToGroupID"); + MarkNativeAsOptional("Steam_CreateHTTPRequest"); + MarkNativeAsOptional("Steam_SetHTTPRequestNetworkActivityTimeout"); + MarkNativeAsOptional("Steam_SetHTTPRequestHeaderValue"); + MarkNativeAsOptional("Steam_SetHTTPRequestGetOrPostParameter"); + MarkNativeAsOptional("Steam_SendHTTPRequest"); + MarkNativeAsOptional("Steam_DeferHTTPRequest"); + MarkNativeAsOptional("Steam_PrioritizeHTTPRequest"); + MarkNativeAsOptional("Steam_GetHTTPResponseHeaderSize"); + MarkNativeAsOptional("Steam_GetHTTPResponseHeaderValue"); + MarkNativeAsOptional("Steam_GetHTTPResponseBodySize"); + MarkNativeAsOptional("Steam_GetHTTPResponseBodyData"); + MarkNativeAsOptional("Steam_WriteHTTPResponseBody"); + MarkNativeAsOptional("Steam_ReleaseHTTPRequest"); + MarkNativeAsOptional("Steam_GetHTTPDownloadProgressPercent"); + MarkNativeAsOptional("Steam_SetHTTPRequestRawPostBody"); + MarkNativeAsOptional("Steam_SetHTTPRequestRawPostBodyFile"); +} +#endif diff --git a/libalternator/tests/corpus/includelibrary/include/steamworks.inc b/libalternator/tests/corpus/includelibrary/include/steamworks.inc new file mode 100644 index 0000000..d256104 --- /dev/null +++ b/libalternator/tests/corpus/includelibrary/include/steamworks.inc @@ -0,0 +1,378 @@ +#if defined _SteamWorks_Included + #endinput +#endif +#define _SteamWorks_Included + +// https://partner.steamgames.com/doc/api + +/* results from UserHasLicenseForApp */ +enum EUserHasLicenseForAppResult +{ + k_EUserHasLicenseResultHasLicense = 0, // User has a license for specified app + k_EUserHasLicenseResultDoesNotHaveLicense = 1, // User does not have a license for the specified app + k_EUserHasLicenseResultNoAuth = 2, // User has not been authenticated +}; + +/* General result codes */ +enum EResult +{ + k_EResultOK = 1, // success + k_EResultFail = 2, // generic failure + k_EResultNoConnection = 3, // no/failed network connection +// k_EResultNoConnectionRetry = 4, // OBSOLETE - removed + k_EResultInvalidPassword = 5, // password/ticket is invalid + k_EResultLoggedInElsewhere = 6, // same user logged in elsewhere + k_EResultInvalidProtocolVer = 7, // protocol version is incorrect + k_EResultInvalidParam = 8, // a parameter is incorrect + k_EResultFileNotFound = 9, // file was not found + k_EResultBusy = 10, // called method busy - action not taken + k_EResultInvalidState = 11, // called object was in an invalid state + k_EResultInvalidName = 12, // name is invalid + k_EResultInvalidEmail = 13, // email is invalid + k_EResultDuplicateName = 14, // name is not unique + k_EResultAccessDenied = 15, // access is denied + k_EResultTimeout = 16, // operation timed out + k_EResultBanned = 17, // VAC2 banned + k_EResultAccountNotFound = 18, // account not found + k_EResultInvalidSteamID = 19, // steamID is invalid + k_EResultServiceUnavailable = 20, // The requested service is currently unavailable + k_EResultNotLoggedOn = 21, // The user is not logged on + k_EResultPending = 22, // Request is pending (may be in process, or waiting on third party) + k_EResultEncryptionFailure = 23, // Encryption or Decryption failed + k_EResultInsufficientPrivilege = 24, // Insufficient privilege + k_EResultLimitExceeded = 25, // Too much of a good thing + k_EResultRevoked = 26, // Access has been revoked (used for revoked guest passes) + k_EResultExpired = 27, // License/Guest pass the user is trying to access is expired + k_EResultAlreadyRedeemed = 28, // Guest pass has already been redeemed by account, cannot be acked again + k_EResultDuplicateRequest = 29, // The request is a duplicate and the action has already occurred in the past, ignored this time + k_EResultAlreadyOwned = 30, // All the games in this guest pass redemption request are already owned by the user + k_EResultIPNotFound = 31, // IP address not found + k_EResultPersistFailed = 32, // failed to write change to the data store + k_EResultLockingFailed = 33, // failed to acquire access lock for this operation + k_EResultLogonSessionReplaced = 34, + k_EResultConnectFailed = 35, + k_EResultHandshakeFailed = 36, + k_EResultIOFailure = 37, + k_EResultRemoteDisconnect = 38, + k_EResultShoppingCartNotFound = 39, // failed to find the shopping cart requested + k_EResultBlocked = 40, // a user didn't allow it + k_EResultIgnored = 41, // target is ignoring sender + k_EResultNoMatch = 42, // nothing matching the request found + k_EResultAccountDisabled = 43, + k_EResultServiceReadOnly = 44, // this service is not accepting content changes right now + k_EResultAccountNotFeatured = 45, // account doesn't have value, so this feature isn't available + k_EResultAdministratorOK = 46, // allowed to take this action, but only because requester is admin + k_EResultContentVersion = 47, // A Version mismatch in content transmitted within the Steam protocol. + k_EResultTryAnotherCM = 48, // The current CM can't service the user making a request, user should try another. + k_EResultPasswordRequiredToKickSession = 49, // You are already logged in elsewhere, this cached credential login has failed. + k_EResultAlreadyLoggedInElsewhere = 50, // You are already logged in elsewhere, you must wait + k_EResultSuspended = 51, // Long running operation (content download) suspended/paused + k_EResultCancelled = 52, // Operation canceled (typically by user: content download) + k_EResultDataCorruption = 53, // Operation canceled because data is ill formed or unrecoverable + k_EResultDiskFull = 54, // Operation canceled - not enough disk space. + k_EResultRemoteCallFailed = 55, // an remote call or IPC call failed + k_EResultPasswordUnset = 56, // Password could not be verified as it's unset server side + k_EResultExternalAccountUnlinked = 57, // External account (int PSN, Facebook...) is not linked to a Steam account + k_EResultPSNTicketInvalid = 58, // PSN ticket was invalid + k_EResultExternalAccountAlreadyLinked = 59, // External account (int PSN, Facebook...) is already linked to some other account, must explicitly request to replace/delete the link first + k_EResultRemoteFileConflict = 60, // The sync cannot resume due to a conflict between the local and remote files + k_EResultIllegalPassword = 61, // The requested new password is not legal + k_EResultSameAsPreviousValue = 62, // new value is the same as the old one ( secret question and answer ) + k_EResultAccountLogonDenied = 63, // account login denied due to 2nd factor authentication failure + k_EResultCannotUseOldPassword = 64, // The requested new password is not legal + k_EResultInvalidLoginAuthCode = 65, // account login denied due to auth code invalid + k_EResultAccountLogonDeniedNoMail = 66, // account login denied due to 2nd factor auth failure - and no mail has been sent + k_EResultHardwareNotCapableOfIPT = 67, + k_EResultIPTInitError = 68, + k_EResultParentalControlRestricted = 69, // operation failed due to parental control restrictions for current user + k_EResultFacebookQueryError = 70, // Facebook query returned an error + k_EResultExpiredLoginAuthCode = 71, // account login denied due to auth code expired + k_EResultIPLoginRestrictionFailed = 72, + k_EResultAccountLockedDown = 73, + k_EResultAccountLogonDeniedVerifiedEmailRequired = 74, + k_EResultNoMatchingURL = 75, + k_EResultBadResponse = 76, // parse failure, missing field, etc. + k_EResultRequirePasswordReEntry = 77, // The user cannot complete the action until they re-enter their password + k_EResultValueOutOfRange = 78, // the value entered is outside the acceptable range + k_EResultUnexpectedError = 79, // something happened that we didn't expect to ever happen + k_EResultDisabled = 80, // The requested service has been configured to be unavailable + k_EResultInvalidCEGSubmission = 81, // The set of files submitted to the CEG server are not valid ! + k_EResultRestrictedDevice = 82, // The device being used is not allowed to perform this action + k_EResultRegionLocked = 83, // The action could not be complete because it is region restricted + k_EResultRateLimitExceeded = 84, // Temporary rate limit exceeded, try again later, different from k_EResultLimitExceeded which may be permanent + k_EResultAccountLoginDeniedNeedTwoFactor = 85, // Need two-factor code to login + k_EResultItemDeleted = 86, // The thing we're trying to access has been deleted + k_EResultAccountLoginDeniedThrottle = 87, // login attempt failed, try to throttle response to possible attacker + k_EResultTwoFactorCodeMismatch = 88, // two factor code mismatch + k_EResultTwoFactorActivationCodeMismatch = 89, // activation code for two-factor didn't match + k_EResultAccountAssociatedToMultiplePartners = 90, // account has been associated with multiple partners + k_EResultNotModified = 91, // data not modified + k_EResultNoMobileDevice = 92, // the account does not have a mobile device associated with it + k_EResultTimeNotSynced = 93, // the time presented is out of range or tolerance + k_EResultSmsCodeFailed = 94, // SMS code failure (no match, none pending, etc.) + k_EResultAccountLimitExceeded = 95, // Too many accounts access this resource + k_EResultAccountActivityLimitExceeded = 96, // Too many changes to this account + k_EResultPhoneActivityLimitExceeded = 97, // Too many changes to this phone + k_EResultRefundToWallet = 98, // Cannot refund to payment method, must use wallet + k_EResultEmailSendFailure = 99, // Cannot send an email + k_EResultNotSettled = 100, // Can't perform operation till payment has settled + k_EResultNeedCaptcha = 101, // Needs to provide a valid captcha + k_EResultGSLTDenied = 102, // a game server login token owned by this token's owner has been banned + k_EResultGSOwnerDenied = 103, // game server owner is denied for other reason (account lock, community ban, vac ban, missing phone) + k_EResultInvalidItemType = 104, // the type of thing we were requested to act on is invalid +}; + +/* This enum is used in client API methods, do not re-number existing values. */ +enum EHTTPMethod +{ + k_EHTTPMethodInvalid = 0, + k_EHTTPMethodGET, + k_EHTTPMethodHEAD, + k_EHTTPMethodPOST, + k_EHTTPMethodPUT, + k_EHTTPMethodDELETE, + k_EHTTPMethodOPTIONS, + + // The remaining HTTP methods are not yet supported, per rfc2616 section 5.1.1 only GET and HEAD are required for + // a compliant general purpose server. We'll likely add more as we find uses for them. + +// k_EHTTPMethodTRACE, +// k_EHTTPMethodCONNECT +}; + + +/* HTTP Status codes that the server can send in response to a request, see rfc2616 section 10.3 for descriptions + of each of these. */ +enum EHTTPStatusCode +{ + // Invalid status code (this isn't defined in HTTP, used to indicate unset in our code) + k_EHTTPStatusCodeInvalid = 0, + + // Informational codes + k_EHTTPStatusCode100Continue = 100, + k_EHTTPStatusCode101SwitchingProtocols = 101, + + // Success codes + k_EHTTPStatusCode200OK = 200, + k_EHTTPStatusCode201Created = 201, + k_EHTTPStatusCode202Accepted = 202, + k_EHTTPStatusCode203NonAuthoritative = 203, + k_EHTTPStatusCode204NoContent = 204, + k_EHTTPStatusCode205ResetContent = 205, + k_EHTTPStatusCode206PartialContent = 206, + + // Redirection codes + k_EHTTPStatusCode300MultipleChoices = 300, + k_EHTTPStatusCode301MovedPermanently = 301, + k_EHTTPStatusCode302Found = 302, + k_EHTTPStatusCode303SeeOther = 303, + k_EHTTPStatusCode304NotModified = 304, + k_EHTTPStatusCode305UseProxy = 305, +// k_EHTTPStatusCode306Unused = 306, // used in old HTTP spec, now unused in 1.1 + k_EHTTPStatusCode307TemporaryRedirect = 307, + + // Error codes + k_EHTTPStatusCode400BadRequest = 400, + k_EHTTPStatusCode401Unauthorized = 401, // You probably want 403 or something else. 401 implies you're sending a WWW-Authenticate header and the client can sent an Authorization header in response. + k_EHTTPStatusCode402PaymentRequired = 402, // This is reserved for future HTTP specs, not really supported by clients + k_EHTTPStatusCode403Forbidden = 403, + k_EHTTPStatusCode404NotFound = 404, + k_EHTTPStatusCode405MethodNotAllowed = 405, + k_EHTTPStatusCode406NotAcceptable = 406, + k_EHTTPStatusCode407ProxyAuthRequired = 407, + k_EHTTPStatusCode408RequestTimeout = 408, + k_EHTTPStatusCode409Conflict = 409, + k_EHTTPStatusCode410Gone = 410, + k_EHTTPStatusCode411LengthRequired = 411, + k_EHTTPStatusCode412PreconditionFailed= 412, + k_EHTTPStatusCode413RequestEntityTooLarge = 413, + k_EHTTPStatusCode414RequestURITooLong = 414, + k_EHTTPStatusCode415UnsupportedMediaType = 415, + k_EHTTPStatusCode416RequestedRangeNotSatisfiable = 416, + k_EHTTPStatusCode417ExpectationFailed = 417, + k_EHTTPStatusCode4xxUnknown = 418, // 418 is reserved, so we'll use it to mean unknown + k_EHTTPStatusCode429TooManyRequests = 429, + + // Server error codes + k_EHTTPStatusCode500InternalServerError = 500, + k_EHTTPStatusCode501NotImplemented = 501, + k_EHTTPStatusCode502BadGateway = 502, + k_EHTTPStatusCode503ServiceUnavailable = 503, + k_EHTTPStatusCode504GatewayTimeout = 504, + k_EHTTPStatusCode505HTTPVersionNotSupported = 505, + k_EHTTPStatusCode5xxUnknown = 599, +}; + +/* list of possible return values from the ISteamGameCoordinator API */ +enum EGCResults +{ + k_EGCResultOK = 0, + k_EGCResultNoMessage = 1, // There is no message in the queue + k_EGCResultBufferTooSmall = 2, // The buffer is too small for the requested message + k_EGCResultNotLoggedOn = 3, // The client is not logged onto Steam + k_EGCResultInvalidMessage = 4, // Something was wrong with the message being sent with SendMessage +}; + +native bool SteamWorks_IsVACEnabled(); +native bool SteamWorks_GetPublicIP(int ipaddr[4]); +native int SteamWorks_GetPublicIPCell(); +native bool SteamWorks_IsLoaded(); +native bool SteamWorks_SetGameDescription(const char[] sDesc); +native bool SteamWorks_SetMapName(const char[] sMapName); +native bool SteamWorks_IsConnected(); +native bool SteamWorks_SetRule(const char[] sKey, const char[] sValue); +native bool SteamWorks_ClearRules(); +native bool SteamWorks_ForceHeartbeat(); +native bool SteamWorks_GetUserGroupStatus(int client, int groupid); +native bool SteamWorks_GetUserGroupStatusAuthID(int authid, int groupid); + +native EUserHasLicenseForAppResult SteamWorks_HasLicenseForApp(int client, int app); +native EUserHasLicenseForAppResult SteamWorks_HasLicenseForAppId(int authid, int app); +native int SteamWorks_GetClientSteamID(int client, char[] sSteamID, int length); + +native bool SteamWorks_RequestStatsAuthID(int authid, int appid); +native bool SteamWorks_RequestStats(int client, int appid); +native bool SteamWorks_GetStatCell(int client, const char[] sKey, int &value); +native bool SteamWorks_GetStatAuthIDCell(int authid, const char[] sKey, int &value); +native bool SteamWorks_GetStatFloat(int client, const char[] sKey, float &value); +native bool SteamWorks_GetStatAuthIDFloat(int authid, const char[] sKey, float &value); + +native Handle SteamWorks_CreateHTTPRequest(EHTTPMethod method, const char[] sURL); +native bool SteamWorks_SetHTTPRequestContextValue(Handle hHandle, any data1, any data2=0); +native bool SteamWorks_SetHTTPRequestNetworkActivityTimeout(Handle hHandle, int timeout); +native bool SteamWorks_SetHTTPRequestHeaderValue(Handle hHandle, const char[] sName, const char[] sValue); +native bool SteamWorks_SetHTTPRequestGetOrPostParameter(Handle hHandle, const char[] sName, const char[] sValue); +native bool SteamWorks_SetHTTPRequestUserAgentInfo(Handle hHandle, const char[] sUserAgentInfo); +native bool SteamWorks_SetHTTPRequestRequiresVerifiedCertificate(Handle hHandle, bool bRequireVerifiedCertificate); +native bool SteamWorks_SetHTTPRequestAbsoluteTimeoutMS(Handle hHandle, int unMilliseconds); + + +typeset SteamWorksHTTPRequestCompleted +{ + function void (Handle hRequest, bool bFailure, bool bRequestSuccessful, EHTTPStatusCode eStatusCode); + function void (Handle hRequest, bool bFailure, bool bRequestSuccessful, EHTTPStatusCode eStatusCode, any data1); + function void (Handle hRequest, bool bFailure, bool bRequestSuccessful, EHTTPStatusCode eStatusCode, any data1, any data2); +}; + +typeset SteamWorksHTTPHeadersReceived +{ + function void (Handle hRequest, bool bFailure); + function void (Handle hRequest, bool bFailure, any data1); + function void (Handle hRequest, bool bFailure, any data1, any data2); +}; + +typeset SteamWorksHTTPDataReceived +{ + function void (Handle hRequest, bool bFailure, int offset, int bytesreceived); + function void (Handle hRequest, bool bFailure, int offset, int bytesreceived, any data1); + function void (Handle hRequest, bool bFailure, int offset, int bytesreceived, any data1, any data2); +}; + +native bool SteamWorks_SetHTTPCallbacks(Handle hHandle, SteamWorksHTTPRequestCompleted fCompleted = INVALID_FUNCTION, SteamWorksHTTPHeadersReceived fHeaders = INVALID_FUNCTION, SteamWorksHTTPDataReceived fData = INVALID_FUNCTION, Handle hCalling = INVALID_HANDLE); +native bool SteamWorks_SendHTTPRequest(Handle hRequest); +native bool SteamWorks_SendHTTPRequestAndStreamResponse(Handle hRequest); +native bool SteamWorks_DeferHTTPRequest(Handle hRequest); +native bool SteamWorks_PrioritizeHTTPRequest(Handle hRequest); +native bool SteamWorks_GetHTTPResponseHeaderSize(Handle hRequest, const char[] sHeader, int &size); +native bool SteamWorks_GetHTTPResponseHeaderValue(Handle hRequest, const char[] sHeader, char[] sValue, int size); +native bool SteamWorks_GetHTTPResponseBodySize(Handle hRequest, int &size); +native bool SteamWorks_GetHTTPResponseBodyData(Handle hRequest, char[] sBody, int length); +native bool SteamWorks_GetHTTPStreamingResponseBodyData(Handle hRequest, int cOffset, char[] sBody, int length); +native bool SteamWorks_GetHTTPDownloadProgressPct(Handle hRequest, float &percent); +native bool SteamWorks_GetHTTPRequestWasTimedOut(Handle hRequest, bool &bWasTimedOut); +native bool SteamWorks_SetHTTPRequestRawPostBody(Handle hRequest, const char[] sContentType, const char[] sBody, int bodylen); + +typeset SteamWorksHTTPBodyCallback +{ + function void (const char[] sData); + function void (const char[] sData, any value); + function void (const int[] data, any value, int datalen); +}; + +native bool SteamWorks_GetHTTPResponseBodyCallback(Handle hRequest, SteamWorksHTTPBodyCallback fCallback, any data = 0, Handle hPlugin = INVALID_HANDLE); +native bool SteamWorks_WriteHTTPResponseBodyToFile(Handle hRequest, const char[] sFileName); + +forward void SW_OnValidateClient(int ownerauthid, int authid); +forward void SteamWorks_OnValidateClient(int ownerauthid, int authid); +forward void SteamWorks_SteamServersConnected(); +forward void SteamWorks_SteamServersConnectFailure(EResult result); +forward void SteamWorks_SteamServersDisconnected(EResult result); + +forward void SteamWorks_RestartRequested(); +forward void SteamWorks_TokenRequested(char[] sToken, int maxlen); + +forward void SteamWorks_OnClientGroupStatus(int authid, int groupid, bool isMember, bool isOfficer); + +forward EGCResults SteamWorks_GCSendMessage(int unMsgType, const char[] pubData, int cubData); +forward void SteamWorks_GCMsgAvailable(int cubData); +forward EGCResults SteamWorks_GCRetrieveMessage(int punMsgType, const char[] pubDest, int cubDest, int pcubMsgSize); + +native EGCResults SteamWorks_SendMessageToGC(int unMsgType, const char[] pubData, int cubData); + +public Extension __ext_SteamWorks = +{ + name = "SteamWorks", + file = "SteamWorks.ext", +#if defined AUTOLOAD_EXTENSIONS + autoload = 1, +#else + autoload = 0, +#endif +#if defined REQUIRE_EXTENSIONS + required = 1, +#else + required = 0, +#endif +}; + +#if !defined REQUIRE_EXTENSIONS +public void __ext_SteamWorks_SetNTVOptional() +{ + MarkNativeAsOptional("SteamWorks_IsVACEnabled"); + MarkNativeAsOptional("SteamWorks_GetPublicIP"); + MarkNativeAsOptional("SteamWorks_GetPublicIPCell"); + MarkNativeAsOptional("SteamWorks_IsLoaded"); + MarkNativeAsOptional("SteamWorks_SetGameDescription"); + MarkNativeAsOptional("SteamWorks_IsConnected"); + MarkNativeAsOptional("SteamWorks_SetRule"); + MarkNativeAsOptional("SteamWorks_ClearRules"); + MarkNativeAsOptional("SteamWorks_ForceHeartbeat"); + MarkNativeAsOptional("SteamWorks_GetUserGroupStatus"); + MarkNativeAsOptional("SteamWorks_GetUserGroupStatusAuthID"); + + MarkNativeAsOptional("SteamWorks_HasLicenseForApp"); + MarkNativeAsOptional("SteamWorks_HasLicenseForAppId"); + MarkNativeAsOptional("SteamWorks_GetClientSteamID"); + + MarkNativeAsOptional("SteamWorks_RequestStatsAuthID"); + MarkNativeAsOptional("SteamWorks_RequestStats"); + MarkNativeAsOptional("SteamWorks_GetStatCell"); + MarkNativeAsOptional("SteamWorks_GetStatAuthIDCell"); + MarkNativeAsOptional("SteamWorks_GetStatFloat"); + MarkNativeAsOptional("SteamWorks_GetStatAuthIDFloat"); + + MarkNativeAsOptional("SteamWorks_SendMessageToGC"); + + MarkNativeAsOptional("SteamWorks_CreateHTTPRequest"); + MarkNativeAsOptional("SteamWorks_SetHTTPRequestContextValue"); + MarkNativeAsOptional("SteamWorks_SetHTTPRequestNetworkActivityTimeout"); + MarkNativeAsOptional("SteamWorks_SetHTTPRequestHeaderValue"); + MarkNativeAsOptional("SteamWorks_SetHTTPRequestGetOrPostParameter"); + + MarkNativeAsOptional("SteamWorks_SetHTTPCallbacks"); + MarkNativeAsOptional("SteamWorks_SendHTTPRequest"); + MarkNativeAsOptional("SteamWorks_SendHTTPRequestAndStreamResponse"); + MarkNativeAsOptional("SteamWorks_DeferHTTPRequest"); + MarkNativeAsOptional("SteamWorks_PrioritizeHTTPRequest"); + MarkNativeAsOptional("SteamWorks_GetHTTPResponseHeaderSize"); + MarkNativeAsOptional("SteamWorks_GetHTTPResponseHeaderValue"); + MarkNativeAsOptional("SteamWorks_GetHTTPResponseBodySize"); + MarkNativeAsOptional("SteamWorks_GetHTTPResponseBodyData"); + MarkNativeAsOptional("SteamWorks_GetHTTPStreamingResponseBodyData"); + MarkNativeAsOptional("SteamWorks_GetHTTPDownloadProgressPct"); + MarkNativeAsOptional("SteamWorks_SetHTTPRequestRawPostBody"); + + MarkNativeAsOptional("SteamWorks_GetHTTPResponseBodyCallback"); + MarkNativeAsOptional("SteamWorks_WriteHTTPResponseBodyToFile"); +} +#endif diff --git a/libalternator/tests/corpus/includelibrary/include/tf2attributes.inc b/libalternator/tests/corpus/includelibrary/include/tf2attributes.inc new file mode 100644 index 0000000..558be80 --- /dev/null +++ b/libalternator/tests/corpus/includelibrary/include/tf2attributes.inc @@ -0,0 +1,306 @@ +#if defined _tf2attributes_included + #endinput +#endif +#define _tf2attributes_included + +/** + * Sets an attribute's value on an entity, adding it if it isn't on the entity. + * + * @param iEntity Entity index to set the attribute on. Must have m_AttributeList. + * @param strAttrib Name of the attribute, as from the "name" key in items_game. + * @param flValue Value to set the attribute to + * + * @return True if the attribute was added successfully, false if entity does not have m_AttributeList. + * @error Invalid entity index or attribute name passed. + */ +native bool TF2Attrib_SetByName(int iEntity, char[] strAttrib, float flValue); + +/** + * Sets an attribute's value on an entity, adding it if it isn't on the entity. + * + * @param iEntity Entity index to set the attribute on. Must have m_AttributeList. + * @param iDefIndex Definition index of the attribute, as from the number on the attribute entry in items_game. + * @param flValue Value to set the attribute to + * + * @return True if the attribute was added successfully, false if entity does not have m_AttributeList. + * @error Invalid entity index or attribute name passed. + */ +native bool TF2Attrib_SetByDefIndex(int iEntity, int iDefIndex, float flValue); + +/** + * Returns the address of an attribute on an entity. + * + * @param iEntity Entity index to get attribute from. Must have m_AttributeList. + * @param strAttrib Name of the attribute, as from the "name" key in items_game. + * + * @return Address of the attribute on the entity, or Address_Null if the attribute does not exist on the entity. + * @error Invalid entity index or attribute name passed. + */ +native Address TF2Attrib_GetByName(int iEntity, char[] strAttrib); + +/** + * Returns the address of an attribute (by attribute index) on an entity. + * + * @param iEntity Entity index to get attribute from. Must have m_AttributeList. + * @param iDefIndex Definition index of the attribute, as from the number on the attribute entry in items_game. + * + * @return Address of the attribute on the entity, or Address_Null if the attribute does not exist on the entity. + * @error Invalid entity index or attribute index passed. + */ +native Address TF2Attrib_GetByDefIndex(int iEntity, int iDefIndex); + +/** + * Removes an attribute from an entity. + * + * @param iEntity Entity index to remove attribute from. Must have m_AttributeList. + * @param strAttrib Name of the attribute, as from the "name" key in items_game. + * + * @return True if the SDKCall was made, false if entity had invalid address or m_AttributeList missing. + * @error Invalid entity index or attribute name passed. + */ +native bool TF2Attrib_RemoveByName(int iEntity, char[] strAttrib); + +/** + * Removes an attribute from an entity. + * + * @param iEntity Entity index to remove attribute from. Must have m_AttributeList. + * @param iDefIndex Definition index of the attribute, as from the number on the attribute entry in items_game. + * + * @return True if the SDKCall was made, false if entity had invalid address or m_AttributeList missing. + * @error Invalid entity index or attribute index passed. + */ +native bool TF2Attrib_RemoveByDefIndex(int iEntity, int iDefIndex); + +/** + * Removes all attributes from an entity. + * + * @param iEntity Entity index to remove attribute from. Must have m_AttributeList. + * + * @return True if the SDKCall was made, false if entity had invalid address or m_AttributeList missing. + * @error Invalid entity index passed. + */ +native bool TF2Attrib_RemoveAll(int iEntity); + +/** + * Clears and presumably rebuilds the attribute cache for an entity, 'refreshing' attributes. + * Call this after making changes to an attribute with any of the TF2Attrib_Set*(Address pAttrib, arg) natives below. + * You may also need to call this on the entity's m_hOwnerEntity if it is a weapon or wearable. + * You do NOT need to call this after calls to TF2Attrib_SetByName, TF2Attrib_Remove, and TF2Attrib_RemoveAll. + * + * @param iEntity Entity index to remove attribute from. Must have m_AttributeList. + * + * @return True if the SDKCall was made, false if entity had invalid address or m_AttributeList missing. + * @error Invalid entity index passed. + */ +native bool TF2Attrib_ClearCache(int iEntity); + +/** + * Sets the value of m_iAttributeDefinitionIndex (the attribute ID) on an attribute. + * Warning, this changes what GetByName/ID and SetByName 'see' as the name of the attribute, + * but will only change attribute's effects if TF2Attrib_ClearCache is called on the entity with the attribute after. + * + * @param pAttrib Address of the attribute. + * @param iDefIndex Value to set m_iAttributeDefinitionIndex to. + * + * @noreturn + */ +native void TF2Attrib_SetDefIndex(Address pAttrib, int iDefIndex); + +/** + * Returns the value of m_iAttributeDefinitionIndex (the attribute ID) on an attribute. + * + * @param pAttrib Address of the attribute. + * + * @return The integer value of m_iAttributeDefinitionIndex on the attribute. + */ +native int TF2Attrib_GetDefIndex(Address pAttrib); + +/** + * Sets the value of m_flValue on an attribute. + * + * @param pAttrib Address of the attribute. + * @param flValue Value to set m_flValue to. + * + * @noreturn + */ +native void TF2Attrib_SetValue(Address pAttrib, float flValue); + +/** + * Returns the value of m_flValue on an attribute. + * + * @param pAttrib Address of the attribute. + * + * @return The floating point value of m_flValue on the attribute. + */ +native float TF2Attrib_GetValue(Address pAttrib); + +/** + * Sets the value of m_nRefundableCurrency on an attribute. + * + * @param pAttrib Address of the attribute. + * @param nCurrency Value to set m_nRefundableCurrency to. + * + * @noreturn + */ +native void TF2Attrib_SetRefundableCurrency(Address pAttrib, int nCurrency); + +/** + * Returns the value of m_nRefundableCurrency on an attribute. + * + * @param pAttrib Address of the attribute. + * + * @return The (unsigned) integer value of m_nRefundableCurrency on the attribute. + */ +native int TF2Attrib_GetRefundableCurrency(Address pAttrib); + +/** + * Returns an array containing the attributes (as indices) present on an entity. + * + * @param iEntity Entity index to get attribute list from. Must have m_AttributeList. + * @param iDefIndices Array of attribute definition indices found on the entity. + * @param iMaxLen Max length of the iDefIndices array, default 20. Does not affect the return value. + * + * @return The number of attributes found on the entity's attribute list (max 20 as of Oct 2017), or -1 if some error happened. + * @error Invalid iEntity or iMaxLen. + */ +native int TF2Attrib_ListDefIndices(int iEntity, int[] iDefIndices, int iMaxLen=20); + +/** + * Returns arrays containing the static attributes and their values present on an item definition. + * + * @param iItemDefIndex Item definition index (e.g. 7 for Shovel) to get static attribute list from. + * @param iAttribIndices Array of attribute definition indices found on the item definition. + * @param flAttribValues Array of attribute values found on the item definition, corresponding to the indices. + * @param iMaxLen Max length of the two arrays passed in, default 16. Does not affect the return value. + * + * @return The number of attributes found on the item definition's static attribute list (max 16 as of June 2017), or -1 if no schema or item definition found. + * @error Invalid iMaxLen, or gamedata for this function failed to load. + */ +native int TF2Attrib_GetStaticAttribs(int iItemDefIndex, int[] iAttribIndices, float[] flAttribValues, int iMaxLen=16); + +/** + * Returns arrays containing the item server (SOC) attributes and their values present on an item definition. + * + * @param iEntity Entity index to get the item server attribute list from. + * @param iAttribIndices Array of attribute definition indices found. + * @param flAttribValues Array of attribute values found, corresponding to the indices. + * @param iMaxLen Max length of the two arrays passed in, default 16. Does not affect the return value. + * + * @return The number of attributes found on the item's SOC attribute list (max 16 as of June 2017), or -1 if some error happened. + * @error Invalid iEntity or iMaxLen, or gamedata for this function failed to load. + */ +native int TF2Attrib_GetSOCAttribs(int iEntity, int[] iAttribIndices, float[] flAttribValues, int iMaxLen=16); + +/** + * Gets whether an attribute is stored as an integer or as a float. + * Use TF2Attrib_SetValue(attribute, view_as(intValue)) on attributes that store values as ints + * to avoid compiler tag mismatch warnings. + * + * @param iDefIndex Index of the attribute (as returned by TF2Attrib_GetDefIndex()). + * + * @return True if attribute value is supposed to be an int, false if float. + */ +native bool TF2Attrib_IsIntegerValue(int iDefIndex); + +/** + * Gets whether the plugin loaded without ANY errors. + * For the purpose of allowing dependencies to ignore the plugin if this returns false. + * Check in OnAllPluginsLoaded() or something. I dunno. + * + * @return True if no errors while loading, false otherwise. + */ +native bool TF2Attrib_IsReady(); + +public SharedPlugin __pl_tf2attributes = +{ + name = "tf2attributes", + file = "tf2attributes.smx", +#if defined REQUIRE_PLUGIN + required = 1, +#else + required = 0, +#endif +}; + +#if !defined REQUIRE_PLUGIN +public __pl_tf2attributes_SetNTVOptional() +{ + MarkNativeAsOptional("TF2Attrib_SetByName"); + MarkNativeAsOptional("TF2Attrib_SetByDefIndex"); + MarkNativeAsOptional("TF2Attrib_GetByName"); + MarkNativeAsOptional("TF2Attrib_GetByDefIndex"); + MarkNativeAsOptional("TF2Attrib_RemoveByName"); + MarkNativeAsOptional("TF2Attrib_RemoveByDefIndex"); + MarkNativeAsOptional("TF2Attrib_RemoveAll"); + MarkNativeAsOptional("TF2Attrib_ClearCache"); + MarkNativeAsOptional("TF2Attrib_SetDefIndex"); + MarkNativeAsOptional("TF2Attrib_GetDefIndex"); + MarkNativeAsOptional("TF2Attrib_SetValue"); + MarkNativeAsOptional("TF2Attrib_GetValue"); + MarkNativeAsOptional("TF2Attrib_SetRefundableCurrency"); + MarkNativeAsOptional("TF2Attrib_GetRefundableCurrency"); + MarkNativeAsOptional("TF2Attrib_ListDefIndices"); + MarkNativeAsOptional("TF2Attrib_GetStaticAttribs"); + MarkNativeAsOptional("TF2Attrib_GetSOCAttribs"); + MarkNativeAsOptional("TF2Attrib_ListDefIndices"); + MarkNativeAsOptional("TF2Attrib_IsIntegerValue"); + MarkNativeAsOptional("TF2Attrib_IsReady"); + + MarkNativeAsOptional("TF2Attrib_SetInitialValue"); + MarkNativeAsOptional("TF2Attrib_GetInitialValue"); + MarkNativeAsOptional("TF2Attrib_SetIsSetBonus"); + MarkNativeAsOptional("TF2Attrib_GetIsSetBonus"); +} +#endif + +//OLD things lie here +//flInitialValue and bSetBonus don't exist anymore +/** + * Sets the value of m_flInitialValue on an attribute. + * + * @param pAttrib Address of the attribute. + * @param flValue Value to set m_flInitialValue to. + * + * @noreturn + */ +//native TF2Attrib_SetInitialValue(Address pAttrib, float flValue); + +/** + * Returns the value of m_flInitialValue on an attribute. + * + * @param pAttrib Address of the attribute. + * + * @return The floating point value of m_flInitialValue on the attribute. + */ +//native float TF2Attrib_GetInitialValue(Address pAttrib); + +/** + * Sets the boolean value of m_bSetBonus on an attribute. + * + * @param pAttrib Address of the attribute. + * @param bSetBonus Value to set m_bSetBonus to. + * + * @noreturn + */ +//native TF2Attrib_SetIsSetBonus(Address pAttrib, bool bSetBonus); + +/** + * Returns the boolean value of m_bSetBonus on an attribute. + * + * @param pAttrib Address of the attribute. + * + * @return The boolean value of m_bSetBonus on the attribute. + */ +//native bool TF2Attrib_GetIsSetBonus(Address pAttrib); + +//stock TF2Attrib_IsIntegerValue(iDefIndex) +//{ +// switch (iDefIndex) +// { +// case 133, 143, 147, 152, 184, 185, 186, 192, 193, 194, 198, 211, 214, 227, 228, 229, 262, 294, 302, 372, 373, 374, 379, 381, 383, 403, 420: +// { +// return true; +// } +// } +// return false; +//} \ No newline at end of file diff --git a/libalternator/tests/corpus/includelibrary/include/tf2items.inc b/libalternator/tests/corpus/includelibrary/include/tf2items.inc new file mode 100644 index 0000000..270cb3e --- /dev/null +++ b/libalternator/tests/corpus/includelibrary/include/tf2items.inc @@ -0,0 +1,248 @@ +#if defined _tf2items_included +#endinput +#endif +#define _tf2items_included + +#include + +// ====[ CONSTANTS ]=========================================================== +#define OVERRIDE_CLASSNAME (1 << 0) // Item will override the entity's classname. +#define OVERRIDE_ITEM_DEF (1 << 1) // Item will override the item's definition index. +#define OVERRIDE_ITEM_LEVEL (1 << 2) // Item will override the entity's level. +#define OVERRIDE_ITEM_QUALITY (1 << 3) // Item will override the entity's quality. +#define OVERRIDE_ATTRIBUTES (1 << 4) // Item will override the attributes for the item with the given ones. +#define OVERRIDE_ALL (0b11111) // Magically enables all the other flags. + +#define PRESERVE_ATTRIBUTES (1 << 5) +#define FORCE_GENERATION (1 << 6) + +// ====[ NATIVES ]============================================================= + +/** + * WARNING: This is for ADVANCED users only! + * You probably want to be using the forward instead of this. + * + * This native will perform a GiveNamedItem call for the specified client in + * order to generate an object based on the specifications of the TF2Items + * object passed to it. + * + * Since the item generation requires all the information that can be passed by + * the extensions natives, the flags will be ignored and all the given + * information will be used. + * + * Remember that if your values aren't correct, this call may end up in a crash, + * so please make sure you fill out everything: Classname, item definition index, + * quality, level and attributes. + * + * @param client Client that the item will be generated for. + * @param item Handle to the TF2Items object that we'll be operating with. + * @return Entity index of the newly created item. + */ +native int TF2Items_GiveNamedItem(int client, Handle item); + +/** + * Creates a TF2Items object, wich can be used to override the parameters of an + * item before it's given to the client, or to create a new completely new one + * with GiveNamedItem. Remember to free the object with CloseHandle() + * + * @param flags Flags used to specify what to override. + * @return Handle to the TFItems object. + */ +native Handle TF2Items_CreateItem(int flags); + +/** + * Sets the flags to determine what the item will override on the GiveNamedItem. + * Use the OVERRIDE_ defines to set what you will be changing. + * + * @param item Handle to the TF2Items object that we'll be operating with. + * @param flags Flags used to specify what to override. + */ +native void TF2Items_SetFlags(Handle item, int flags); + +/** + * Sets the new entity classname used for the item's entity. + * + * @param item Handle to the TF2Items object that we'll be operating with. + * @param classname New classname to use for the entity. + */ +native void TF2Items_SetClassname(Handle item, char[] classname); + +/** + * Sets the item's Definition Index, wich tells the game what item it is. Each + * weapon/hat/item has an unique definition index. Find these out in the + * game_items.txt + * + * @param item Handle to the TF2Items object that we'll be operating with. + * @param itemDefIndex New definition index. + */ +native void TF2Items_SetItemIndex(Handle item, int itemDefIndex); + +/** + * Sets the item's quality value, wich determines what color will be used for + * the item name. Valid values are from 0 to 9. + * + * @param item Handle to the TF2Items object that we'll be operating with. + * @param quality New item quality. + */ +native void TF2Items_SetQuality(Handle item, int quality); + +/** + * Sets the item's level value. This value can range from 0 to 127. + * + * @param item Handle to the TF2Items object that we'll be operating with. + * @param level New item level. + */ +native void TF2Items_SetLevel(Handle item, int level); + +/** + * Sets the number of attributes that will be used on the item. The maximum + * number of attributes that can be allocated ranges from 0 to 15. + * + * @param item Handle to the TF2Items object that we'll be operating with. + * @param attributes Number of attributes. + */ +native void TF2Items_SetNumAttributes(Handle item, int attributes); + +/** + * Setups the given attribute index to use the attribute and values specified + * with iAttribDefIndex and flValue. Remember the iSlotIndex ranges from 0 to 15. + * + * @param item Handle to the TF2Items object that we'll be operating with. + * @param slot The attribute slot index, ranges from 0 to 15. + * @param attribute The attribute definition index, as it appears on game_items.txt. + * @param value The value assigned to the attribute (how much health, damage, etc.). + */ +native void TF2Items_SetAttribute(Handle item, int slot, int attribute, float value); + +/** + * Retrieves the flags used to determine what the item will override on the + * GiveNamedItem call. + * + * @param item Handle to the TF2Items object that we'll be operating with. + * @return Returns the flags used by the item. + */ +native int TF2Items_GetFlags(Handle item); + +/** + * Gets the entity classname we'll use for the item. + * + * @param item Handle to the TF2Items object that we'll be operating with. + * @return Size of entity classname string + */ +native int TF2Items_GetClassname(Handle item, char[] dest, int size); + +/** + * Gets the new item definition index we'll use to override. + * + * @param item Handle to the TF2Items object that we'll be operating with. + * @return New definition index. + */ +native int TF2Items_GetItemIndex(Handle item); + +/** + * Gets the item's quality value, wich determines what color will be used for + * the item name. Valid values are from 0 to 9. But if set to 0 and attributes + * are also changed, this value will be overridden to 9 + * + * @param item Handle to the TF2Items object that we'll be operating with. + * @return New entity quality. + */ +native int TF2Items_GetQuality(Handle item); + +/** + * Gets the item's level value. This value can range from 0 to 127. + * + * @param item Handle to the TF2Items object that we'll be operating with. + * @return New entity level. + */ +native int TF2Items_GetLevel(Handle item); + +/** + * Gets the number of attributes that will be used on the item. The maximum + * number of attributes that can be allocated ranges from 0 to 15. + * + * @param item Handle to the TF2Items object that we'll be operating with. + * @return Number of attributes. + */ +native int TF2Items_GetNumAttributes(Handle item); + +/** + * Retrieves the attribute definition index used at the specified index on the + * item object. Remember the iSlotIndex ranges from 0 to 15. + * + * @param item Handle to the TF2Items object that we'll be operating with. + * @return The attribute definition index to use. + */ +native int TF2Items_GetAttributeId(Handle item, int slot); + +/** + * Retrieves the value used for the attribute at the specified index on the item + * object. Remember the iSlotIndex ranges from 0 to 15. + * + * @param item Handle to the TF2Items object that we'll be operating with. + * @return The attribute value to use. + */ +native float TF2Items_GetAttributeValue(Handle item, int slot); + +// ====[ FORWARDS ]============================================================ +/** + * Called when an item is about to be given to a client. + * Return Plugin_Changed to override the item to use the configuration of the item object. + * Return Plugin_Continue to keep them intact. + * Return Plugin_Handled to stop the item being given to the player. + * Make sure the client gets atleast one weapon. + * + * @param client Client Index. + * @param classname The classname of the entity that will be generated. + * @param itemDefIndex Item definition index. + * @param item Handle to a TF2Item object wich describes what values will be overriden. + */ +forward Action TF2Items_OnGiveNamedItem(int client, char[] classname, int itemDefIndex, Handle &item); + +forward void TF2Items_OnGiveNamedItem_Post(int client, char[] classname, int itemDefIndex, int level, int quality, int entity); + +/** + * Do not edit below this line! + */ +public Extension __ext_tf2items = +{ + name = "TF2Items", + file = "tf2items.ext.2.ep2v", + autoload = 0, + #if defined REQUIRE_EXTENSIONS + required = 1, + #else + required = 0, + #endif +} + +/** +* I'll just leave this here... +* +* _(º< _(º< _(º< _(º< _(º< _(º< _(º< _(º< _(º< +* \__) \__) \__) \__) \__) \__) \__) \__) \__) +* . . +* // // __ +* __ ______||_//_.´.´ +* _/__`´ ¯ ` +* / / _ _ \ +* / /( · ) ( · ) | +* / | ¯ __ ¯ _/\/| +* | \ ___.-´ `-.___ \ / +* \ \( ` ´ `)| \ +* \ ) // \ +* \/ / | | | +* / / | | | +* · | | \___/ +* | \_ _/ \ ____ +* · `----------´ |´ \ +* \ / _·´ +* \ / _-´ +* `-._ _·´-´ +* _.-´`---________--´ \ +* ´-.-. _--· / . ._ \ +* ´ `´ `·´ `´ +* >º)_ >º)_ >º)_ >º)_ >º)_ >º)_ >º)_ >º)_ >º)_ +* (__/ (__/ (__/ (__/ (__/ (__/ (__/ (__/ (__/ +* +*/ diff --git a/libalternator/tests/corpus/includelibrary/include/tf2items_stocks.inc b/libalternator/tests/corpus/includelibrary/include/tf2items_stocks.inc new file mode 100644 index 0000000..0740a79 --- /dev/null +++ b/libalternator/tests/corpus/includelibrary/include/tf2items_stocks.inc @@ -0,0 +1,117 @@ +#if defined _tf2items_stocks_included +#endinput +#endif +#define _tf2items_stocks_included + +#include + +methodmap TF2Item < Handle { + public TF2Item(int iFlags) { + return view_as< TF2Item >( TF2Items_CreateItem(iFlags) ); + } + /////////////////////////////// + + property int iFlags { + public get() { return TF2Items_GetFlags(this); } + public set( int iVal ) { TF2Items_SetFlags(this, iVal); } + } + property int iItemIndex { + public get() {return TF2Items_GetItemIndex(this);} + public set( int iVal ) {TF2Items_SetItemIndex(this, iVal);} + } + property int iQuality { + public get() {return TF2Items_GetQuality(this);} + public set( int iVal ) {TF2Items_SetQuality(this, iVal);} + } + property int iLevel { + public get() {return TF2Items_GetLevel(this);} + public set( int iVal ) {TF2Items_SetLevel(this, iVal);} + } + property int iNumAttribs { + public get() {return TF2Items_GetNumAttributes(this);} + public set( int iVal ) {TF2Items_SetNumAttributes(this, iVal);} + } + /////////////////////////////// + + public int GiveNamedItem(int iClient) { + return TF2Items_GiveNamedItem(iClient, this); + } + public void SetClassname(char[] strClassName) { + TF2Items_SetClassname(this, strClassName); + } + public void GetClassname(char[] strDest, int iDestSize) { + TF2Items_GetClassname(this, strDest, iDestSize); + } + public void SetAttribute(int iSlotIndex, int iAttribDefIndex, float flValue) { + TF2Items_SetAttribute(this, iSlotIndex, iAttribDefIndex, flValue); + } + public int GetAttribID(int iSlotIndex) { + return TF2Items_GetAttributeId(this, iSlotIndex); + } + public float GetAttribValue(int iSlotIndex) { + return TF2Items_GetAttributeValue(this, iSlotIndex); + } + /**************************************************************/ +}; + +stock TF2Item TF2Item_PrepareItemHandle(TF2Item hItem, char[] name="", int index=-1, const char[] att="", bool dontpreserve=false) +{ + static TF2Item hWeapon = null; + int addattribs = 0; + char weaponAttribsArray[32][32]; + int attribCount = ExplodeString(att, "; ", weaponAttribsArray, 32, 32); + + int flags = OVERRIDE_ATTRIBUTES; + if( !dontpreserve ) + flags |= PRESERVE_ATTRIBUTES; + + if( !hWeapon ) + hWeapon = new TF2Item(flags); + else hWeapon.iFlags = flags; + + if( hItem != null ) { + addattribs = hItem.iNumAttribs; + if( addattribs ) { + for( int i; i < 2 * addattribs; i += 2 ) { + bool dontAdd = false; + int attribIndex = hItem.GetAttribID(i); + for( int z; z < attribCount+i; z += 2 ) { + if( StringToInt(weaponAttribsArray[z]) == attribIndex ) { + dontAdd = true; + break; + } + } + if( !dontAdd ) { + IntToString(attribIndex, weaponAttribsArray[i+attribCount], 32); + FloatToString(hItem.GetAttribValue(i), weaponAttribsArray[i+1+attribCount], 32); + } + } + attribCount += 2*addattribs; + } + delete hItem; + } + + if( name[0] != '\0' ) { + flags |= OVERRIDE_CLASSNAME; + hWeapon.SetClassname(name); + } + if( index != -1 ) { + flags |= OVERRIDE_ITEM_DEF; + hWeapon.iItemIndex = index; + } + if( attribCount > 1 ) { + hWeapon.iNumAttribs = (attribCount/2); + int i2 = 0; + for( int i; i. + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + */ + +#if defined _json_included + #endinput +#endif +#define _json_included + +#include +#include +#include +#include +#include +#include +#include + +/** + * Calculates the buffer size required to store an encoded JSON instance. + * + * @param obj Object to encode. + * @param options Bitwise combination of `JSON_ENCODE_*` options. + * @param depth The current depth of the encoder. + * @return The required buffer size. + */ +stock int json_encode_size(JSON_Object obj, int options = JSON_NONE, int depth = 0) +{ + bool pretty_print = (options & JSON_ENCODE_PRETTY) != 0; + + bool is_array = obj.IsArray; + + int size = 1; // for opening bracket + + // used in key iterator + int json_size = obj.Length; + JSON_Object child = null; + bool is_empty = true; + int str_length = 0; + + int key_length = 0; + for (int i = 0; i < json_size; i += 1) { + key_length = is_array ? JSON_INT_BUFFER_SIZE : obj.GetKeySize(i); + char[] key = new char[key_length]; + + if (is_array) { + IntToString(i, key, key_length); + } else { + obj.GetKey(i, key, key_length); + } + + // skip keys that are marked as hidden + if (obj.GetHidden(key)) { + continue; + } + + JSONCellType type = obj.GetType(key); + // skip keys of unknown type + if (type == JSON_Type_Invalid) { + continue; + } + + if (pretty_print) { + size += strlen(JSON_PP_NEWLINE); + size += (depth + 1) * strlen(JSON_PP_INDENT); + } + + if (! is_array) { + // add the size of the key and + 1 for : + size += json_cell_string_size(key) + 1; + + if (pretty_print) { + size += strlen(JSON_PP_AFTER_COLON); + } + } + + switch (type) { + case JSON_Type_String: { + str_length = obj.GetSize(key); + char[] value = new char[str_length]; + obj.GetString(key, value, str_length); + + size += json_cell_string_size(value); + } + case JSON_Type_Int: { + size += JSON_INT_BUFFER_SIZE; + } + #if SM_INT64_SUPPORTED + case JSON_Type_Int64: { + size += JSON_INT64_BUFFER_SIZE; + } + #endif + case JSON_Type_Float: { + size += JSON_FLOAT_BUFFER_SIZE; + } + case JSON_Type_Bool: { + size += JSON_BOOL_BUFFER_SIZE; + } + case JSON_Type_Object: { + child = obj.GetObject(key); + size += child != null ? json_encode_size(child, options, depth + 1) : JSON_NULL_BUFFER_SIZE; + } + } + + // increment for comma + size += 1; + + is_empty = false; + } + + if (! is_empty) { + // remove the final comma + size -= 1; + + if (pretty_print) { + size += strlen(JSON_PP_NEWLINE); + size += depth * strlen(JSON_PP_INDENT); + } + } + + size += 2; // closing bracket + NULL + + return size; +} + +/** + * Encodes a JSON instance into its string representation. + * + * @param obj Object to encode. + * @param output String buffer to store output. + * @param max_size Maximum size of string buffer. + * @param options Bitwise combination of `JSON_ENCODE_*` options. + * @param depth The current depth of the encoder. + */ +stock void json_encode( + JSON_Object obj, + char[] output, + int max_size, + int options = JSON_NONE, + int depth = 0 +) +{ + bool pretty_print = (options & JSON_ENCODE_PRETTY) != 0; + + bool is_array = obj.IsArray; + strcopy(output, max_size, is_array ? "[" : "{"); + + // used in key iterator + int json_size = obj.Length; + int builder_size = 0; + int str_length = 1; + JSON_Object child = null; + int cell_length = 0; + bool is_empty = true; + + int key_length = 0; + for (int i = 0; i < json_size; i += 1) { + key_length = is_array ? JSON_INT_BUFFER_SIZE : obj.GetKeySize(i); + char[] key = new char[key_length]; + + if (is_array) { + IntToString(i, key, key_length); + } else { + obj.GetKey(i, key, key_length); + } + + // skip keys that are marked as hidden + if (obj.GetHidden(key)) { + continue; + } + + JSONCellType type = obj.GetType(key); + // skip keys of unknown type + if (type == JSON_Type_Invalid) { + continue; + } + + // determine the length of the char[] needed to represent our cell data + cell_length = 0; + switch (type) { + case JSON_Type_String: { + str_length = obj.GetSize(key); + char[] value = new char[str_length]; + obj.GetString(key, value, str_length); + + cell_length = json_cell_string_size(value); + } + case JSON_Type_Int: { + cell_length = JSON_INT_BUFFER_SIZE; + } + #if SM_INT64_SUPPORTED + case JSON_Type_Int64: { + cell_length = JSON_INT64_BUFFER_SIZE; + } + #endif + case JSON_Type_Float: { + cell_length = JSON_FLOAT_BUFFER_SIZE; + } + case JSON_Type_Bool: { + cell_length = JSON_BOOL_BUFFER_SIZE; + } + case JSON_Type_Object: { + child = obj.GetObject(key); + cell_length = child != null + ? json_encode_size(child, options, depth + 1) + : JSON_NULL_BUFFER_SIZE; + } + } + + // fit the contents into the cell + char[] cell = new char[cell_length]; + switch (type) { + case JSON_Type_String: { + char[] value = new char[str_length]; + obj.GetString(key, value, str_length); + json_cell_string(value, cell, cell_length); + } + case JSON_Type_Int: { + int value = obj.GetInt(key); + IntToString(value, cell, cell_length); + } + #if SM_INT64_SUPPORTED + case JSON_Type_Int64: { + int value[2]; + obj.GetInt64(key, value); + Int64ToString(value, cell, cell_length); + } + #endif + case JSON_Type_Float: { + float value = obj.GetFloat(key); + FloatToString(value, cell, cell_length); + + // trim trailing 0s from float output up until decimal point + int last_char = strlen(cell) - 1; + while (cell[last_char] == '0' && cell[last_char - 1] != '.') { + cell[last_char--] = '\0'; + } + } + case JSON_Type_Bool: { + bool value = obj.GetBool(key); + strcopy(cell, cell_length, value ? "true" : "false"); + } + case JSON_Type_Object: { + if (child != null) { + json_encode(child, cell, cell_length, options, depth + 1); + } else { + strcopy(cell, cell_length, "null"); + } + } + } + + // make the builder fit our key:value + // use previously determined cell length and + 1 for , + builder_size = cell_length + 1; + if (! is_array) { + // get the length of the key and + 1 for : + builder_size += json_cell_string_size(key) + 1; + + if (pretty_print) { + builder_size += strlen(JSON_PP_AFTER_COLON); + } + } + + char[] builder = new char[builder_size]; + strcopy(builder, builder_size, ""); + + // add the key if we're working with an object + if (! is_array) { + json_cell_string(key, builder, builder_size); + StrCat(builder, builder_size, ":"); + + if (pretty_print) { + StrCat(builder, builder_size, JSON_PP_AFTER_COLON); + } + } + + // add the value and a trailing comma + StrCat(builder, builder_size, cell); + StrCat(builder, builder_size, ","); + + // prepare pretty printing then send builder to output afterwards + if (pretty_print) { + StrCat(output, max_size, JSON_PP_NEWLINE); + + for (int j = 0; j < depth + 1; j += 1) { + StrCat(output, max_size, JSON_PP_INDENT); + } + } + + StrCat(output, max_size, builder); + + is_empty = false; + } + + if (! is_empty) { + // remove the final comma + output[strlen(output) - 1] = '\0'; + + if (pretty_print) { + StrCat(output, max_size, JSON_PP_NEWLINE); + + for (int j = 0; j < depth; j += 1) { + StrCat(output, max_size, JSON_PP_INDENT); + } + } + } + + // append closing bracket + StrCat(output, max_size, is_array ? "]" : "}"); +} + +/** + * Decodes a JSON string into a JSON instance. + * + * @param buffer Buffer to decode. + * @param options Bitwise combination of `JSON_DECODE_*` options. + * @param pos Current position of the decoder as bytes + * offset into the buffer. + * @param depth Current nested depth of the decoder. + * @return JSON instance or null if decoding failed becase + * the buffer didn't contain valid JSON. + * @error If the buffer does not contain valid JSON, + * an error will be thrown. + */ +stock JSON_Object json_decode( + const char[] buffer, + int options = JSON_NONE, + int &pos = 0, + int depth = 0 +) +{ + int length = strlen(buffer); + // skip preceding whitespace + if (! json_skip_whitespace(buffer, length, pos)) { + json_set_last_error("buffer ended early at position %d", pos); + + return null; + } + + bool is_array = false; + JSON_Array arr = null; + JSON_Object obj = null; + if (buffer[pos] == '{') { + is_array = false; + obj = new JSON_Object(); + } else if (buffer[pos] == '[') { + is_array = true; + arr = new JSON_Array(); + } else { + json_set_last_error("no object or array found at position %d", pos); + + return null; + } + + bool allow_single_quotes = (options & JSON_DECODE_SINGLE_QUOTES) > 0; + + bool empty_checked = false; + + // while we haven't reached the end of our structure + while ( + (! is_array && buffer[pos] != '}') + || (is_array && buffer[pos] != ']') + ) { + // pos is either an opening structure or comma, so increment past it + pos += 1; + + // skip any whitespace preceding the element + if (! json_skip_whitespace(buffer, length, pos)) { + json_set_last_error("buffer ended early at position %d", pos); + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + + return null; + } + + // if we haven't checked for empty yet and we are at the end + // of an object or array, we can stop here (empty structure) + if (! empty_checked) { + if ( + (! is_array && buffer[pos] == '}') + || (is_array && buffer[pos] == ']') + ) { + break; + } + + empty_checked = true; + } + + int key_length = 1; + if (! is_array) { + // if dealing with an object, look for the key and determine length + if (! json_is_string(buffer[pos], allow_single_quotes)) { + json_set_last_error("expected key string at position %d", pos); + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + + return null; + } + + key_length = json_extract_string_size(buffer, length, pos); + } + + char[] key = new char[key_length]; + + if (! is_array) { + // extract the key from the buffer + json_extract_string(buffer, length, pos, key, key_length, is_array); + + // skip any whitespace following the key + if (! json_skip_whitespace(buffer, length, pos)) { + json_set_last_error("buffer ended early at position %d", pos); + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + + return null; + } + + // ensure that we find a colon + if (buffer[pos++] != ':') { + json_set_last_error( + "expected colon after key at position %d", + pos + ); + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + + return null; + } + + // skip any whitespace following the colon + if (! json_skip_whitespace(buffer, length, pos)) { + json_set_last_error("buffer ended early at position %d", pos); + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + + return null; + } + } + + int cell_length = 1; + JSONCellType cell_type = JSON_Type_Invalid; + if (buffer[pos] == '{' || buffer[pos] == '[') { + cell_type = JSON_Type_Object; + } else if (json_is_string(buffer[pos], allow_single_quotes)) { + cell_type = JSON_Type_String; + cell_length = json_extract_string_size(buffer, length, pos); + } else { + // in this particular instance, we use JSON_Type_Invalid to + // represent any type that isn't an object or string + cell_length = json_extract_until_end_size( + buffer, + length, + pos, + is_array + ); + } + + if (! is_array && obj.HasKey(key)) { + obj.Remove(key); + } + + char[] cell = new char[cell_length]; + switch (cell_type) { + case JSON_Type_Object: { + // if we are dealing with an object or array, decode recursively + JSON_Object value = json_decode( + buffer, + options, + pos, + depth + 1 + ); + + // decoding failed, error will be logged in json_decode + if (value == null) { + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + + return null; + } + + if (is_array) { + arr.PushObject(value); + } else { + obj.SetObject(key, value); + } + } + case JSON_Type_String: { + // if we are dealing with a string, attempt to extract it + if (! json_extract_string( + buffer, + length, + pos, + cell, + cell_length, + is_array + )) { + json_set_last_error( + "couldn't extract string at position %d", + pos + ); + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + + return null; + } + + if (is_array) { + arr.PushString(cell); + } else { + obj.SetString(key, cell); + } + } + case JSON_Type_Invalid: { + if (! json_extract_until_end( + buffer, + length, + pos, + cell, + cell_length, + is_array + )) { + json_set_last_error( + "couldn't extract until end at position %d", + pos + ); + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + + return null; + } + + if (strlen(cell) == 0) { + json_set_last_error( + "empty cell encountered at position %d", + pos + ); + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + + return null; + } + + if (json_is_int(cell)) { + int value = StringToInt(cell); + #if SM_INT64_SUPPORTED + if (json_is_int64(cell, value)) { + int values[2]; + StringToInt64(cell, values); + + if (is_array) { + arr.PushInt64(values); + } else { + obj.SetInt64(key, values); + } + } else { + if (is_array) { + arr.PushInt(value); + } else { + obj.SetInt(key, value); + } + } + #else + if (is_array) { + arr.PushInt(value); + } else { + obj.SetInt(key, value); + } + #endif + } else if (json_is_float(cell)) { + float value = StringToFloat(cell); + if (is_array) { + arr.PushFloat(value); + } else { + obj.SetFloat(key, value); + } + } else if (StrEqual(cell, "true") || StrEqual(cell, "false")) { + bool value = StrEqual(cell, "true"); + if (is_array) { + arr.PushBool(value); + } else { + obj.SetBool(key, value); + } + } else if (StrEqual(cell, "null")) { + if (is_array) { + arr.PushObject(null); + } else { + obj.SetObject(key, null); + } + } else { + json_set_last_error( + "unknown type encountered at position %d: %s", + pos, + cell + ); + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + + return null; + } + } + } + + if (! json_skip_whitespace(buffer, length, pos)) { + json_set_last_error("buffer ended early at position %d", pos); + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + + return null; + } + } + + // skip remaining whitespace and ensure we're at the end of the buffer + pos += 1; + if (json_skip_whitespace(buffer, length, pos) && depth == 0) { + json_set_last_error( + "unexpected data after structure end at position %d", + pos + ); + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + + return null; + } + + return is_array ? view_as(arr) : obj; +} + +/** + * Encodes the object with the options provided and writes + * the output to the file at the path specified. + * + * @param obj Object to encode/write to file. + * @param path Path of file to write to. + * @param options Options to pass to `json_encode`. + * @return True on success, false otherwise. + */ +stock bool json_write_to_file( + JSON_Object obj, + const char[] path, + int options = JSON_NONE +) +{ + File f = OpenFile(path, "wb"); + if (f == null) { + return false; + } + + int size = json_encode_size(obj, options); + char[] buffer = new char[size]; + json_encode(obj, buffer, size, options); + + bool success = f.WriteString(buffer, false); + delete f; + + return success; +} + +/** + * Reads and decodes the contents of a JSON file. + * + * @param path Path of file to read from. + * @param options Options to pass to `json_decode`. + * @return The decoded object on success, null otherwise. + */ +stock JSON_Object json_read_from_file(const char[] path, int options = JSON_NONE) +{ + File f = OpenFile(path, "rb"); + if (f == null) { + return null; + } + + f.Seek(0, SEEK_END); + int size = f.Position + 1; + char[] buffer = new char[size]; + + f.Seek(0, SEEK_SET); + f.ReadString(buffer, size); + delete f; + + return json_decode(buffer, options); +} + +/** + * Creates a shallow copy of the specified object. + * + * @param obj Object to copy. + * @return A shallow copy of the specified object. + */ +stock JSON_Object json_copy_shallow(JSON_Object obj) +{ + bool isArray = obj.IsArray; + JSON_Object result = isArray + ? view_as(new JSON_Array()) + : new JSON_Object(); + + if (isArray) { + view_as(result).Concat(view_as(obj)); + } else { + result.Merge(obj); + } + + return result; +} + +/** + * Creates a deep copy of the specified object. + * + * @param obj Object to copy. + * @return A deep copy of the specified object. + */ +stock JSON_Object json_copy_deep(JSON_Object obj) +{ + JSON_Object result = json_copy_shallow(obj); + + int length = obj.Length; + int key_length = 0; + for (int i = 0; i < length; i += 1) { + key_length = obj.GetKeySize(i); + char[] key = new char[key_length]; + obj.GetKey(i, key, key_length); + + // only deep copy objects + JSONCellType type = obj.GetType(key); + if (type != JSON_Type_Object) { + continue; + } + + JSON_Object value = obj.GetObject(key); + result.SetObject(key, value != null ? json_copy_deep(value) : null); + } + + return result; +} + +/** + * Recursively cleans up the instance and any instances stored within. + * + * @param obj Object to clean up. + */ +stock void json_cleanup(JSON_Object obj) +{ + if (obj == null) { + return; + } + + int length = obj.Length; + int key_length = 0; + for (int i = 0; i < length; i += 1) { + key_length = obj.GetKeySize(i); + char[] key = new char[key_length]; + obj.GetKey(i, key, key_length); + + // only clean up objects + JSONCellType type = obj.GetType(key); + if (type != JSON_Type_Object) { + continue; + } + + JSON_Object value = obj.GetObject(key); + if (value != null) { + json_cleanup(value); + } + } + + obj.Super.Cleanup(); +} + +/** + * Cleans up an object and sets the passed variable to null. + * + * @param obj Object to clean up. + */ +stock void json_cleanup_and_delete(JSON_Object &obj) +{ + json_cleanup(obj); + obj = null; +} diff --git a/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/array.inc b/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/array.inc new file mode 100644 index 0000000..5f41162 --- /dev/null +++ b/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/array.inc @@ -0,0 +1,955 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * A pure SourcePawn JSON encoder/decoder. + * https://github.com/clugg/sm-json + * + * sm-json (C)2022 James Dickens. (clug) + * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + */ + +#if defined _json_array_included + #endinput +#endif +#define _json_array_included + +#include +#include +#include +#include + +methodmap JSON_Array < JSON_Object +{ + /** + * @section Helpers + */ + + /** + * Views the instance as its superclass to access overridden methods. + */ + property JSON_Object Super + { + public get() + { + return view_as(this); + } + } + + /** + * The enforced type of the array. + */ + property JSONCellType Type + { + public get() + { + return view_as(this.Meta.GetOptionalValue( + JSON_ENFORCE_TYPE_KEY, + JSON_Type_Invalid + )); + } + + public set(JSONCellType value) + { + if (value == JSON_Type_Invalid) { + this.Meta.Remove(JSON_ENFORCE_TYPE_KEY); + } else { + this.Meta.SetValue(JSON_ENFORCE_TYPE_KEY, value); + } + } + } + + /** + * Checks whether the array accepts the type provided. + * + * @param type Type to check for enforcement. + * @return True if the type can be used, false otherwise. + */ + public bool CanUseType(JSONCellType type) + { + return this.Type == JSON_Type_Invalid || this.Type == type; + } + + /** + * Checks whether the object has an index. + * + * @param index Index to check existence of. + * @return True if the index exists, false otherwise. + */ + public bool HasKey(int index) + { + return index >= 0 && index < this.Length; + } + + /** + * @section Metadata Getters + */ + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * @internal + * + * @see MetaStringMap.GetMeta + */ + public any GetMeta(int index, JSONMetaInfo meta, any default_value) + { + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return default_value; + } + + return this.Super.GetMeta(key, meta, default_value); + } + + /** + * Gets the cell type stored at an index. + * + * @param index Index to get value type for. + * @return Value type for index provided, + * or JSON_Type_Invalid if it does not exist. + */ + public JSONCellType GetType(int index) + { + return view_as( + this.GetMeta(index, JSON_Meta_Type, JSON_Type_Invalid) + ); + } + + /** + * Gets the length of the string stored at an index. + * + * @param index Index to get string length for. + * @return Length of string at index provided, + * or -1 if it is not a string/does not exist. + */ + public int GetSize(int index) + { + return view_as(this.GetMeta(index, JSON_Meta_Size, -1)); + } + + /** + * Gets whether the index should be hidden from encoding. + * + * @param index Index to get hidden state for. + * @return Whether or not the index should be hidden. + */ + public bool GetHidden(int index) + { + return view_as(this.GetMeta(index, JSON_Meta_Hidden, false)); + } + + /** + * @section Metadata Setters + */ + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * @internal + * + * @see JSON_Object.SetMeta + */ + public bool SetMeta(int index, JSONMetaInfo meta, any value) + { + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + return this.Super.SetMeta(key, meta, value); + } + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * @internal + * + * @see JSON_Object.RemoveMeta + */ + public bool RemoveMeta(int index, JSONMetaInfo meta) + { + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + return this.Super.RemoveMeta(key, meta); + } + + /** + * Sets whether the index should be hidden from encoding. + * + * @param index Index to set hidden state for. + * @param hidden Whether or not the index should be hidden. + * @return True on success, false otherwise. + */ + public bool SetHidden(int index, bool hidden) + { + return this.SetMeta(index, JSON_Meta_Hidden, hidden); + } + + /** + * @section Getters + */ + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see MetaStringMap.GetValue + */ + public bool GetValue(int index, any &value) + { + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + return this.Super.GetValue(key, value); + } + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * @internal + * + * @see MetaStringMap.GetOptionalValue + */ + public any GetOptionalValue(int index, any default_value = -1) + { + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + return this.Super.GetOptionalValue(key, default_value); + } + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see MetaStringMap.GetString + */ + public bool GetString(int index, char[] value, int max_size, int &size = 0) + { + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + return this.Super.GetString(key, value, max_size, size); + } + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see MetaStringMap.GetInt + */ + public int GetInt(int index, int default_value = -1) + { + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return default_value; + } + + return this.Super.GetInt(key, default_value); + } + + #if SM_INT64_SUPPORTED + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see JSON_Object.GetInt64 + */ + public bool GetInt64(int index, int value[2]) + { + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + return this.Super.GetInt64(key, value); + } + #endif + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see MetaStringMap.GetFloat + */ + public float GetFloat(int index, float default_value = -1.0) + { + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return default_value; + } + + return this.Super.GetFloat(key, default_value); + } + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see MetaStringMap.GetBool + */ + public bool GetBool(int index, bool default_value = false) + { + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return default_value; + } + + return this.Super.GetBool(key, default_value); + } + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see MetaStringMap.GetObject + */ + public JSON_Object GetObject(int index, JSON_Object default_value = null) + { + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return default_value; + } + + return this.Super.GetObject(key, default_value); + } + + /** + * @section Setters + */ + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see JSON_Object.SetString + */ + public bool SetString(int index, const char[] value) + { + if (! this.CanUseType(JSON_Type_String)) { + return false; + } + + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + return this.Super.SetString(key, value); + } + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see JSON_Object.SetInt + */ + public bool SetInt(int index, int value) + { + if (! this.CanUseType(JSON_Type_Int)) { + return false; + } + + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + return this.Super.SetInt(key, value); + } + + #if SM_INT64_SUPPORTED + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see JSON_Object.SetInt64 + */ + public bool SetInt64(int index, int value[2]) + { + if (! this.CanUseType(JSON_Type_Int64)) { + return false; + } + + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + return this.Super.SetInt64(key, value); + } + #endif + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see JSON_Object.SetFloat + */ + public bool SetFloat(int index, float value) + { + if (! this.CanUseType(JSON_Type_Float)) { + return false; + } + + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + return this.Super.SetFloat(key, value); + } + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see JSON_Object.SetBool + */ + public bool SetBool(int index, bool value) + { + if (! this.CanUseType(JSON_Type_Bool)) { + return false; + } + + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + return this.Super.SetBool(key, value); + } + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see JSON_Object.SetObject + */ + public bool SetObject(int index, JSON_Object value) + { + if (! this.CanUseType(JSON_Type_Object)) { + return false; + } + + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + return this.Super.SetObject(key, value); + } + + /** + * @section Pushers + */ + + /** + * Pushes a string to the end of the array. + * + * @param value Value to push. + * @return The element's index on success, -1 otherwise. + */ + public int PushString(const char[] value) + { + int index = this.Length; + if (! this.SetString(index, value)) { + return -1; + } + + return index; + } + + /** + * Pushes an int to the end of the array. + * + * @param value Value to push. + * @return The element's index on success, -1 otherwise. + */ + public int PushInt(int value) + { + int index = this.Length; + if (! this.SetInt(index, value)) { + return -1; + } + + return index; + } + + #if SM_INT64_SUPPORTED + /** + * Pushes an int64 to the end of the array. + * + * @param value Value to push. + * @return The element's index on success, -1 otherwise. + */ + public int PushInt64(int value[2]) + { + int index = this.Length; + if (! this.SetInt64(index, value)) { + return -1; + } + + return index; + } + #endif + + /** + * Pushes a float to the end of the array. + * + * @param value Value to push. + * @return The element's index on success, -1 otherwise. + */ + public int PushFloat(float value) + { + int index = this.Length; + if (! this.SetFloat(index, value)) { + return -1; + } + + return index; + } + + /** + * Pushes a bool to the end of the array. + * + * @param value Value to push. + * @return The element's index on success, -1 otherwise. + */ + public int PushBool(bool value) + { + int index = this.Length; + if (! this.SetBool(index, value)) { + return -1; + } + + return index; + } + + /** + * Pushes a JSON object to the end of the array. + * + * @param value Value to push. + * @return The element's index on success, -1 otherwise. + */ + public int PushObject(JSON_Object value) + { + int index = this.Length; + if (! this.SetObject(index, value)) { + return -1; + } + + return index; + } + + /** + * @section Search Helpers + */ + + /** + * Finds the index of a value in the array. + * + * @param value Value to search for. + * @return The index of the value if it is found, -1 otherwise. + */ + public int IndexOf(any value) + { + any current; + int length = this.Length; + for (int i = 0; i < length; i += 1) { + if (this.GetValue(i, current) && value == current) { + return i; + } + } + + return -1; + } + + /** + * Finds the index of a string in the array. + * + * @param value String to search for. + * @return The index of the string if it is found, -1 otherwise. + */ + public int IndexOfString(const char[] value) + { + int length = this.Length; + for (int i = 0; i < length; i += 1) { + if (this.GetType(i) != JSON_Type_String) { + continue; + } + + int current_size = this.GetSize(i); + char[] current = new char[current_size]; + this.GetString(i, current, current_size); + if (StrEqual(value, current)) { + return i; + } + } + + return -1; + } + + /** + * Determines whether the array contains a value. + * + * @param value Value to search for. + * @return True if the value is found, false otherwise. + */ + public bool Contains(any value) + { + return this.IndexOf(value) != -1; + } + + /** + * Determines whether the array contains a string. + * + * @param value String to search for. + * @return True if the string is found, false otherwise. + */ + public bool ContainsString(const char[] value) + { + return this.IndexOfString(value) != -1; + } + + /** + * @section Misc + */ + + /** + * Removes an index and its related meta-keys from the array, + * and shifts down all following element indices. + * + * @param key Key to remove. + * @return True on success, false if the value was never set. + */ + public bool Remove(int index) + { + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + int length = this.Length; + + // remove existing value at index + if (! this.Super.Remove(key)) { + return false; + } + + // shift all following elements down + char current_key[JSON_INT_BUFFER_SIZE]; + for (int oldIndex = index + 1; oldIndex < length; oldIndex += 1) { + int newIndex = oldIndex - 1; + JSONCellType type = this.GetType(oldIndex); + + switch (type) { + case JSON_Type_String: { + int str_length = this.GetSize(oldIndex); + char[] str_value = new char[str_length]; + + this.GetString(oldIndex, str_value, str_length); + this.SetString(newIndex, str_value); + } + case JSON_Type_Int: { + this.SetInt(newIndex, this.GetInt(oldIndex)); + } + #if SM_INT64_SUPPORTED + case JSON_Type_Int64: { + int value[2]; + this.GetInt64(oldIndex, value); + this.SetInt64(newIndex, value); + } + #endif + case JSON_Type_Float: { + this.SetFloat(newIndex, this.GetFloat(oldIndex)); + } + case JSON_Type_Bool: { + this.SetBool(newIndex, this.GetBool(oldIndex)); + } + case JSON_Type_Object: { + this.SetObject(newIndex, this.GetObject(oldIndex)); + } + } + + this.SetHidden(newIndex, this.GetHidden(oldIndex)); + + if (this.GetKey( + oldIndex, + current_key, + sizeof(current_key) + )) { + this.Super.Remove(current_key); + } + } + + return true; + } + + /** + * Concatenates the entries from the specified array + * on to the end of this array. + * + * @param from Array to concat entries from. + * @return True on success, false otherwise. + * @error If the object being merged is an object or the + * arrays being merged don't have the same strict + * type set, an error will be thrown. + */ + public bool Concat(JSON_Array from) + { + if (! this.IsArray || ! from.IsArray) { + json_set_last_error("attempted to concat using object(s)"); + + return false; + } + + if (this.Type != from.Type) { + json_set_last_error( + "attempted to concat arrays with mismatched strict types" + ); + + return false; + } + + int current_length = this.Length; + int json_size = from.Length; + for (int i = 0; i < json_size; i += 1) { + JSONCellType type = from.GetType(i); + // skip keys of unknown type + if (type == JSON_Type_Invalid) { + continue; + } + + // push value onto array + switch (type) { + case JSON_Type_String: { + int length = from.GetSize(i); + char[] value = new char[length]; + from.GetString(i, value, length); + + this.PushString(value); + } + case JSON_Type_Int: { + this.PushInt(from.GetInt(i)); + } + #if SM_INT64_SUPPORTED + case JSON_Type_Int64: { + int value[2]; + from.GetInt64(i, value); + this.PushInt64(value); + } + #endif + case JSON_Type_Float: { + this.PushFloat(from.GetFloat(i)); + } + case JSON_Type_Bool: { + this.PushBool(from.GetBool(i)); + } + case JSON_Type_Object: { + this.PushObject(from.GetObject(i)); + } + } + + this.SetHidden(current_length + i, from.GetHidden(i)); + } + + return true; + } + + /** + * Typed Helpers + */ + + /** + * The length of the longest string in the array. + */ + property int MaxStringLength + { + public get() + { + int max = -1; + int current = -1; + int length = this.Length; + for (int i = 0; i < length; i += 1) { + if (this.GetType(i) != JSON_Type_String) { + continue; + } + + current = this.GetSize(i); + if (current > max) { + max = current; + } + } + + return max; + } + } + + /** + * Sets the array to enforce a specific type. + * This will fail if there are any existing elements + * in the array which are not of the same type. + * + * @param type Type to enforce. + * @return True if the type was enforced successfully, false otherwise. + */ + public bool EnforceType(JSONCellType type) + { + if (type == JSON_Type_Invalid) { + this.Type = type; + + return true; + } + + int length = this.Length; + for (int i = 0; i < length; i += 1) { + if (this.GetType(i) != type) { + return false; + } + } + + this.Type = type; + + return true; + } + + /** + * Imports a native array's values into the instance. + * + * @param type Type of native values. + * @param values Array of values. + * @param size Size of array. + * @return True on success, false otherwise. + */ + public bool ImportValues(JSONCellType type, any[] values, int size) + { + bool success = true; + for (int i = 0; i < size; i += 1) { + switch (type) { + case JSON_Type_Int: { + success = success && this.PushInt(values[i]) > -1; + } + case JSON_Type_Float: { + success = success && this.PushFloat(values[i]) > -1; + } + case JSON_Type_Bool: { + success = success && this.PushBool(values[i]) > -1; + } + case JSON_Type_Object: { + success = success && this.PushObject(values[i]) > -1; + } + } + } + + return success; + } + + /** + * Imports a native array's strings into the instance. + * + * @param strings Array of strings. + * @param size Size of array. + * @return True on success, false otherwise. + */ + public bool ImportStrings(const char[][] strings, int size) + { + bool success = true; + for (int i = 0; i < size; i += 1) { + success = success && this.PushString(strings[i]) > -1; + } + + return success; + } + + /** + * Exports the instance's values into a native array. + * + * @param values Array to export to. + * @param max_size Maximum size of array. + */ + public void ExportValues(any[] values, int max_size) + { + int length = this.Length; + if (length < max_size) { + max_size = length; + } + + for (int i = 0; i < max_size; i += 1) { + this.GetValue(i, values[i]); + } + } + + /** + * Exports the instance's strings into a native array. + * + * @param values Array to export to. + * @param max_size Maximum size of array. + * @param max_string_size Maximum size of array elements. + */ + public void ExportStrings( + char[][] values, + int max_size, + int max_string_size + ) { + int length = this.Length; + if (length < max_size) { + max_size = length; + } + + for (int i = 0; i < max_size; i += 1) { + this.GetString(i, values[i], max_string_size); + } + } + + /** + * json.inc Aliases + */ + + /** @see JSON_Object.ShallowCopy */ + public JSON_Array ShallowCopy() + { + return view_as(this.Super.ShallowCopy()); + } + + /** @see JSON_Object.DeepCopy */ + public JSON_Array DeepCopy() + { + return view_as(this.Super.DeepCopy()); + } + + /** + * @section Constructor + */ + + /** + * Creates a new JSON_Array. + * + * @param type The type to enforce for this array, or + * JSON_Type_Invalid for no enforced type. + * @return A new JSON_Array. + */ + public JSON_Array(JSONCellType type = JSON_Type_Invalid) + { + JSON_Array self = view_as(new JSON_Object()); + self.Meta.SetBool(JSON_ARRAY_KEY, true); + self.EnforceType(type); + + return self; + } +}; diff --git a/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/definitions.inc b/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/definitions.inc new file mode 100644 index 0000000..eba7b9c --- /dev/null +++ b/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/definitions.inc @@ -0,0 +1,180 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * A pure SourcePawn JSON encoder/decoder. + * https://github.com/clugg/sm-json + * + * sm-json (C)2022 James Dickens. (clug) + * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + */ + +#if defined _json_definitions_included + #endinput +#endif +#define _json_definitions_included + +#include +#include + +#define SM_INT64_SUPPORTED SOURCEMOD_V_MAJOR >= 1 \ + && SOURCEMOD_V_MINOR >= 11 \ + && SOURCEMOD_V_REV >= 6861 + +/** + * @section Settings + */ + +/** Used when no options are desired. */ +#define JSON_NONE 0 + +/** + * @section json_encode settings + */ + +/** Should encoded output be pretty printed? */ +#define JSON_ENCODE_PRETTY 1 << 0 + +/** + * @section json_decode settings + */ + +/** Should single quote wrapped strings be accepted during decoding? */ +#define JSON_DECODE_SINGLE_QUOTES 1 << 0 + +/** + * @section json_merge settings + */ + +/** During merge, should existing keys be replaced if they exist in both objects? */ +#define JSON_MERGE_REPLACE 1 << 0 + +/** During merge, should existing objects be cleaned up if they exist in + * both objects? (only applies when JSON_MERGE_REPLACE is also set) */ +#define JSON_MERGE_CLEANUP 1 << 1 + +/** + * @section Pretty Print Constants + * + * Used to determine how pretty printed JSON should be formatted when encoded. + * You can modify these if you prefer your JSON formatted differently. + */ + +char JSON_PP_AFTER_COLON[32] = " "; +char JSON_PP_INDENT[32] = " "; +char JSON_PP_NEWLINE[32] = "\n"; + +/** + * @section Buffer Size Constants + */ + +/** The longest representable integer ("-2147483648") + NULL terminator */ +#define JSON_INT_BUFFER_SIZE 12 + +#if SM_INT64_SUPPORTED +/** The longest representable int64 ("-9223372036854775808") + NULL terminator */ +#define JSON_INT64_BUFFER_SIZE 21 +#endif + +/** You may need to change this if you are working with large floats. */ +#define JSON_FLOAT_BUFFER_SIZE 32 + +/** "true"|"false" + NULL terminator */ +#define JSON_BOOL_BUFFER_SIZE 6 + +/** "null" + NULL terminator */ +#define JSON_NULL_BUFFER_SIZE 5 + +/** + * @section Array/Object Constants + */ + +#define JSON_ARRAY_KEY "is_array" +#define JSON_ENFORCE_TYPE_KEY "enforce_type" + +/** + * Types of cells within a JSON object + */ +enum JSONCellType { + JSON_Type_Invalid = -1, + JSON_Type_String = 0, + JSON_Type_Int, + #if SM_INT64_SUPPORTED + JSON_Type_Int64, + #endif + JSON_Type_Float, + JSON_Type_Bool, + JSON_Type_Object +}; + +/** + * Types of metadata a JSON element can have + */ +enum JSONMetaInfo { + JSON_Meta_Type = 0, + JSON_Meta_Size, + JSON_Meta_Hidden, + JSON_Meta_Index +} + +/** + * An array of all possible meta info values. + */ +JSONMetaInfo JSON_ALL_METADATA[4] = { + JSON_Meta_Type, JSON_Meta_Size, JSON_Meta_Hidden, JSON_Meta_Index +}; + +/** + * Calculates the length required to store a meta key + * for a specified key/metainfo combination. + * @internal + * + * @param key + * @return The length required to store the meta key. + */ +stock int json_meta_key_length(const char[] key) +{ + // %s:%d + return strlen(key) + 1 + JSON_INT_BUFFER_SIZE; +} + +/** + * Formats the key/metainfo combination into a buffer. + * @internal + * + * @param output String buffer to store output. + * @param max_size Maximum size of string buffer. + * @param key Key to generate metakey for. + * @param meta Meta information to generate metakey for. + */ +stock void json_format_meta_key( + char[] output, + int max_size, + const char[] key, + JSONMetaInfo meta +) +{ + FormatEx(output, max_size, "k|%s:%d", key, view_as(meta)); +} diff --git a/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/helpers/decode.inc b/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/helpers/decode.inc new file mode 100644 index 0000000..4912b7f --- /dev/null +++ b/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/helpers/decode.inc @@ -0,0 +1,567 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * A pure SourcePawn JSON encoder/decoder. + * https://github.com/clugg/sm-json + * + * sm-json (C)2022 James Dickens. (clug) + * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + */ + +#if defined _json_helpers_decode_included + #endinput +#endif +#define _json_helpers_decode_included + +#include +#include + +/** + * @section Determine Buffer Contents + */ + +/** + * Checks whether the character at the beginning of the buffer is whitespace. + * + * @param buffer String buffer of data. + * @return True if the first character in the buffer + * is whitespace, false otherwise. + */ +stock bool json_is_whitespace(const char[] buffer) +{ + return buffer[0] == ' ' + || buffer[0] == '\t' + || buffer[0] == '\r' + || buffer[0] == '\n'; +} + +/** + * Checks whether the character at the beginning + * of the buffer is the start of a string. + * + * @param buffer String buffer of data. + * @param allow_single_quotes Should strings using single quotes be accepted? + * @return True if the first character in the buffer + * is the start of a string, false otherwise. + */ +stock bool json_is_string(const char[] buffer, bool allow_single_quotes = false) +{ + return buffer[0] == '"' || (allow_single_quotes && buffer[0] == '\''); +} + +/** + * Checks whether the buffer provided contains an int. + * + * @param buffer String buffer of data. + * @return True if buffer contains an int, false otherwise. + */ +stock bool json_is_int(const char[] buffer) +{ + int length = strlen(buffer); + if (length == 0) { + return false; + } + + bool starts_with_zero = false; + bool has_digit_gt_zero = false; + + for (int i = 0; i < length; i += 1) { + // allow minus as first character only + if (i == 0 && buffer[i] == '-') { + continue; + } + + if (! IsCharNumeric(buffer[i])) { + return false; + } + + if (buffer[i] == '0') { + if (starts_with_zero) { + // detect repeating leading zeros + return false; + } else if (! has_digit_gt_zero) { + starts_with_zero = true; + } + } else if (starts_with_zero) { + // detect numbers with leading zero + return false; + } else { + has_digit_gt_zero = true; + } + } + + return true; +} + +#if SM_INT64_SUPPORTED +/** + * Checks whether the buffer provided contains an int64, assuming it has + * already been validated as an int and attempted to convert to an int32. + * + * @param buffer String buffer of data. + * @param value Converted int value to compare with. + * @return True if buffer contains an int64, false otherwise. + */ +stock bool json_is_int64(const char[] buffer, int value) +{ + if ( + (value == 0 && ! StrEqual(buffer, "0")) + || (value == -1 && ! StrEqual(buffer, "-1")) + ) { + // failed to produce output of validated int, must be 64bit + return true; + } + + if (buffer[0] != '-' && value < 0) { + // 32-bit unsigned positive int which is incorrectly + // interpreted as a negative signed int by sourcepawn + return true; + } + + if (buffer[0] == '-' && value > 0) { + return true; + } + + return false; +} +#endif + +/** + * Checks whether the buffer provided contains a float. + * + * @param buffer String buffer of data. + * @return True if buffer contains a float, false otherwise. + */ +stock bool json_is_float(const char[] buffer) +{ + int length = strlen(buffer); + if (length == 0) { + return false; + } + + bool starts_with_zero = false; + bool has_digit_gt_zero = false; + bool after_decimal = false; + bool has_digit_after_decimal = false; + bool after_exponent = false; + bool has_digit_after_exponent = false; + + for (int i = 0; i < length; i += 1) { + // allow minus as first character only + if (i == 0 && buffer[i] == '-') { + continue; + } + + // if we haven't encountered a decimal or exponent yet + if (! after_decimal && ! after_exponent) { + if (buffer[i] == '.') { + // if we encounter a decimal before any digits + if (! starts_with_zero && ! has_digit_gt_zero) { + return false; + } + + after_decimal = true; + } else if (buffer[i] == 'e' || buffer[i] == 'E') { + // if we encounter an exponent before any digits + if (! starts_with_zero && ! has_digit_gt_zero) { + return false; + } + + after_exponent = true; + } else if (IsCharNumeric(buffer[i])) { + if (buffer[i] == '0') { + if (starts_with_zero) { + // detect repeating leading zeros + return false; + } else if (! has_digit_gt_zero) { + starts_with_zero = true; + } + } else { + if (starts_with_zero) { + // detect numbers with leading zero + return false; + } + + has_digit_gt_zero = true; + } + } else { + return false; + } + } else if (after_decimal && ! after_exponent) { + // after decimal has been encountered, allow any numerics + if (IsCharNumeric(buffer[i])) { + has_digit_after_decimal = true; + } else if (buffer[i] == 'e' || buffer[i] == 'E') { + if (! has_digit_after_decimal) { + // detect exponents directly after decimal + return false; + } + + after_exponent = true; + } else { + return false; + } + } else if (after_exponent) { + if ( + (buffer[i] == '+' || buffer[i] == '-') + && (buffer[i - 1] == 'e' || buffer[i - 1] == 'E') + ) { + // allow + or - directly after exponent + continue; + } else if (IsCharNumeric(buffer[i])) { + has_digit_after_exponent = true; + } else { + return false; + } + } + } + + // if we have a decimal, there should be digit(s) after it + if (after_decimal && ! has_digit_after_decimal) { + return false; + } + + // if we have an exponent, there should be digit(s) after it + if (after_exponent && ! has_digit_after_exponent) { + return false; + } + + // we should have reached an exponent, decimal, or both + // otherwise this number can be handled by the int parser + return after_decimal || after_exponent; +} + +/** + * Checks whether the character at the beginning of the buffer + * is considered a valid 'end point' for an element, + * such as a colon (indicating the end of a key), + * a comma (indicating the end of an element), + * or the end of an object or array. + * + * @param buffer String buffer of data. + * @param is_array Whether the decoder is processing an array. + * @return True if the first character in the buffer + * is a valid element end point, false otherwise. + */ +stock bool json_is_at_end(const char[] buffer, bool is_array) +{ + return buffer[0] == ',' + || (! is_array && (buffer[0] == ':' || buffer[0] == '}')) + || (is_array && buffer[0] == ']'); +} + +/** + * @section Extract Contents from Buffer + */ + +/** + * Moves the position until it reaches a non-whitespace + * character or the end of the buffer's maximum size. + * + * @param buffer String buffer of data. + * @param max_size Maximum size of string buffer. + * @param pos Position to increment. + * @return True if pos has not reached the end + * of the buffer, false otherwise. + */ +stock bool json_skip_whitespace(const char[] buffer, int max_size, int &pos) +{ + while (json_is_whitespace(buffer[pos]) && pos < max_size) { + pos += 1; + } + + return pos < max_size; +} + +/** + * Calculates the size of the buffer required to store the next + * JSON cell stored in the provided buffer at the provided position. + * This function is quite forgiving of malformed input and shouldn't be + * relied upon as proof that the input is valid. + * + * @param buffer String buffer of data. + * @param max_size Maximum size of string buffer. + * @param pos Position to increment. + * @param is_array Whether the decoder is processing an array. + * @return The size of the buffer required to store the cell. + */ +stock int json_extract_until_end_size( + const char[] buffer, + int max_size, + int pos, + bool is_array +) +{ + int length = 1; // for NULL terminator + + // while we haven't hit whitespace, an end point or the end of the buffer + while ( + ! json_is_whitespace(buffer[pos]) + && ! json_is_at_end(buffer[pos], is_array) + && pos < max_size + ) { + pos += 1; + length += 1; + } + + return length; +} + +/** + * Extracts a JSON cell from the buffer until a valid end point is reached. + * + * @param buffer String buffer of data. + * @param max_size Maximum size of string buffer. + * @param pos Position to increment. + * @param output String buffer to store output. + * @param output_max_size Maximum size of output string buffer. + * @param is_array Whether the decoder is processing an array. + * @return True if pos has not reached the end + * of the buffer, false otherwise. + */ +stock bool json_extract_until_end( + const char[] buffer, + int max_size, + int &pos, + char[] output, + int output_max_size, + bool is_array +) { + strcopy(output, output_max_size, ""); + + // set start to position of first character in cell + int start = pos; + + // while we haven't hit whitespace, an end point or the end of the buffer + while ( + ! json_is_whitespace(buffer[pos]) + && ! json_is_at_end(buffer[pos], is_array) + && pos < max_size + ) { + pos += 1; + } + + // set end to the current position + int end = pos; + + // skip any following whitespace + json_skip_whitespace(buffer, max_size, pos); + + // if we aren't at a valid endpoint, extraction has failed + if (! json_is_at_end(buffer[pos], is_array)) { + return false; + } + + // copy only from start with length end - start + NULL terminator + strcopy(output, end - start + 1, buffer[start]); + + return pos < max_size; +} + +/** + * Calculates the size of the buffer required to store the next + * JSON string stored in the provided buffer at the provided position. + * This function is quite forgiving of malformed input and shouldn't be + * relied upon as proof that the input is valid. + * + * @param buffer String buffer of data. + * @param max_size Maximum size of string buffer. + * @param pos Position to increment. + * @return The size of the buffer required to store the string. + */ +stock int json_extract_string_size(const char[] buffer, int max_size, int pos) +{ + int length = 1; // for NULL terminator + + // store initial quote + char quote = buffer[pos]; + + // increment past opening quote + pos += 1; + + // while we haven't hit the end of the buffer + int continuous_backslashes = 0; + while (pos < max_size) { + if (buffer[pos] == quote) { + // if we have an even number of preceding backslashes, + // the quote isn't escaped so this is the end of the string + if (continuous_backslashes % 2 == 0) { + break; + } + } + + if (buffer[pos] == '\\') { + continuous_backslashes += 1; + } else { + continuous_backslashes = 0; + } + + // pass over the character as it is part of the string + pos += 1; + length += 1; + } + + return length; +} + +/** + * Extracts a JSON string from the buffer until a valid end point is reached. + * + * @param buffer String buffer of data. + * @param max_size Maximum size of string buffer. + * @param pos Position to increment. + * @param output String buffer to store output. + * @param output_max_size Maximum size of output string buffer. + * @param is_array Whether the decoder is processing an array. + * @return True if pos has not reached the end + * of the buffer, false otherwise. + */ +stock bool json_extract_string( + const char[] buffer, + int max_size, + int &pos, + char[] output, + int output_max_size, + bool is_array +) { + strcopy(output, output_max_size, ""); + + // store initial quote + char quote = buffer[pos]; + + // increment past opening quote + pos += 1; + + // set start to position of first character in string + int start = pos; + + // while we haven't hit the end of the buffer + int continuous_backslashes = 0; + while (pos < max_size) { + // check for unescaped control characters + if ( + buffer[pos] == '\b' + || buffer[pos] == '\f' + || buffer[pos] == '\n' + || buffer[pos] == '\r' + || buffer[pos] == '\t' + ) { + return false; + } + + if (buffer[pos] == quote) { + // if we have an even number of preceding backslashes, + // the quote isn't escaped so this is the end of the string + if (continuous_backslashes % 2 == 0) { + break; + } + } + + if (buffer[pos] == '\\') { + continuous_backslashes += 1; + } else { + if (continuous_backslashes % 2 != 0) { + if (buffer[pos] == 'u') { + if (pos + 4 >= max_size) { + // less than 4 characters left in the buffer + return false; + } + + // ensure next 4 chars are hex and not a high surrogate + for (int i = 0; i < 4; i += 1) { + pos += 1; + + if (! json_char_is_hex(buffer[pos])) { + return false; + } + + if ( + i == 1 + && buffer[pos - 1] == 'D' + && buffer[pos] >= '8' + ) { + // detected a high surrogate value + return false; + } + } + + // jump back to the last hex char so it is safe to continue + pos -= 1; + } else if ( + buffer[pos] != '"' + && buffer[pos] != '\'' + && buffer[pos] != '/' + && buffer[pos] != 'b' + && buffer[pos] != 'f' + && buffer[pos] != 'n' + && buffer[pos] != 'r' + && buffer[pos] != 't' + ) { + // illegal escape detected + return false; + } + } + + continuous_backslashes = 0; + } + + // pass over the character as it is part of the string + pos += 1; + } + + // set end to the current position + int end = pos; + + // increment past closing quote + pos += 1; + + // skip trailing whitespace + if (! json_skip_whitespace(buffer, max_size, pos)) { + return false; + } + + // if we haven't reached an ending character at the end of the cell, + // there is likely junk data not encapsulated by a string + if (! json_is_at_end(buffer[pos], is_array)) { + return false; + } + + // copy only from start with length end - start + NULL terminator + int length = end - start + 1; + strcopy( + output, + length > output_max_size ? output_max_size : length, + buffer[start] + ); + + if (quote == '\'') { + ReplaceString(output, max_size, "\\'", "'"); + } + + json_unescape_string(output, max_size); + + return pos < max_size; +} diff --git a/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/helpers/errors.inc b/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/helpers/errors.inc new file mode 100644 index 0000000..7c3241c --- /dev/null +++ b/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/helpers/errors.inc @@ -0,0 +1,64 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * A pure SourcePawn JSON encoder/decoder. + * https://github.com/clugg/sm-json + * + * sm-json (C)2022 James Dickens. (clug) + * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + */ + +#if defined _json_helpers_errors_included + #endinput +#endif +#define _json_helpers_errors_included + +static char g_jsonLastError[1024] = ""; + +/** + * Stores the error provided as the 'last error' for later access. + * @internal + * + * @param error Error to store. + * @param ... Further arguments to pass to message formatter. + */ +stock void json_set_last_error(const char[] error, any ...) +{ + VFormat(g_jsonLastError, sizeof(g_jsonLastError), error, 2); +} + +/** + * Retrieves the last error encountered and stores it in the buffer provided. + * + * @param buffer String buffer. + * @param max_size Maximum size of string buffer. + * @return True if the error was copied successfuly, + * false otherwise. + */ +stock bool json_get_last_error(char[] buffer, int max_size) +{ + return strcopy(buffer, max_size, g_jsonLastError) > 0; +} diff --git a/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/helpers/metastringmap.inc b/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/helpers/metastringmap.inc new file mode 100644 index 0000000..b89134a --- /dev/null +++ b/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/helpers/metastringmap.inc @@ -0,0 +1,237 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * A pure SourcePawn JSON encoder/decoder. + * https://github.com/clugg/sm-json + * + * sm-json (C)2022 James Dickens. (clug) + * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + */ + +#if defined _metastringmap_included + #endinput +#endif +#define _metastringmap_included + +#include +#include + +/** + * A TypedStringMap which contains a nested `Data` `TypedStringMap` property. + * Standard methods and properties have been overridden to run against `Data`, + * but you can access the parent methods/properties using the `Meta` property. + */ +methodmap MetaStringMap < TypedStringMap +{ + /** + * @section Properties + */ + + /** + * Views the instance as its superclass to access overridden methods. + */ + property TypedStringMap Meta + { + public get() + { + return view_as(this); + } + } + + /** + * Gets the nested stringmap where data is stored. + */ + property TypedStringMap Data + { + public get() + { + return view_as(this.Meta.GetHandle("data")); + } + + public set(TypedStringMap value) + { + this.Meta.SetHandle("data", value); + } + } + + /** @see TypedStringMap.Length */ + property int Length { + public get() + { + return this.Data.Length; + } + } + + /** + * @section Getters + */ + + /** @see StringMap.GetValue */ + public bool GetValue(const char[] key, any &value) + { + return this.Data.GetValue(key, value); + } + + /** + * @see TypedStringMap.GetOptionalValue + * @internal + */ + public any GetOptionalValue(const char[] key, any default_value = -1) + { + return this.Data.GetOptionalValue(key, default_value); + } + + /** @see StringMap.GetString */ + public bool GetString( + const char[] key, + char[] value, + int max_size, + int &size = 0 + ) { + return this.Data.GetString(key, value, max_size, size); + } + + /** @see TypedStringMap.GetInt */ + public int GetInt(const char[] key, int default_value = -1) + { + return this.Data.GetInt(key, default_value); + } + + /** @see TypedStringMap.GetFloat */ + public float GetFloat(const char[] key, float default_value = -1.0) + { + return this.Data.GetFloat(key, default_value); + } + + /** @see TypedStringMap.GetBool */ + public bool GetBool(const char[] key, bool default_value = false) + { + return this.Data.GetBool(key, default_value); + } + + /** @see TypedStringMap.GetHandle */ + public Handle GetHandle(const char[] key, Handle default_value = null) + { + return this.Data.GetHandle(key, default_value); + } + + /** + * @section Setters + */ + + /** @see StringMap.SetValue */ + public bool SetValue(const char[] key, any value) + { + return this.Data.SetValue(key, value); + } + + /** @see StringMap.SetString */ + public bool SetString(const char[] key, const char[] value) + { + return this.Data.SetString(key, value); + } + + /** @see TypedStringMap.SetInt */ + public bool SetInt(const char[] key, int value) + { + return this.Data.SetInt(key, value); + } + + /** @see TypedStringMap.SetFloat */ + public bool SetFloat(const char[] key, float value) + { + return this.Data.SetFloat(key, value); + } + + /** @see TypedStringMap.SetBool */ + public bool SetBool(const char[] key, bool value) + { + return this.Data.SetBool(key, value); + } + + /** @see TypedStringMap.SetHandle */ + public bool SetHandle(const char[] key, Handle value) + { + return this.Data.SetHandle(key, value); + } + + /** @see StringMap.Remove */ + public bool Remove(const char[] key) + { + return this.Data.Remove(key); + } + + /** + * @section Misc + */ + + /** @see TypedStringMap.HasKey */ + public bool HasKey(const char[] key) + { + return this.Data.HasKey(key); + } + + /** @see StringMap.Clear */ + public void Clear() + { + TypedStringMap data = this.Data; + data.Clear(); + this.Meta.Clear(); + this.Data = data; + } + + /** + * Deletes the instance's data StringMap as well as the instance itself. + */ + public void Cleanup() + { + delete this.Data; + delete this; + } + + /** @see StringMap.Snapshot */ + public StringMapSnapshot Snapshot() + { + return this.Data.Snapshot(); + } + + /** + * @section Constructor + */ + + /** + * Creates a new MetaStringMap. + * + * @return A new MetaStringMap. + */ + public MetaStringMap() + { + MetaStringMap self = view_as(new TypedStringMap()); + self.Data = new TypedStringMap(); + + return self; + } +}; diff --git a/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/helpers/string.inc b/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/helpers/string.inc new file mode 100644 index 0000000..e01400c --- /dev/null +++ b/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/helpers/string.inc @@ -0,0 +1,247 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * A pure SourcePawn JSON encoder/decoder. + * https://github.com/clugg/sm-json + * + * sm-json (C)2022 James Dickens. (clug) + * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + */ + +#if defined _json_helpers_string_included + #endinput +#endif +#define _json_helpers_string_included + +#include + +/** + * Mapping characters to their escaped form. + */ +char JSON_STRING_NORMAL[][] = { + "\\", "\"", "/", "\b", "\f", "\n", "\r", "\t" +}; +char JSON_STRING_ESCAPED[][] = { + "\\\\", "\\\"", "\\/", "\\b", "\\f", "\\n", "\\r", "\\t" +}; + +/** + * Escapes a string in-place in a buffer. + * + * @param buffer String buffer. + * @param max_size Maximum size of string buffer. + */ +stock void json_escape_string(char[] buffer, int max_size) +{ + for (int i = 0; i < sizeof(JSON_STRING_NORMAL); i += 1) { + ReplaceString( + buffer, + max_size, + JSON_STRING_NORMAL[i], + JSON_STRING_ESCAPED[i] + ); + } + + int length = strlen(buffer) + 1; + for (int pos = 0; pos < length && pos < max_size; pos += 1) { + if (buffer[pos] < 0x80) { + // skip standard ascii values + continue; + } + + // consume the ascii bytes of the next utf8 character + int ascii_size; + int utf8 = json_ascii_to_utf8(buffer[pos], length - pos, ascii_size); + if (ascii_size <= 0) { + continue; + } + + // convert the utf8 value to escaped format + char escaped[7]; + FormatEx(escaped, sizeof(escaped), "\\u%04x", utf8); + + // duplicate the consumed byte array + ascii_size += 1; + char[] ascii = new char[ascii_size]; + for (int i = 0; i < ascii_size; i += 1) { + ascii[i] = buffer[pos + i]; + } + ascii[ascii_size - 1] = '\0'; + + // replace bytes with the escaped value + int replacements = ReplaceString(buffer, max_size, ascii, escaped); + + // calculate new string length based on replacements made + length -= replacements * ascii_size - 1; + length += replacements * sizeof(escaped) - 1; + + // skip to the last of the bytes we just replaced + pos += sizeof(escaped) - 2; + } +} + +/** + * Unescapes a string in-place in a buffer. + * + * @param buffer String buffer. + * @param max_size Maximum size of string buffer. + */ +stock void json_unescape_string(char[] buffer, int max_size) +{ + int length = strlen(buffer) + 1; + int continuous_backslashes = 0; + for (int pos = 0; pos < length && pos < max_size; pos += 1) { + if (buffer[pos] == '\\') { + continuous_backslashes += 1; + } else { + if (continuous_backslashes % 2 != 0 && buffer[pos] == 'u') { + // consume the entire escape starting at backslash + pos -= 1; + char escaped[7]; + for (int i = 0; i < 6; i += 1) { + escaped[i] = buffer[pos + i]; + } + escaped[sizeof(escaped) - 1] = '\0'; + + // convert the hex to decimal + int utf8 = StringToInt(escaped[2], 16); + + // convert the utf8 to ascii + int ascii_size = json_utf8_to_ascii_size(utf8) + 1; + char[] ascii = new char[ascii_size]; + int written = json_utf8_to_ascii(utf8, ascii, ascii_size); + + // replace the escaped value with ascii bytes + int replacements = ReplaceString( + buffer, + max_size, + escaped, + ascii, + false + ); + + // calculate new string length based on replacements made + length -= replacements * sizeof(escaped) - 1; + length += replacements * written; + + // skip to the last of the bytes we just replaced + pos += written - 1; + } + + continuous_backslashes = 0; + } + } + + for (int i = 0; i < sizeof(JSON_STRING_NORMAL); i += 1) { + ReplaceString( + buffer, + max_size, + JSON_STRING_ESCAPED[i], + JSON_STRING_NORMAL[i] + ); + } +} + +/** + * Checks whether the provided character is a valid hexadecimal character. + * + * @param c Character to check. + * @return True if c is a hexadecimal character, false otherwise. + */ +stock bool json_char_is_hex(int c) +{ + return ( + (c >= '0' && c <= '9') + || (c >= 'a' && c <= 'f') + || (c >= 'A' && c <= 'F') + ); +} + +/** + * Calculates the maximum buffer length required to + * store the JSON cell representation of a string. + * + * @param length The length of the string. + * @return Maximum buffer length. + */ +stock int json_cell_string_size(const char[] input) +{ + int size = 3; // for outside quotes + NULL terminator + + bool foundEscapeTarget = false; + int length = strlen(input); + for (int pos = 0; pos < length; pos += 1) { + foundEscapeTarget = false; + for (int i = 0; i < sizeof(JSON_STRING_NORMAL); i += 1) { + if (input[pos] == JSON_STRING_NORMAL[i][0]) { + size += 2; + foundEscapeTarget = true; + break; + } + } + + if (foundEscapeTarget) { + continue; + } + + if (input[pos] < 0x80) { + size += 1; + continue; + } + + // consume the ascii bytes of the next utf8 character + int ascii_size; + json_ascii_to_utf8(input[pos], length - pos, ascii_size); + if (ascii_size <= 0) { + continue; + } + + pos += ascii_size - 1; + size += 6; // for unicode escape is \uXXXX + } + + return size; +} + +/** + * Generates the JSON cell representation of a string. + * + * @param input Value to generate output for. + * @param output String buffer to store output. + * @param max_size Maximum size of string buffer. + */ +stock void json_cell_string(const char[] input, char[] output, int max_size) +{ + // add input string to output, offset for start/end quotes + strcopy(output[1], max_size - 2, input); + + // escape the output + json_escape_string(output[1], max_size - 2); + + // surround output with quotations + output[0] = '"'; + StrCat(output, max_size, "\""); +} diff --git a/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/helpers/typedstringmap.inc b/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/helpers/typedstringmap.inc new file mode 100644 index 0000000..07ef8c0 --- /dev/null +++ b/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/helpers/typedstringmap.inc @@ -0,0 +1,222 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * A pure SourcePawn JSON encoder/decoder. + * https://github.com/clugg/sm-json + * + * sm-json (C)2022 James Dickens. (clug) + * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + */ + +#if defined _typedstringmap_included + #endinput +#endif +#define _typedstringmap_included + +#include + +/** @see StringMap.ContainsKey */ +#define TRIE_SUPPORTS_CONTAINSKEY SOURCEMOD_V_MAJOR >= 1 \ + && SOURCEMOD_V_MINOR >= 11 \ + && SOURCEMOD_V_REV >= 6646 + +/** + * A StringMap with typed getters and setters. + */ +methodmap TypedStringMap < StringMap +{ + /** + * @section Properies + */ + + /** @see StringMap.Size */ + property int Length { + public get() + { + return this.Size; + } + } + + /** + * @section Misc + */ + + /** @see StringMap.ContainsKey */ + public bool HasKey(const char[] key) + { + #if TRIE_SUPPORTS_CONTAINSKEY + return this.ContainsKey(key); + #else + int dummy_int; + char dummy_str[1]; + + return this.GetValue(key, dummy_int) + || this.GetString(key, dummy_str, sizeof(dummy_str)); + #endif + } + + /** + * @section Getters + */ + + // GetValue is implemented natively by StringMap + + /** + * Retrieves the value stored at a key. + * @internal + * + * @param key Key to retrieve value for. + * @param default_value Value to return if the key does not exist. + * @return Value stored at key. + */ + public any GetOptionalValue(const char[] key, any default_value = -1) + { + any value; + return this.GetValue(key, value) ? value : default_value; + } + + // GetString is implemented natively by StringMap + + /** + * Retrieves the int stored at a key. + * + * @param key Key to retrieve int value for. + * @param default_value Value to return if the key does not exist. + * @return Value stored at key. + */ + public int GetInt(const char[] key, int default_value = -1) + { + return view_as(this.GetOptionalValue(key, default_value)); + } + + /** + * Retrieves the float stored at a key. + * + * @param key Key to retrieve float value for. + * @param default_value Value to return if the key does not exist. + * @return Value stored at key. + */ + public float GetFloat(const char[] key, float default_value = -1.0) + { + return view_as(this.GetOptionalValue(key, default_value)); + } + + /** + * Retrieves the bool stored at a key. + * + * @param key Key to retrieve bool value for. + * @param default_value Value to return if the key does not exist. + * @return Value stored at key. + */ + public bool GetBool(const char[] key, bool default_value = false) + { + return view_as(this.GetOptionalValue(key, default_value)); + } + + /** + * Retrieves the handle stored at a key. + * + * @param key Key to retrieve handle value for. + * @param default_value Value to return if the key does not exist. + * @return Value stored at key. + */ + public Handle GetHandle( + const char[] key, + Handle default_value = null + ) { + return view_as(this.GetOptionalValue(key, default_value)); + } + + /** + * @section Setters + */ + + // SetValue is implemented natively by StringMap + + // SetString is implemented natively by StringMap + + /** + * Sets the int stored at a key. + * + * @param key Key to set to int value. + * @param value Value to set. + * @return True on success, false otherwise. + */ + public bool SetInt(const char[] key, int value) + { + return this.SetValue(key, value); + } + + /** + * Sets the float stored at a key. + * + * @param key Key to set to float value. + * @param value Value to set. + * @return True on success, false otherwise. + */ + public bool SetFloat(const char[] key, float value) + { + return this.SetValue(key, value); + } + + /** + * Sets the bool stored at a key. + * + * @param key Key to set to bool value. + * @param value Value to set. + * @return True on success, false otherwise. + */ + public bool SetBool(const char[] key, bool value) + { + return this.SetValue(key, value); + } + + /** + * Sets the handle stored at a key. + * + * @param key Key to set to object value. + * @param value Value to set. + * @return True on success, false otherwise. + */ + public bool SetHandle(const char[] key, Handle value) + { + return this.SetValue(key, value); + } + + /** + * @section Constructor + */ + + /** + * Creates a new TypedStringMap. + * + * @return A new TypedStringMap. + */ + public TypedStringMap() + { + return view_as(CreateTrie()); + } +}; diff --git a/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/helpers/unicode.inc b/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/helpers/unicode.inc new file mode 100644 index 0000000..1c4bb43 --- /dev/null +++ b/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/helpers/unicode.inc @@ -0,0 +1,185 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * A pure SourcePawn JSON encoder/decoder. + * https://github.com/clugg/sm-json + * + * sm-json (C)2022 James Dickens. (clug) + * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + */ + +#if defined _json_helpers_unicode_included + #endinput +#endif +#define _json_helpers_unicode_included + +// most of the code here is adapted from https://dev.w3.org/XML/encoding.c + +/** + * Calculates how many bytes will be required to store the ASCII + * representation of a UTF-8 character. + * + * @param c The UTF-8 character. + * @return The number of bytes required, or -1 if c is invalid. + */ +stock int json_utf8_to_ascii_size(int c) +{ + if (c < 0 || c > 0x10FFFF) { + return -1; + } + + if (c < 0x80) { + return 1; + } else if (c < 0x800) { + return 2; + } else if (c < 0x10000) { + if (c >= 0xD800 && c <= 0xDFFF) { + // high surrogate + return -1; + } + + return 3; + } + + return 4; +} + +/** + * Converts a UTF-8 character to its ASCII representation. + * + * @param c The UTF-8 character. + * @param output String buffer to store output. + * @param max_size Maximum size of string buffer. + * @return The number of bytes written, or -1 if c is invalid. + */ +stock int json_utf8_to_ascii(int c, char[] output, int max_size) +{ + if (max_size < 1) { + return 0; + } + + if (c < 0 || c > 0x10FFFF) { + return -1; + } + + int size = 0; + if (c < 0x80) { + size = 1; + output[0] = c; + } else if (c < 0x800) { + size = 2; + output[0] = ((c >> 6) & 0x1F) | 0xC0; + } else if (c < 0x10000) { + if (c >= 0xD800 && c <= 0xDFFF) { + // high surrogate + return -1; + } + + size = 3; + output[0] = ((c >> 12) & 0x0F) | 0xE0; + } else { + size = 4; + output[0] = ((c >> 18) & 0x07) | 0xF0; + } + + if (size >= max_size) { + return -1; + } + + // first byte has already been calculated, calculate the rest + int i; + for (i = 1; i < size; i += 1) { + output[i] = ((c >> ((size - i - 1) * 6)) & 0x3F) | 0x80; + } + + return i; +} + +/** + * Converts bytes to their UTF-8 int representation. + * + * @param ascii The ascii/bytes to convert. + * @param max_size Maximum size of ascii. + * @return The UTF-8 int representation. + */ +stock int json_ascii_to_utf8(const char[] ascii, int max_size, int &size) +{ + size = 0; + if (max_size < 1) { + return -1; + } + + int c = 0; + if ((ascii[0] & 0x80) != 0) { + if (max_size < 2) { + return -1; + } + + if ((ascii[1] & 0xC0) != 0x80) { + return -1; + } + + if ((ascii[0] & 0xE0) == 0xE0) { + if (max_size < 3) { + return -1; + } + + if ((ascii[2] & 0xC0) != 0x80) { + return -1; + } + + if ((ascii[0] & 0xF0) == 0xF0) { + if (max_size < 4) { + return -1; + } + + if ((ascii[0] & 0xF8) != 0xF0 || (ascii[3] & 0xC0) != 0x80) { + return -1; + } + + size = 4; + c = (ascii[0] & 0x07) << 18; + } else { + size = 3; + c = (ascii[0] & 0x0F) << 12; + } + } else { + size = 2; + c = (ascii[0] & 0x1F) << 6; + } + } else { + size = 1; + c = ascii[0]; + } + + // first byte has already been calculated, calculate the rest + int i; + for (i = 1; i < size; i += 1) { + c |= (ascii[i] & 0x3F) << ((size - i - 1) * 6); + } + + return c; +} diff --git a/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/object.inc b/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/object.inc new file mode 100644 index 0000000..71a0603 --- /dev/null +++ b/libalternator/tests/corpus/json/addons/sourcemod/scripting/include/json/object.inc @@ -0,0 +1,724 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * A pure SourcePawn JSON encoder/decoder. + * https://github.com/clugg/sm-json + * + * sm-json (C)2022 James Dickens. (clug) + * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + */ + +#if defined _json_object_included + #endinput +#endif +#define _json_object_included + +#include +#include +#include +#include + +methodmap JSON_Object < MetaStringMap +{ + /** + * @section Properties + */ + + /** + * Views the instance as its superclass to access overridden methods. + */ + property MetaStringMap Super + { + public get() + { + return view_as(this); + } + } + + /** + * Whether the current object is an array. + */ + property bool IsArray { + public get() + { + return this.Meta.GetBool(JSON_ARRAY_KEY); + } + } + + /** + * @section Iteration Helpers + */ + + /** + * @section Metadata Getters + */ + + /** + * Gets the requested meta info for a key. + * @internal + * + * @param key Key to get meta info for. + * @param meta Meta info to get. + * @param default_value Value to return if meta does not exist. + * @return The meta value. + */ + public any GetMeta( + const char[] key, + JSONMetaInfo meta, + any default_value + ) { + int max_size = json_meta_key_length(key); + char[] meta_key = new char[max_size]; + json_format_meta_key(meta_key, max_size, key, meta); + + return this.Meta.GetOptionalValue(meta_key, default_value); + } + + /** + * Gets the cell type stored at a key. + * + * @param key Key to get value type for. + * @return Value type for key provided, + * or JSON_Type_Invalid if it does not exist. + */ + public JSONCellType GetType(const char[] key) + { + return view_as( + this.GetMeta(key, JSON_Meta_Type, JSON_Type_Invalid) + ); + } + + /** + * Gets the size of the string stored at a key. + * + * @param key Key to get buffer size for. + * @return Buffer size for string at key provided, + * or -1 if it is not a string/does not exist. + */ + public int GetSize(const char[] key) + { + return view_as(this.GetMeta(key, JSON_Meta_Size, -1)); + } + + /** + * Gets whether the key should be hidden from encoding. + * + * @param key Key to get hidden state for. + * @return Whether or not the key should be hidden. + */ + public bool GetHidden(const char[] key) + { + return view_as(this.GetMeta(key, JSON_Meta_Hidden, false)); + } + + /** + * Gets the index of a key. + * + * @param key Key to get index of. + * @return Index of the key provided, or -1 if it does not exist. + */ + public int GetIndex(const char[] key) + { + return view_as(this.GetMeta(key, JSON_Meta_Index, -1)); + } + + /** + * Gets the key stored at an index. + * If an array, will convert the index to its string value. + * If an array, will return false if the index is not between [0, length]. + * + * @param index Index of key. + * @param value Buffer to store key at. + * @param max_size Maximum size of value buffer. + * @return True on success, false otherwise. + */ + public bool GetKey(int index, char[] value, int max_size) + { + char[] index_key = new char[JSON_INT_BUFFER_SIZE]; + if (IntToString(index, index_key, JSON_INT_BUFFER_SIZE) == 0) { + return false; + } + + if (this.IsArray) { + // allow access of one past last index for intermediary operations + if (index < 0 || index > this.Length) { + return false; + } + + strcopy(value, max_size, index_key); + + return true; + } + + return this.Meta.GetString(index_key, value, max_size); + } + + /** + * Returns the buffer size required to store the key at the specified index. + * + * @param index Index of key. + * @return Buffer size required to store key. + */ + public int GetKeySize(int index) + { + if (this.IsArray) { + return JSON_INT_BUFFER_SIZE; + } + + int max_size = JSON_INT_BUFFER_SIZE + 4; + char[] index_size_key = new char[max_size]; + FormatEx(index_size_key, max_size, "%d:len", index); + + return this.Meta.GetInt(index_size_key); + } + + /** + * @section Metadata Setters + */ + + /** + * Sets meta info on a key. + * @internal + * + * @param key Key to set meta info for. + * @param meta Meta info to set. + * @param value Value to set. + * @return True on success, false otherwise. + */ + public bool SetMeta(const char[] key, JSONMetaInfo meta, any value) + { + int max_size = json_meta_key_length(key); + char[] meta_key = new char[max_size]; + json_format_meta_key(meta_key, max_size, key, meta); + + return this.Meta.SetValue(meta_key, value); + } + + /** + * Removes meta info from a key. + * @internal + * + * @param key Key to remove meta info from. + * @param meta Meta info to remove. + * @return True on success, false otherwise. + */ + public bool RemoveMeta(const char[] key, JSONMetaInfo meta) + { + int max_size = json_meta_key_length(key); + char[] meta_key = new char[max_size]; + json_format_meta_key(meta_key, max_size, key, meta); + + return this.Meta.Remove(meta_key); + } + + /** + * Sets whether the key should be hidden from encoding. + * + * @param key Key to set hidden state for. + * @param hidden Whether or not the key should be hidden. + * @return True on success, false otherwise. + */ + public bool SetHidden(const char[] key, bool hidden) + { + return hidden + ? this.SetMeta(key, JSON_Meta_Hidden, hidden) + : this.RemoveMeta(key, JSON_Meta_Hidden); + } + + /** + * Tracks a key, setting it's type and index where necessary. + * @internal + * + * @param key Key to track. If the key already + * exists, it's index will not be changed. + * @param type Type to set key to. If a valid type is + * provided, the key's type will be updated. + * @param index Index to set key to. If the index is not + * provided (-1), the object length will be used. + * @return True on success, false otherwise. + */ + public bool TrackKey( + const char[] key, + JSONCellType type = JSON_Type_Invalid, + int index = -1 + ) { + // track type if provided + if (type != JSON_Type_Invalid) { + this.SetMeta(key, JSON_Meta_Type, type); + } + + if (this.IsArray) { + return true; + } + + if (index == -1) { + index = this.Length; + + // skip tracking index if we're pushing to end & key already exists + if (this.HasKey(key)) { + if (type != JSON_Type_Invalid && type != JSON_Type_String) { + // remove any existing size + this.RemoveMeta(key, JSON_Meta_Size); + } + + return true; + } + } + + char[] index_key = new char[JSON_INT_BUFFER_SIZE]; + IntToString(index, index_key, JSON_INT_BUFFER_SIZE); + + int max_size = JSON_INT_BUFFER_SIZE + 8; + char[] index_size_key = new char[max_size]; + FormatEx(index_size_key, max_size, "%d:len", index); + + return this.Meta.SetString(index_key, key) + && this.Meta.SetInt(index_size_key, strlen(key) + 1) + && this.SetMeta(key, JSON_Meta_Index, index); + } + + /** + * Untracks a key, cleaning up all it's meta and indexing data. + * @internal + * + * @param key Key to untrack. + * @return True on success, false otherwise. + */ + public bool UntrackKey(const char[] key) + { + int index = this.GetIndex(key); + + for (int i = 0; i < sizeof(JSON_ALL_METADATA); i += 1) { + this.RemoveMeta(key, JSON_ALL_METADATA[i]); + } + + if (this.IsArray) { + return true; + } + + if (index == -1) { + return false; + } + + char[] index_key = new char[JSON_INT_BUFFER_SIZE]; + IntToString(index, index_key, JSON_INT_BUFFER_SIZE); + + int max_size = JSON_INT_BUFFER_SIZE + 8; + char[] index_size_key = new char[max_size]; + FormatEx(index_size_key, max_size, "%d:len", index); + + if (! this.Meta.Remove(index_key) || ! this.Meta.Remove(index_size_key)) { + return false; + } + + int length = this.Length; + int last_index = length - 1; + if (index < last_index) { + for (int i = index + 1; i < length; i += 1) { + int new_key_size = this.GetKeySize(i); + char[] new_key = new char[new_key_size]; + this.GetKey(i, new_key, new_key_size); + + this.TrackKey(new_key, JSON_Type_Invalid, i - 1); + } + + IntToString(last_index, index_key, JSON_INT_BUFFER_SIZE); + FormatEx(index_size_key, max_size, "%d:len", last_index); + + this.Meta.Remove(index_key); + this.Meta.Remove(index_size_key); + } + + return true; + } + + /** + * @section Getters + */ + + #if SM_INT64_SUPPORTED + /** + * Retrieves the int64 stored at a key. + * + * @param key Key to retrieve int64 value for. + * @param value Int buffer to store output. + * @return True on success, false otherwise. + */ + public bool GetInt64(const char[] key, int value[2]) + { + return this.Data.GetArray(key, value, 2); + } + #endif + + /** + * Retrieves the JSON object stored at a key. + * + * @param key Key to retrieve object value for. + * @param default_value Value to return if the key does not exist. + * @return Value stored at key. + */ + public JSON_Object GetObject( + const char[] key, + JSON_Object default_value = null + ) { + return view_as(this.GetHandle(key, default_value)); + } + + /** + * @section Setters + */ + + /** + * Sets the string stored at a key. + * + * @param key Key to set to string value. + * @param value Value to set. + * @return True on success, false otherwise. + */ + public bool SetString(const char[] key, const char[] value) + { + return this.TrackKey(key, JSON_Type_String) + && this.Super.SetString(key, value) + && this.SetMeta(key, JSON_Meta_Size, strlen(value) + 1); + } + + /** + * Sets the int stored at a key. + * + * @param key Key to set to int value. + * @param value Value to set. + * @return True on success, false otherwise. + */ + public bool SetInt(const char[] key, int value) + { + return this.TrackKey(key, JSON_Type_Int) + && this.Super.SetInt(key, value); + } + + #if SM_INT64_SUPPORTED + /** + * Sets the int64 stored at a key. + * + * @param key Key to set to int64 value. + * @param value Value to set. + * @return True on success, false otherwise. + */ + public bool SetInt64(const char[] key, int value[2]) + { + return this.TrackKey(key, JSON_Type_Int64) + && this.Data.SetArray(key, value, 2); + } + #endif + + /** + * Sets the float stored at a key. + * + * @param key Key to set to float value. + * @param value Value to set. + * @return True on success, false otherwise. + */ + public bool SetFloat(const char[] key, float value) + { + return this.TrackKey(key, JSON_Type_Float) + && this.Super.SetFloat(key, value); + } + + /** + * Sets the bool stored at a key. + * + * @param key Key to set to bool value. + * @param value Value to set. + * @return True on success, false otherwise. + */ + public bool SetBool(const char[] key, bool value) + { + return this.TrackKey(key, JSON_Type_Bool) + && this.Super.SetBool(key, value); + } + + /** + * Sets the JSON object stored at a key. + * + * @param key Key to set to object value. + * @param value Value to set. + * @return True on success, false otherwise. + */ + public bool SetObject(const char[] key, JSON_Object value) + { + return this.TrackKey(key, JSON_Type_Object) + && this.Super.SetHandle(key, value); + } + + /** + * @section Misc + */ + + /** + * Removes an item from the object by key. + * + * @param key Key of object to remove. + * @return True on success, false if the value was never set. + */ + public bool Remove(const char[] key) + { + return this.UntrackKey(key) && this.Super.Remove(key); + } + + /** + * Renames the key of an existing item in the object. + * + * @param from Existing key to rename. + * @param to New key. + * @param replace Should the 'to' key should be replaced if it exists? + * @return True on success, false otherwise. + */ + public bool Rename( + const char[] from, + const char[] to, + bool replace = true + ) { + JSONCellType type = this.GetType(from); + if (type == JSON_Type_Invalid) { + return false; + } + + if (StrEqual(from, to, true)) { + return true; + } + + bool toExists = this.HasKey(to); + if (toExists) { + if (! replace) { + return false; + } + + this.Remove(to); + } + + switch (type) { + case JSON_Type_String: { + int length = this.GetSize(from); + char[] value = new char[length]; + this.GetString(from, value, length); + this.SetString(to, value); + } + case JSON_Type_Int: { + this.SetInt(to, this.GetInt(from)); + } + #if SM_INT64_SUPPORTED + case JSON_Type_Int64: { + int value[2]; + this.GetInt64(from, value); + this.SetInt64(to, value); + } + #endif + case JSON_Type_Float: { + this.SetFloat(to, this.GetFloat(from)); + } + case JSON_Type_Bool: { + this.SetBool(to, this.GetBool(from)); + } + case JSON_Type_Object: { + this.SetObject(to, this.GetObject(from)); + } + } + + this.SetHidden(to, this.GetHidden(from)); + + this.Remove(from); + + return true; + } + + /** + * Merges in the entries from the specified object, + * optionally replacing existing entries with the same key. + * + * @param from Object to merge entries from. + * @param options Bitwise combination of `JSON_MERGE_*` options. + * @return True on success, false otherwise. + * @error If the object being merged is an array, + * an error will be thrown. + */ + public bool Merge(JSON_Object from, int options = JSON_MERGE_REPLACE) + { + if (this.IsArray || from.IsArray) { + json_set_last_error("attempted to merge using an array"); + + return false; + } + + bool replace = (options & JSON_MERGE_REPLACE) > 0; + bool autocleanup = (options & JSON_MERGE_CLEANUP) > 0; + + int json_size = from.Length; + int key_length = 0; + for (int i = 0; i < json_size; i += 1) { + key_length = from.GetKeySize(i); + char[] key = new char[key_length]; + from.GetKey(i, key, key_length); + + // skip already existing keys if we aren't in replace mode + bool key_already_exists = this.HasKey(key); + if (! replace && key_already_exists) { + continue; + } + + JSONCellType type = from.GetType(key); + // skip keys of unknown type + if (type == JSON_Type_Invalid) { + continue; + } + + // merge value onto structure + switch (type) { + case JSON_Type_String: { + int length = from.GetSize(key); + char[] value = new char[length]; + from.GetString(key, value, length); + + this.SetString(key, value); + } + case JSON_Type_Int: { + this.SetInt(key, from.GetInt(key)); + } + #if SM_INT64_SUPPORTED + case JSON_Type_Int64: { + int value[2]; + from.GetInt64(key, value); + this.SetInt64(key, value); + } + #endif + case JSON_Type_Float: { + this.SetFloat(key, from.GetFloat(key)); + } + case JSON_Type_Bool: { + this.SetBool(key, from.GetBool(key)); + } + case JSON_Type_Object: { + JSON_Object value = from.GetObject(key); + + if (autocleanup && key_already_exists) { + JSON_Object existing = this.GetObject(key); + if (existing != value) { + json_cleanup_and_delete(existing); + } + } + + this.SetObject(key, value); + } + } + + this.SetHidden(key, from.GetHidden(key)); + } + + return true; + } + + /** + * @section json.inc Aliases + */ + + /** + * Makes a global call with this + * instance passed as the object. + * + * @see json_encode_size + */ + public int EncodeSize(int options = JSON_NONE) + { + return json_encode_size(this, options); + } + + /** + * Makes a global call with this + * instance passed as the object. + * + * @see json_encode + */ + public void Encode(char[] output, int max_size, int options = JSON_NONE) + { + json_encode(this, output, max_size, options); + } + + /** + * Makes a global call with this + * instance passed as the object. + * + * @see json_write_to_file + */ + public bool WriteToFile(const char[] path, int options = JSON_NONE) + { + return json_write_to_file(this, path, options); + } + + /** + * Makes a global call with this + * instance passed as the object. + * + * @see json_copy_deep + */ + public JSON_Object ShallowCopy() + { + return json_copy_shallow(this); + } + + /** + * Makes a global call with this + * instance passed as the object. + * + * @see json_copy_deep + */ + public JSON_Object DeepCopy() + { + return json_copy_deep(this); + } + + /** + * Makes a global call with this + * instance passed as the object. + * + * @see json_cleanup + */ + public void Cleanup() + { + json_cleanup(this); + } + + /** + * @section Constructor + */ + + /** + * Creates a new JSON_Object. + * + * @return A new JSON_Object. + */ + public JSON_Object() + { + return view_as(new MetaStringMap()); + } +}; diff --git a/libalternator/tests/corpus/json/addons/sourcemod/scripting/json_test.sp b/libalternator/tests/corpus/json/addons/sourcemod/scripting/json_test.sp new file mode 100644 index 0000000..cc2a7ed --- /dev/null +++ b/libalternator/tests/corpus/json/addons/sourcemod/scripting/json_test.sp @@ -0,0 +1,1400 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * A pure SourcePawn JSON encoder/decoder. + * https://github.com/clugg/sm-json + * + * sm-json (C)2022 James Dickens. (clug) + * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + */ + +#pragma semicolon 1 +#pragma newdecls required + +#include +#include +#include + +public Plugin myinfo = { + name = "JSON Tester", + author = "clug", + description = "Tests dumping and loading JSON objects.", + version = "5.0.0", + url = "https://github.com/clugg/sm-json" +}; + +/** + * @section Globals + */ + +char json_encode_output[1024]; + +/** + * @section Helpers + */ + +/** + * Encodes a JSON_Object to a hardcoded output. + */ +void _json_encode(JSON_Object obj, int options = JSON_NONE) +{ + obj.Encode(json_encode_output, sizeof(json_encode_output), options); + + Test_Assert( + "calculated buffer size fits output", + json_encode_size(obj, options) > strlen(json_encode_output) + ); +} + +/** + * Encodes a JSON_Object and prints it to the test output. + */ +void print_json(JSON_Object obj, int options = JSON_NONE) +{ + _json_encode(obj, options); + Test_Output("%s", json_encode_output); +} + +/** + * Removes the specified index from the array and confirms + * that the removed value no longer exists. + */ +bool check_array_remove(JSON_Array arr, int index) +{ + Test_Output("Removing element at index %d", index); + + // get current value at index + JSONCellType type = arr.GetType(index); + int str_size = 0; + if (type == JSON_Type_String) { + str_size = arr.GetSize(index); + } + + any value; + char[] str = new char[str_size]; + + if (type == JSON_Type_String) { + arr.GetString(index, str, str_size); + } else { + arr.GetValue(index, value); + } + + // remove the index from the array + arr.Remove(index); + print_json(arr); + + // confirm that it is gone + int found = -1; + if (type == JSON_Type_String) { + found = arr.IndexOfString(str); + } else { + found = arr.IndexOf(value); + } + + if (found != -1) { + LogError( + "json_test: found removed value in array at position %d", + found + ); + + return false; + } + + return true; +} + +void check_key_at_index(JSON_Object obj, const char[] key, int index) +{ + Test_AssertEqual("key -> index", index, obj.GetIndex(key)); + + int max_size = JSON_INT_BUFFER_SIZE + 8; + char[] index_size_key = new char[max_size]; + FormatEx(index_size_key, max_size, "%d:len", index); + + int key_size = obj.Meta.GetInt(index_size_key); + char[] real_key = new char[key_size]; + + char[] index_key = new char[JSON_INT_BUFFER_SIZE]; + IntToString(index, index_key, JSON_INT_BUFFER_SIZE); + obj.Meta.GetString(index_key, real_key, key_size); + + Test_AssertStringsEqual("keys match", key, real_key); +} + +void check_no_key_at_index(JSON_Object obj, int index) +{ + int max_size = JSON_INT_BUFFER_SIZE + 8; + char[] index_size_key = new char[max_size]; + FormatEx(index_size_key, max_size, "%d:len", index); + + Test_AssertFalse("no key length tracked for index", obj.Meta.HasKey(index_size_key)); + + char[] index_key = new char[JSON_INT_BUFFER_SIZE]; + IntToString(index, index_key, JSON_INT_BUFFER_SIZE); + + Test_AssertFalse("no key tracked for index", obj.Meta.HasKey(index_key)); +} + +/** + * @section Methodmaps + */ + +methodmap Weapon < JSON_Object +{ + public Weapon() + { + return view_as(new JSON_Object()); + } + + property int id + { + public get() + { + return this.GetInt("id"); + } + + public set(int value) + { + this.SetInt("id", value); + } + } + + public bool GetName(char[] buffer, int max_size) + { + return this.GetString("name", buffer, max_size); + } + + public void SetName(const char[] value) + { + this.SetString("name", value); + } +} + +methodmap Player < JSON_Object +{ + public Player() + { + return view_as(new JSON_Object()); + } + + property int id + { + public get() + { + return this.GetInt("id"); + } + + public set(int value) + { + this.SetInt("id", value); + } + } + + property Weapon weapon + { + public get() + { + return view_as(this.GetObject("weapon")); + } + + public set(Weapon value) + { + this.SetObject("weapon", view_as(value)); + } + } +} + +/** + * @section Tests + **/ + +void it_should_decode(char[] data) +{ + JSON_Object obj = json_decode(data); + if (Test_AssertNotNull("obj", obj)) { + json_cleanup_and_delete(obj); + } +} + +void it_should_not_decode(char[] data) +{ + JSON_Object obj = json_decode(data); + if (! Test_AssertNull("obj", obj)) { + _json_encode(obj); + LogError( + "json_test: malformed JSON was parsed as valid: %s", + json_encode_output + ); + + json_cleanup_and_delete(obj); + } + + char error[1024]; + Test_Assert( + "last error is not empty", + json_get_last_error(error, sizeof(error)) + ); + Test_Output("%s", error); +} + +void it_should_encode_empty_objects() +{ + JSON_Object obj = new JSON_Object(); + _json_encode(obj); + json_cleanup_and_delete(obj); + + Test_AssertStringsEqual("output", json_encode_output, "{}"); +} + +void it_should_encode_empty_arrays() +{ + JSON_Array arr = new JSON_Array(); + _json_encode(arr); + json_cleanup_and_delete(arr); + + Test_AssertStringsEqual("output", json_encode_output, "[]"); +} + +void it_should_support_objects() +{ + JSON_Object obj = new JSON_Object(); + Test_Assert("set string", obj.SetString("str", "leet")); + Test_Assert("set escaped string", obj.SetString("escaped_str", "\"leet\"")); + Test_Assert("set int", obj.SetInt("int", 9001)); + Test_Assert("set negative int", obj.SetInt("negative_int", -9001)); + Test_Assert("set int zero", obj.SetInt("int_zero", 0)); + Test_Assert("set float", obj.SetFloat("float", 13.37)); + Test_Assert("set negative float", obj.SetFloat("negative_float", -13.37)); + Test_Assert("set float zero", obj.SetFloat("float_zero", 0.0)); + Test_Assert("set true", obj.SetBool("true", true)); + Test_Assert("set false", obj.SetBool("false", false)); + Test_Assert("set handle", obj.SetObject("handle", null)); + + print_json(obj); + json_cleanup_and_delete(obj); + + JSON_Object decoded = json_decode(json_encode_output); + if (! Test_AssertNotNull("decoded", decoded)) { + // if this assertion fails, testing cannot continue + return; + } + + char string[32]; + Test_Assert("get decoded string", decoded.GetString("str", string, sizeof(string))); + Test_AssertStringsEqual("decoded string", string, "leet"); + Test_Assert("get decoded escaped string", decoded.GetString("escaped_str", string, sizeof(string))); + Test_AssertStringsEqual("decoded escaped string", string, "\"leet\""); + Test_AssertEqual("decoded int", decoded.GetInt("int"), 9001); + Test_AssertEqual("decoded negative int", decoded.GetInt("negative_int"), -9001); + Test_AssertEqual("decoded int zero", decoded.GetInt("int_zero"), 0); + Test_AssertFloatsEqual("decoded float", decoded.GetFloat("float"), 13.37); + Test_AssertFloatsEqual("decoded negative float", decoded.GetFloat("negative_float"), -13.37); + Test_AssertFloatsEqual("decoded float zero", decoded.GetFloat("float_zero"), 0.0); + Test_AssertTrue("decoded true", decoded.GetBool("true")); + Test_AssertFalse("decoded false", decoded.GetBool("false")); + Test_AssertNull("decoded handle", decoded.GetObject("handle")); + + json_cleanup_and_delete(decoded); +} + +void it_should_support_objects_with_ordered_keys() +{ + JSON_Object obj = new JSON_Object(); + obj.SetInt("first", 1); + check_key_at_index(obj, "first", 0); + obj.SetInt("second", 2); + check_key_at_index(obj, "second", 1); + obj.SetInt("third", 3); + check_key_at_index(obj, "third", 2); + + check_no_key_at_index(obj, 3); + + _json_encode(obj); + Test_AssertStringsEqual("output upon insert", json_encode_output, "{\"first\":1,\"second\":2,\"third\":3}"); + + obj.Remove("second"); + check_key_at_index(obj, "first", 0); + check_key_at_index(obj, "third", 1); + + check_no_key_at_index(obj, 2); + obj.SetBool("last", true); + check_key_at_index(obj, "last", 2); + + check_no_key_at_index(obj, 3); + + _json_encode(obj); + Test_AssertStringsEqual("output after removing second and adding", json_encode_output, "{\"first\":1,\"third\":3,\"last\":true}"); + + obj.Remove("first"); + check_key_at_index(obj, "third", 0); + check_key_at_index(obj, "last", 1); + + check_no_key_at_index(obj, 2); + + _json_encode(obj); + Test_AssertStringsEqual("output after removing first", json_encode_output, "{\"third\":3,\"last\":true}"); + + obj.Remove("last"); + check_key_at_index(obj, "third", 0); + + check_no_key_at_index(obj, 1); + + _json_encode(obj); + Test_AssertStringsEqual("output after removing last", json_encode_output, "{\"third\":3}"); + + + obj.Remove("third"); + + check_no_key_at_index(obj, 0); + + _json_encode(obj); + Test_AssertStringsEqual("output after removing third", json_encode_output, "{}"); + + json_cleanup_and_delete(obj); +} + +void it_should_support_decoding_objects_with_ordered_keys() +{ + JSON_Object obj = json_decode("{\"first\":1,\"second\":2,\"third\":3}"); + _json_encode(obj); + + Test_AssertStringsEqual("decode -> encode output", json_encode_output, "{\"first\":1,\"second\":2,\"third\":3}"); + + json_cleanup_and_delete(obj); +} + +void it_should_support_decoding_objects_with_duplicate_keys() +{ + JSON_Object obj = json_decode("{\"third\":1,\"second\":2,\"third\":\"hello\"}"); + Test_AssertEqual("key type", obj.GetType("third"), JSON_Type_String); + + _json_encode(obj); + + Test_AssertStringsEqual("decode -> encode output", json_encode_output, "{\"second\":2,\"third\":\"hello\"}"); + + json_cleanup_and_delete(obj); +} + +void it_should_support_arrays() +{ + JSON_Array arr = new JSON_Array(); + Test_AssertNotEqual("push string", arr.PushString("leet"), -1); + Test_AssertNotEqual("push escaped string", arr.PushString("\"leet\""), -1); + Test_AssertNotEqual("push int", arr.PushInt(9001), -1); + Test_AssertNotEqual("push negative int", arr.PushInt(-9001), -1); + Test_AssertNotEqual("push int zero", arr.PushInt(0), -1); + Test_AssertNotEqual("push float", arr.PushFloat(13.37), -1); + Test_AssertNotEqual("push negative float", arr.PushFloat(-13.37), -1); + Test_AssertNotEqual("push float zero", arr.PushFloat(0.0), -1); + Test_AssertNotEqual("push true", arr.PushBool(true), -1); + Test_AssertNotEqual("push false", arr.PushBool(false), -1); + Test_AssertNotEqual("push handle", arr.PushObject(null), -1); + + print_json(arr); + json_cleanup_and_delete(arr); + + JSON_Array decoded = view_as( + json_decode(json_encode_output) + ); + if (! Test_AssertNotNull("decoded", decoded)) { + // if this assertion fails, testing cannot continue + return; + } + + int index = 0; + char string[32]; + Test_Assert("get decoded string", decoded.GetString(index++, string, sizeof(string))); + Test_AssertStringsEqual("decoded string", string, "leet"); + Test_Assert("get decoded escaped string",decoded.GetString(index++, string, sizeof(string))); + Test_AssertStringsEqual("decoded escaped string", string, "\"leet\""); + Test_AssertEqual("decoded int", decoded.GetInt(index++), 9001); + Test_AssertEqual("decoded negative int", decoded.GetInt(index++), -9001); + Test_AssertEqual("decoded int zero", decoded.GetInt(index++), 0); + Test_AssertFloatsEqual("decoded float", decoded.GetFloat(index++), 13.37); + Test_AssertFloatsEqual("decoded negative float", decoded.GetFloat(index++), -13.37); + Test_AssertFloatsEqual("decoded float zero", decoded.GetFloat(index++), 0.0); + Test_AssertTrue("decoded true", decoded.GetBool(index++)); + Test_AssertFalse("decoded false", decoded.GetBool(index++)); + Test_AssertNull("decoded handle", decoded.GetObject(index++)); + + json_cleanup_and_delete(decoded); +} + +void it_should_support_objects_nested_in_objects() +{ + JSON_Object nested_obj = new JSON_Object(); + nested_obj.SetBool("nested", true); + + JSON_Object obj = new JSON_Object(); + obj.SetBool("nested", false); + obj.SetObject("object", nested_obj); + + print_json(obj); + Test_AssertTrue("object.nested", obj.GetObject("object").GetBool("nested")); + + json_cleanup_and_delete(obj); +} + +void it_should_support_objects_nested_in_arrays() +{ + JSON_Object nested_obj = new JSON_Object(); + nested_obj.SetBool("nested", true); + + JSON_Array arr = new JSON_Array(); + arr.PushObject(nested_obj); + + print_json(arr); + Test_AssertTrue("0.nested", arr.GetObject(0).GetBool("nested")); + + json_cleanup_and_delete(arr); +} + +void it_should_support_arrays_nested_in_objects() +{ + JSON_Array nested_arr = new JSON_Array(); + nested_arr.PushBool(true); + + JSON_Object obj = new JSON_Object(); + obj.SetObject("array", nested_arr); + + print_json(obj); + JSON_Array obj_array = view_as(obj.GetObject("array")); + Test_AssertTrue("array.0", obj_array.GetBool(0)); + + json_cleanup_and_delete(obj); +} + +void it_should_support_arrays_nested_in_arrays() +{ + JSON_Array nested_arr = new JSON_Array(); + nested_arr.PushBool(true); + + JSON_Array arr = new JSON_Array(); + arr.PushObject(nested_arr); + + print_json(arr); + JSON_Array arr_array = view_as(arr.GetObject(0)); + Test_AssertTrue("0.0", arr_array.GetBool(0)); + + json_cleanup_and_delete(arr); +} + +void it_should_support_basic_methodmaps() +{ + Player player = new Player(); + player.id = 1; + + print_json(player); + json_cleanup_and_delete(player); + + Player decoded = view_as(json_decode(json_encode_output)); + Test_AssertEqual("decoded.id", decoded.id, 1); + + json_cleanup_and_delete(decoded); +} + +void it_should_support_nested_methodmaps() +{ + Weapon weapon = new Weapon(); + weapon.id = 1; + weapon.SetName("ak47"); + + Player player = new Player(); + player.id = 1; + player.weapon = weapon; + + print_json(player); + Test_AssertEqual("weapon.id", weapon.id, 1); + Test_Output("changing weapon.id via player"); + player.weapon.id = 2; + Test_AssertEqual("weapon.id", weapon.id, 2); + + json_cleanup_and_delete(player); +} + +void it_should_pretty_print_an_empty_array() +{ + JSON_Array empty_arr = new JSON_Array(); + _json_encode(empty_arr, JSON_ENCODE_PRETTY); + json_cleanup_and_delete(empty_arr); + + Test_AssertStringsEqual("output", json_encode_output, "[]"); +} + +void it_should_pretty_print_an_empty_object() +{ + JSON_Object empty_obj = new JSON_Object(); + _json_encode(empty_obj, JSON_ENCODE_PRETTY); + json_cleanup_and_delete(empty_obj); + + Test_AssertStringsEqual("output", json_encode_output, "{}"); +} + +void it_should_pretty_print_with_nesting() +{ + JSON_Object parent_obj = new JSON_Object(); + parent_obj.SetBool("pretty_printing", true); + + JSON_Object nested_obj = new JSON_Object(); + nested_obj.SetObject("im_indented", null); + + JSON_Array sub_nested_arr = new JSON_Array(); + sub_nested_arr.PushInt(1); + + JSON_Object sub_sub_nested_obj = new JSON_Object(); + sub_sub_nested_obj.SetBool("shallow", false); + + sub_nested_arr.PushObject(sub_sub_nested_obj); + nested_obj.SetObject("second_depth", sub_nested_arr); + parent_obj.SetObject("first_depth", nested_obj); + + _json_encode(parent_obj, JSON_ENCODE_PRETTY); + json_cleanup_and_delete(parent_obj); + + Test_AssertStringsEqual("output", json_encode_output, "{\n \"pretty_printing\": true,\n \"first_depth\": {\n \"im_indented\": null,\n \"second_depth\": [\n 1,\n {\n \"shallow\": false\n }\n ]\n }\n}"); +} + +void it_should_pretty_print_with_custom_formatting() +{ + JSON_Object parent_obj = new JSON_Object(); + parent_obj.SetBool("pretty_printing", true); + + JSON_Object nested_obj = new JSON_Object(); + nested_obj.SetObject("im_indented", null); + + JSON_Array sub_nested_arr = new JSON_Array(); + sub_nested_arr.PushInt(1); + + JSON_Object sub_sub_nested_obj = new JSON_Object(); + sub_sub_nested_obj.SetBool("shallow", false); + + sub_nested_arr.PushObject(sub_sub_nested_obj); + nested_obj.SetObject("second_depth", sub_nested_arr); + parent_obj.SetObject("first_depth", nested_obj); + + strcopy(JSON_PP_AFTER_COLON, sizeof(JSON_PP_AFTER_COLON), " "); + strcopy(JSON_PP_INDENT, sizeof(JSON_PP_INDENT), ""); + strcopy(JSON_PP_NEWLINE, sizeof(JSON_PP_NEWLINE), " "); + + _json_encode(parent_obj, JSON_ENCODE_PRETTY); + json_cleanup_and_delete(parent_obj); + + Test_AssertStringsEqual("output", json_encode_output, "{ \"pretty_printing\": true, \"first_depth\": { \"im_indented\": null, \"second_depth\": [ 1, { \"shallow\": false } ] } }"); + + // reset to defaults + strcopy(JSON_PP_AFTER_COLON, sizeof(JSON_PP_AFTER_COLON), " "); + strcopy(JSON_PP_INDENT, sizeof(JSON_PP_INDENT), " "); + strcopy(JSON_PP_NEWLINE, sizeof(JSON_PP_NEWLINE), "\n"); +} + +void it_should_trim_floats() +{ + JSON_Array arr = new JSON_Array(); + arr.PushFloat(0.0); + arr.PushFloat(1.0); + arr.PushFloat(10.01); + arr.PushFloat(-0.0); + arr.PushFloat(-1.0); + arr.PushFloat(-10.01); + + _json_encode(arr); + json_cleanup_and_delete(arr); + + Test_AssertStringsEqual("output", json_encode_output, "[0.0,1.0,10.01,-0.0,-1.0,-10.01]"); +} + +#if SM_INT64_SUPPORTED +void it_should_support_int64() +{ + char[] input = "[-9999999999,-2147483649,-2147483648,-1,0,1,2147483647,4294967295,4294967296,9999999999]"; + + JSON_Array arr = view_as(json_decode(input)); + Test_AssertEqual("0 type", arr.GetType(0), JSON_Type_Int64); + Test_AssertEqual("1 type", arr.GetType(1), JSON_Type_Int64); + Test_AssertEqual("2 type", arr.GetType(2), JSON_Type_Int); + Test_AssertEqual("3 type", arr.GetType(3), JSON_Type_Int); + Test_AssertEqual("4 type", arr.GetType(4), JSON_Type_Int); + Test_AssertEqual("5 type", arr.GetType(5), JSON_Type_Int); + Test_AssertEqual("6 type", arr.GetType(6), JSON_Type_Int); + Test_AssertEqual("7 type", arr.GetType(7), JSON_Type_Int64); + Test_AssertEqual("8 type", arr.GetType(8), JSON_Type_Int64); + Test_AssertEqual("9 type", arr.GetType(9), JSON_Type_Int64); + + _json_encode(arr); + json_cleanup_and_delete(arr); + + Test_AssertStringsEqual("output", json_encode_output, input); +} +#endif + +void it_should_remove_meta_keys_from_arrays() +{ + JSON_Array arr = new JSON_Array(); + arr.PushString("hello"); + arr.PushInt(0); + + Test_AssertEqual("0 type", arr.GetType(0), JSON_Type_String); + Test_AssertEqual("0 length", arr.GetSize(0), 6); + Test_AssertEqual("1 type", arr.GetType(1), JSON_Type_Int); + + arr.Remove(0); + + Test_AssertEqual("0 type", arr.GetType(0), JSON_Type_Int); + Test_AssertEqual("0 length", arr.GetSize(0), -1); + Test_AssertEqual("1 type", arr.GetType(1), JSON_Type_Invalid); + + json_cleanup_and_delete(arr); +} + +void it_should_remove_meta_keys_from_objects() +{ + JSON_Object obj = new JSON_Object(); + obj.SetString("hello", "world"); + obj.SetInt("zero", 0); + + Test_AssertEqual("hello type", obj.GetType("hello"), JSON_Type_String); + Test_AssertEqual("hello length", obj.GetSize("hello"), 6); + Test_AssertEqual("zero type", obj.GetType("zero"), JSON_Type_Int); + + obj.Remove("hello"); + + Test_AssertEqual("hello type", obj.GetType("hello"), JSON_Type_Invalid); + Test_AssertEqual("hello length", obj.GetSize("hello"), -1); + Test_AssertEqual("zero type", obj.GetType("zero"), JSON_Type_Int); + + json_cleanup_and_delete(obj); +} + +void it_should_shift_array_down_after_removed_index() +{ + JSON_Array arr = new JSON_Array(); + arr.PushString("leet"); + arr.PushString("\"leet\""); + arr.PushInt(9001); + arr.PushFloat(-13.37); + arr.PushBool(true); + arr.PushBool(false); + + print_json(arr); + + Test_Assert("remove first element", check_array_remove(arr, 0)); + Test_Assert("remove last element", check_array_remove(arr, arr.Length - 1)); + int max = arr.Length - 1; + Test_Assert("remove random element", check_array_remove(arr, GetRandomInt(0, max))); + Test_AssertEqual("array length", arr.Length, max); + + json_cleanup_and_delete(arr); +} + +void it_should_not_merge_using_array() +{ + JSON_Object obj = new JSON_Object(); + JSON_Array arr = new JSON_Array(); + JSON_Array arr2 = new JSON_Array(); + + Test_Assert("merge failed: obj <- arr", obj.Merge(arr) == false); + Test_Assert("merge failed: arr <- arr", arr.Merge(arr2) == false); + + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + json_cleanup_and_delete(arr2); +} + +void it_should_merge_objects_with_replacement() +{ + JSON_Object obj1 = new JSON_Object(); + obj1.SetInt("key1", 1); + obj1.SetBool("replaced", false); + obj1.SetHidden("replaced", false); + + JSON_Object obj2 = new JSON_Object(); + obj2.SetInt("key2", 2); + obj2.SetBool("replaced", true); + obj2.SetHidden("replaced", true); + + if (! Test_Assert("merged successfully", obj1.Merge(obj2))) { + // if this assertion fails, testing cannot continue + return; + } + + print_json(obj1); + + Test_Assert("merged has key2", obj1.HasKey("key2")); + Test_AssertEqual("merged key2 type", obj1.GetType("key2"), JSON_Type_Int); + Test_AssertEqual("merged key2", obj1.GetInt("key2"), 2); + Test_AssertEqual("merged replaced type", obj1.GetType("replaced"), JSON_Type_Bool); + Test_AssertTrue("merged replaced", obj1.GetBool("replaced")); + Test_AssertTrue("merged replaced hidden", obj1.GetHidden("replaced")); + + json_cleanup_and_delete(obj1); + json_cleanup_and_delete(obj2); +} + +void it_should_merge_objects_without_replacement() +{ + JSON_Object obj1 = new JSON_Object(); + obj1.SetInt("key1", 1); + obj1.SetBool("replaced", false); + obj1.SetHidden("replaced", false); + + JSON_Object obj2 = new JSON_Object(); + obj2.SetInt("key2", 2); + obj2.SetBool("replaced", true); + obj2.SetHidden("replaced", true); + + if (! Test_Assert("merged successfully", obj1.Merge(obj2, JSON_NONE))) { + // if this assertion fails, testing cannot continue + return; + } + + print_json(obj1); + + Test_Assert("merged has key2", obj1.HasKey("key2")); + Test_AssertEqual("merged key2 type", obj1.GetType("key2"), JSON_Type_Int); + Test_AssertEqual("merged key2", obj1.GetInt("key2"), 2); + Test_AssertEqual("merged replaced type", obj1.GetType("replaced"), JSON_Type_Bool); + Test_AssertFalse("merged replaced", obj1.GetBool("replaced")); + Test_AssertFalse("merged replaced hidden", obj1.GetHidden("replaced")); + + json_cleanup_and_delete(obj1); + json_cleanup_and_delete(obj2); +} + +void it_should_not_concat_using_object() +{ + JSON_Array arr = new JSON_Array(); + JSON_Array obj = view_as(new JSON_Object()); + + Test_Assert("concat failed: arr <- obj", arr.Concat(obj) == false); + + json_cleanup_and_delete(arr); + json_cleanup_and_delete(obj); +} + +void it_should_concat_arrays() +{ + JSON_Array arr1 = new JSON_Array(); + arr1.PushInt(1); + arr1.SetHidden(0, true); + + JSON_Array arr2 = new JSON_Array(); + arr2.PushInt(2); + arr2.PushInt(3); + arr2.PushInt(4); + arr2.SetHidden(2, true); + + if (! Test_Assert("merged successfully", arr1.Concat(arr2))) { + // if this assertion fails, testing cannot continue + return; + } + + print_json(arr1); + + Test_AssertEqual("merged length", arr1.Length, 4); + Test_AssertStringsEqual("output", json_encode_output, "[2,3]"); + + json_cleanup_and_delete(arr1); + json_cleanup_and_delete(arr2); +} + +void it_should_copy_flat_arrays() +{ + JSON_Array arr = new JSON_Array(); + arr.PushInt(1); + arr.PushInt(2); + arr.PushInt(3); + + JSON_Array copy = arr.DeepCopy(); + Test_AssertEqual("copy length", copy.Length, arr.Length); + Test_AssertEqual("copy 0", copy.GetInt(0), 1); + Test_AssertEqual("copy 1", copy.GetInt(1), 2); + Test_AssertEqual("copy 2", copy.GetInt(2), 3); + + arr.PushInt(4); + + Test_AssertNotEqual("copy length", copy.Length, arr.Length); + + json_cleanup_and_delete(arr); + json_cleanup_and_delete(copy); +} + +void it_should_copy_flat_objects() +{ + JSON_Object obj = new JSON_Object(); + obj.SetInt("key1", 1); + obj.SetInt("key2", 2); + obj.SetInt("key3", 3); + + JSON_Object copy = obj.DeepCopy(); + Test_AssertEqual("copy length", copy.Length, obj.Length); + Test_AssertEqual("copy key1", copy.GetInt("key1"), 1); + Test_AssertEqual("copy key2", copy.GetInt("key2"), 2); + Test_AssertEqual("copy key3", copy.GetInt("key3"), 3); + + obj.SetInt("key4", 4); + + Test_AssertNotEqual("copy length", copy.Length, obj.Length); + + json_cleanup_and_delete(obj); + json_cleanup_and_delete(copy); +} + +void it_should_shallow_copy_arrays() +{ + JSON_Array arr = new JSON_Array(); + arr.PushObject(new JSON_Array()); + + JSON_Array copy = arr.ShallowCopy(); + + Test_AssertEqual("copy length", copy.Length, arr.Length); + Test_Assert("copy 0 == arr 0", copy.GetObject(0) == arr.GetObject(0)); + + json_cleanup_and_delete(arr); + copy.Remove(0); + json_cleanup_and_delete(copy); +} + +void it_should_shallow_copy_objects() +{ + JSON_Object obj = new JSON_Object(); + obj.SetObject("nested", new JSON_Object()); + + JSON_Object copy = obj.ShallowCopy(); + + Test_AssertEqual("copy length", copy.Length, obj.Length); + Test_Assert("copy nested == obj nested", copy.GetObject("nested") == obj.GetObject("nested")); + + json_cleanup_and_delete(obj); + copy.Remove("nested"); + json_cleanup_and_delete(copy); +} + +void it_should_preserve_ordered_keys_on_shallow_copy() +{ + JSON_Object obj = new JSON_Object(); + obj.SetInt("keyA", 1); + obj.SetInt("key2", 2); + obj.SetInt("keyC", 3); + obj.SetInt("key4", 4); + + JSON_Object copy = obj.ShallowCopy(); + + int length = obj.Length; + int original_key_length = 0; + int copy_key_length = 0; + char key_assert_desc[16]; + for (int i = 0; i < length; i += 1) { + FormatEx(key_assert_desc, sizeof(key_assert_desc), "copy key %d", i); + + original_key_length = obj.GetKeySize(i); + char[] original_key = new char[original_key_length]; + obj.GetKey(i, original_key, original_key_length); + + copy_key_length = copy.GetKeySize(i); + char[] copy_key = new char[copy_key_length]; + copy.GetKey(i, copy_key, copy_key_length); + + Test_AssertStringsEqual(key_assert_desc, original_key, copy_key); + } + + json_cleanup_and_delete(obj); + json_cleanup_and_delete(copy); +} + +void it_should_deep_copy_arrays() +{ + JSON_Array arr = new JSON_Array(); + arr.PushObject(new JSON_Array()); + + JSON_Array copy = arr.DeepCopy(); + + Test_AssertEqual("copy length", copy.Length, arr.Length); + Test_Assert("copy 0 != arr 0", copy.GetObject(0) != arr.GetObject(0)); + + json_cleanup_and_delete(arr); + json_cleanup_and_delete(copy); +} + +void it_should_deep_copy_objects() +{ + JSON_Object obj = new JSON_Object(); + obj.SetObject("nested", new JSON_Object()); + obj.SetObject("null", null); + + JSON_Object copy = obj.DeepCopy(); + + Test_AssertEqual("copy length", copy.Length, obj.Length); + Test_Assert("copy nested != obj nested", copy.GetObject("nested") != obj.GetObject("nested")); + Test_Assert("null is still null", copy.GetObject("null") == obj.GetObject("null")); + + json_cleanup_and_delete(obj); + json_cleanup_and_delete(copy); +} + +void it_should_allow_single_quotes() +{ + // array + JSON_Array arr = view_as( + json_decode( + "['single quotes', \"double quotes\", 'single \\'single\\' quotes', 'single \"double\" quotes', \"double 'single' quotes\", \"double \\\"double\\\" quotes\"]", + JSON_DECODE_SINGLE_QUOTES + ) + ); + + if (Test_AssertNotNull("array", arr)) { + print_json(arr); + Test_AssertEqual("array length", arr.Length, 6); + + json_cleanup_and_delete(arr); + } + + // object + JSON_Object obj = json_decode( + "{'key': \"value\"}", + JSON_DECODE_SINGLE_QUOTES + ); + + if (Test_AssertNotNull("object", obj)) { + print_json(obj); + Test_Assert("object has key", obj.HasKey("key")); + + json_cleanup_and_delete(obj); + } +} + +void it_should_return_default_values_for_missing_elements() +{ + // array + JSON_Array arr = new JSON_Array(); + + Test_AssertEqual("array default int value", arr.GetInt(0, 1), 1); + Test_AssertFloatsEqual("array default float value", arr.GetFloat(0, 1.0), 1.0); + Test_AssertTrue("array default bool value", arr.GetBool(0, true)); + Test_AssertNull("array default null value", arr.GetObject(0, null)); + Test_AssertEqual("array default arr value", arr.GetObject(0, arr), arr); + + json_cleanup_and_delete(arr); + + // object + JSON_Object obj = new JSON_Object(); + + Test_AssertEqual("object default int value", obj.GetInt("_", 1), 1); + Test_AssertFloatsEqual("object default float value", obj.GetFloat("_", 1.0), 1.0); + Test_AssertTrue("object default bool value", obj.GetBool("_", true)); + Test_AssertNull("object default null value", obj.GetObject("_", null)); + Test_AssertEqual("object default obj value", obj.GetObject("_", obj), obj); + + json_cleanup_and_delete(obj); +} + +void it_should_autocleanup_merged_objects() +{ + JSON_Object obj1 = new JSON_Object(); + JSON_Object obj2 = new JSON_Object(); + JSON_Object nested1 = new JSON_Object(); + JSON_Object nested2 = new JSON_Object(); + + Test_Output("merging without replacement"); + obj1.SetObject("nested", nested1); + obj2.SetObject("nested", nested2); + obj1.Merge(obj2, JSON_MERGE_CLEANUP); + + Test_Assert("nested1 is valid", IsValidHandle(nested1)); + Test_Assert("nested2 is valid", IsValidHandle(nested2)); + + Test_Output("merging with replacement and without cleanup"); + obj1.SetObject("nested", nested1); + obj2.SetObject("nested", nested2); + obj1.Merge(obj2, JSON_MERGE_REPLACE); + + Test_Assert("nested1 is valid", IsValidHandle(nested1)); + Test_Assert("nested2 is valid", IsValidHandle(nested2)); + + Test_Output("merging with replacement and with cleanup"); + obj1.SetObject("nested", nested1); + obj2.SetObject("nested", nested2); + obj1.Merge(obj2, JSON_MERGE_REPLACE | JSON_MERGE_CLEANUP); + + Test_Assert("nested1 is invalid", ! IsValidHandle(nested1)); + Test_Assert("nested2 is valid", IsValidHandle(nested2)); + + json_cleanup_and_delete(obj1); + obj2.Remove("nested"); + json_cleanup_and_delete(obj2); +} + +void it_should_enforce_types_in_arrays() +{ + JSON_Array arr = new JSON_Array(JSON_Type_Int); + + Test_AssertNotEqual("push int", arr.PushInt(9001), -1); + Test_AssertNotEqual("push negative int", arr.PushInt(-9001), -1); + Test_AssertEqual("push string", arr.PushString("leet"), -1); + Test_AssertEqual("push float", arr.PushFloat(13.37), -1); + Test_AssertEqual("push bool", arr.PushBool(true), -1); + Test_AssertEqual("push null", arr.PushObject(null), -1); + + json_cleanup_and_delete(arr); +} + +void it_should_not_enable_strict_type_on_inconsistent_array() +{ + JSON_Array arr = new JSON_Array(); + arr.PushObject(null); + + Test_Assert("EnforceType failed", ! arr.EnforceType(JSON_Type_Int)); + + json_cleanup_and_delete(arr); +} + +void it_should_import_ints() +{ + int ints[] = {1, 2, 3}; + JSON_Array arr = new JSON_Array(); + Test_Assert("imported successfully", arr.ImportValues(JSON_Type_Int, ints, sizeof(ints))); + + _json_encode(arr); + json_cleanup_and_delete(arr); + + Test_AssertStringsEqual("output", json_encode_output, "[1,2,3]"); +} + +void it_should_import_floats() +{ + float floats[] = {1.1, 2.2, 3.3}; + JSON_Array arr = new JSON_Array(); + Test_Assert("imported successfully", arr.ImportValues(JSON_Type_Float, floats, sizeof(floats))); + + _json_encode(arr); + json_cleanup_and_delete(arr); + + Test_AssertStringsEqual("output", json_encode_output, "[1.1,2.2,3.3]"); +} + +void it_should_import_bools() +{ + bool bools[] = {true, false}; + JSON_Array arr = new JSON_Array(); + Test_Assert("imported successfully", arr.ImportValues(JSON_Type_Bool, bools, sizeof(bools))); + + _json_encode(arr); + json_cleanup_and_delete(arr); + + Test_AssertStringsEqual("output", json_encode_output, "[true,false]"); +} + +void it_should_import_strings() +{ + char strings[][] = {"hello", "world"}; + JSON_Array arr = new JSON_Array(); + Test_Assert("imported successfully", arr.ImportStrings(strings, sizeof(strings))); + + _json_encode(arr); + json_cleanup_and_delete(arr); + + Test_AssertStringsEqual("output", json_encode_output, "[\"hello\",\"world\"]"); +} + +void it_should_export_ints() +{ + JSON_Array arr = view_as(json_decode("[1,2,3]")); + int size = arr.Length; + int[] values = new int[size]; + arr.ExportValues(values, size); + + print_json(arr); + json_cleanup_and_delete(arr); + + Test_AssertEqual("values[0]", values[0], 1); + Test_AssertEqual("values[1]", values[1], 2); + Test_AssertEqual("values[2]", values[2], 3); +} + +void it_should_export_floats() +{ + JSON_Array arr = view_as(json_decode("[1.1,2.2,3.3]")); + int size = arr.Length; + float[] values = new float[size]; + arr.ExportValues(values, size); + + print_json(arr); + json_cleanup_and_delete(arr); + + Test_AssertFloatsEqual("values[0]", values[0], 1.1); + Test_AssertFloatsEqual("values[1]", values[1], 2.2); + Test_AssertFloatsEqual("values[2]", values[2], 3.3); +} + +void it_should_export_bools() +{ + JSON_Array arr = view_as(json_decode("[true, false]")); + int size = arr.Length; + bool[] values = new bool[size]; + arr.ExportValues(values, size); + + print_json(arr); + json_cleanup_and_delete(arr); + + Test_AssertTrue("values[0]", values[0]); + Test_AssertFalse("values[1]", values[1]); +} + +void it_should_export_strings() +{ + JSON_Array arr = view_as(json_decode("[\"hello\",\"world\"]")); + int size = arr.Length; + int str_length = arr.MaxStringLength; + char[][] values = new char[size][str_length]; + arr.ExportStrings(values, size, str_length); + + print_json(arr); + json_cleanup_and_delete(arr); + + Test_AssertStringsEqual("values[0]", values[0], "hello"); + Test_AssertStringsEqual("values[1]", values[1], "world"); +} + +void it_should_support_unicode() +{ + JSON_Array arr = view_as(json_decode("[\"\\u0073tarts with\",\"ends wit\\u0068\",\"\\u0021\",\"\\u00a1\",\"\\u2122\",\"\\u2b50\"]")); + + char expected_values[][] = { + "starts with", + "ends with", + "!", // 1 byte + "¡", // 2 bytes + "™", // 3 bytes + "⭐" // 4 bytes + }; + + int length = arr.Length; + char element_name[8]; + for (int i = 0; i < length; i += 1) { + FormatEx(element_name, sizeof(element_name), "arr[%d]", i); + + int element_length = arr.GetSize(i); + char[] element = new char[element_length]; + arr.GetString(i, element, element_length); + Test_AssertStringsEqual(element_name, element, expected_values[i]); + } + + _json_encode(arr); + json_cleanup_and_delete(arr); + + Test_AssertStringsEqual("encoded", json_encode_output, "[\"starts with\",\"ends with\",\"!\",\"\\u00a1\",\"\\u2122\",\"\\u2b50\"]"); +} + +void it_should_rename_object_keys() +{ + JSON_Object obj = new JSON_Object(); + obj.SetInt("key1", 1); + obj.SetInt("key2", 2); + obj.SetInt("key3", 3); + + Test_AssertFalse("rename existing key with no replace fails", obj.Rename("key1", "key2", false)); + Test_AssertEqual("key1", obj.GetInt("key1"), 1); + Test_AssertEqual("key2", obj.GetInt("key2"), 2); + + Test_AssertTrue("rename existing key with replace succeeds", obj.Rename("key1", "key2")); + Test_AssertFalse("key1 doesn't exist", obj.HasKey("key1")); + Test_AssertEqual("key2", obj.GetInt("key2"), 1); + + Test_AssertTrue("rename new key succeeds", obj.Rename("key3", "key4")); + Test_AssertFalse("key3 doesn't exist", obj.HasKey("key3")); + Test_AssertEqual("key4", obj.GetInt("key4"), 3); + + json_cleanup_and_delete(obj); +} + +void it_should_maintain_hidden_on_renamed_object_keys() +{ + JSON_Object obj = new JSON_Object(); + obj.SetInt("key1", 1); + obj.SetHidden("key1", true); + + obj.Rename("key1", "key2"); + Test_AssertTrue("key2 hidden", obj.GetHidden("key2")); + + json_cleanup_and_delete(obj); +} + +void it_should_calculate_string_sizes_correctly() +{ + Test_AssertEqual("input", json_extract_string_size("\"hello world\" junk", 19, 0), 12); + Test_AssertEqual("output-normal", json_cell_string_size("test"), 7); + Test_AssertEqual("output-basic-escapes", json_cell_string_size("tes\n"), 8); + Test_AssertEqual("output-basic-escapes", json_cell_string_size("\tes\n"), 9); + Test_AssertEqual("output-unicode", json_cell_string_size("test¡"), 13); + Test_AssertEqual("output-unicode", json_cell_string_size("tm™"), 11); + Test_AssertEqual("output-unicode", json_cell_string_size("star⭐"), 13); +} + +void it_should_read_and_write_files() +{ + char[] file = "test.json"; + char[] input = "[1,2,3,4,5]"; + JSON_Array original = view_as(json_decode(input)); + Test_Assert("write to file", original.WriteToFile(file)); + + JSON_Object read = json_read_from_file(file); + Test_Assert("read from file", read != null); + _json_encode(read); + + Test_AssertStringsEqual("input matches output", input, json_encode_output); + + json_cleanup_and_delete(original); + json_cleanup_and_delete(read); + DeleteFile(file); +} + +public void OnPluginStart() +{ + Test_SetBoxWidth(56); + Test_StartSection("sm-json test suite"); + + Test_Run("it_should_encode_empty_objects", it_should_encode_empty_objects); + Test_Run("it_should_encode_empty_arrays", it_should_encode_empty_arrays); + Test_Run("it_should_support_objects", it_should_support_objects); + Test_Run("it_should_support_objects_with_ordered_keys", it_should_support_objects_with_ordered_keys); + Test_Run("it_should_support_decoding_objects_with_ordered_keys", it_should_support_decoding_objects_with_ordered_keys); + Test_Run("it_should_support_decoding_objects_with_duplicate_keys", it_should_support_decoding_objects_with_duplicate_keys); + Test_Run("it_should_support_arrays", it_should_support_arrays); + Test_Run("it_should_support_objects_nested_in_objects", it_should_support_objects_nested_in_objects); + Test_Run("it_should_support_objects_nested_in_arrays", it_should_support_objects_nested_in_arrays); + Test_Run("it_should_support_arrays_nested_in_objects", it_should_support_arrays_nested_in_objects); + Test_Run("it_should_support_arrays_nested_in_arrays", it_should_support_arrays_nested_in_arrays); + Test_Run("it_should_support_basic_methodmaps", it_should_support_basic_methodmaps); + Test_Run("it_should_support_nested_methodmaps", it_should_support_nested_methodmaps); + + // the following tests were acquired from https://www.json.org/JSON_checker/ + // a few additional tests have been added for completeness + + char should_decode[][] = { + "[\n \"JSON Test Pattern pass1\",\n {\"object with 1 member\":[\"array with 1 element\"]},\n {},\n [],\n -42,\n true,\n false,\n null,\n {\n \"integer\": 1234567890,\n \"real\": -9876.543210,\n \"e\": 0.123456789e-12,\n \"E\": 1.234567890E+34,\n \"\": 23456789012E66,\n \"zero\": 0,\n \"one\": 1,\n \"space\": \" \",\n \"quote\": \"\\\"\",\n \"backslash\": \"\\\\\",\n \"controls\": \"\\b\\f\\n\\r\\t\",\n \"slash\": \"/ & \\/\",\n \"alpha\": \"abcdefghijklmnopqrstuvwyz\",\n \"ALPHA\": \"ABCDEFGHIJKLMNOPQRSTUVWYZ\",\n \"digit\": \"0123456789\",\n \"0123456789\": \"digit\",\n \"special\": \"`1~!@#$%^&*()_+-={':[,]}|;.?\",\n \"hex\": \"\\u0123\\u4567\\u89AB\\uCDEF\\uabcd\\uef4A\",\n \"true\": true,\n \"false\": false,\n \"null\": null,\n \"array\":[ ],\n \"object\":{ },\n \"address\": \"50 St. James Street\",\n \"url\": \"http://www.JSON.org/\",\n \"comment\": \"// /* */\": \" \",\n \" s p a c e d \" :[1,2 , 3\n\n,\n\n4 , 5 , 6 ,7 ],\"compact\":[1,2,3,4,5,6,7],\n \"jsontext\": \"{\\\"object with 1 member\\\":[\\\"array with 1 element\\\"]}\",\n \"quotes\": \"" \\u0022 %22 0x22 034 "\",\n \"\\/\\\\\\\"\\uCAFE\\uBABE\\uAB98\\uFCDE\\ubcda\\uef4A\\b\\f\\n\\r\\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?\"\n: \"A key can be any string\"\n },\n 0.5 ,98.6\n,\n99.44\n,\n\n1066,\n1e1,\n0.1e1,\n1e-1,\n1e00,2e+00,2e-00\n,\"rosebud\"]", + "[[[[[[[[[[[[[[[[[[[\"Not too deep\"]]]]]]]]]]]]]]]]]]]", + "{\n \"JSON Test Pattern pass3\": {\n \"The outermost value\": \"must be an object or array.\",\n \"In this test\": \"It is an object.\"\n }\n}\n", + "[\"unicode not high surrogate \\uD7FF\"]" + }; + for (int i = 0; i < sizeof(should_decode); i += 1) { + Test_BeforeRun("it_should_decode"); + Test_Output("index %d", i); + it_should_decode(should_decode[i]); + Test_AfterRun(); + } + + char should_not_decode[][] = { + "\"A JSON payload should be an object or array, not a string.\"", + "-1", "-0", "0", "1", + "-1.1", "-0.0", "0.0", "1.1", + "true", "false", + "null", + "{\"Extra value after close\": true} \"misplaced quoted value\"", + "{\"Illegal expression\": 1 + 2}", + "{\"Illegal invocation\": alert()}", + "{\"Numbers cannot have leading zeroes\": 013}", + "{\"Numbers cannot be hex\": 0x14}", + "[\"Illegal backslash escape: \\x15\"]", + "[\\naked]", + "[\"Illegal backslash escape: \\017\"]", + "{\"Missing colon\" null}", + "[\"Unclosed array\"", + "{\"Double colon\":: null}", + "{\"Comma instead of colon\", null}", + "[\"Colon instead of comma\": false]", + "[\"Bad value\", truth]", + "['single quote']", + "[\"\ttab\tcharacter\tin\tstring\t\"]", + "[\"tab\\ character\\ in\\ string\\ \"]", + "[\"line\nbreak\"]", + "[\"line\\\nbreak\"]", + "[0e]", + "{unquoted_key: \"keys must be quoted\"}", + "[0e+]", + "[0e+-1]", + "{\"Comma instead if closing brace\": true,", + "[\"mismatch\"}", + "[\"extra comma\",]", + "[\"double extra comma\",,]", + "[ , \"<-- missing value\"]", + "[\"Comma after the close\"],", + "[\"Extra close\"]]", + "{\"Extra comma\": true,}", + "[\"unicode too short \\uABC\"]", + "[\"unicode not hex \\uFFFZ\"]", + "[\"unicode high surrogate \\uD800\"]", + "[\"unicode high surrogate \\uDFFF\"]" + }; + for (int i = 0; i < sizeof(should_not_decode); i += 1) { + Test_BeforeRun("it_should_not_decode"); + Test_Output("index %d", i); + it_should_not_decode(should_not_decode[i]); + Test_AfterRun(); + } + + Test_Run("it_should_pretty_print_an_empty_array", it_should_pretty_print_an_empty_array); + Test_Run("it_should_pretty_print_an_empty_object", it_should_pretty_print_an_empty_object); + Test_Run("it_should_pretty_print_with_nesting", it_should_pretty_print_with_nesting); + Test_Run("it_should_pretty_print_with_custom_formatting", it_should_pretty_print_with_custom_formatting); + Test_Run("it_should_trim_floats", it_should_trim_floats); + Test_Run("it_should_remove_meta_keys_from_arrays", it_should_remove_meta_keys_from_arrays); + Test_Run("it_should_remove_meta_keys_from_objects", it_should_remove_meta_keys_from_objects); + Test_Run("it_should_shift_array_down_after_removed_index", it_should_shift_array_down_after_removed_index); + Test_Run("it_should_not_merge_using_array", it_should_not_merge_using_array); + Test_Run("it_should_merge_objects_with_replacement", it_should_merge_objects_with_replacement); + Test_Run("it_should_merge_objects_without_replacement", it_should_merge_objects_without_replacement); + Test_Run("it_should_not_concat_using_object", it_should_not_concat_using_object); + Test_Run("it_should_concat_arrays", it_should_concat_arrays); + Test_Run("it_should_copy_flat_arrays", it_should_copy_flat_arrays); + Test_Run("it_should_copy_flat_objects", it_should_copy_flat_objects); + Test_Run("it_should_shallow_copy_arrays", it_should_shallow_copy_arrays); + Test_Run("it_should_shallow_copy_objects", it_should_shallow_copy_objects); + Test_Run("it_should_preserve_ordered_keys_on_shallow_copy", it_should_preserve_ordered_keys_on_shallow_copy); + Test_Run("it_should_deep_copy_arrays", it_should_deep_copy_arrays); + Test_Run("it_should_deep_copy_objects", it_should_deep_copy_objects); + Test_Run("it_should_allow_single_quotes", it_should_allow_single_quotes); + Test_Run("it_should_return_default_values_for_missing_elements", it_should_return_default_values_for_missing_elements); + Test_Run("it_should_autocleanup_merged_objects", it_should_autocleanup_merged_objects); + Test_Run("it_should_enforce_types_in_arrays", it_should_enforce_types_in_arrays); + Test_Run("it_should_not_enable_strict_type_on_inconsistent_array", it_should_not_enable_strict_type_on_inconsistent_array); + Test_Run("it_should_import_ints", it_should_import_ints); + Test_Run("it_should_import_floats", it_should_import_floats); + Test_Run("it_should_import_bools", it_should_import_bools); + Test_Run("it_should_import_strings", it_should_import_strings); + Test_Run("it_should_export_ints", it_should_export_ints); + Test_Run("it_should_export_floats", it_should_export_floats); + Test_Run("it_should_export_bools", it_should_export_bools); + Test_Run("it_should_export_strings", it_should_export_strings); + Test_Run("it_should_support_unicode", it_should_support_unicode); + Test_Run("it_should_rename_object_keys", it_should_rename_object_keys); + Test_Run("it_should_maintain_hidden_on_renamed_object_keys", it_should_maintain_hidden_on_renamed_object_keys); + Test_Run("it_should_calculate_string_sizes_correctly", it_should_calculate_string_sizes_correctly); + #if SM_INT64_SUPPORTED + Test_Run("it_should_support_int64", it_should_support_int64); + #endif + Test_Run("it_should_read_and_write_files", it_should_read_and_write_files); + + Test_EndSection(); +} diff --git a/libalternator/tests/corpus/multicolors/addons/sourcemod/scripting/include/multicolors.inc b/libalternator/tests/corpus/multicolors/addons/sourcemod/scripting/include/multicolors.inc new file mode 100644 index 0000000..6da1297 --- /dev/null +++ b/libalternator/tests/corpus/multicolors/addons/sourcemod/scripting/include/multicolors.inc @@ -0,0 +1,515 @@ +#if defined _multicolors_included + #endinput +#endif +#define _multicolors_included + +#define MuCo_VERSION "2.2.0" + +#include "multicolors/morecolors" +#include "multicolors/colors" + +/* +* +* Credits: +* - Popoklopsi +* - Powerlord +* - exvel +* - Dr. McKay +* - Rushaway / maxime1907 (SRCDSLAB) +* +* Based on stamm-colors +* - https://github.com/popoklopsi/Stamm/blob/master/include/stamm/stamm-colors.inc +* +*/ + + + +#define PREFIX_MAX_LENGTH 64 +#define PREFIX_SEPARATOR "{default} " + +/* Global var to check whether colors are fixed or not */ +static bool g_bCFixColors; + +static char g_sCPrefix[PREFIX_MAX_LENGTH]; + +/** + * Add a chat prefix before all chat msg + * + * @param sPrefix Prefix + */ +stock void CSetPrefix(const char[] sPrefix, any ...) { + if (!sPrefix[0]) { + return; + } + + SetGlobalTransTarget(LANG_SERVER); + VFormat(g_sCPrefix, sizeof(g_sCPrefix) - strlen(PREFIX_SEPARATOR), sPrefix, 2); + + // Add ending space + Format(g_sCPrefix, sizeof(g_sCPrefix), "%s%s", g_sCPrefix, PREFIX_SEPARATOR); +} + +/** + * Add a chat prefix before all chat msg + * + * @param sPrefix Prefix + */ +stock void CClearPrefix() { + g_sCPrefix[0] = '\0'; +} + +/** + * Writes a message to a client with the correct stock for the game. + * + * @param client Client index. + * @param message Message (formatting rules). + * + * @error If the client is not connected an error will be thrown. + */ +stock void CPrintToChat(int client, const char[] message, any ...) { + char buffer[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(client); + VFormat(buffer, sizeof(buffer), message, 3); + + if (!g_bCFixColors) { + CFixColors(); + } + + if (!IsSource2009()) { + C_PrintToChat(client, "%s%s", g_sCPrefix, buffer); + } + else { + MC_PrintToChat(client, "%s%s", g_sCPrefix, buffer); + } +} + +/** + * Prints a message to all clients in the chat area. + * Supports color tags. + * + * @param client Client index. + * @param message Message (formatting rules) + */ +stock void CPrintToChatAll(const char[] message, any ...) { + if (!g_bCFixColors) { + CFixColors(); + } + + char buffer[MAX_BUFFER_LENGTH]; + if (!IsSource2009()) { + for (int i = 1; i <= MaxClients; ++i) { + if (IsClientInGame(i) && !IsFakeClient(i) && !C_SkipList[i]) { + SetGlobalTransTarget(i); + VFormat(buffer, sizeof(buffer), message, 2); + + C_PrintToChat(i, "%s", buffer); + } + + C_SkipList[i] = false; + } + } + else { + MC_InitFastColors(); + + char buffer2[MAX_BUFFER_LENGTH]; + + for (int i = 1; i <= MaxClients; ++i) { + if (!IsClientInGame(i) || MC_SkipList[i]) { + MC_SkipList[i] = false; + continue; + } + + SetGlobalTransTarget(i); + Format(buffer, sizeof(buffer), "\x01%s", message); + VFormat(buffer2, sizeof(buffer2), buffer, 2); + + MC_ReplaceColorCodes(buffer2); + MC_SendMessage(i, buffer2); + } + } +} + +/** + * Writes a message to all of a client's observers. + * + * @param target Client index. + * @param message Message (formatting rules). + */ +stock void CPrintToChatObservers(int target, const char[] message, any ...) { + char buffer[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(LANG_SERVER); + VFormat(buffer, sizeof(buffer), message, 3); + + if (!g_bCFixColors) { + CFixColors(); + } + + int observee; + int ObserverMode; + + for (int client = 1; client <= MaxClients; client++) { + if (!IsClientInGame(client) || IsPlayerAlive(client) || IsFakeClient(client)) { + continue; + } + + ObserverMode = GetEntProp(client, Prop_Send, "m_iObserverMode"); + if (ObserverMode != 4 && ObserverMode != 5) { + continue; + } + + observee = GetEntPropEnt(client, Prop_Send, "m_hObserverTarget"); + if (observee == target) { + CPrintToChat(client, buffer); + } + } +} + +/** + * Writes a message to a client with the correct stock for the game. + * + * @param client Client index. + * @param author Author index. + * @param message Message (formatting rules). + * + * @error If the client is not connected an error will be thrown. + */ +stock void CPrintToChatEx(int client, int author, const char[] message, any ...) { + char buffer[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(client); + VFormat(buffer, sizeof(buffer), message, 4); + + if (!g_bCFixColors) { + CFixColors(); + } + + if (!IsSource2009()) { + C_PrintToChatEx(client, author, "%s%s", g_sCPrefix, buffer); + } + else { + MC_PrintToChatEx(client, author, "%s%s", g_sCPrefix, buffer); + } +} + +/** + * Writes a message to all clients with the correct stock for the game. + * + * @param author Author index. + * @param message Message (formatting rules). + */ +stock void CPrintToChatAllEx(int author, const char[] message, any ...) { + char buffer[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(LANG_SERVER); + VFormat(buffer, sizeof(buffer), message, 3); + + if (!g_bCFixColors) { + CFixColors(); + } + + if (!IsSource2009()) { + C_PrintToChatAllEx(author, "%s%s", g_sCPrefix, buffer); + } + else { + MC_PrintToChatAllEx(author, "%s%s", g_sCPrefix, buffer); + } +} + +/** + * Writes a message to all of a client's observers with the correct + * game stock. + * + * @param target Client index. + * @param message Message (formatting rules). + */ +stock void CPrintToChatObserversEx(int target, const char[] message, any ...) { + char buffer[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(LANG_SERVER); + VFormat(buffer, sizeof(buffer), message, 3); + + if (!g_bCFixColors) { + CFixColors(); + } + + int observee; + int ObserverMode; + for (int client = 1; client <= MaxClients; client++) { + if (!IsClientInGame(client) || IsPlayerAlive(client) || IsFakeClient(client)) { + continue; + } + + ObserverMode = GetEntProp(client, Prop_Send, "m_iObserverMode"); + if (ObserverMode != 4 && ObserverMode != 5) { + continue; + } + + observee = GetEntPropEnt(client, Prop_Send, "m_hObserverTarget"); + if (observee == target) { + CPrintToChatEx(client, target, buffer); + } + } +} + +/** + * Replies to a command with colors + * + * @param client Client to reply to + * @param message Message (formatting rules) + */ +stock void CReplyToCommand(int client, const char[] message, any ...) { + char buffer[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(client); + VFormat(buffer, sizeof(buffer), message, 3); + + if (!g_bCFixColors) { + CFixColors(); + } + + if (!IsSource2009()) { + C_ReplyToCommand(client, "%s%s", g_sCPrefix, buffer); + } + else { + MC_ReplyToCommand(client, "%s%s", g_sCPrefix, buffer); + } +} + +/** + * Replies to a command with colors + * + * @param client Client to reply to + * @param author Client to use for {teamcolor} + * @param message Message (formatting rules) + */ +stock void CReplyToCommandEx(int client, int author, const char[] message, any ...) { + char buffer[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(client); + VFormat(buffer, sizeof(buffer), message, 4); + + if (!g_bCFixColors) { + CFixColors(); + } + + if (!IsSource2009()) { + C_ReplyToCommandEx(client, author, "%s%s", g_sCPrefix, buffer); + } + else { + MC_ReplyToCommandEx(client, author, "%s%s", g_sCPrefix, buffer); + } +} + +/** + * Remove all tags and print to server + * + * @param message Message (formatting rules) + */ +stock void CPrintToServer(const char[] message, any ...) { + char buffer[MAX_MESSAGE_LENGTH]; + char prefixBuffer[PREFIX_MAX_LENGTH]; + SetGlobalTransTarget(LANG_SERVER); + VFormat(buffer, sizeof(buffer), message, 2); + strcopy(prefixBuffer, sizeof(prefixBuffer), g_sCPrefix); + + if (!g_bCFixColors) { + CFixColors(); + } + + CRemoveTags(buffer, sizeof(buffer)); + CRemoveTags(prefixBuffer, sizeof(prefixBuffer)); + + PrintToServer("%s%s", prefixBuffer, buffer); +} + +/** + * Displays usage of an admin command to users depending on the + * setting of the sm_show_activity cvar. + * + * This version does not display a message to the originating client + * if used from chat triggers or menus. If manual replies are used + * for these cases, then this function will suffice. Otherwise, + * CShowActivity2() is slightly more useful. + * Supports color tags. + * + * @param client Client index doing the action, or 0 for server. + * @param format Formatting rules. + * @param ... Variable number of format parameters. + * + * @error + */ +stock void CShowActivity(int author, const char[] message, any ...) { + char buffer[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(LANG_SERVER); + VFormat(buffer, sizeof(buffer), message, 3); + + if (!g_bCFixColors) { + CFixColors(); + } + + if (!IsSource2009()) { + C_ShowActivity(author, "%s", buffer); + } + else { + MC_ShowActivity(author, "%s", buffer); + } +} + +/** + * Same as C_ShowActivity(), except the tag parameter is used instead of "[SM] " (note that you must supply any spacing). + * Supports color tags. + * + * @param client Client index doing the action, or 0 for server. + * @param tags Tag to display with. + * @param format Formatting rules. + * @param ... Variable number of format parameters. + * + * @error + */ +stock void CShowActivityEx(int author, const char[] tag, const char[] message, any ...) { + char buffer[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(LANG_SERVER); + VFormat(buffer, sizeof(buffer), message, 4); + + if (!g_bCFixColors) { + CFixColors(); + } + + if (!IsSource2009()) { + C_ShowActivityEx(author, tag, "%s", buffer); + } + else { + MC_ShowActivityEx(author, tag, "%s", buffer); + } +} + +/** + * Displays usage of an admin command to users depending on the setting of the sm_show_activity cvar. + * All users receive a message in their chat text, except for the originating client, + * who receives the message based on the current ReplySource. + * Supports color tags. + * + * @param client Client index doing the action, or 0 for server. + * @param tags Tag to prepend to the message. + * @param format Formatting rules. + * @param ... Variable number of format parameters. + * + * @error + */ + stock void CShowActivity2(int author, const char[] tag, const char[] message, any ...) { + char buffer[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(LANG_SERVER); + VFormat(buffer, sizeof(buffer), message, 4); + + if (!g_bCFixColors) { + CFixColors(); + } + + if (!IsSource2009()) { + C_ShowActivity2(author, tag, "%s", buffer); + } + else { + MC_ShowActivity2(author, tag, "%s", buffer); + } +} + + +/** + * Replaces color tags in a string with color codes + * + * @param message String. + * @param maxlength Maximum length of the string buffer. + */ +stock void CFormatColor(char[] message, int maxlength, int author = -1) { + if (!g_bCFixColors) { + CFixColors(); + } + + if (!IsSource2009()) { + if (author == 0) { + author = -1; + } + + C_Format(message, maxlength, author); + } + else { + if (author == -1) { + author = 0; + } + + MC_ReplaceColorCodes(message, author, false, maxlength); + } +} + +/** + * Removes color tags from a message + * + * @param message Message to remove tags from + * @param maxlen Maximum buffer length + */ +stock void CRemoveTags(char[] message, int maxlen) { + if (!IsSource2009()) { + C_RemoveTags(message, maxlen); + } + else { + MC_RemoveTags(message, maxlen); + } +} + +/** + * Fixes missing Lightgreen color. + */ +stock void CFixColors() { + g_bCFixColors = true; + + // Replace lightgreen if not exists + if (!C_ColorAllowed(Color_Lightgreen)) { + if (C_ColorAllowed(Color_Lime)) { + C_ReplaceColor(Color_Lightgreen, Color_Lime); + } + else if (C_ColorAllowed(Color_Olive)) { + C_ReplaceColor(Color_Lightgreen, Color_Olive); + } + } +} + +/** + * Gets the color code from the color name + */ +stock bool CGetColor(const char[] key, char[] output, int maxlen) +{ + if (!IsSource2009()) { + for (int i = 0; i < sizeof(C_Tag); i++) { + if (strcmp(key, C_Tag[i]) != 0) { + continue; + } + + strcopy(output, maxlen, C_TagCode[i]); + } + + return true; + } + else { + return MC_FastGetColor(key, output, maxlen); + } +} + +/** +* Inits the colors list +*/ +stock void CInitColors() +{ + if (!IsSource2009()) { + // Do Nothing. Not implanted yet + } + else { + MC_InitFastColors(); + } +} + +stock bool IsSource2009() { + static bool checked = false; + static bool result = false; + + if (!checked) { + EngineVersion engine = GetEngineVersion(); + result = (engine == Engine_CSS || engine == Engine_HL2DM || engine == Engine_DODS || engine == Engine_TF2 || engine == Engine_SDK2013); + checked = true; + } + return result; +} diff --git a/libalternator/tests/corpus/multicolors/addons/sourcemod/scripting/include/multicolors/colors.inc b/libalternator/tests/corpus/multicolors/addons/sourcemod/scripting/include/multicolors/colors.inc new file mode 100644 index 0000000..170ec97 --- /dev/null +++ b/libalternator/tests/corpus/multicolors/addons/sourcemod/scripting/include/multicolors/colors.inc @@ -0,0 +1,905 @@ +/************************************************************************** + * * + * Colored Chat Functions * + * Author: exvel, Editor: Popoklopsi, Powerlord, Bara * + * Version: 2.0.0-MC * + * * + **************************************************************************/ + + +#if defined _colors_included + #endinput +#endif +#define _colors_included + +#define MAX_MESSAGE_LENGTH 250 +#define MAX_COLORS 18 + +#define SERVER_INDEX 0 +#define NO_INDEX -1 +#define NO_PLAYER -2 + +enum C_Colors { + Color_Default = 0, + Color_Darkred, + Color_Green, + Color_Lightgreen, + Color_Red, + Color_Blue, + Color_Olive, + Color_Lime, + Color_Lightred, + Color_Purple, + Color_Grey, + Color_Yellow, + Color_Orange, + Color_Bluegrey, + Color_Lightblue, + Color_Darkblue, + Color_Grey2, + Color_Orchid, + Color_Lightred2 +} + +/* C_Colors' properties */ +char C_Tag[][] = {"{default}", "{darkred}", "{green}", "{lightgreen}", "{red}", "{blue}", "{olive}", "{lime}", "{lightred}", "{purple}", "{grey}", "{yellow}", "{orange}", "{bluegrey}", "{lightblue}", "{darkblue}", "{grey2}", "{orchid}", "{lightred2}"}; +char C_TagCode[][] = {"\x01", "\x02", "\x04", "\x03", "\x03", "\x03", "\x05", "\x06", "\x07", "\x03", "\x08", "\x09", "\x10", "\x0A", "\x0B", "\x0C", "\x0D", "\x0E", "\x0F"}; +bool C_TagReqSayText2[] = {false, false, false, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false}; +bool C_EventIsHooked; +bool C_SkipList[MAXPLAYERS+1]; + +/* Game default profile */ +bool C_Profile_Colors[] = {true, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false}; +int C_Profile_TeamIndex[] = {NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX}; +bool C_Profile_SayText2; + +static ConVar sm_show_activity; + +/** + * Prints a message to a specific client in the chat area. + * Supports color tags. + * + * @param client Client index. + * @param szMessage Message (formatting rules). + * @return No return + * + * On error/Errors: If the client is not connected an error will be thrown. + */ +stock void C_PrintToChat(int client, const char[] szMessage, any ...) { + if (client <= 0 || client > MaxClients) { + ThrowError("Invalid client index %d", client); + } + + if (!IsClientInGame(client)) { + ThrowError("Client %d is not in game", client); + } + + char szBuffer[MAX_MESSAGE_LENGTH]; + char szCMessage[MAX_MESSAGE_LENGTH]; + + SetGlobalTransTarget(client); + + Format(szBuffer, sizeof(szBuffer), "\x01%s", szMessage); + VFormat(szCMessage, sizeof(szCMessage), szBuffer, 3); + + int index = C_Format(szCMessage, sizeof(szCMessage)); + + if (index == NO_INDEX) { + PrintToChat(client, "%s", szCMessage); + } + else { + C_SayText2(client, index, szCMessage); + } +} + +/** + * Reples to a message in a command. A client index of 0 will use PrintToServer(). + * If the command was from the console, PrintToConsole() is used. If the command was from chat, C_PrintToChat() is used. + * Supports color tags. + * + * @param client Client index, or 0 for server. + * @param szMessage Formatting rules. + * @param ... Variable number of format parameters. + * @return No return + * + * On error/Errors: If the client is not connected or invalid. + */ +stock void C_ReplyToCommand(int client, const char[] szMessage, any ...) { + char szCMessage[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(client); + VFormat(szCMessage, sizeof(szCMessage), szMessage, 3); + + if (client == 0) { + C_RemoveTags(szCMessage, sizeof(szCMessage)); + PrintToServer("%s", szCMessage); + } + else if (GetCmdReplySource() == SM_REPLY_TO_CONSOLE) { + C_RemoveTags(szCMessage, sizeof(szCMessage)); + PrintToConsole(client, "%s", szCMessage); + } + else { + C_PrintToChat(client, "%s", szCMessage); + } +} + +/** + * Reples to a message in a command. A client index of 0 will use PrintToServer(). + * If the command was from the console, PrintToConsole() is used. If the command was from chat, C_PrintToChat() is used. + * Supports color tags. + * + * @param client Client index, or 0 for server. + * @param author Author index whose color will be used for teamcolor tag. + * @param szMessage Formatting rules. + * @param ... Variable number of format parameters. + * @return No return + * + * On error/Errors: If the client is not connected or invalid. + */ +stock void C_ReplyToCommandEx(int client, int author, const char[] szMessage, any ...) { + char szCMessage[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(client); + VFormat(szCMessage, sizeof(szCMessage), szMessage, 4); + + if (client == 0) { + C_RemoveTags(szCMessage, sizeof(szCMessage)); + PrintToServer("%s", szCMessage); + } + else if (GetCmdReplySource() == SM_REPLY_TO_CONSOLE) { + C_RemoveTags(szCMessage, sizeof(szCMessage)); + PrintToConsole(client, "%s", szCMessage); + } + else { + C_PrintToChatEx(client, author, "%s", szCMessage); + } +} + +/** + * Prints a message to all clients in the chat area. + * Supports color tags. + * + * @param client Client index. + * @param szMessage Message (formatting rules) + * @return No return + */ +stock void C_PrintToChatAll(const char[] szMessage, any ...) { + char szBuffer[MAX_MESSAGE_LENGTH]; + + for (int i = 1; i <= MaxClients; ++i) { + if (IsClientInGame(i) && !IsFakeClient(i) && !C_SkipList[i]) { + SetGlobalTransTarget(i); + VFormat(szBuffer, sizeof(szBuffer), szMessage, 2); + + C_PrintToChat(i, "%s", szBuffer); + } + + C_SkipList[i] = false; + } +} + +/** + * Prints a message to a specific client in the chat area. + * Supports color tags and teamcolor tag. + * + * @param client Client index. + * @param author Author index whose color will be used for teamcolor tag. + * @param szMessage Message (formatting rules). + * @return No return + * + * On error/Errors: If the client or author are not connected an error will be thrown. + */ +stock void C_PrintToChatEx(int client, int author, const char[] szMessage, any ...) { + if (client <= 0 || client > MaxClients) { + ThrowError("Invalid client index %d", client); + } + + if (!IsClientInGame(client)) { + ThrowError("Client %d is not in game", client); + } + + if (author < 0 || author > MaxClients) { + ThrowError("Invalid client index %d", author); + } + + char szBuffer[MAX_MESSAGE_LENGTH]; + char szCMessage[MAX_MESSAGE_LENGTH]; + + SetGlobalTransTarget(client); + + Format(szBuffer, sizeof(szBuffer), "\x01%s", szMessage); + VFormat(szCMessage, sizeof(szCMessage), szBuffer, 4); + + int index = C_Format(szCMessage, sizeof(szCMessage), author); + + if (index == NO_INDEX) { + PrintToChat(client, "%s", szCMessage); + } + else { + C_SayText2(client, author, szCMessage); + } +} + +/** + * Prints a message to all clients in the chat area. + * Supports color tags and teamcolor tag. + * + * @param author Author index whos color will be used for teamcolor tag. + * @param szMessage Message (formatting rules). + * @return No return + * + * On error/Errors: If the author is not connected an error will be thrown. + */ +stock void C_PrintToChatAllEx(int author, const char[] szMessage, any ...) { + if (author < 0 || author > MaxClients) { + ThrowError("Invalid client index %d", author); + } + + if (!IsClientInGame(author)) { + ThrowError("Client %d is not in game", author); + } + + char szBuffer[MAX_MESSAGE_LENGTH]; + + for (int i = 1; i <= MaxClients; ++i) { + if (IsClientInGame(i) && !IsFakeClient(i) && !C_SkipList[i]) { + SetGlobalTransTarget(i); + VFormat(szBuffer, sizeof(szBuffer), szMessage, 3); + + C_PrintToChatEx(i, author, "%s", szBuffer); + } + + C_SkipList[i] = false; + } +} + +/** + * Removes color tags from the string. + * + * @param szMessage String. + * @return No return + */ +stock void C_RemoveTags(char[] szMessage, int maxlength) { + for (int i = 0; i < MAX_COLORS; i++) { + ReplaceString(szMessage, maxlength, C_Tag[i], "", false); + } + + ReplaceString(szMessage, maxlength, "{teamcolor}", "", false); +} + +/** + * Checks whether a color is allowed or not + * + * @param tag Color Tag. + * @return True when color is supported, otherwise false + */ +stock bool C_ColorAllowed(C_Colors color) { + if (!C_EventIsHooked) { + C_SetupProfile(); + + C_EventIsHooked = true; + } + + return C_Profile_Colors[color]; +} + +/** + * Replace the color with another color + * Handle with care! + * + * @param color color to replace. + * @param newColor color to replace with. + * @noreturn + */ +stock void C_ReplaceColor(C_Colors color, C_Colors newColor) { + if (!C_EventIsHooked) { + C_SetupProfile(); + + C_EventIsHooked = true; + } + + C_Profile_Colors[color] = C_Profile_Colors[newColor]; + C_Profile_TeamIndex[color] = C_Profile_TeamIndex[newColor]; + + C_TagReqSayText2[color] = C_TagReqSayText2[newColor]; + Format(C_TagCode[color], sizeof(C_TagCode[]), C_TagCode[newColor]); +} + +/** + * This function should only be used right in front of + * C_PrintToChatAll or C_PrintToChatAllEx and it tells + * to those funcions to skip specified client when printing + * message to all clients. After message is printed client will + * no more be skipped. + * + * @param client Client index + * @return No return + */ +stock void C_SkipNextClient(int client) { + if (client <= 0 || client > MaxClients) { + ThrowError("Invalid client index %d", client); + } + + C_SkipList[client] = true; +} + +/** + * Replaces color tags in a string with color codes + * + * @param szMessage String. + * @param maxlength Maximum length of the string buffer. + * @return Client index that can be used for SayText2 author index + * + * On error/Errors: If there is more then one team color is used an error will be thrown. + */ +stock int C_Format(char[] szMessage, int maxlength, int author = NO_INDEX) { + /* Hook event for auto profile setup on map start */ + if (!C_EventIsHooked) { + C_SetupProfile(); + HookEvent("server_spawn", C_Event_MapStart, EventHookMode_PostNoCopy); + + C_EventIsHooked = true; + } + + int iRandomPlayer = NO_INDEX; + + // On CS:GO set invisible precolor + if (GetEngineVersion() == Engine_CSGO) { + Format(szMessage, maxlength, " %s", szMessage); + } + + /* If author was specified replace {teamcolor} tag */ + if (author != NO_INDEX) { + if (C_Profile_SayText2) { + ReplaceString(szMessage, maxlength, "{teamcolor}", "\x03", false); + + iRandomPlayer = author; + } + /* If saytext2 is not supported by game replace {teamcolor} with green tag */ + else { + ReplaceString(szMessage, maxlength, "{teamcolor}", C_TagCode[Color_Green], false); + } + } + else { + ReplaceString(szMessage, maxlength, "{teamcolor}", "", false); + } + + /* For other color tags we need a loop */ + for (int i = 0; i < MAX_COLORS; i++) { + /* If tag not found - skip */ + if (StrContains(szMessage, C_Tag[i], false) == -1) { + continue; + } + + /* If tag is not supported by game replace it with green tag */ + else if (!C_Profile_Colors[i]) { + ReplaceString(szMessage, maxlength, C_Tag[i], C_TagCode[Color_Green], false); + } + + /* If tag doesn't need saytext2 simply replace */ + else if (!C_TagReqSayText2[i]) { + ReplaceString(szMessage, maxlength, C_Tag[i], C_TagCode[i], false); + } + + /* Tag needs saytext2 */ + else { + /* If saytext2 is not supported by game replace tag with green tag */ + if (!C_Profile_SayText2) { + ReplaceString(szMessage, maxlength, C_Tag[i], C_TagCode[Color_Green], false); + } + + /* Game supports saytext2 */ + else { + /* If random player for tag wasn't specified replace tag and find player */ + if (iRandomPlayer == NO_INDEX) { + /* Searching for valid client for tag */ + iRandomPlayer = C_FindRandomPlayerByTeam(C_Profile_TeamIndex[i]); + + /* If player not found replace tag with green color tag */ + if (iRandomPlayer == NO_PLAYER) { + ReplaceString(szMessage, maxlength, C_Tag[i], C_TagCode[Color_Green], false); + } + + /* If player was found simply replace */ + else { + ReplaceString(szMessage, maxlength, C_Tag[i], C_TagCode[i], false); + } + + } + /* If found another team color tag throw error */ + else { + //ReplaceString(szMessage, maxlength, C_Tag[i], ""); + ThrowError("Using two team colors in one message is not allowed"); + } + } + } + } + + return iRandomPlayer; +} + +/** + * Finds a random player with specified team + * + * @param color_team Client team. + * @return Client index or NO_PLAYER if no player found + */ +stock int C_FindRandomPlayerByTeam(int color_team) { + if (color_team == SERVER_INDEX) { + return 0; + } + + int[] players = new int[MaxClients]; + int count; + + for (int i = 1; i <= MaxClients; ++i) { + if (IsClientInGame(i) && GetClientTeam(i) == color_team) { + players[count++] = i; + } + } + + if (count) { + return players[GetRandomInt(0, count-1)]; + } + + return NO_PLAYER; +} + +/** + * Sends a SayText2 usermessage to a client + * + * @param szMessage Client index + * @param maxlength Author index + * @param szMessage Message + * @return No return. + */ +stock void C_SayText2(int client, int author, const char[] szMessage) { + Handle hBuffer = StartMessageOne("SayText2", client, USERMSG_RELIABLE|USERMSG_BLOCKHOOKS); + + if (GetFeatureStatus(FeatureType_Native, "GetUserMessageType") == FeatureStatus_Available && GetUserMessageType() == UM_Protobuf) { + Protobuf pb = UserMessageToProtobuf(hBuffer); + pb.SetInt("ent_idx", author); + pb.SetBool("chat", true); + pb.SetString("msg_name", szMessage); + pb.AddString("params", ""); + pb.AddString("params", ""); + pb.AddString("params", ""); + pb.AddString("params", ""); + } + else { + BfWrite bf = UserMessageToBfWrite(hBuffer); + bf.WriteByte(author); + bf.WriteByte(true); + bf.WriteString(szMessage); + } + + EndMessage(); +} + +/** + * Creates game color profile + * This function must be edited if you want to add more games support + * + * @return No return. + */ +stock void C_SetupProfile() { + EngineVersion engine = GetEngineVersion(); + + if (engine == Engine_CSS) { + C_Profile_Colors[Color_Lightgreen] = true; + C_Profile_Colors[Color_Red] = true; + C_Profile_Colors[Color_Blue] = true; + C_Profile_Colors[Color_Olive] = true; + C_Profile_TeamIndex[Color_Lightgreen] = SERVER_INDEX; + C_Profile_TeamIndex[Color_Red] = 2; + C_Profile_TeamIndex[Color_Blue] = 3; + C_Profile_SayText2 = true; + } + else if (engine == Engine_CSGO) { + C_Profile_Colors[Color_Red] = true; + C_Profile_Colors[Color_Blue] = true; + C_Profile_Colors[Color_Olive] = true; + C_Profile_Colors[Color_Darkred] = true; + C_Profile_Colors[Color_Lime] = true; + C_Profile_Colors[Color_Lightred] = true; + C_Profile_Colors[Color_Purple] = true; + C_Profile_Colors[Color_Grey] = true; + C_Profile_Colors[Color_Yellow] = true; + C_Profile_Colors[Color_Orange] = true; + C_Profile_Colors[Color_Bluegrey] = true; + C_Profile_Colors[Color_Lightblue] = true; + C_Profile_Colors[Color_Darkblue] = true; + C_Profile_Colors[Color_Grey2] = true; + C_Profile_Colors[Color_Orchid] = true; + C_Profile_Colors[Color_Lightred2] = true; + C_Profile_TeamIndex[Color_Red] = 2; + C_Profile_TeamIndex[Color_Blue] = 3; + C_Profile_SayText2 = true; + } + else if (engine == Engine_TF2) { + C_Profile_Colors[Color_Lightgreen] = true; + C_Profile_Colors[Color_Red] = true; + C_Profile_Colors[Color_Blue] = true; + C_Profile_Colors[Color_Olive] = true; + C_Profile_TeamIndex[Color_Lightgreen] = SERVER_INDEX; + C_Profile_TeamIndex[Color_Red] = 2; + C_Profile_TeamIndex[Color_Blue] = 3; + C_Profile_SayText2 = true; + } + else if (engine == Engine_Left4Dead || engine == Engine_Left4Dead2) { + C_Profile_Colors[Color_Lightgreen] = true; + C_Profile_Colors[Color_Red] = true; + C_Profile_Colors[Color_Blue] = true; + C_Profile_Colors[Color_Olive] = true; + C_Profile_TeamIndex[Color_Lightgreen] = SERVER_INDEX; + C_Profile_TeamIndex[Color_Red] = 3; + C_Profile_TeamIndex[Color_Blue] = 2; + C_Profile_SayText2 = true; + } + else if (engine == Engine_HL2DM) { + /* hl2mp profile is based on mp_teamplay convar */ + if (GetConVarBool(FindConVar("mp_teamplay"))) { + C_Profile_Colors[Color_Red] = true; + C_Profile_Colors[Color_Blue] = true; + C_Profile_Colors[Color_Olive] = true; + C_Profile_TeamIndex[Color_Red] = 3; + C_Profile_TeamIndex[Color_Blue] = 2; + C_Profile_SayText2 = true; + } + else { + C_Profile_SayText2 = false; + C_Profile_Colors[Color_Olive] = true; + } + } + else if (engine == Engine_DODS) { + C_Profile_Colors[Color_Olive] = true; + C_Profile_SayText2 = false; + } + /* Profile for other games */ + else { + if (GetUserMessageId("SayText2") == INVALID_MESSAGE_ID) { + C_Profile_SayText2 = false; + } + else { + C_Profile_Colors[Color_Red] = true; + C_Profile_Colors[Color_Blue] = true; + C_Profile_TeamIndex[Color_Red] = 2; + C_Profile_TeamIndex[Color_Blue] = 3; + C_Profile_SayText2 = true; + } + } +} + +public void C_Event_MapStart(Event event, const char[] name, bool dontBroadcast) { + C_SetupProfile(); + + for (int i = 1; i <= MaxClients; ++i) { + C_SkipList[i] = false; + } +} + +/** + * Displays usage of an admin command to users depending on the + * setting of the sm_show_activity cvar. + * + * This version does not display a message to the originating client + * if used from chat triggers or menus. If manual replies are used + * for these cases, then this function will suffice. Otherwise, + * C_ShowActivity2() is slightly more useful. + * Supports color tags. + * + * @param client Client index doing the action, or 0 for server. + * @param format Formatting rules. + * @param ... Variable number of format parameters. + * @noreturn + * @error + */ +stock int C_ShowActivity(int client, const char[] format, any ...) { + if (sm_show_activity == null) { + sm_show_activity = FindConVar("sm_show_activity"); + } + + char tag[] = "[SM] "; + + char szBuffer[MAX_MESSAGE_LENGTH]; + //char szCMessage[MAX_MESSAGE_LENGTH]; + int value = sm_show_activity.IntValue; + ReplySource replyto = GetCmdReplySource(); + + char name[MAX_NAME_LENGTH] = "Console"; + char sign[MAX_NAME_LENGTH] = "ADMIN"; + bool display_in_chat = false; + if (client != 0) { + if (client < 0 || client > MaxClients || !IsClientConnected(client)) { + ThrowError("Client index %d is invalid", client); + } + + GetClientName(client, name, sizeof(name)); + AdminId id = GetUserAdmin(client); + if (id == INVALID_ADMIN_ID || !GetAdminFlag(id, Admin_Generic, Access_Effective)) { + sign = "PLAYER"; + } + + /* Display the message to the client? */ + if (replyto == SM_REPLY_TO_CONSOLE) { + SetGlobalTransTarget(client); + VFormat(szBuffer, sizeof(szBuffer), format, 3); + + C_RemoveTags(szBuffer, sizeof(szBuffer)); + PrintToConsole(client, "%s%s", tag, szBuffer); + display_in_chat = true; + } + } + else { + SetGlobalTransTarget(LANG_SERVER); + VFormat(szBuffer, sizeof(szBuffer), format, 3); + + C_RemoveTags(szBuffer, sizeof(szBuffer)); + PrintToServer("%s%s", tag, szBuffer); + } + + if (!value) { + return 1; + } + + for (int i = 1; i <= MaxClients; ++i) { + if (!IsClientInGame(i) || IsFakeClient(i) || (display_in_chat && i == client)) { + continue; + } + + AdminId id = GetUserAdmin(i); + SetGlobalTransTarget(i); + if (id == INVALID_ADMIN_ID || !GetAdminFlag(id, Admin_Generic, Access_Effective)) { + /* Treat this as a normal user. */ + if ((value & 1) | (value & 2)) { + char newsign[MAX_NAME_LENGTH]; + + if ((value & 2) || (i == client)) { + newsign = name; + } + else { + newsign = sign; + } + + VFormat(szBuffer, sizeof(szBuffer), format, 3); + + C_PrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + else { + /* Treat this as an admin user */ + bool is_root = GetAdminFlag(id, Admin_Root, Access_Effective); + if ((value & 4) || (value & 8) || ((value & 16) && is_root)) { + char newsign[MAX_NAME_LENGTH]; + + if ((value & 8) || ((value & 16) && is_root) || (i == client)) { + newsign = name; + } + else { + newsign = sign; + } + + VFormat(szBuffer, sizeof(szBuffer), format, 3); + + C_PrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + } + + return 1; +} + +/** + * Same as C_ShowActivity(), except the tag parameter is used instead of "[SM] " (note that you must supply any spacing). + * Supports color tags. + * + * @param client Client index doing the action, or 0 for server. + * @param tags Tag to display with. + * @param format Formatting rules. + * @param ... Variable number of format parameters. + * @noreturn + * @error + */ +stock int C_ShowActivityEx(int client, const char[] tag, const char[] format, any ...) { + if (sm_show_activity == null) { + sm_show_activity = FindConVar("sm_show_activity"); + } + + char szTag[MAX_MESSAGE_LENGTH]; + strcopy(szTag, sizeof(szTag), tag); + MC_RemoveTags(szTag, sizeof(szTag)); + + char szBuffer[MAX_MESSAGE_LENGTH]; + //char szCMessage[MAX_MESSAGE_LENGTH]; + int value = sm_show_activity.IntValue; + ReplySource replyto = GetCmdReplySource(); + + char name[MAX_NAME_LENGTH] = "Console"; + char sign[MAX_NAME_LENGTH] = "ADMIN"; + bool display_in_chat = false; + if (client != 0) { + if (client < 0 || client > MaxClients || !IsClientConnected(client)) { + ThrowError("Client index %d is invalid", client); + } + + GetClientName(client, name, sizeof(name)); + AdminId id = GetUserAdmin(client); + if (id == INVALID_ADMIN_ID || !GetAdminFlag(id, Admin_Generic, Access_Effective)) { + sign = "PLAYER"; + } + + /* Display the message to the client? */ + if (replyto == SM_REPLY_TO_CONSOLE) { + SetGlobalTransTarget(client); + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + C_RemoveTags(szBuffer, sizeof(szBuffer)); + PrintToConsole(client, "%s%s", szTag, szBuffer); + display_in_chat = true; + } + } + else { + SetGlobalTransTarget(LANG_SERVER); + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + C_RemoveTags(szBuffer, sizeof(szBuffer)); + PrintToServer("%s%s", szTag, szBuffer); + } + + if (!value) { + return 1; + } + + for (int i = 1; i <= MaxClients; ++i) { + if (!IsClientInGame(i) || IsFakeClient(i) || (display_in_chat && i == client)) { + continue; + } + + AdminId id = GetUserAdmin(i); + SetGlobalTransTarget(i); + if (id == INVALID_ADMIN_ID || !GetAdminFlag(id, Admin_Generic, Access_Effective)) { + /* Treat this as a normal user. */ + if ((value & 1) | (value & 2)) { + char newsign[MAX_NAME_LENGTH]; + + if ((value & 2) || (i == client)) { + newsign = name; + } + else { + newsign = sign; + } + + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + C_PrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + else { + /* Treat this as an admin user */ + bool is_root = GetAdminFlag(id, Admin_Root, Access_Effective); + if ((value & 4) || (value & 8) || ((value & 16) && is_root)) { + char newsign[MAX_NAME_LENGTH]; + + if ((value & 8) || ((value & 16) && is_root) || (i == client)) { + newsign = name; + } + else { + newsign = sign; + } + + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + C_PrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + } + + return 1; +} + +/** + * Displays usage of an admin command to users depending on the setting of the sm_show_activity cvar. + * All users receive a message in their chat text, except for the originating client, + * who receives the message based on the current ReplySource. + * Supports color tags. + * + * @param client Client index doing the action, or 0 for server. + * @param tags Tag to prepend to the message. + * @param format Formatting rules. + * @param ... Variable number of format parameters. + * @noreturn + * @error + */ +stock int C_ShowActivity2(int client, const char[] tag, const char[] format, any ...) { + if (sm_show_activity == null) { + sm_show_activity = FindConVar("sm_show_activity"); + } + + char szTag[MAX_MESSAGE_LENGTH]; + strcopy(szTag, sizeof(szTag), tag); + MC_RemoveTags(szTag, sizeof(szTag)); + + char szBuffer[MAX_MESSAGE_LENGTH]; + //char szCMessage[MAX_MESSAGE_LENGTH]; + int value = sm_show_activity.IntValue; + // ReplySource replyto = GetCmdReplySource(); + + char name[MAX_NAME_LENGTH] = "Console"; + char sign[MAX_NAME_LENGTH] = "ADMIN"; + if (client != 0) { + if (client < 0 || client > MaxClients || !IsClientConnected(client)) { + ThrowError("Client index %d is invalid", client); + } + + GetClientName(client, name, sizeof(name)); + AdminId id = GetUserAdmin(client); + if (id == INVALID_ADMIN_ID || !GetAdminFlag(id, Admin_Generic, Access_Effective)) { + sign = "PLAYER"; + } + + SetGlobalTransTarget(client); + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + /* We don't display directly to the console because the chat text + * simply gets added to the console, so we don't want it to print + * twice. + */ + C_PrintToChatEx(client, client, "%s%s", szTag, szBuffer); + } + else { + SetGlobalTransTarget(LANG_SERVER); + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + C_RemoveTags(szBuffer, sizeof(szBuffer)); + PrintToServer("%s%s", szTag, szBuffer); + } + + if (!value) { + return 1; + } + + for (int i = 1; i <= MaxClients; ++i) { + if (!IsClientInGame(i) || IsFakeClient(i) || i == client) { + continue; + } + + AdminId id = GetUserAdmin(i); + SetGlobalTransTarget(i); + if (id == INVALID_ADMIN_ID + || !GetAdminFlag(id, Admin_Generic, Access_Effective)) { + /* Treat this as a normal user. */ + if ((value & 1) | (value & 2)) { + char newsign[MAX_NAME_LENGTH]; + + if ((value & 2)) { + newsign = name; + } + else { + newsign = sign; + } + + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + C_PrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + else { + /* Treat this as an admin user */ + bool is_root = GetAdminFlag(id, Admin_Root, Access_Effective); + if ((value & 4) || (value & 8) || ((value & 16) && is_root)) { + char newsign[MAX_NAME_LENGTH]; + + if ((value & 8) || ((value & 16) && is_root)) { + newsign = name; + } + else { + newsign = sign; + } + + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + C_PrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + } + + return 1; +} diff --git a/libalternator/tests/corpus/multicolors/addons/sourcemod/scripting/include/multicolors/morecolors.inc b/libalternator/tests/corpus/multicolors/addons/sourcemod/scripting/include/multicolors/morecolors.inc new file mode 100644 index 0000000..2f92ff4 --- /dev/null +++ b/libalternator/tests/corpus/multicolors/addons/sourcemod/scripting/include/multicolors/morecolors.inc @@ -0,0 +1,1140 @@ +// MOAR COLORS +// By Dr. McKay +// Inspired by: https://forums.alliedmods.net/showthread.php?t=96831 + +#if defined _more_colors_included + #endinput +#endif +#define _more_colors_included + +#pragma newdecls optional + +#define MORE_COLORS_VERSION "3.0.1-MC" +#define MC_MAX_MESSAGE_LENGTH 256 +#define MAX_BUFFER_LENGTH (MC_MAX_MESSAGE_LENGTH * 4) +#define MAX_COLOR_TAG_LENGTH 32 +#define COLOR_HASH_SIZE 512 // 512 slots for ~172 colors = 34% load factor (optimal performance, minimal collisions) + +#define MCOLOR_RED 0xFF4040 +#define MCOLOR_BLUE 0x99CCFF +#define MCOLOR_GRAY 0xCCCCCC +#define MCOLOR_GREEN 0x3EFF3E + +#define MC_GAME_DODS 0 + +bool MC_SkipList[MAXPLAYERS+1]; +int MC_TeamColors[][] = {{0xCCCCCC, 0x4D7942, 0xFF4040}}; // Multi-dimensional array for games that don't support SayText2. First index is the game index (as defined by the GAME_ defines), second index is team. 0 = spectator, 1 = team1, 2 = team2 +char g_sColorKeys[COLOR_HASH_SIZE][32]; +char g_sColorValues[COLOR_HASH_SIZE][16]; +static int g_iColorHash[COLOR_HASH_SIZE]; +static int g_iColorCount = 0; + +static ConVar sm_show_activity; + +/** + * Prints a message to a specific client in the chat area. + * Supports color tags. + * + * @param client Client index. + * @param message Message (formatting rules). + * + * On error/Errors: If the client is not connected an error will be thrown. + */ +stock void MC_PrintToChat(int client, const char[] message, any ...) { + MC_InitFastColors(); + + if (client <= 0 || client > MaxClients) { + ThrowError("Invalid client index %i", client); + } + + if (!IsClientInGame(client)) { + ThrowError("Client %i is not in game", client); + } + + char buffer[MAX_BUFFER_LENGTH]; + char buffer2[MAX_BUFFER_LENGTH]; + + SetGlobalTransTarget(client); + Format(buffer, sizeof(buffer), "\x01%s", message); + VFormat(buffer2, sizeof(buffer2), buffer, 3); + + MC_ReplaceColorCodes(buffer2); + MC_SendMessage(client, buffer2); +} + +/** + * Prints a message to all clients in the chat area. + * Supports color tags. + * + * @param client Client index. + * @param message Message (formatting rules). + */ +stock void MC_PrintToChatAll(const char[] message, any ...) { + MC_InitFastColors(); + + char buffer[MAX_BUFFER_LENGTH], buffer2[MAX_BUFFER_LENGTH]; + + for (int i = 1; i <= MaxClients; ++i) { + if (!IsClientInGame(i) || MC_SkipList[i]) { + MC_SkipList[i] = false; + continue; + } + + SetGlobalTransTarget(i); + Format(buffer, sizeof(buffer), "\x01%s", message); + VFormat(buffer2, sizeof(buffer2), buffer, 2); + + MC_ReplaceColorCodes(buffer2); + MC_SendMessage(i, buffer2); + } +} + +/** + * Prints a message to a specific client in the chat area. + * Supports color tags and teamcolor tag. + * + * @param client Client index. + * @param author Author index whose color will be used for teamcolor tag. + * @param message Message (formatting rules). + * + * On error/Errors: If the client or author are not connected an error will be thrown + */ +stock void MC_PrintToChatEx(int client, int author, const char[] message, any ...) { + MC_InitFastColors(); + + if (client <= 0 || client > MaxClients) { + ThrowError("Invalid client index %i", client); + } + + if (!IsClientInGame(client)) { + ThrowError("Client %i is not in game", client); + } + + if (author < 0 || author > MaxClients) { + ThrowError("Invalid client index %i", author); + } + + if ((author != 0) && !IsClientInGame(author)) { + ThrowError("Client %i is not in game", author); + } + + char buffer[MAX_BUFFER_LENGTH], buffer2[MAX_BUFFER_LENGTH]; + SetGlobalTransTarget(client); + Format(buffer, sizeof(buffer), "\x01%s", message); + VFormat(buffer2, sizeof(buffer2), buffer, 4); + MC_ReplaceColorCodes(buffer2, author); + MC_SendMessage(client, buffer2, author); +} + +/** + * Prints a message to all clients in the chat area. + * Supports color tags and teamcolor tag. + * + * @param author Author index whose color will be used for teamcolor tag. + * @param message Message (formatting rules). + * + * On error/Errors: If the author is not connected an error will be thrown. + */ +stock void MC_PrintToChatAllEx(int author, const char[] message, any ...) { + MC_InitFastColors(); + + if (author < 0 || author > MaxClients) { + ThrowError("Invalid client index %i", author); + } + + if ((author != 0) && !IsClientInGame(author)) { + ThrowError("Client %i is not in game", author); + } + + char buffer[MAX_BUFFER_LENGTH]; + char buffer2[MAX_BUFFER_LENGTH]; + + for (int i = 1; i <= MaxClients; ++i) { + if (!IsClientInGame(i) || MC_SkipList[i]) { + MC_SkipList[i] = false; + continue; + } + + SetGlobalTransTarget(i); + Format(buffer, sizeof(buffer), "\x01%s", message); + VFormat(buffer2, sizeof(buffer2), buffer, 3); + + MC_ReplaceColorCodes(buffer2, author); + MC_SendMessage(i, buffer2, author); + } +} + +/** + * Sends a SayText2 usermessage + * + * @param client Client to send usermessage to + * @param message Message to send + */ +stock void MC_SendMessage(int client, const char[] message, int author = 0) { + if (author == 0) { + author = client; + } + + char buffer[MC_MAX_MESSAGE_LENGTH]; + strcopy(buffer, sizeof(buffer), message); + + UserMsg index = GetUserMessageId("SayText2"); + if (index == INVALID_MESSAGE_ID) { + if (GetEngineVersion() == Engine_DODS) { + int team = GetClientTeam(author); + if (team == 0) { + ReplaceString(buffer, sizeof(buffer), "\x03", "\x04", false); // Unassigned gets green + } + else { + char temp[16]; + Format(temp, sizeof(temp), "\x07%06X", MC_TeamColors[MC_GAME_DODS][team - 1]); + ReplaceString(buffer, sizeof(buffer), "\x03", temp, false); + } + } + + PrintToChat(client, "%s", buffer); + return; + } + + Handle buf = StartMessageOne("SayText2", client, USERMSG_RELIABLE|USERMSG_BLOCKHOOKS); + if (GetFeatureStatus(FeatureType_Native, "GetUserMessageType") == FeatureStatus_Available && GetUserMessageType() == UM_Protobuf) { + Protobuf pb = UserMessageToProtobuf(buf); + pb.SetInt("ent_idx", author); + pb.SetBool("chat", true); + pb.SetString("msg_name", buffer); + pb.AddString("params", ""); + pb.AddString("params", ""); + pb.AddString("params", ""); + pb.AddString("params", ""); + } + else { + BfWrite bf = UserMessageToBfWrite(buf); + bf.WriteByte(author); // Message author + bf.WriteByte(true); // Chat message + bf.WriteString(buffer); // Message text + } + + EndMessage(); +} + +/** + * This function should only be used right in front of + * MC_PrintToChatAll or MC_PrintToChatAllEx. It causes those functions + * to skip the specified client when printing the message. + * After printing the message, the client will no longer be skipped. + * + * @param client Client index + */ +stock void MC_SkipNextClient(int client) { + if (client <= 0 || client > MaxClients) { + ThrowError("Invalid client index %i", client); + } + + MC_SkipList[client] = true; +} + +/** + * Replaces color tags in a string with color codes (used internally by MC_PrintToChat, MC_PrintToChatAll, MC_PrintToChatEx, and MC_PrintToChatAllEx + * + * @param buffer String. + * @param author Optional client index to use for {teamcolor} tags, or 0 for none + * @param removeTags Optional boolean value to determine whether we're replacing tags with colors, or just removing tags, used by MC_RemoveTags + * @param maxlen Optional value for max buffer length, used by MC_RemoveTags + * + * On error/Errors: If the client index passed for author is invalid or not in game. + */ +stock void MC_ReplaceColorCodes(char[] buffer, int author = 0, bool removeTags = false, int maxlen = MAX_BUFFER_LENGTH) { + MC_InitFastColors(); + if (!removeTags) { + MC_FastReplaceString(buffer, maxlen, "{default}", "\x01"); + + if (author != 0) { + if (author < 0 || author > MaxClients) { + ThrowError("Invalid client index %i", author); + } + + if (!IsClientInGame(author)) { + ThrowError("Client %i is not in game", author); + } + + MC_FastReplaceString(buffer, maxlen, "{teamcolor}", "\x03"); + } + } + else { + MC_FastReplaceString(buffer, maxlen, "{default}", ""); + MC_FastReplaceString(buffer, maxlen, "{teamcolor}", ""); + } + + int outPos = 0; + int len = strlen(buffer); + int i = 0; + + char sOutputBuffer[MAX_BUFFER_LENGTH]; + char sTempTag[32]; + + while (i < len && outPos < maxlen - 16) { + if (buffer[i] == '{') { + int start = i + 1; + int end = -1; + + // Find closing brace + for (int j = start; j < len && j < start + MAX_COLOR_TAG_LENGTH; j++) { + if (buffer[j] == '}') { + end = j; + break; + } + } + + if (end != -1) { + int tagLen = end - start; + + // Extract tag + int copyLen = (tagLen < 31) ? tagLen : 31; + for (int k = 0; k < copyLen; k++) { + sTempTag[k] = buffer[start + k]; + } + sTempTag[copyLen] = '\0'; + + MC_StrToLower(sTempTag); + + char value[16]; + bool found = false; + + // Handle hex colors + if (sTempTag[0] == '#' && (tagLen == 7 || tagLen == 9)) { + if (tagLen == 7) { + value[0] = '\x07'; + strcopy(value[1], 9, sTempTag[1]); + } else { + value[0] = '\x08'; + strcopy(value[1], 9, sTempTag[1]); + } + found = true; + } else { + found = MC_FastGetColor(sTempTag, value, sizeof(value)); + } + + if (found && !removeTags) { + // Copy color code directly + int valLen = strlen(value); + for (int k = 0; k < valLen && outPos < maxlen - 1; k++) { + sOutputBuffer[outPos++] = value[k]; + } + i = end + 1; + continue; + } else if (removeTags) { + i = end + 1; + continue; + } + } + } + + sOutputBuffer[outPos++] = buffer[i++]; + } + + sOutputBuffer[outPos] = '\0'; + strcopy(buffer, maxlen, sOutputBuffer); +} + +/** + * Gets a part of a string + * + * @param input String to get the part from + * @param output Buffer to write to + * @param maxlen Max length of output buffer + * @param start Position to start at + * @param numChars Number of characters to return, or 0 for the end of the string + */ +stock void CSubString(const char[] input, char[] output, int maxlen, int start, int numChars = 0) { + int i = 0; + for (;;) { + if (i == maxlen - 1 || i >= numChars || input[start + i] == '\0') { + output[i] = '\0'; + return; + } + + output[i] = input[start + i]; + i++; + } +} + +/** + * Converts a string to lowercase + * + * @param buffer String to convert + */ +stock void MC_StrToLower(char[] buffer) { + for (int i = 0; buffer[i]; i++) { + if (buffer[i] >= 'A' && buffer[i] <= 'Z') { + buffer[i] |= 0x20; + } + } +} + +/** + * Adds a color to the colors hash table + * + * @param name Color name, without braces + * @param color Hexadecimal representation of the color (0xRRGGBB) + * @return True if color was added successfully, false if a color already exists with that name + */ +stock bool MC_AddColor(const char[] name, int color) { + MC_InitFastColors(); + + char value[16]; + char newName[64]; + strcopy(newName, sizeof(newName), name); + MC_StrToLower(newName); + + if (MC_FastGetColor(newName, value, sizeof(value))) { + return false; + } + + char colorValue[16]; + Format(colorValue, sizeof(colorValue), "\x07%06X", color); + MC_FastSetColor(newName, colorValue); + return true; +} + +/** + * Removes color tags from a message + * + * @param message Message to remove tags from + * @param maxlen Maximum buffer length + */ +stock void MC_RemoveTags(char[] message, int maxlen) { + MC_ReplaceColorCodes(message, 0, true, maxlen); +} + +/** + * Replies to a command with colors + * + * @param client Client to reply to + * @param message Message (formatting rules) + */ +stock void MC_ReplyToCommand(int client, const char[] message, any ...) { + char buffer[MAX_BUFFER_LENGTH]; + SetGlobalTransTarget(client); + VFormat(buffer, sizeof(buffer), message, 3); + + if (client == 0) { + MC_RemoveTags(buffer, sizeof(buffer)); + PrintToServer("%s", buffer); + } + else if (GetCmdReplySource() == SM_REPLY_TO_CONSOLE) { + MC_RemoveTags(buffer, sizeof(buffer)); + PrintToConsole(client, "%s", buffer); + } + else { + MC_PrintToChat(client, "%s", buffer); + } +} + +/** + * Replies to a command with colors + * + * @param client Client to reply to + * @param author Client to use for {teamcolor} + * @param message Message (formatting rules) + */ +stock void MC_ReplyToCommandEx(int client, int author, const char[] message, any ...) { + char buffer[MAX_BUFFER_LENGTH]; + SetGlobalTransTarget(client); + VFormat(buffer, sizeof(buffer), message, 4); + + if (client == 0) { + MC_RemoveTags(buffer, sizeof(buffer)); + PrintToServer("%s", buffer); + } + else if (GetCmdReplySource() == SM_REPLY_TO_CONSOLE) { + MC_RemoveTags(buffer, sizeof(buffer)); + PrintToConsole(client, "%s", buffer); + } + else { + MC_PrintToChatEx(client, author, "%s", buffer); + } +} + +/** + * Displays usage of an admin command to users depending on the + * setting of the sm_show_activity cvar. + * + * This version does not display a message to the originating client + * if used from chat triggers or menus. If manual replies are used + * for these cases, then this function will suffice. Otherwise, + * MC_ShowActivity2() is slightly more useful. + * Supports color tags. + * + * @param client Client index doing the action, or 0 for server. + * @param format Formatting rules. + * @param ... Variable number of format parameters. + * @error + */ +stock int MC_ShowActivity(int client, const char[] format, any ...) { + if (sm_show_activity == null) { + sm_show_activity = FindConVar("sm_show_activity"); + } + + char tag[] = "[SM] "; + + char szBuffer[MC_MAX_MESSAGE_LENGTH]; + //char szCMessage[MC_MAX_MESSAGE_LENGTH]; + int value = sm_show_activity.IntValue; + ReplySource replyto = GetCmdReplySource(); + + char name[MAX_NAME_LENGTH] = "Console"; + char sign[MAX_NAME_LENGTH] = "ADMIN"; + bool display_in_chat = false; + if (client != 0) { + if (client < 0 || client > MaxClients || !IsClientConnected(client)) + ThrowError("Client index %d is invalid", client); + + GetClientName(client, name, sizeof(name)); + AdminId id = GetUserAdmin(client); + if (id == INVALID_ADMIN_ID || !id.HasFlag(Admin_Generic, Access_Effective)) { + sign = "PLAYER"; + } + + /* Display the message to the client? */ + if (replyto == SM_REPLY_TO_CONSOLE) { + SetGlobalTransTarget(client); + VFormat(szBuffer, sizeof(szBuffer), format, 3); + + MC_RemoveTags(szBuffer, sizeof(szBuffer)); + PrintToConsole(client, "%s%s", tag, szBuffer); + display_in_chat = true; + } + } + else { + SetGlobalTransTarget(LANG_SERVER); + VFormat(szBuffer, sizeof(szBuffer), format, 3); + + MC_RemoveTags(szBuffer, sizeof(szBuffer)); + PrintToServer("%s%s", tag, szBuffer); + } + + if (!value) { + return 1; + } + + for (int i = 1; i <= MaxClients; ++i) { + if (!IsClientInGame(i) || IsFakeClient(i) || (display_in_chat && i == client)) { + continue; + } + + AdminId id = GetUserAdmin(i); + SetGlobalTransTarget(i); + if (id == INVALID_ADMIN_ID || !id.HasFlag(Admin_Generic, Access_Effective)) { + /* Treat this as a normal user. */ + if ((value & 1) | (value & 2)) { + char newsign[MAX_NAME_LENGTH]; + + if ((value & 2) || (i == client)) { + newsign = name; + } + else { + newsign = sign; + } + + VFormat(szBuffer, sizeof(szBuffer), format, 3); + + MC_PrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + else { + /* Treat this as an admin user */ + bool is_root = id.HasFlag(Admin_Root, Access_Effective); + if ((value & 4) || (value & 8) || ((value & 16) && is_root)) { + char newsign[MAX_NAME_LENGTH]; + + if ((value & 8) || ((value & 16) && is_root) || (i == client)) { + newsign = name; + } + else { + newsign = sign; + } + + VFormat(szBuffer, sizeof(szBuffer), format, 3); + + MC_PrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + } + + return 1; +} + +/** + * Same as MC_ShowActivity(), except the tag parameter is used instead of "[SM] " (note that you must supply any spacing). + * Supports color tags. + * + * @param client Client index doing the action, or 0 for server. + * @param tags Tag to display with. + * @param format Formatting rules. + * @param ... Variable number of format parameters. + * @error + */ +stock int MC_ShowActivityEx(int client, const char[] tag, const char[] format, any ...) { + if (sm_show_activity == null) { + sm_show_activity = FindConVar("sm_show_activity"); + } + + char szTag[MC_MAX_MESSAGE_LENGTH]; + strcopy(szTag, sizeof(szTag), tag); + MC_RemoveTags(szTag, sizeof(szTag)); + + char szBuffer[MC_MAX_MESSAGE_LENGTH]; + //char szCMessage[MC_MAX_MESSAGE_LENGTH]; + int value = sm_show_activity.IntValue; + ReplySource replyto = GetCmdReplySource(); + + char name[MAX_NAME_LENGTH] = "Console"; + char sign[MAX_NAME_LENGTH] = "ADMIN"; + bool display_in_chat = false; + if (client != 0) { + if (client < 0 || client > MaxClients || !IsClientConnected(client)) { + ThrowError("Client index %d is invalid", client); + } + + GetClientName(client, name, sizeof(name)); + AdminId id = GetUserAdmin(client); + if (id == INVALID_ADMIN_ID || !id.HasFlag(Admin_Generic, Access_Effective)) { + sign = "PLAYER"; + } + + /* Display the message to the client? */ + if (replyto == SM_REPLY_TO_CONSOLE) { + SetGlobalTransTarget(client); + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + MC_RemoveTags(szBuffer, sizeof(szBuffer)); + PrintToConsole(client, "%s%s", szTag, szBuffer); + display_in_chat = true; + } + } + else { + SetGlobalTransTarget(LANG_SERVER); + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + MC_RemoveTags(szBuffer, sizeof(szBuffer)); + PrintToServer("%s%s", szTag, szBuffer); + } + + if (!value) { + return 1; + } + + for (int i = 1; i <= MaxClients; ++i) { + if (!IsClientInGame(i) || IsFakeClient(i) || (display_in_chat && i == client)) { + continue; + } + + AdminId id = GetUserAdmin(i); + SetGlobalTransTarget(i); + if (id == INVALID_ADMIN_ID || !id.HasFlag(Admin_Generic, Access_Effective)) { + /* Treat this as a normal user. */ + if ((value & 1) | (value & 2)) { + char newsign[MAX_NAME_LENGTH]; + + if ((value & 2) || (i == client)) { + newsign = name; + } + else { + newsign = sign; + } + + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + MC_PrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + else { + /* Treat this as an admin user */ + bool is_root = id.HasFlag(Admin_Root, Access_Effective); + if ((value & 4) || (value & 8) || ((value & 16) && is_root)) { + char newsign[MAX_NAME_LENGTH]; + + if ((value & 8) || ((value & 16) && is_root) || (i == client)) { + newsign = name; + } + else { + newsign = sign; + } + + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + MC_PrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + } + + return 1; +} + +/** + * Displays usage of an admin command to users depending on the setting of the sm_show_activity cvar. + * All users receive a message in their chat text, except for the originating client, + * who receives the message based on the current ReplySource. + * Supports color tags. + * + * @param client Client index doing the action, or 0 for server. + * @param tags Tag to prepend to the message. + * @param format Formatting rules. + * @param ... Variable number of format parameters. + * @error + */ +stock int MC_ShowActivity2(int client, const char[] tag, const char[] format, any ...) { + if (sm_show_activity == null) { + sm_show_activity = FindConVar("sm_show_activity"); + } + + char szTag[MC_MAX_MESSAGE_LENGTH]; + strcopy(szTag, sizeof(szTag), tag); + MC_RemoveTags(szTag, sizeof(szTag)); + + char szBuffer[MC_MAX_MESSAGE_LENGTH]; + //char szCMessage[MC_MAX_MESSAGE_LENGTH]; + int value = sm_show_activity.IntValue; + // ReplySource replyto = GetCmdReplySource(); + + char name[MAX_NAME_LENGTH] = "Console"; + char sign[MAX_NAME_LENGTH] = "ADMIN"; + + if (client != 0) { + if (client < 0 || client > MaxClients || !IsClientConnected(client)) { + ThrowError("Client index %d is invalid", client); + } + + GetClientName(client, name, sizeof(name)); + + AdminId id = GetUserAdmin(client); + if (id == INVALID_ADMIN_ID || !id.HasFlag(Admin_Generic, Access_Effective)) { + sign = "PLAYER"; + } + + SetGlobalTransTarget(client); + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + /* We don't display directly to the console because the chat text + * simply gets added to the console, so we don't want it to print + * twice. + */ + MC_PrintToChatEx(client, client, "%s%s", szTag, szBuffer); + } + else { + SetGlobalTransTarget(LANG_SERVER); + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + MC_RemoveTags(szBuffer, sizeof(szBuffer)); + PrintToServer("%s%s", szTag, szBuffer); + } + + if (!value) { + return 1; + } + + for (int i = 1; i <= MaxClients; ++i) { + if (!IsClientInGame(i) || IsFakeClient(i) || i == client) { + continue; + } + + AdminId id = GetUserAdmin(i); + SetGlobalTransTarget(i); + if (id == INVALID_ADMIN_ID || !id.HasFlag(Admin_Generic, Access_Effective)) { + /* Treat this as a normal user. */ + if ((value & 1) | (value & 2)) { + char newsign[MAX_NAME_LENGTH]; + + if ((value & 2)) { + newsign = name; + } + else { + newsign = sign; + } + + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + MC_PrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + else { + /* Treat this as an admin user */ + bool is_root = id.HasFlag(Admin_Root, Access_Effective); + if ((value & 4) || (value & 8) || ((value & 16) && is_root)) { + char newsign[MAX_NAME_LENGTH]; + + + if ((value & 8) || ((value & 16) && is_root)) { + newsign = name; + } + else { + newsign = sign; + } + + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + MC_PrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + } + + return 1; +} + +/** + * Determines whether a color name exists + * + * @param color The color name to check + * @return True if the color exists, false otherwise + */ +stock bool CColorExists(const char[] color) { + MC_InitFastColors(); + char temp[16]; + char colorName[64]; + strcopy(colorName, sizeof(colorName), color); + MC_StrToLower(colorName); + return MC_FastGetColor(colorName, temp, sizeof(temp)); +} + +/** + * Returns the hexadecimal representation of a client's team color (will initialize the hash table if needed) + * + * @param client Client to get the team color for + * @return Client's team color in hexadecimal, or green if unknown + * On error/Errors: If the client index passed is invalid or not in game. + */ +stock int CGetTeamColor(int client) { + if (client <= 0 || client > MaxClients) { + ThrowError("Invalid client index %i", client); + } + + if (!IsClientInGame(client)) { + ThrowError("Client %i is not in game", client); + } + + MC_InitFastColors(); + + int value; + switch(GetClientTeam(client)) { + case 1: { + value = MCOLOR_GRAY; + } + case 2: { + value = MCOLOR_RED; + } + case 3: { + value = MCOLOR_BLUE; + } + default: { + value = MCOLOR_GREEN; + } + } + + return value; +} + +stock void MC_InitFastColors() { + if (g_iColorCount > 0) + return; + + MC_FastSetColor("aliceblue", "\x07F0F8FF"); + MC_FastSetColor("allies", "\x074D7942"); // same as Allies team in DoD:S + MC_FastSetColor("ancient", "\x07EB4B4B"); // same as Ancient item rarity in Dota 2 + MC_FastSetColor("antiquewhite", "\x07FAEBD7"); + MC_FastSetColor("aqua", "\x0700FFFF"); + MC_FastSetColor("aquamarine", "\x077FFFD4"); + MC_FastSetColor("arcana", "\x07ADE55C"); // same as Arcana item rarity in Dota 2 + MC_FastSetColor("axis", "\x07FF4040"); // same as Axis team in DoD:S + MC_FastSetColor("azure", "\x07007FFF"); + MC_FastSetColor("beige", "\x07F5F5DC"); + MC_FastSetColor("bisque", "\x07FFE4C4"); + MC_FastSetColor("black", "\x07000000"); + MC_FastSetColor("blanchedalmond", "\x07FFEBCD"); + MC_FastSetColor("blue", "\x0799CCFF"); // same as BLU/Counter-Terrorist team color + MC_FastSetColor("blueviolet", "\x078A2BE2"); + MC_FastSetColor("brown", "\x07A52A2A"); + MC_FastSetColor("burlywood", "\x07DEB887"); + MC_FastSetColor("cadetblue", "\x075F9EA0"); + MC_FastSetColor("chartreuse", "\x077FFF00"); + MC_FastSetColor("chocolate", "\x07D2691E"); + MC_FastSetColor("collectors", "\x07AA0000"); // same as Collector's item quality in TF2 + MC_FastSetColor("common", "\x07B0C3D9"); // same as Common item rarity in Dota 2 + MC_FastSetColor("community", "\x0770B04A"); // same as Community item quality in TF2 + MC_FastSetColor("coral", "\x07FF7F50"); + MC_FastSetColor("cornflowerblue", "\x076495ED"); + MC_FastSetColor("cornsilk", "\x07FFF8DC"); + MC_FastSetColor("corrupted", "\x07A32C2E"); // same as Corrupted item quality in Dota 2 + MC_FastSetColor("crimson", "\x07DC143C"); + MC_FastSetColor("cyan", "\x0700FFFF"); + MC_FastSetColor("darkblue", "\x0700008B"); + MC_FastSetColor("darkcyan", "\x07008B8B"); + MC_FastSetColor("darkgoldenrod", "\x07B8860B"); + MC_FastSetColor("darkgray", "\x07A9A9A9"); + MC_FastSetColor("darkgrey", "\x07A9A9A9"); + MC_FastSetColor("darkgreen", "\x07006400"); + MC_FastSetColor("darkkhaki", "\x07BDB76B"); + MC_FastSetColor("darkmagenta", "\x078B008B"); + MC_FastSetColor("darkolivegreen", "\x07556B2F"); + MC_FastSetColor("darkorange", "\x07FF8C00"); + MC_FastSetColor("darkorchid", "\x079932CC"); + MC_FastSetColor("darkred", "\x078B0000"); + MC_FastSetColor("darksalmon", "\x07E9967A"); + MC_FastSetColor("darkseagreen", "\x078FBC8F"); + MC_FastSetColor("darkslateblue", "\x07483D8B"); + MC_FastSetColor("darkslategray", "\x072F4F4F"); + MC_FastSetColor("darkslategrey", "\x072F4F4F"); + MC_FastSetColor("darkturquoise", "\x0700CED1"); + MC_FastSetColor("darkviolet", "\x079400D3"); + MC_FastSetColor("deeppink", "\x07FF1493"); + MC_FastSetColor("deepskyblue", "\x0700BFFF"); + MC_FastSetColor("dimgray", "\x07696969"); + MC_FastSetColor("dimgrey", "\x07696969"); + MC_FastSetColor("dodgerblue", "\x071E90FF"); + MC_FastSetColor("exalted", "\x07CCCCCD"); // same as Exalted item quality in Dota 2 + MC_FastSetColor("firebrick", "\x07B22222"); + MC_FastSetColor("floralwhite", "\x07FFFAF0"); + MC_FastSetColor("forestgreen", "\x07228B22"); + MC_FastSetColor("frozen", "\x074983B3"); // same as Frozen item quality in Dota 2 + MC_FastSetColor("fuchsia", "\x07FF00FF"); + MC_FastSetColor("fullblue", "\x070000FF"); + MC_FastSetColor("fullred", "\x07FF0000"); + MC_FastSetColor("gainsboro", "\x07DCDCDC"); + MC_FastSetColor("genuine", "\x074D7455"); // same as Genuine item quality in TF2 + MC_FastSetColor("ghostwhite", "\x07F8F8FF"); + MC_FastSetColor("gold", "\x07FFD700"); + MC_FastSetColor("goldenrod", "\x07DAA520"); + MC_FastSetColor("gray", "\x07CCCCCC"); // same as spectator team color + MC_FastSetColor("grey", "\x07CCCCCC"); + MC_FastSetColor("green", "\x073EFF3E"); + MC_FastSetColor("greenyellow", "\x07ADFF2F"); + MC_FastSetColor("haunted", "\x0738F3AB"); // same as Haunted item quality in TF2 + MC_FastSetColor("honeydew", "\x07F0FFF0"); + MC_FastSetColor("hotpink", "\x07FF69B4"); + MC_FastSetColor("immortal", "\x07E4AE33"); // same as Immortal item rarity in Dota 2 + MC_FastSetColor("indianred", "\x07CD5C5C"); + MC_FastSetColor("indigo", "\x074B0082"); + MC_FastSetColor("ivory", "\x07FFFFF0"); + MC_FastSetColor("khaki", "\x07F0E68C"); + MC_FastSetColor("lavender", "\x07E6E6FA"); + MC_FastSetColor("lavenderblush", "\x07FFF0F5"); + MC_FastSetColor("lawngreen", "\x077CFC00"); + MC_FastSetColor("legendary", "\x07D32CE6"); // same as Legendary item rarity in Dota 2 + MC_FastSetColor("lemonchiffon", "\x07FFFACD"); + MC_FastSetColor("lightblue", "\x07ADD8E6"); + MC_FastSetColor("lightcoral", "\x07F08080"); + MC_FastSetColor("lightcyan", "\x07E0FFFF"); + MC_FastSetColor("lightgoldenrodyellow", "\x07FAFAD2"); + MC_FastSetColor("lightgray", "\x07D3D3D3"); + MC_FastSetColor("lightgrey", "\x07D3D3D3"); + MC_FastSetColor("lightgreen", "\x0799FF99"); + MC_FastSetColor("lightpink", "\x07FFB6C1"); + MC_FastSetColor("lightsalmon", "\x07FFA07A"); + MC_FastSetColor("lightseagreen", "\x0720B2AA"); + MC_FastSetColor("lightskyblue", "\x0787CEFA"); + MC_FastSetColor("lightslategray", "\x07778899"); + MC_FastSetColor("lightslategrey", "\x07778899"); + MC_FastSetColor("lightsteelblue", "\x07B0C4DE"); + MC_FastSetColor("lightyellow", "\x07FFFFE0"); + MC_FastSetColor("lime", "\x0700FF00"); + MC_FastSetColor("limegreen", "\x0732CD32"); + MC_FastSetColor("linen", "\x07FAF0E6"); + MC_FastSetColor("magenta", "\x07FF00FF"); + MC_FastSetColor("maroon", "\x07800000"); + MC_FastSetColor("mediumaquamarine", "\x0766CDAA"); + MC_FastSetColor("mediumblue", "\x070000CD"); + MC_FastSetColor("mediumorchid", "\x07BA55D3"); + MC_FastSetColor("mediumpurple", "\x079370D8"); + MC_FastSetColor("mediumseagreen", "\x073CB371"); + MC_FastSetColor("mediumslateblue", "\x077B68EE"); + MC_FastSetColor("mediumspringgreen", "\x0700FA9A"); + MC_FastSetColor("mediumturquoise", "\x0748D1CC"); + MC_FastSetColor("mediumvioletred", "\x07C71585"); + MC_FastSetColor("midnightblue", "\x07191970"); + MC_FastSetColor("mintcream", "\x07F5FFFA"); + MC_FastSetColor("mistyrose", "\x07FFE4E1"); + MC_FastSetColor("moccasin", "\x07FFE4B5"); + MC_FastSetColor("mythical", "\x078847FF"); // same as Mythical item rarity in Dota 2 + MC_FastSetColor("navajowhite", "\x07FFDEAD"); + MC_FastSetColor("navy", "\x07000080"); + MC_FastSetColor("normal", "\x07B2B2B2"); // same as Normal item quality in TF2 + MC_FastSetColor("oldlace", "\x07FDF5E6"); + MC_FastSetColor("olive", "\x079EC34F"); + MC_FastSetColor("olivedrab", "\x076B8E23"); + MC_FastSetColor("orange", "\x07FFA500"); + MC_FastSetColor("orangered", "\x07FF4500"); + MC_FastSetColor("orchid", "\x07DA70D6"); + MC_FastSetColor("palegoldenrod", "\x07EEE8AA"); + MC_FastSetColor("palegreen", "\x0798FB98"); + MC_FastSetColor("paleturquoise", "\x07AFEEEE"); + MC_FastSetColor("palevioletred", "\x07D87093"); + MC_FastSetColor("papayawhip", "\x07FFEFD5"); + MC_FastSetColor("peachpuff", "\x07FFDAB9"); + MC_FastSetColor("peru", "\x07CD853F"); + MC_FastSetColor("pink", "\x07FFC0CB"); + MC_FastSetColor("plum", "\x07DDA0DD"); + MC_FastSetColor("powderblue", "\x07B0E0E6"); + MC_FastSetColor("purple", "\x07800080"); + MC_FastSetColor("rare", "\x074B69FF"); // same as Rare item rarity in Dota 2 + MC_FastSetColor("red", "\x07FF4040"); // same as RED/Terrorist team color + MC_FastSetColor("rosybrown", "\x07BC8F8F"); + MC_FastSetColor("royalblue", "\x074169E1"); + MC_FastSetColor("saddlebrown", "\x078B4513"); + MC_FastSetColor("salmon", "\x07FA8072"); + MC_FastSetColor("sandybrown", "\x07F4A460"); + MC_FastSetColor("seagreen", "\x072E8B57"); + MC_FastSetColor("seashell", "\x07FFF5EE"); + MC_FastSetColor("selfmade", "\x0770B04A"); // same as Self-Made item quality in TF2 + MC_FastSetColor("sienna", "\x07A0522D"); + MC_FastSetColor("silver", "\x07C0C0C0"); + MC_FastSetColor("skyblue", "\x0787CEEB"); + MC_FastSetColor("slateblue", "\x076A5ACD"); + MC_FastSetColor("slategray", "\x07708090"); + MC_FastSetColor("slategrey", "\x07708090"); + MC_FastSetColor("snow", "\x07FFFAFA"); + MC_FastSetColor("springgreen", "\x0700FF7F"); + MC_FastSetColor("steelblue", "\x074682B4"); + MC_FastSetColor("strange", "\x07CF6A32"); // same as Strange item quality in TF2 + MC_FastSetColor("tan", "\x07D2B48C"); + MC_FastSetColor("teal", "\x07008080"); + MC_FastSetColor("thistle", "\x07D8BFD8"); + MC_FastSetColor("tomato", "\x07FF6347"); + MC_FastSetColor("turquoise", "\x0740E0D0"); + MC_FastSetColor("uncommon", "\x07B0C3D9"); // same as Uncommon item rarity in Dota 2 + MC_FastSetColor("unique", "\x07FFD700"); // same as Unique item quality in TF2 + MC_FastSetColor("unusual", "\x078650AC"); // same as Unusual item quality in TF2 + MC_FastSetColor("valve", "\x07A50F79"); // same as Valve item quality in TF2 + MC_FastSetColor("vintage", "\x07476291"); // same as Vintage item quality in TF2 + MC_FastSetColor("violet", "\x07EE82EE"); + MC_FastSetColor("wheat", "\x07F5DEB3"); + MC_FastSetColor("white", "\x07FFFFFF"); + MC_FastSetColor("whitesmoke", "\x07F5F5F5"); + MC_FastSetColor("yellow", "\x07FFFF00"); + MC_FastSetColor("yellowgreen", "\x079ACD32"); +} + +/** + * Computes FNV-1a (Fowler-Noll-Vo) hash for a string + * FNV-1a provides better avalanche characteristics than FNV-1 + * and is widely used in hash tables for its speed and distribution quality + * 2166136261 is the FNV-1a 32-bit offset basis + * 16777619 is the FNV-1a 32-bit prime multiplier + * + * @param str String to hash + * @return 32-bit hash value + */ +stock int MC_FastHash(const char[] str) { + int hash = 0x811C9DC5; // FNV-1a 32-bit offset basis + for (int i = 0; str[i]; i++) { + hash ^= str[i]; + hash *= 16777619; + } + return hash & (COLOR_HASH_SIZE - 1); +} + +/** + * Inserts or updates a color in the hash table using linear probing + * Updates existing keys, adds new ones, or silently fails if table is full + * Uses FNV-1a hash with cycle detection to prevent infinite loops + * Silent fail occurs only if: empty key/value, 256+ unique colors, or hash table completely full + */ +stock void MC_FastSetColor(const char[] key, const char[] value) { + if (!key[0] || !value[0] || g_iColorCount >= COLOR_HASH_SIZE) + return; + + int hash = MC_FastHash(key); + int originalHash = hash; + int probeCount = 0; + + // Linear probing with cycle detection + do { + int existingIndex = g_iColorHash[hash]; + + // Found empty slot + if (existingIndex == 0) { + strcopy(g_sColorKeys[g_iColorCount], sizeof(g_sColorKeys[]), key); + strcopy(g_sColorValues[g_iColorCount], sizeof(g_sColorValues[]), value); + g_iColorHash[hash] = g_iColorCount + 1; + g_iColorCount++; + return; + } + + // Check if key already exists (update case) + existingIndex--; + if (strcmp(g_sColorKeys[existingIndex], key, false) == 0) { + strcopy(g_sColorValues[existingIndex], sizeof(g_sColorValues[]), value); + return; + } + + hash = (hash + 1) & (COLOR_HASH_SIZE - 1); + probeCount++; + + // Prevent infinite loop if table is full + if (probeCount >= COLOR_HASH_SIZE) { + return; // Table is completely full, cannot insert + } + } while (hash != originalHash); +} + +stock bool MC_FastGetColor(const char[] key, char[] output, int maxlen) { + if (!key[0]) + return false; + + int hash = MC_FastHash(key); + int originalHash = hash; + + do { + int index = g_iColorHash[hash]; + if (index == 0) + return false; + + index--; + if (strcmp(g_sColorKeys[index], key, false) == 0) { + strcopy(output, maxlen, g_sColorValues[index]); + return true; + } + + hash = (hash + 1) & (COLOR_HASH_SIZE - 1); + } while (hash != originalHash); + + return false; +} + +stock void MC_FastReplaceString(char[] text, int maxlen, const char[] search, const char[] replace) { + if (!search[0] || !text[0]) + return; + + char localBuffer[MAX_BUFFER_LENGTH]; + + int textLen = strlen(text); + int searchLen = strlen(search); + int replaceLen = strlen(replace); + int tempPos = 0; + int i = 0; + + while (i < textLen && tempPos < maxlen - 1) { + if (i <= textLen - searchLen) { + // Manual character comparison instead of strncmp + bool match = true; + for (int j = 0; j < searchLen; j++) { + if (text[i + j] != search[j]) { + match = false; + break; + } + } + + if (match) { + // Found match, copy replacement + for (int k = 0; k < replaceLen && tempPos < maxlen - 1; k++) { + localBuffer[tempPos++] = replace[k]; + } + i += searchLen; + continue; + } + } + + localBuffer[tempPos++] = text[i++]; + } + + localBuffer[tempPos] = '\0'; + strcopy(text, maxlen, localBuffer); +} diff --git a/libalternator/tests/corpus/ripext/pawn/scripting/include/ripext.inc b/libalternator/tests/corpus/ripext/pawn/scripting/include/ripext.inc new file mode 100644 index 0000000..9fc02e3 --- /dev/null +++ b/libalternator/tests/corpus/ripext/pawn/scripting/include/ripext.inc @@ -0,0 +1,26 @@ +#if defined _ripext_included_ + #endinput +#endif +#define _ripext_included_ + +#include +#include + +/** + * Do not edit below this line! + */ +public Extension __ext_rip = +{ + name = "REST in Pawn", + file = "rip.ext", +#if defined AUTOLOAD_EXTENSIONS + autoload = 1, +#else + autoload = 0, +#endif +#if defined REQUIRE_EXTENSIONS + required = 1, +#else + required = 0, +#endif +}; diff --git a/libalternator/tests/corpus/ripext/pawn/scripting/include/ripext/http.inc b/libalternator/tests/corpus/ripext/pawn/scripting/include/ripext/http.inc new file mode 100644 index 0000000..3122f22 --- /dev/null +++ b/libalternator/tests/corpus/ripext/pawn/scripting/include/ripext/http.inc @@ -0,0 +1,359 @@ +enum HTTPStatus +{ + HTTPStatus_Invalid = 0, + + // 1xx Informational + HTTPStatus_Continue = 100, + HTTPStatus_SwitchingProtocols = 101, + + // 2xx Success + HTTPStatus_OK = 200, + HTTPStatus_Created = 201, + HTTPStatus_Accepted = 202, + HTTPStatus_NonAuthoritativeInformation = 203, + HTTPStatus_NoContent = 204, + HTTPStatus_ResetContent = 205, + HTTPStatus_PartialContent = 206, + + // 3xx Redirection + HTTPStatus_MultipleChoices = 300, + HTTPStatus_MovedPermanently = 301, + HTTPStatus_Found = 302, + HTTPStatus_SeeOther = 303, + HTTPStatus_NotModified = 304, + HTTPStatus_UseProxy = 305, + HTTPStatus_TemporaryRedirect = 307, + HTTPStatus_PermanentRedirect = 308, + + // 4xx Client Error + HTTPStatus_BadRequest = 400, + HTTPStatus_Unauthorized = 401, + HTTPStatus_PaymentRequired = 402, + HTTPStatus_Forbidden = 403, + HTTPStatus_NotFound = 404, + HTTPStatus_MethodNotAllowed = 405, + HTTPStatus_NotAcceptable = 406, + HTTPStatus_ProxyAuthenticationRequired = 407, + HTTPStatus_RequestTimeout = 408, + HTTPStatus_Conflict = 409, + HTTPStatus_Gone = 410, + HTTPStatus_LengthRequired = 411, + HTTPStatus_PreconditionFailed = 412, + HTTPStatus_RequestEntityTooLarge = 413, + HTTPStatus_RequestURITooLong = 414, + HTTPStatus_UnsupportedMediaType = 415, + HTTPStatus_RequestedRangeNotSatisfiable = 416, + HTTPStatus_ExpectationFailed = 417, + HTTPStatus_MisdirectedRequest = 421, + HTTPStatus_TooEarly = 425, + HTTPStatus_UpgradeRequired = 426, + HTTPStatus_PreconditionRequired = 428, + HTTPStatus_TooManyRequests = 429, + HTTPStatus_RequestHeaderFieldsTooLarge = 431, + HTTPStatus_UnavailableForLegalReasons = 451, + + // 5xx Server Error + HTTPStatus_InternalServerError = 500, + HTTPStatus_NotImplemented = 501, + HTTPStatus_BadGateway = 502, + HTTPStatus_ServiceUnavailable = 503, + HTTPStatus_GatewayTimeout = 504, + HTTPStatus_HTTPVersionNotSupported = 505, + HTTPStatus_VariantAlsoNegotiates = 506, + HTTPStatus_NotExtended = 510, + HTTPStatus_NetworkAuthenticationRequired = 511, +}; + +typeset HTTPRequestCallback +{ + function void (HTTPResponse response, any value); + function void (HTTPResponse response, any value, const char[] error); +}; + +typeset HTTPFileCallback +{ + function void (HTTPStatus status, any value); + function void (HTTPStatus status, any value, const char[] error); +}; + +methodmap HTTPRequest < Handle +{ + // Creates an HTTP request. + // + // The Handle is automatically freed when the request is performed. + // Otherwise, the Handle must be freed via delete or CloseHandle(). + // + // @param url URL to the REST API endpoint. + public native HTTPRequest(const char[] url); + + // Appends a parameter to the form data. + // + // The parameter name and value are encoded according to RFC 3986. + // + // @param name Parameter name. + // @param format Formatting rules. + // @param ... Variable number of format parameters. + public native void AppendFormParam(const char[] name, const char[] format, any ...); + + // Appends a query parameter to the URL. + // + // The parameter name and value are encoded according to RFC 3986. + // + // @param name Parameter name. + // @param format Formatting rules. + // @param ... Variable number of format parameters. + public native void AppendQueryParam(const char[] name, const char[] format, any ...); + + // Sets the credentials for HTTP Basic authentication. + // + // @param username Username to use. + // @param password Password to use. + public native void SetBasicAuth(const char[] username, const char[] password); + + // Sets an HTTP header. + // + // @param name Header name. + // @param format Formatting rules. + // @param ... Variable number of format parameters. + public native void SetHeader(const char[] name, const char[] format, any ...); + + // Performs an HTTP GET request. + // + // This function closes the request Handle after completing. + // + // @param callback A function to use as a callback when the request has finished. + // @param value Optional value to pass to the callback function. + public native void Get(HTTPRequestCallback callback, any value = 0); + + // Performs an HTTP POST request. + // + // This function closes the request Handle after completing. + // + // @param data JSON data to send. + // @param callback A function to use as a callback when the request has finished. + // @param value Optional value to pass to the callback function. + public native void Post(JSON data, HTTPRequestCallback callback, any value = 0); + + // Performs an HTTP PUT request. + // + // This function closes the request Handle after completing. + // + // @param data JSON data to send. + // @param callback A function to use as a callback when the request has finished. + // @param value Optional value to pass to the callback function. + public native void Put(JSON data, HTTPRequestCallback callback, any value = 0); + + // Performs an HTTP PATCH request. + // + // This function closes the request Handle after completing. + // + // @param data JSON data to send. + // @param callback A function to use as a callback when the request has finished. + // @param value Optional value to pass to the callback function. + public native void Patch(JSON data, HTTPRequestCallback callback, any value = 0); + + // Performs an HTTP DELETE request. + // + // This function closes the request Handle after completing. + // + // @param callback A function to use as a callback when the request has finished. + // @param value Optional value to pass to the callback function. + public native void Delete(HTTPRequestCallback callback, any value = 0); + + // Downloads a file. + // + // This function closes the request Handle after completing. + // + // @param path File path to write to. + // @param callback A function to use as a callback when the download has finished. + // @param value Optional value to pass to the callback function. + public native void DownloadFile(const char[] path, HTTPFileCallback callback, any value = 0); + + // Uploads a file. + // + // This function performs an HTTP PUT request. The file contents are sent in the request body. + // This function closes the request Handle after completing. + // + // @param path File path to read from. + // @param callback A function to use as a callback when the upload has finished. + // @param value Optional value to pass to the callback function. + public native void UploadFile(const char[] path, HTTPFileCallback callback, any value = 0); + + // Performs an HTTP POST request with form data. + // + // This function closes the request Handle after completing. + // + // @param callback A function to use as a callback when the request has finished. + // @param value Optional value to pass to the callback function. + public native void PostForm(HTTPRequestCallback callback, any value = 0); + + // Connect timeout in seconds. Defaults to 10. + property int ConnectTimeout { + public native get(); + public native set(int connectTimeout); + } + + // Maximum number of redirects to follow. Defaults to 5. + property int MaxRedirects { + public native get(); + public native set(int maxRedirects); + } + + // Maximum download speed in bytes per second. Defaults to unlimited speed. + property int MaxRecvSpeed { + public native get(); + public native set(int maxSpeed); + } + + // Maximum upload speed in bytes per second. Defaults to unlimited speed. + property int MaxSendSpeed { + public native get(); + public native set(int maxSpeed); + } + + // Timeout in seconds. Defaults to 30. + property int Timeout { + public native get(); + public native set(int timeout); + } +} + +methodmap HTTPResponse +{ + // Retrieves an HTTP header from the response. + // + // @param name Header name. + // @param buffer String buffer to store value. + // @param maxlength Maximum length of the string buffer. + // @return True on success, false if the header was not found. + public native bool GetHeader(const char[] name, char[] buffer, int maxlength); + + // Retrieves the JSON data of the response. + // + // @error Invalid JSON response. + property JSON Data { + public native get(); + } + + // Retrieves the HTTP status of the response. + property HTTPStatus Status { + public native get(); + } +}; + +// Deprecated. Use HTTPRequest instead. +methodmap HTTPClient < Handle +{ + // Creates an HTTP client. + // + // The HTTPClient must be freed via delete or CloseHandle(). + // + // @param baseURL Base URL to the REST API. + #pragma deprecated Use HTTPRequest instead. + public native HTTPClient(const char[] baseURL); + + // Sets an HTTP header to be used for all requests. + // + // @param name Header name. + // @param value String value to set. + #pragma deprecated Use HTTPRequest.SetHeader() instead. + public native void SetHeader(const char[] name, const char[] value); + + // Performs an HTTP GET request. + // + // @param endpoint API endpoint to request. + // @param callback A function to use as a callback when the request has finished. + // @param value Optional value to pass to the callback function. + #pragma deprecated Use HTTPRequest.Get() instead. + public native void Get(const char[] endpoint, HTTPRequestCallback callback, any value = 0); + + // Performs an HTTP POST request. + // + // @param endpoint API endpoint to request. + // @param data JSON data to send. + // @param callback A function to use as a callback when the request has finished. + // @param value Optional value to pass to the callback function. + #pragma deprecated Use HTTPRequest.Post() instead. + public native void Post(const char[] endpoint, JSON data, HTTPRequestCallback callback, any value = 0); + + // Performs an HTTP PUT request. + // + // @param endpoint API endpoint to request. + // @param data JSON data to send. + // @param callback A function to use as a callback when the request has finished. + // @param value Optional value to pass to the callback function. + #pragma deprecated Use HTTPRequest.Put() instead. + public native void Put(const char[] endpoint, JSON data, HTTPRequestCallback callback, any value = 0); + + // Performs an HTTP PATCH request. + // + // @param endpoint API endpoint to request. + // @param data JSON data to send. + // @param callback A function to use as a callback when the request has finished. + // @param value Optional value to pass to the callback function. + #pragma deprecated Use HTTPRequest.Patch() instead. + public native void Patch(const char[] endpoint, JSON data, HTTPRequestCallback callback, any value = 0); + + // Performs an HTTP DELETE request. + // + // @param endpoint API endpoint to request. + // @param callback A function to use as a callback when the request has finished. + // @param value Optional value to pass to the callback function. + #pragma deprecated Use HTTPRequest.Delete() instead. + public native void Delete(const char[] endpoint, HTTPRequestCallback callback, any value = 0); + + // Downloads a file. + // + // @param endpoint API endpoint to download from. + // @param path File path to write to. + // @param callback A function to use as a callback when the download has finished. + // @param value Optional value to pass to the callback function. + #pragma deprecated Use HTTPRequest.DownloadFile() instead. + public native void DownloadFile(const char[] endpoint, const char[] path, HTTPFileCallback callback, any value = 0); + + // Uploads a file. + // + // This function performs an HTTP PUT request. The file contents are sent in the request body. + // + // @param endpoint API endpoint to upload to. + // @param path File path to read from. + // @param callback A function to use as a callback when the upload has finished. + // @param value Optional value to pass to the callback function. + #pragma deprecated Use HTTPRequest.UploadFile() instead. + public native void UploadFile(const char[] endpoint, const char[] path, HTTPFileCallback callback, any value = 0); + + // Connect timeout in seconds. Defaults to 10. + #pragma deprecated Use HTTPRequest.ConnectTimeout instead. + property int ConnectTimeout { + public native get(); + public native set(int connectTimeout); + } + + // Follow HTTP 3xx redirects. Defaults to true. + #pragma deprecated Use HTTPRequest.MaxRedirects instead. + property bool FollowLocation { + public native get(); + public native set(bool followLocation); + } + + // Timeout in seconds. Defaults to 30. + #pragma deprecated Use HTTPRequest.Timeout instead. + property int Timeout { + public native get(); + public native set(int timeout); + } + + // Maximum upload speed in bytes per second. Defaults to unlimited speed. + #pragma deprecated Use HTTPRequest.MaxSendSpeed instead. + property int MaxSendSpeed { + public native get(); + public native set(int speed); + } + + // Maximum download speed in bytes per second. Defaults to unlimited speed. + #pragma deprecated Use HTTPRequest.MaxRecvSpeed instead. + property int MaxRecvSpeed { + public native get(); + public native set(int speed); + } +}; diff --git a/libalternator/tests/corpus/ripext/pawn/scripting/include/ripext/json.inc b/libalternator/tests/corpus/ripext/pawn/scripting/include/ripext/json.inc new file mode 100644 index 0000000..9d13ef9 --- /dev/null +++ b/libalternator/tests/corpus/ripext/pawn/scripting/include/ripext/json.inc @@ -0,0 +1,400 @@ +// Decoding flags +enum +{ + JSON_REJECT_DUPLICATES = 0x1, /**< Error if any JSON object contains duplicate keys */ + JSON_DISABLE_EOF_CHECK = 0x2, /**< Allow extra data after a valid JSON array or object */ + JSON_DECODE_ANY = 0x4, /**< Decode any value */ + JSON_DECODE_INT_AS_REAL = 0x8, /**< Interpret all numbers as floats */ + JSON_ALLOW_NUL = 0x10 /**< Allow \u0000 escape inside string values */ +}; + +// Encoding flags +enum +{ + JSON_COMPACT = 0x20, /**< Compact representation */ + JSON_ENSURE_ASCII = 0x40, /**< Escape all Unicode characters outside the ASCII range */ + JSON_SORT_KEYS = 0x80, /**< Sort object keys */ + JSON_ENCODE_ANY = 0x200, /**< Encode any value */ + JSON_ESCAPE_SLASH = 0x400, /**< Escape / with \/ */ + JSON_EMBED = 0x10000 /**< Omit opening and closing braces of the top-level object */ +}; + +// Maximum indentation +static const int JSON_MAX_INDENT = 0x1F; + +// Pretty-print the result, indenting with n spaces +stock int JSON_INDENT(int n) +{ + return n & JSON_MAX_INDENT; +} + +// Output floats with at most n digits of precision +stock int JSON_REAL_PRECISION(int n) +{ + return (n & 0x1F) << 11; +} + +// Generic type for encoding JSON. +methodmap JSON < Handle +{ + // Writes the JSON string representation to a file. + // + // @param file File to write to. + // @param flags Encoding flags. + // @return True on success, false on failure. + public native bool ToFile(const char[] file, int flags = 0); + + // Retrieves the JSON string representation. + // + // @param buffer String buffer to write to. + // @param maxlength Maximum length of the string buffer. + // @param flags Encoding flags. + // @return True on success, false on failure. + public native bool ToString(char[] buffer, int maxlength, int flags = 0); +}; + +methodmap JSONObject < JSON +{ + // Creates a JSON object. A JSON object maps strings (called "keys") to values. Keys in a + // JSON object are unique. That is, there is at most one entry in the map for a given key. + // + // The JSONObject must be freed via delete or CloseHandle(). + public native JSONObject(); + + // Loads a JSON object from a file. + // + // @param file File to read from. + // @param flags Decoding flags. + // @return Object handle, or null on failure. + // @error Invalid JSON. + public static native JSONObject FromFile(const char[] file, int flags = 0); + + // Loads a JSON object from a string. + // + // @param buffer String buffer to load into the JSON object. + // @param flags Decoding flags. + // @return Object handle, or null on failure. + // @error Invalid JSON. + public static native JSONObject FromString(const char[] buffer, int flags = 0); + + // Retrieves an array or object value from the object. + // + // The JSON must be freed via delete or CloseHandle(). + // + // @param key Key string. + // @return Value read. + // @error Invalid key. + public native JSON Get(const char[] key); + + // Retrieves a boolean value from the object. + // + // @param key Key string. + // @return Value read. + // @error Invalid key. + public native bool GetBool(const char[] key); + + // Retrieves a float value from the object. + // + // @param key Key string. + // @return Value read. + // @error Invalid key. + public native float GetFloat(const char[] key); + + // Retrieves an integer value from the object. + // + // @param key Key string. + // @return Value read. + // @error Invalid key. + public native int GetInt(const char[] key); + + // Retrieves a 64-bit integer value from the object. + // + // @param key Key string. + // @param buffer String buffer to store value. + // @param maxlength Maximum length of the string buffer. + // @return True on success, false if the key was not found. + public native bool GetInt64(const char[] key, char[] buffer, int maxlength); + + // Retrieves a string value from the object. + // + // @param key Key string. + // @param buffer String buffer to store value. + // @param maxlength Maximum length of the string buffer. + // @return True on success. False if the key was not found, or the value is not a string. + public native bool GetString(const char[] key, char[] buffer, int maxlength); + + // Returns whether or not a value in the object is null. + // + // @param key Key string. + // @return True if the value is null, false otherwise. + // @error Invalid key. + public native bool IsNull(const char[] key); + + // Returns whether or not a key exists in the object. + // + // @param key Key string. + // @return True if the key exists, false otherwise. + public native bool HasKey(const char[] key); + + // Sets an array or object value in the object, either inserting a new entry or replacing an old one. + // + // @param key Key string. + // @param value Value to store at this key. + // @return True on success, false on failure. + public native bool Set(const char[] key, JSON value); + + // Sets a boolean value in the object, either inserting a new entry or replacing an old one. + // + // @param key Key string. + // @param value Value to store at this key. + // @return True on success, false on failure. + public native bool SetBool(const char[] key, bool value); + + // Sets a float value in the object, either inserting a new entry or replacing an old one. + // + // @param key Key string. + // @param value Value to store at this key. + // @return True on success, false on failure. + public native bool SetFloat(const char[] key, float value); + + // Sets an integer value in the object, either inserting a new entry or replacing an old one. + // + // @param key Key string. + // @param value Value to store at this key. + // @return True on success, false on failure. + public native bool SetInt(const char[] key, int value); + + // Sets a 64-bit integer value in the object, either inserting a new entry or replacing an old one. + // + // @param key Key string. + // @param value Value to store at this key. + // @return True on success, false on failure. + public native bool SetInt64(const char[] key, const char[] value); + + // Sets a null value in the object, either inserting a new entry or replacing an old one. + // + // @param key Key string. + // @return True on success, false on failure. + public native bool SetNull(const char[] key); + + // Sets a string value in the object, either inserting a new entry or replacing an old one. + // + // @param key Key string. + // @param value Value to store at this key. + // @return True on success, false on failure. + public native bool SetString(const char[] key, const char[] value); + + // Removes an entry from the object. + // + // @param key Key string. + // @return True on success, false if the key was not found. + public native bool Remove(const char[] key); + + // Clears the object of all entries. + // @return True on success, false on failure. + public native bool Clear(); + + // Returns an iterator for the object's keys. See JSONObjectKeys. + public native JSONObjectKeys Keys(); + + // Retrieves the size of the object. + property int Size { + public native get(); + } +}; + +/** + * A JSONObjectKeys is created via JSONObject.Keys(). It allows the keys of an + * object to be iterated. The JSONObjectKeys must be freed with delete or + * CloseHandle(). + */ +methodmap JSONObjectKeys < Handle +{ + // Reads an object key, then advances to the next key if any. + // + // @param buffer String buffer to store key. + // @param maxlength Maximum length of the string buffer. + // @return True on success, false if there are no more keys. + public native bool ReadKey(char[] buffer, int maxlength); +}; + +methodmap JSONArray < JSON +{ + // Creates a JSON array. + // + // The JSONArray must be freed via delete or CloseHandle(). + public native JSONArray(); + + // Loads a JSON array from a file. + // + // @param file File to read from. + // @param flags Decoding flags. + // @return Array handle, or null on failure. + // @error Invalid JSON. + public static native JSONArray FromFile(const char[] file, int flags = 0); + + // Loads a JSON array from a string. + // + // @param buffer String buffer to load into the JSON array. + // @param flags Decoding flags. + // @return Array handle, or null on failure. + // @error Invalid JSON. + public static native JSONArray FromString(const char[] buffer, int flags = 0); + + // Retrieves an array or object value from the array. + // + // The JSON must be freed via delete or CloseHandle(). + // + // @param index Index in the array. + // @return Value read. + // @error Invalid index. + public native JSON Get(int index); + + // Retrieves a boolean value from the array. + // + // @param index Index in the array. + // @return Value read. + // @error Invalid index. + public native bool GetBool(int index); + + // Retrieves a float value from the array. + // + // @param index Index in the array. + // @return Value read. + // @error Invalid index. + public native float GetFloat(int index); + + // Retrieves an integer value from the array. + // + // @param index Index in the array. + // @return Value read. + // @error Invalid index. + public native int GetInt(int index); + + // Retrieves an 64-bit integer value from the array. + // + // @param index Index in the array. + // @param buffer Buffer to copy to. + // @param maxlength Maximum size of the buffer. + // @error Invalid index. + public native void GetInt64(int index, char[] buffer, int maxlength); + + // Retrieves a string value from the array. + // + // @param index Index in the array. + // @param buffer Buffer to copy to. + // @param maxlength Maximum size of the buffer. + // @return True on success, false if the value is not a string. + // @error Invalid index. + public native bool GetString(int index, char[] buffer, int maxlength); + + // Returns whether or not a value in the array is null. + // + // @param index Index in the array. + // @return True if the value is null, false otherwise. + // @error Invalid index. + public native bool IsNull(int index); + + // Sets an array or object value in the array. + // + // @param index Index in the array. + // @param value Value to set. + // @return True on success, false on failure. + public native bool Set(int index, JSON value); + + // Sets a boolean value in the array. + // + // @param index Index in the array. + // @param value Value to set. + // @return True on success, false on failure. + public native bool SetBool(int index, bool value); + + // Sets a float value in the array. + // + // @param index Index in the array. + // @param value Value to set. + // @return True on success, false on failure. + public native bool SetFloat(int index, float value); + + // Sets an integer value in the array. + // + // @param index Index in the array. + // @param value Value to set. + // @return True on success, false on failure. + public native bool SetInt(int index, int value); + + // Sets a 64 bit integer value in the array. + // + // @param index Index in the array. + // @param value 64-bit integer value to set. + // @return True on success, false on failure. + public native bool SetInt64(int index, const char[] value); + + // Sets a null value in the array. + // + // @param index Index in the array. + // @return True on success, false on failure. + public native bool SetNull(int index); + + // Sets a string value in the array. + // + // @param index Index in the array. + // @param value String value to set. + // @return True on success, false on failure. + public native bool SetString(int index, const char[] value); + + // Pushes an array or object value onto the end of the array, adding a new index. + // + // @param value Value to push. + // @return True on success, false on failure. + public native bool Push(JSON value); + + // Pushes a boolean value onto the end of the array, adding a new index. + // + // @param value Value to push. + // @return True on success, false on failure. + public native bool PushBool(bool value); + + // Pushes a float value onto the end of the array, adding a new index. + // + // @param value Value to push. + // @return True on success, false on failure. + public native bool PushFloat(float value); + + // Pushes an integer value onto the end of the array, adding a new index. + // + // @param value Value to push. + // @return True on success, false on failure. + public native bool PushInt(int value); + + // Pushes a 64-bit integer value onto the end of the array, adding a new index. + // + // @param value 64-bit integer value to push. + // @return True on success, false on failure. + public native bool PushInt64(const char[] value); + + // Pushes a null value onto the end of the array, adding a new index. + // @return True on success, false on failure. + public native bool PushNull(); + + // Pushes a string value onto the end of the array, adding a new index. + // + // @param value String value to push. + // @return True on success, false on failure. + public native bool PushString(const char[] value); + + // Removes an entry from the array. + // + // @param index Index in the array to remove. + // @return True on success, false on invalid index. + public native bool Remove(int index); + + // Clears the array of all entries. + // @return True on success, false on failure. + public native bool Clear(); + + // Retrieves the size of the array. + property int Length { + public native get(); + } +}; diff --git a/libalternator/tests/corpus/ripext/pawn/scripting/ripext-test.sp b/libalternator/tests/corpus/ripext/pawn/scripting/ripext-test.sp new file mode 100644 index 0000000..3f56c55 --- /dev/null +++ b/libalternator/tests/corpus/ripext/pawn/scripting/ripext-test.sp @@ -0,0 +1,141 @@ +#include +#include + +#pragma newdecls required +#pragma semicolon 1 + +#define API_BASE_URL "https://nghttp2.org/httpbin" + +public Plugin myinfo = +{ + name = "REST in Pawn - Tests", + author = "Tsunami", + description = "Test HTTP and JSON natives", + version = "1.0.0", + url = "http://www.tsunami-productions.nl" +}; + + +char sHTTPTags[][] = { + "GET", + "POST", + "PUT", + "PATCH", + "DELETE", + "GZIP", + "AUTH", + "BEARER", + "REDIRECT", + "FORM", +}; + + +public void OnPluginStart() +{ + HTTPRequest hHTTPRequest; + JSONObject hJSONObject = CreateJSONObject(); + + char sImagePath[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, sImagePath, sizeof(sImagePath), "data/ripext-test.jpg"); + + hHTTPRequest = new HTTPRequest(API_BASE_URL..."/get"); + hHTTPRequest.AppendQueryParam("name[]", "%s", "%&✓"); + hHTTPRequest.AppendQueryParam("name[]", "%s", "<=>"); + hHTTPRequest.Get(OnHTTPResponse, 0); + + hHTTPRequest = new HTTPRequest(API_BASE_URL..."/post"); + hHTTPRequest.Post(hJSONObject, OnHTTPResponse, 1); + + hHTTPRequest = new HTTPRequest(API_BASE_URL..."/put"); + hHTTPRequest.Put(hJSONObject, OnHTTPResponse, 2); + + hHTTPRequest = new HTTPRequest(API_BASE_URL..."/patch"); + hHTTPRequest.Patch(hJSONObject, OnHTTPResponse, 3); + + hHTTPRequest = new HTTPRequest(API_BASE_URL..."/delete"); + hHTTPRequest.Delete(OnHTTPResponse, 4); + + hHTTPRequest = new HTTPRequest(API_BASE_URL..."/gzip"); + hHTTPRequest.Get(OnHTTPResponse, 5); + + hHTTPRequest = new HTTPRequest(API_BASE_URL..."/basic-auth/user/pass"); + hHTTPRequest.SetBasicAuth("user", "pass"); + hHTTPRequest.Get(OnHTTPResponse, 6); + + hHTTPRequest = new HTTPRequest(API_BASE_URL..."/bearer"); + hHTTPRequest.SetHeader("Authorization", "Bearer %s", "token"); + hHTTPRequest.Get(OnHTTPResponse, 7); + + hHTTPRequest = new HTTPRequest(API_BASE_URL..."/redirect/1"); + hHTTPRequest.Get(OnHTTPResponse, 8); + + hHTTPRequest = new HTTPRequest(API_BASE_URL..."/post"); + hHTTPRequest.AppendFormParam("name[]", "%s", "%&✓"); + hHTTPRequest.AppendFormParam("name[]", "%s", "<=>"); + hHTTPRequest.PostForm(OnHTTPResponse, 9); + + hHTTPRequest = new HTTPRequest(API_BASE_URL..."/image/jpeg"); + hHTTPRequest.DownloadFile(sImagePath, OnImageDownloaded); + + JSONObjectKeys hJSONObjectKeys = hJSONObject.Keys(); + char sKey[64]; + + while (hJSONObjectKeys.ReadKey(sKey, sizeof(sKey))) { + PrintToServer("[JSON] Read Object Key: %s", sKey); + } + + delete hJSONObjectKeys; + delete hJSONObject; +} + +void OnHTTPResponse(HTTPResponse response, any value) +{ + if (response.Status != HTTPStatus_OK) { + PrintToServer("[ERR] %s Status: %d", sHTTPTags[value], response.Status); + return; + } + + char sData[1024]; + response.Data.ToString(sData, sizeof(sData), JSON_INDENT(4)); + + PrintToServer("[OK] %s Response:\n%s", sHTTPTags[value], sData); +} + +void OnImageDownloaded(HTTPStatus status, any value) +{ + if (status != HTTPStatus_OK) { + PrintToServer("[ERR] Download Status: %d", status); + return; + } + + PrintToServer("[OK] Download Complete"); +} + +JSONObject CreateJSONObject() +{ + JSONObject hJSONObject = new JSONObject(); + hJSONObject.SetInt("id", 1); + hJSONObject.SetInt64("int64", "9223372036854775800"); + hJSONObject.SetFloat("jsonrpc", 2.0); + hJSONObject.SetBool("bool", true); + hJSONObject.SetString("method", "subtract"); + hJSONObject.SetNull("null"); + + JSONObject hJSONObjectInArray = new JSONObject(); + hJSONObjectInArray.SetInt("id", 1); + + JSONArray hJSONArray = new JSONArray(); + hJSONArray.Push(hJSONObjectInArray); + hJSONArray.PushInt(1); + hJSONArray.PushInt64("9223372036854775800"); + hJSONArray.PushFloat(2.0); + hJSONArray.PushBool(true); + hJSONArray.PushString("string"); + hJSONArray.PushNull(); + + hJSONObject.Set("params", hJSONArray); + + delete hJSONObjectInArray; + delete hJSONArray; + return hJSONObject; +} diff --git a/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/include/sbpp_server_ip.inc b/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/include/sbpp_server_ip.inc new file mode 100644 index 0000000..eb075e1 --- /dev/null +++ b/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/include/sbpp_server_ip.inc @@ -0,0 +1,73 @@ +// ************************************************************************* +// This file is part of SourceBans++. +// +// Copyright (C) 2014-2026 SourceBans++ Dev Team +// +// SourceBans++ is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, per version 3 of the License. +// ************************************************************************* + +#if defined _sbpp_server_ip_included +#endinput +#endif +#define _sbpp_server_ip_included + +#define SBPP_SERVER_IP_LENGTH 16 + +stock bool SBPP_IsValidServerIpOverride(const char[] ip) +{ + if (ip[0] == '\0') + { + return true; + } + + char octets[5][16]; + if (ExplodeString(ip, ".", octets, sizeof(octets), sizeof(octets[])) != 4) + { + return false; + } + + for (int octet = 0; octet < 4; octet++) + { + int length = strlen(octets[octet]); + if (length == 0 || length > 3) + { + return false; + } + + for (int character = 0; character < length; character++) + { + if (!IsCharNumeric(octets[octet][character])) + { + return false; + } + } + + if (StringToInt(octets[octet]) > 255) + { + return false; + } + } + + return true; +} + +stock void SBPP_ResolveServerIp(ConVar hostIp, const char[] configuredIp, char[] output, int maxLength) +{ + if (configuredIp[0] != '\0') + { + strcopy(output, maxLength, configuredIp); + return; + } + + int pieces[4]; + int longIp = hostIp.IntValue; + + pieces[0] = (longIp >> 24) & 0x000000FF; + pieces[1] = (longIp >> 16) & 0x000000FF; + pieces[2] = (longIp >> 8) & 0x000000FF; + pieces[3] = longIp & 0x000000FF; + + FormatEx(output, maxLength, "%d.%d.%d.%d", pieces[0], pieces[1], pieces[2], pieces[3]); +} diff --git a/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/include/sbpp_version.inc b/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/include/sbpp_version.inc new file mode 100644 index 0000000..a002a20 --- /dev/null +++ b/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/include/sbpp_version.inc @@ -0,0 +1,23 @@ +// ************************************************************************* +// Auto-generated by scripts/resolve-plugin-version.sh — do not edit. +// Fallback checked in for direct `spcomp` builds; CI/release overwrites +// this file before compile. Re-run: +// game/addons/sourcemod/scripting/scripts/resolve-plugin-version.sh +// ************************************************************************* + +#if defined _sbpp_version_included +#endinput +#endif +#define _sbpp_version_included + +#define MAJOR_REVISION 2 +#define MINOR_REVISION 1 + +#define SOURCEBANSPP_VERSION_MAJOR MAJOR_REVISION +#define SOURCEBANSPP_VERSION_MINOR MINOR_REVISION + +#define SB_VERSION_MAJOR "dev" +#define SB_VERSION_MINOR "0" +#define SB_VERSION_PATCH "0" + +#define SB_VERSION "dev" diff --git a/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/include/sourcebanschecker.inc b/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/include/sourcebanschecker.inc new file mode 100644 index 0000000..f0be9fd --- /dev/null +++ b/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/include/sourcebanschecker.inc @@ -0,0 +1,85 @@ +// ************************************************************************* +// This file is part of SourceBans++. +// +// Copyright (C) 2014-2024 SourceBans++ Dev Team +// +// SourceBans++ is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, per version 3 of the License. +// +// SourceBans++ is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with SourceBans++. If not, see . +// +// This file based off work(s) covered by the following copyright(s): +// +// SourceBans 1.4.11 +// Copyright (C) 2007-2015 SourceBans Team - Part of GameConnect +// Licensed under GNU GPL version 3, or later. +// Page: - +// +// ************************************************************************* + +#if defined _sourcebanschecker_included +#endinput +#endif +#define _sourcebanschecker_included + +public SharedPlugin __pl_sourcebanschecker = +{ + name = "sourcebans++", + file = "sbpp_checker.smx", + #if defined REQUIRE_PLUGIN + required = 1 + #else + required = 0 + #endif +}; + +#if !defined REQUIRE_PLUGIN +public void __pl_sourcebanschecker_SetNTVOptional() +{ + MarkNativeAsOptional("SBPP_CheckerGetClientsBans"); + MarkNativeAsOptional("SBPP_CheckerGetClientsComms"); + MarkNativeAsOptional("SBPP_CheckerGetClientsMutes"); + MarkNativeAsOptional("SBPP_CheckerGetClientsGags"); +} +#endif + + +/********************************************************* + * Get the number of bans of a client. + * + * @param iClient The client index of who you want to get the number of bans. + * @return The number of bans of the client. + *********************************************************/ +native int SBPP_CheckerGetClientsBans(int iClient); + + +/********************************************************* + * Get the number of comms bans of a client. + * + * @param iClient The client index of who you want to get the number of comms bans. + * @return The number of comms bans of the client. + *********************************************************/ +native int SBPP_CheckerGetClientsComms(int iClient); + +/********************************************************* + * Get the number of mutes of a client. + * + * @param iClient The client index of who you want to get the number of mutes. + * @return The number of mutes of the client. + *********************************************************/ +native int SBPP_CheckerGetClientsMutes(int iClient); + +/********************************************************* + * Get the number of gags of a client. + * + * @param iClient The client index of who you want to get the number of gags. + * @return The number of gags of the client. + *********************************************************/ +native int SBPP_CheckerGetClientsGags(int iClient); diff --git a/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/include/sourcebanspp.inc b/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/include/sourcebanspp.inc new file mode 100644 index 0000000..6584cec --- /dev/null +++ b/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/include/sourcebanspp.inc @@ -0,0 +1,175 @@ +// ************************************************************************* +// This file is part of SourceBans++. +// +// Copyright (C) 2014-2024 SourceBans++ Dev Team +// +// SourceBans++ is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, per version 3 of the License. +// +// SourceBans++ is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with SourceBans++. If not, see . +// +// This file based off work(s) covered by the following copyright(s): +// +// SourceBans 1.4.11 +// Copyright (C) 2007-2015 SourceBans Team - Part of GameConnect +// Licensed under GNU GPL version 3, or later. +// Page: - +// +// ************************************************************************* + +#if defined _sourcebanspp_included +#endinput +#endif +#define _sourcebanspp_included + +// ------------------------------------------------------------------------- +// Native API revision (aligned with SourceBans++ 2.0 panel / plugin release) +// ------------------------------------------------------------------------- +// +// MAJOR_REVISION tracks the documented native API epoch (2 = 2.0 line). +// Bump MAJOR only when a change requires third-party plugins to recompile +// (removed/changed native signatures, removed forwards). Bump MINOR for +// additive natives/forwards that old .smx files can ignore at runtime. +// +// 2.0 epoch (this header): +// sourcebanspp.inc — library "sourcebans++" (sbpp_main.smx) +// Additive: forward SBPP_OnClientPostAdminCheck (#1431). +// Additive: native SBPP_BanPlayerBySteamId (#1511). +// Unchanged: SBPP_BanPlayer; SBBanPlayer remains deprecated. +// Existing third-party .smx that only use prior natives/forwards +// generally keep working without a rebuild. +// +// sourcebanschecker.inc — RegPluginLibrary "sourcechecker++" (#1034) +// Recompile / update detection if you #include checker and use +// LibraryExists/GetFeatureStatus: was "sourcebans++", now +// "sourcechecker++". Additive natives: SBPP_CheckerGetClientsMutes, +// SBPP_CheckerGetClientsGags (#1032); require sbpp_checker.smx that +// implements them if you call those natives. +// +// Optional compile-time guard (add to your plugin if you ship against +// this header and want a hard fail when the epoch drifts): +// +// #if SOURCEBANSPP_VERSION_MAJOR != 2 +// #error Rebuild against SourceBans++ 2.0 sourcebanspp.inc +// #endif +// +// Version constants — scripts/resolve-plugin-version.sh writes +// include/sbpp_version.inc (release tag → configs/version.json → +// git describe → "dev"; mirrors Sbpp\Version::resolve). Checked-in +// fallback supports direct spcomp; release/plugin-build CI regenerates. +#include + +public SharedPlugin __pl_sourcebanspp = +{ + name = "sourcebans++", + file = "sbpp_main.smx", + #if defined REQUIRE_PLUGIN + required = 1 + #else + required = 0 + #endif +}; + +#if !defined REQUIRE_PLUGIN +public void __pl_sourcebanspp_SetNTVOptional() +{ + MarkNativeAsOptional("SBBanPlayer"); + MarkNativeAsOptional("SBPP_BanPlayer"); + MarkNativeAsOptional("SBPP_BanPlayerBySteamId"); + MarkNativeAsOptional("SBPP_ReportPlayer"); +} +#endif + + +/********************************************************* + * Ban Player from server + * + * @param iAdmin The client index of the admin who is banning the client + * @param iTarget The client index of the player to ban + * @param iTime The time to ban the player for (in minutes, 0 = permanent) + * @param sReason The reason to ban the player from the server + * @noreturn + *********************************************************/ +#pragma deprecated Use SBPP_BanPlayer() instead. +native void SBBanPlayer(int iAdmin, int iTarget, int iTime, const char[] sReason); + +/********************************************************* + * Ban Player from server + * + * @param iAdmin The client index of the admin who is banning the client + * @param iTarget The client index of the player to ban + * @param iTime The time to ban the player for (in minutes, 0 = permanent) + * @param sReason The reason to ban the player from the server + * @noreturn + *********************************************************/ +native void SBPP_BanPlayer(int iAdmin, int iTarget, int iTime, const char[] sReason); + +/********************************************************* + * Ban a player by SteamID — supports offline/disconnected players. + * Inserts the ban directly into the SourceBans++ database. + * Fires SBPP_OnBanPlayer with iTarget = -1 to indicate an offline ban. + * + * @param iAdmin Client index of the admin (0 = server / automated) + * @param steamId SteamID string, e.g. "STEAM_0:0:12345" + * @param name Player name; may be empty string if unknown + * @param iTime Ban duration in minutes (0 = permanent) + * @param sReason Reason for the ban + * @noreturn + *********************************************************/ +native void SBPP_BanPlayerBySteamId(int iAdmin, const char[] steamId, const char[] name, int iTime, const char[] sReason); + +/********************************************************* + * Reports a player + * + * @param iReporter The client index of the reporter + * @param iTarget The client index of the player to report + * @param sReason The reason to report the player + * @noreturn + *********************************************************/ +native void SBPP_ReportPlayer(int iReporter, int iTarget, const char[] sReason); + +/********************************************************* + * Called when the admin banning the player. + * + * @param iAdmin The client index of the admin who is banning the client + * @param iTarget The client index of the player to ban (-1 for an offline SteamID ban) + * @param iTime The time to ban the player for (in minutes, 0 = permanent) + * @param sReason The reason to ban the player from the server + *********************************************************/ +forward void SBPP_OnBanPlayer(int iAdmin, int iTarget, int iTime, const char[] sReason); + +/********************************************************* + * Called when a new report is inserted + * + * @param iReporter The client index of the reporter + * @param iTarget The client index of the player to report + * @param sReason The reason to report the player + * @noreturn + *********************************************************/ +forward void SBPP_OnReportPlayer(int iReporter, int iTarget, const char[] sReason); + +/********************************************************* + * Called when OnClientPreAdminCheck is about to be triggered + * + * @param part The AdminCachePart being processed + * @return bBlock If enabled, blocks the trigger for OnClientPreAdminCheck + *********************************************************/ +forward bool SBPP_OnClientPreAdminCheck(AdminCachePart part); + +/********************************************************* + * Called after a client's admin rights have been fully applied + * from the database (flags and groups assigned, cache checked). + * + * @param client The client index who received admin rights + * @noreturn + *********************************************************/ +forward void SBPP_OnClientPostAdminCheck(int client); + +//Yarr! diff --git a/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/include/sourcecomms.inc b/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/include/sourcecomms.inc new file mode 100644 index 0000000..89447f5 --- /dev/null +++ b/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/include/sourcecomms.inc @@ -0,0 +1,143 @@ +// ************************************************************************* +// This file is part of SourceBans++. +// +// Copyright (C) 2014-2024 SourceBans++ Dev Team +// +// SourceBans++ is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, per version 3 of the License. +// +// SourceBans++ is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with SourceBans++. If not, see . +// +// This file based off work(s) covered by the following copyright(s): +// +// SourceComms 0.9.266 +// Copyright (C) 2013-2014 Alexandr Duplishchev +// Licensed under GNU GPL version 3, or later. +// Page: - +// +// ************************************************************************* + +#if defined _sourcecomms_included +#endinput +#endif +#define _sourcecomms_included + +#define SBPPComms_VERSION_MAJOR "1" +#define SBPPComms_VERSION_MINOR "8" +#define SBPPComms_VERSION_PATCH "4" + +#define SBPPComms_VERSION SBPPComms_VERSION_MAJOR..."."...SBPPComms_VERSION_MINOR..."."...SBPPComms_VERSION_PATCH + +/** + * @section Int definitions for punishments types. + */ + +#define TYPE_MUTE 1 /**< Voice Mute */ +#define TYPE_GAG 2 /**< Gag (text chat) */ +#define TYPE_SILENCE 3 /**< Silence (mute + gag) */ +#define TYPE_UNMUTE 4 /**< Voice Unmute*/ +#define TYPE_UNGAG 5 /**< Ungag*/ +#define TYPE_UNSILENCE 6 /**< Unsilence */ +#define TYPE_TEMP_UNMUTE 14 /**< Temp mute removed */ +#define TYPE_TEMP_UNGAG 15 /**< Temp gag removed */ +#define TYPE_TEMP_UNSILENCE 16 /**< Temp silence removed */ + +/* Punishments types */ +enum bType { + bNot = 0, // Player chat or voice is not blocked + bSess, // ... blocked for player session (until reconnect) + bTime, // ... blocked for some time + bPerm // ... permanently blocked +} + +/** + * Sets a client's mute state. + * + * @param client Client index. + * @param muteState True to mute client, false to unmute. + * -------------------------------------Parameters below this line are used only for muteState=true------------------------------------- + * ----------------------------------for muteState=false these parameters are ignored (saveToDB=false)---------------------------------- + * @param muteLength Length of punishment in minutes. Value < 0 muting client for session. Permanent (0) is not allowed at this time. + * @param saveToDB If true, punishment will be saved in database. + * @param reason Reason for punishment. + * @return True if this caused a change in mute state, false otherwise. + */ +native bool SourceComms_SetClientMute(int client, bool muteState, int muteLength = -1, bool saveToDB = false, const char[] reason = "Muted through natives"); + +/** + * Sets a client's gag state. + * + * @param client Client index. + * @param gagState True to gag client, false to ungag. + * --------------------------------------Parameters below this line are used only for gagState=true-------------------------------------- + * -----------------------------------for gagState=false these parameters are ignored (saveToDB=false)----------------------------------- + * @param gagLength Length of punishment in minutes. Value < 0 gagging client for session. Permanent (0) is not allowed at this time. + * @param saveToDB If true, punishment will be saved in database. + * @param reason Reason for punishment. + * @return True if this caused a change in gag state, false otherwise. + */ +native bool SourceComms_SetClientGag(int client, bool gagState, int gagLength = -1, bool saveToDB = false, const char[] reason = "Gagged through natives"); + +/** + * Returns the client's mute type + * + * @param client The client index of the player to check mute status + * @return The client's current mute type index (see enum bType in the begin). + */ +native bType SourceComms_GetClientMuteType(int client); + + +/** + * Returns the client's gag type + * + * @param client The client index of the player to check gag status + * @return The client's current gag type index (see enum bType in the begin). + */ +native bType SourceComms_GetClientGagType(int client); + +/** + * Called when added communication block for player. + * + * @param client The client index of the admin who is blocking the client. + * @param target The client index of the player to blocked. + * @param time The time to blocked the player for (in minutes, 0 = permanent). + * @param type The type of block. See section "Int definitions for punishments types". + * @param reason The reason to block the player. + */ +forward void SourceComms_OnBlockAdded(int client, int target, int time, int type, char[] reason); + +/** + * Called when removed communication block for player. + * + * @param client The client index of the admin who is unblocking the client. + * @param target The client index of the player to blocked. + * @param type The type of unblock. See section "Int definitions for punishments types". + * @param reason The reason to unblock the player. + */ +forward void SourceComms_OnBlockRemoved(int client, int target, int type, char[] reason); + +public SharedPlugin __pl_sourcecomms = +{ + name = "sourcecomms++", + file = "sbpp_comms.smx", + #if defined REQUIRE_PLUGIN + required = 1 + #else + required = 0 + #endif +}; + +public void __pl_sourcecomms_SetNTVOptional() +{ + MarkNativeAsOptional("SourceComms_SetClientMute"); + MarkNativeAsOptional("SourceComms_SetClientGag"); + MarkNativeAsOptional("SourceComms_GetClientMuteType"); + MarkNativeAsOptional("SourceComms_GetClientGagType"); +} diff --git a/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/sbpp_admcfg.sp b/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/sbpp_admcfg.sp new file mode 100644 index 0000000..ffdc550 --- /dev/null +++ b/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/sbpp_admcfg.sp @@ -0,0 +1,187 @@ +// ************************************************************************* +// This file is part of SourceBans++. +// +// Copyright (C) 2014-2024 SourceBans++ Dev Team +// +// SourceBans++ is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, per version 3 of the License. +// +// SourceBans++ is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with SourceBans++. If not, see . +// +// This file is based off work(s) covered by the following copyright(s): +// +// SourceMod Admin File Reader Plugin +// Copyright (C) 2004-2008 AlliedModders LLC +// Licensed under GNU GPL version 3 +// Page: +// +// ************************************************************************* + +#pragma semicolon 1 +#pragma newdecls required + +#include + +public Plugin myinfo = +{ + name = "SourceBans++: Admin Config Loader", + author = "AlliedModders LLC, SourceBans++ Dev Team", + description = "Reads Admin Files", + version = "1.8.0", + url = "https://sbpp.github.io" +}; + + +/** Various parsing globals */ +bool g_LoggedFileName = false; /* Whether or not the file name has been logged */ +int g_ErrorCount = 0; /* Current error count */ +int g_IgnoreLevel = 0; /* Nested ignored section count, so users can screw up files safely */ +int g_CurrentLine = 0; /* Current line we're on */ +char g_Filename[PLATFORM_MAX_PATH]; /* Used for error messages */ + +/* Maps a lowercased server-group name to its GroupId. Populated while the + * groups config is parsed and consulted as the case-insensitive fallback in + * FindAdmGroupInsensitive (issue #1503). Declared before the sub-file includes + * so ReadGroups() (which fills it) can see it. */ +StringMap g_GroupNameMap = null; + +#include "sbpp_admcfg/sbpp_admin_groups.sp" +#include "sbpp_admcfg/sbpp_admin_users.sp" + +public void OnRebuildAdminCache(AdminCachePart part) +{ + if (part == AdminCache_Groups) { + ReadGroups(); + } else if (part == AdminCache_Admins) { + ReadUsers(); + } +} + +public void OnPluginEnd() +{ + // SourceMod reclaims the handle on unload anyway; released explicitly for + // hygiene so the case-insensitive group map doesn't linger. + delete g_GroupNameMap; +} + +void ParseError(const char[] format, any...) +{ + char buffer[512]; + + if (!g_LoggedFileName) + { + LogError("Error(s) Detected Parsing %s", g_Filename); + g_LoggedFileName = true; + } + + VFormat(buffer, sizeof(buffer), format, 2); + + LogError(" (line %d) %s", g_CurrentLine, buffer); + + g_ErrorCount++; +} + +void InitGlobalStates() +{ + g_ErrorCount = 0; + g_IgnoreLevel = 0; + g_CurrentLine = 0; + g_LoggedFileName = false; +} + +// Copy src into dest, lowercasing every character. Used to build/consult the +// case-insensitive group-name map keys (StringMap keys are case-sensitive). +// CharToLower is ASCII-only, which is fine here: the case drift this fixes is +// ASCII (e.g. "Admin" vs "admin"), and any multibyte bytes pass through +// unchanged and identically on both the register and lookup sides, so a +// non-ASCII name still maps to itself. +void LowercaseCopy(char[] dest, int maxlength, const char[] src) +{ + if (maxlength <= 0) + { + return; + } + + int i = 0; + for (; i < maxlength - 1 && src[i] != '\0'; i++) + { + dest[i] = CharToLower(src[i]); + } + dest[i] = '\0'; +} + +// Record a parsed server-group name so FindAdmGroupInsensitive can resolve a +// later reference to it regardless of case. Called as each group section is +// created while reading sb_admin_groups.cfg. The key is the case-folded name, +// so if two group sections differ only in case ("Admin" and "admin") they map +// to one key and the last-registered id wins in the case-folded fallback +// keyspace. That collision only affects references that miss the exact-case +// FindAdmGroup below; SourceMod itself keeps such groups distinct. +void RegisterGroupName(const char[] group_name, GroupId id) +{ + if (g_GroupNameMap == null || id == INVALID_GROUP_ID) + { + return; + } + + // srvgroups.name is varchar(120); the generator fetches it into a + // 128-char buffer, so size to match and avoid truncating long names. + char lower[128]; + LowercaseCopy(lower, sizeof(lower), group_name); + g_GroupNameMap.SetValue(lower, id); +} + +// Resolve an admin-group name to its GroupId, tolerating case differences. +// +// SourceMod's admin cache matches group names case-sensitively (both +// FindAdmGroup and CreateAdmGroup key off a case-sensitive map) and exposes no +// native to enumerate loaded groups, so we track the names ourselves as the +// groups config is parsed. The SourceBans++ config files (sb_admin_groups.cfg / +// sb_admins.cfg) are generated from a database where a group's name can drift +// in case between the group definition and an admin's group reference: legacy +// AMXBans / SourceBans imports, manual DB edits, case-sensitive DB collations, +// or an older generator build that copied the admin's raw srv_group string +// instead of the canonical srvgroups.name. A group defined as "admin" but +// referenced as "Admin" would otherwise fail with a spurious "Unknown group" +// error and silently drop the admin's group inheritance. Try the fast +// exact-case lookup first (covers groups created by any plugin), then fall back +// to the case-folded map (covers the SourceBans++ groups this reader tracks) so +// the reference resolves regardless of the stored case (issue #1503). +// +// Freshness invariant: the GroupIds in the map are only valid for the current +// admin-cache generation. This holds because SourceMod rebuilds groups before +// admins and ReadGroups() clears+repopulates the map on every AdminCache_Groups +// rebuild, so the map never outlives the group cache it describes. The map does +// legitimately persist across an admins-only rebuild (SourceMod leaves the group +// cache untouched then, so the stored ids stay valid). +GroupId FindAdmGroupInsensitive(const char[] group_name) +{ + GroupId id = FindAdmGroup(group_name); + if (id != INVALID_GROUP_ID) + { + return id; + } + + if (g_GroupNameMap == null) + { + return INVALID_GROUP_ID; + } + + char lower[128]; + LowercaseCopy(lower, sizeof(lower), group_name); + + int mapped; + if (g_GroupNameMap.GetValue(lower, mapped)) + { + return view_as(mapped); + } + + return INVALID_GROUP_ID; +} diff --git a/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/sbpp_checker.sp b/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/sbpp_checker.sp new file mode 100644 index 0000000..e06781f --- /dev/null +++ b/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/sbpp_checker.sp @@ -0,0 +1,741 @@ +// ************************************************************************* +// This file is part of SourceBans++. +// +// Copyright (C) 2014-2024 SourceBans++ Dev Team +// +// SourceBans++ is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, per version 3 of the License. +// +// SourceBans++ is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with SourceBans++. If not, see . +// +// This file is based off work(s) covered by the following copyright(s): +// +// SourceBans Checker 1.0.2 +// Copyright (C) 2010-2013 Nicholas Hastings +// Licensed under GNU GPL version 3, or later. +// Page: +// +// ************************************************************************* + +#pragma semicolon 1 +#pragma newdecls required + +#include +#include + +#define VERSION SB_VERSION +#define LISTBANS_USAGE "sm_listbans <#userid|name> - Lists a user's prior bans from Sourcebans" +#define LISTCOMMS_USAGE "sm_listcomms <#userid|name> - Lists a user's prior comms from Sourcebans" +#define INVALID_TARGET -1 +#define Prefix "\x04[SourceBans++]\x01 " + +bool g_bLate = false; +bool g_bPrintCheckOnConnect = true; + +char g_DatabasePrefix[10] = "sb"; + +SMCParser g_ConfigParser; +Database g_DB; + +enum DatabaseState /* Database connection state */ +{ + DatabaseState_None = 0, + DatabaseState_Wait, + DatabaseState_Connecting, + DatabaseState_Connected, +} +DatabaseState g_DatabaseState; +int g_iSequence = 0; +int g_iConnectLock = 0; + +int g_iBanCounts[MAXPLAYERS + 1]; +int g_iMuteCounts[MAXPLAYERS + 1]; +int g_iGagCounts[MAXPLAYERS + 1]; + +public Plugin myinfo = +{ + name = "SourceBans++: Bans Checker", + author = "psychonic, Ca$h Munny, SourceBans++ Dev Team", + description = "Notifies admins of prior bans from Sourcebans upon player connect.", + version = VERSION, + url = "https://sbpp.github.io" +}; + +public void OnPluginStart() +{ + LoadTranslations("common.phrases"); + LoadTranslations("sbpp_checker.phrases"); + + CreateConVar("sbchecker_version", VERSION, "", FCVAR_NOTIFY); + RegAdminCmd("sm_listbans", OnListSourceBansCmd, ADMFLAG_GENERIC, LISTBANS_USAGE); + RegAdminCmd("sm_listcomms", OnListSourceCommsCmd, ADMFLAG_GENERIC, LISTCOMMS_USAGE); + RegAdminCmd("sb_reload", OnReloadCmd, ADMFLAG_RCON, "Reload sourcebans config and ban reason menu options"); + + DB_Connect(); + + if (g_bLate) + { + LateLoading(); + } +} + +public void OnMapStart() +{ + ReadConfig(); +} + +public Action OnReloadCmd(int client, int args) +{ + ReadConfig(); + return Plugin_Handled; +} + +stock bool DB_Connect() +{ + if (g_DB) + { + return true; + } + + if (g_DatabaseState == DatabaseState_Wait) + { + return false; + } + + if (g_DatabaseState != DatabaseState_Connecting) + { + g_DatabaseState = DatabaseState_Connecting; + g_iConnectLock = ++g_iSequence; + Database.Connect(OnDatabaseConnected, "sourcebans", g_iConnectLock); + } + + return false; +} + +public void OnDatabaseConnected(Database db, const char[] error, any data) +{ + if (data != g_iConnectLock || g_DB) + { + if (db) + delete db; + return; + } + + g_iConnectLock = 0; + + if (db == null) + { + g_DatabaseState = DatabaseState_None; + SetFailState("Failed to connect to SourceBans DB, %s", error); + } + + g_DatabaseState = DatabaseState_Connected; + g_DB = db; +} + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + RegPluginLibrary("sourcechecker++"); + + CreateNative("SBPP_CheckerGetClientsBans", Native_SBCheckerGetClientsBans); + CreateNative("SBPP_CheckerGetClientsComms", Native_SBCheckerGetClientsComms); + CreateNative("SBPP_CheckerGetClientsMutes", Native_SBCheckerGetClientsMutes); + CreateNative("SBPP_CheckerGetClientsGags", Native_SBCheckerGetClientsGags); + + g_bLate = late; + + return APLRes_Success; +} + +public int Native_SBCheckerGetClientsBans(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + return g_iBanCounts[client]; +} + +public int Native_SBCheckerGetClientsComms(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + return g_iMuteCounts[client] + g_iGagCounts[client]; +} + +public int Native_SBCheckerGetClientsMutes(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + return g_iMuteCounts[client]; +} + +public int Native_SBCheckerGetClientsGags(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + return g_iGagCounts[client]; +} + +public void OnClientAuthorized(int client, const char[] auth) +{ + if (g_DB == null) + return; + + /* Do not check bots nor check player with lan steamid. */ + if (auth[0] == 'B' || auth[9] == 'L') + return; + + char query[512], ip[30]; + GetClientIP(client, ip, sizeof(ip)); + FormatEx(query, sizeof(query), + "SELECT COUNT(bid) FROM %s_bans WHERE (type = 0 AND authid LIKE 'STEAM_%%:%s') \ + UNION ALL \ + SELECT COUNT(bid) FROM %s_bans WHERE (type = 1 AND ip = '%s') \ + UNION ALL \ + SELECT COUNT(bid) FROM %s_comms WHERE authid LIKE 'STEAM_%%:%s' AND type = 1 \ + UNION ALL \ + SELECT COUNT(bid) FROM %s_comms WHERE authid LIKE 'STEAM_%%:%s' AND type = 2", + g_DatabasePrefix, auth[8], + g_DatabasePrefix, ip, + g_DatabasePrefix, auth[8], + g_DatabasePrefix, auth[8] + ); + + g_DB.Query(OnConnectBanCheck, query, GetClientUserId(client), DBPrio_Low); +} + +public void OnConnectBanCheck(Database db, DBResultSet results, const char[] error, any userid) +{ + if (results == null) + { + CheckDBConnection(error); + } + + int client = GetClientOfUserId(userid); + if (!client || results == null || !results.FetchRow()) + return; + + // SteamID bans + g_iBanCounts[client] = results.FetchInt(0); + + // IP bans + if (results.FetchRow()) + g_iBanCounts[client] += results.FetchInt(0); + + // Mutes (type = 1) + if (results.FetchRow()) + g_iMuteCounts[client] = results.FetchInt(0); + + // Gags (type = 2) + if (results.FetchRow()) + g_iGagCounts[client] = results.FetchInt(0); + + int bancount = g_iBanCounts[client]; + int commcount = g_iMuteCounts[client] + g_iGagCounts[client]; + + if (!g_bPrintCheckOnConnect) + return; + + if ( bancount && commcount ) { + PrintToBanAdmins("%s%t", Prefix, "Ban and Comm Warning", client, bancount, ((bancount > 1 || bancount == 0) ? "s":""), commcount, ((commcount > 1 || commcount == 0) ? "s":"")); + } + else if ( commcount ) { + PrintToBanAdmins("%s%t", Prefix, "Comm Warning", client, commcount, ((commcount > 1 || commcount == 0) ? "s":"")); + } + else if ( bancount ) { + PrintToBanAdmins("%s%t", Prefix, "Ban Warning", client, bancount, ((bancount > 1 || bancount == 0) ? "s":"")); + } +} + +public Action OnListSourceBansCmd(int client, int args) +{ + if (args < 1) + { + ReplyToCommand(client, LISTBANS_USAGE); + } + + if (g_DB == INVALID_HANDLE) + { + ReplyToCommand(client, "Error: Database not ready."); + return Plugin_Handled; + } + + char targetarg[64]; + GetCmdArg(1, targetarg, sizeof(targetarg)); + + int target = FindTarget(client, targetarg, true, true); + if (target == INVALID_TARGET) + { + ReplyToCommand(client, "Error: Could not find a target matching '%s'.", targetarg); + return Plugin_Handled; + } + + char auth[32]; + if (!GetClientAuthId(target, AuthId_Steam2, auth, sizeof(auth)) + || auth[0] == 'B' || auth[9] == 'L') + { + ReplyToCommand(client, "Error: Could not retrieve %N's steam id.", target); + return Plugin_Handled; + } + + char query[1024], ip[30]; + GetClientIP(target, ip, sizeof(ip)); + FormatEx(query, sizeof(query), "SELECT created, %s_admins.user, ends, length, reason, RemoveType FROM %s_bans LEFT JOIN %s_admins ON %s_bans.aid = %s_admins.aid WHERE ((type = 0 AND %s_bans.authid REGEXP '^STEAM_[0-9]:%s$') OR (type = 1 AND ip = '%s')) AND ((length > '0' AND ends > UNIX_TIMESTAMP()) OR RemoveType IS NOT NULL)", g_DatabasePrefix, g_DatabasePrefix, g_DatabasePrefix, g_DatabasePrefix, g_DatabasePrefix, g_DatabasePrefix, auth[8], ip); + + char targetName[MAX_NAME_LENGTH]; + GetClientName(target, targetName, sizeof(targetName)); + + DataPack dataPack = new DataPack(); + dataPack.WriteCell((client == 0) ? 0 : GetClientUserId(client)); + dataPack.WriteString(targetName); + + g_DB.Query(OnListBans, query, dataPack, DBPrio_Low); + + if (client == 0) + { + ReplyToCommand(client, "%sNote: if you are using this command through an rcon tool, you will not see results.", Prefix); + } + else + { + ReplyToCommand(client, "\x04%s\x01 Look for %N's ban results in console.", Prefix, target); + } + + return Plugin_Handled; +} + +public void OnListBans(Database db, DBResultSet results, const char[] error, DataPack dataPack) +{ + dataPack.Reset(); + int clientuid = dataPack.ReadCell(); + int client = GetClientOfUserId(clientuid); + char targetName[MAX_NAME_LENGTH]; + dataPack.ReadString(targetName, sizeof(targetName)); + delete dataPack; + + if (clientuid > 0 && client == 0) + return; + + if (results == null) + { + CheckDBConnection(error); + PrintListResponse(clientuid, client, "%sDB error while retrieving bans for %s:\n%s", Prefix, targetName, error); + return; + } + + if (results.RowCount == 0) + { + PrintListResponse(clientuid, client, "%sNo bans found for %s.", Prefix, targetName); + return; + } + + PrintListResponse(clientuid, client, "%sListing bans for %s", Prefix, targetName); + PrintListResponse(clientuid, client, "Ban Date Banned By Length End Date R Reason"); + PrintListResponse(clientuid, client, "-------------------------------------------------------------------------------"); + while (results.FetchRow()) + { + char createddate[11] = " "; + char bannedby[11] = " "; + char lenstring[11] = "N/A "; + char enddate[11] = "N/A "; + char reason[28]; + char RemoveType[2] = " "; + + if (!results.IsFieldNull(0)) + { + FormatTime(createddate, sizeof(createddate), "%Y-%m-%d", results.FetchInt(0)); + } + + if (!results.IsFieldNull(1)) + { + int size_bannedby = sizeof(bannedby); + results.FetchString(1, bannedby, size_bannedby); + int len = results.FetchSize(1); + if (len > size_bannedby - 1) + { + reason[size_bannedby - 4] = '.'; + reason[size_bannedby - 3] = '.'; + reason[size_bannedby - 2] = '.'; + } + else + { + for (int i = len; i < size_bannedby - 1; i++) + { + bannedby[i] = ' '; + } + } + } + + // NOT NULL + int size_lenstring = sizeof(lenstring); + int length = results.FetchInt(3); + if (length == 0) + { + strcopy(lenstring, size_lenstring, "Permanent "); + } + else + { + int len = IntToString(length, lenstring, size_lenstring); + if (len < size_lenstring - 1) + { + // change the '\0' to a ' '. the original \0 at the end will still be there + lenstring[len] = ' '; + } + } + + if (!results.IsFieldNull(2)) + { + FormatTime(enddate, sizeof(enddate), "%Y-%m-%d", results.FetchInt(2)); + } + + // NOT NULL + int reason_size = sizeof(reason); + results.FetchString(4, reason, reason_size); + int len = results.FetchSize(4); + if (len > reason_size - 1) + { + reason[reason_size - 4] = '.'; + reason[reason_size - 3] = '.'; + reason[reason_size - 2] = '.'; + } + else + { + for (int i = len; i < reason_size - 1; i++) + { + reason[i] = ' '; + } + } + + if (!results.IsFieldNull(5)) + { + results.FetchString(5, RemoveType, sizeof(RemoveType)); + } + + PrintListResponse(clientuid, client, "%s %s %s %s %s %s", createddate, bannedby, lenstring, enddate, RemoveType, reason); + } +} + +public Action OnListSourceCommsCmd(int client, int args) +{ + if (args < 1) + { + ReplyToCommand(client, LISTCOMMS_USAGE); + } + + if (g_DB == INVALID_HANDLE) + { + ReplyToCommand(client, "Error: Database not ready."); + return Plugin_Handled; + } + + char targetarg[64]; + GetCmdArg(1, targetarg, sizeof(targetarg)); + + int target = FindTarget(client, targetarg, true, true); + if (target == INVALID_TARGET) + { + ReplyToCommand(client, "Error: Could not find a target matching '%s'.", targetarg); + return Plugin_Handled; + } + + char auth[32]; + if (!GetClientAuthId(target, AuthId_Steam2, auth, sizeof(auth)) + || auth[0] == 'B' || auth[9] == 'L') + { + ReplyToCommand(client, "Error: Could not retrieve %N's steam id.", target); + return Plugin_Handled; + } + + char query[1024]; + FormatEx(query, sizeof(query), "SELECT created, %s_admins.user, ends, length, reason, RemoveType, type FROM %s_comms LEFT JOIN %s_admins ON %s_comms.aid = %s_admins.aid WHERE %s_comms.authid REGEXP '^STEAM_[0-9]:%s$' AND ((length > '0' AND ends > UNIX_TIMESTAMP()) OR RemoveType IS NOT NULL)", g_DatabasePrefix, g_DatabasePrefix, g_DatabasePrefix, g_DatabasePrefix, g_DatabasePrefix, g_DatabasePrefix, auth[8]); + + char targetName[MAX_NAME_LENGTH]; + GetClientName(target, targetName, sizeof(targetName)); + + DataPack dataPack = new DataPack(); + dataPack.WriteCell((client == 0) ? 0 : GetClientUserId(client)); + dataPack.WriteString(targetName); + + g_DB.Query(OnListComms, query, dataPack, DBPrio_Low); + + if (client == 0) + { + ReplyToCommand(client, "%sNote: if you are using this command through an rcon tool, you will not see results.", Prefix); + } + else + { + ReplyToCommand(client, "\x04%s\x01 Look for %N's comm results in console.", Prefix, target); + } + + return Plugin_Handled; +} + +public void OnListComms(Database db, DBResultSet results, const char[] error, DataPack dataPack) +{ + dataPack.Reset(); + int clientuid = dataPack.ReadCell(); + int client = GetClientOfUserId(clientuid); + char targetName[MAX_NAME_LENGTH]; + dataPack.ReadString(targetName, sizeof(targetName)); + delete dataPack; + + if (clientuid > 0 && client == 0) + return; + + if (results == null) + { + CheckDBConnection(error); + PrintListResponse(clientuid, client, "%sDB error while retrieving comms for %s:\n%s", Prefix, targetName, error); + return; + } + + if (results.RowCount == 0) + { + PrintListResponse(clientuid, client, "%sNo comms found for %s.", Prefix, targetName); + return; + } + + PrintListResponse(clientuid, client, "%sListing comms for %s", Prefix, targetName); + PrintListResponse(clientuid, client, "Ban Date Blocked By Length End Date T R Reason"); + PrintListResponse(clientuid, client, "-------------------------------------------------------------------------------"); + while (results.FetchRow()) + { + char createddate[11] = " "; + char bannedby[11] = " "; + char lenstring[11] = "N/A "; + char enddate[11] = "N/A "; + char reason[23]; + char CommType[2] = " "; + char RemoveType[2] = " "; + + if (!results.IsFieldNull(0)) + { + FormatTime(createddate, sizeof(createddate), "%Y-%m-%d", results.FetchInt(0)); + } + + if (!results.IsFieldNull(1)) + { + int size_bannedby = sizeof(bannedby); + results.FetchString(1, bannedby, size_bannedby); + int len = results.FetchSize(1); + if (len > size_bannedby - 1) + { + reason[size_bannedby - 4] = '.'; + reason[size_bannedby - 3] = '.'; + reason[size_bannedby - 2] = '.'; + } + else + { + for (int i = len; i < size_bannedby - 1; i++) + { + bannedby[i] = ' '; + } + } + } + + // NOT NULL + int size_lenstring = sizeof(lenstring); + int length = results.FetchInt(3); + if (length == 0) + { + strcopy(lenstring, size_lenstring, "Permanent "); + } + else + { + int len = IntToString(length, lenstring, size_lenstring); + if (len < size_lenstring - 1) + { + // change the '\0' to a ' '. the original \0 at the end will still be there + lenstring[len] = ' '; + } + } + + if (!results.IsFieldNull(2)) + { + FormatTime(enddate, sizeof(enddate), "%Y-%m-%d", results.FetchInt(2)); + } + + // NOT NULL + int reason_size = sizeof(reason); + results.FetchString(4, reason, reason_size); + int len = results.FetchSize(4); + if (len > reason_size - 1) + { + reason[reason_size - 4] = '.'; + reason[reason_size - 3] = '.'; + reason[reason_size - 2] = '.'; + } + else + { + for (int i = len; i < reason_size - 1; i++) + { + reason[i] = ' '; + } + } + + if (!results.IsFieldNull(5)) + { + results.FetchString(5, RemoveType, sizeof(RemoveType)); + } + // NOT NULL + results.FetchString(6, CommType, sizeof(RemoveType)); + if(StrEqual(CommType,"1")) + strcopy(CommType, sizeof(CommType), "M"); + if(StrEqual(CommType,"2")) + strcopy(CommType, sizeof(CommType), "G"); + + PrintListResponse(clientuid, client, "%s %s %s %s %s %s %s", createddate, bannedby, lenstring, enddate, CommType, RemoveType, reason); + } +} + +void PrintListResponse(int userid, int client, const char[] format, any ...) +{ + char msg[192]; + VFormat(msg, sizeof(msg), format, 4); + + if (userid == 0) + { + PrintToServer("%s", msg); + } + else + { + PrintToConsole(client, "%s", msg); + } +} + +void PrintToBanAdmins(const char[] format, any ...) +{ + char msg[256]; + + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i) && !IsFakeClient(i) && CheckCommandAccess(i, "sm_listsourcebans", ADMFLAG_GENERIC)) + { + SetGlobalTransTarget(i); + VFormat(msg, sizeof(msg), format, 2); + PrintToChat(i, "%s", msg); + } + } +} + +stock void ReadConfig() +{ + InitializeConfigParser(); + + if (g_ConfigParser == null) + { + return; + } + + char ConfigFile[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, ConfigFile, sizeof(ConfigFile), "configs/sourcebans/sourcebans.cfg"); + + if (FileExists(ConfigFile)) + { + InternalReadConfig(ConfigFile); + } + else + { + char Error[PLATFORM_MAX_PATH + 64]; + FormatEx(Error, sizeof(Error), "FATAL *** ERROR *** can not find %s", ConfigFile); + SetFailState(Error); + } +} + +static void InitializeConfigParser() +{ + if (g_ConfigParser == null) + { + g_ConfigParser = new SMCParser(); + g_ConfigParser.OnEnterSection = ReadConfig_NewSection; + g_ConfigParser.OnKeyValue = ReadConfig_KeyValue; + g_ConfigParser.OnLeaveSection = ReadConfig_EndSection; + } +} + +static void InternalReadConfig(const char[] path) +{ + SMCError err = g_ConfigParser.ParseFile(path); + + if (err != SMCError_Okay) + { + char buffer[64]; + PrintToServer("%s", g_ConfigParser.GetErrorString(err, buffer, sizeof(buffer)) ? buffer : "Fatal parse error"); + } +} + +public SMCResult ReadConfig_NewSection(SMCParser smc, const char[] name, bool opt_quotes) +{ + return SMCParse_Continue; +} + +public SMCResult ReadConfig_KeyValue(SMCParser smc, const char[] key, const char[] value, bool key_quotes, bool value_quotes) +{ + if (strcmp("DatabasePrefix", key, false) == 0) + { + strcopy(g_DatabasePrefix, sizeof(g_DatabasePrefix), value); + + if (g_DatabasePrefix[0] == '\0') + { + g_DatabasePrefix = "sb"; + } + } + + if (strcmp("PrintCheckOnConnect", key, false) == 0) + { + if (strcmp("0", value, false) == 0) + { + g_bPrintCheckOnConnect = false; + } + } + + return SMCParse_Continue; +} + +public SMCResult ReadConfig_EndSection(SMCParser smc) +{ + return SMCParse_Continue; +} + +stock void LateLoading() +{ + char sSteam32ID[64]; + for (int i = 1; i <= MaxClients; i++) + { + if (!IsClientConnected(i) || !IsClientInGame(i) || IsFakeClient(i) || !IsClientAuthorized(i)) + continue; + + GetClientAuthId(i, AuthId_Steam2, sSteam32ID, sizeof(sSteam32ID)); + OnClientAuthorized(i, sSteam32ID); + } +} + +public Action Timer_ReconnectDB(Handle timer) +{ + g_DatabaseState = DatabaseState_None; + DB_Connect(); + return Plugin_Continue; +} + +void CheckDBConnection(const char[] error) +{ + if (StrContains(error, "Lost connection", false) != -1 || StrContains(error, "gone away", false) != -1) + { + if (g_DB != null) + { + delete g_DB; + g_DB = null; + } + + if (g_DatabaseState != DatabaseState_Wait) + { + g_DatabaseState = DatabaseState_Wait; + CreateTimer(2.0, Timer_ReconnectDB, _, TIMER_FLAG_NO_MAPCHANGE); + } + } + else + { + LogError("SourceChecker: Database query error: %s", error); + } +} diff --git a/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/sbpp_comms.sp b/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/sbpp_comms.sp new file mode 100644 index 0000000..c239bc8 --- /dev/null +++ b/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/sbpp_comms.sp @@ -0,0 +1,3523 @@ +// ************************************************************************* +// This file is part of SourceBans++. +// +// Copyright (C) 2014-2024 SourceBans++ Dev Team +// +// SourceBans++ is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, per version 3 of the License. +// +// SourceBans++ is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with SourceBans++. If not, see . +// +// This file is based off work(s) covered by the following copyright(s): +// +// SourceComms 0.9.266 +// Copyright (C) 2013-2014 Alexandr Duplishchev +// Licensed under GNU GPL version 3, or later. +// Page: - +// +// ************************************************************************* + +#pragma semicolon 1 +#pragma newdecls required + +#include +#include +#include +#include + +#undef REQUIRE_PLUGIN +#include + +#define UNBLOCK_FLAG ADMFLAG_CHEATS +#define DATABASE "sourcebans" + +// #define DEBUG +// #define LOG_QUERIES + +// Do not edit below this line // +//-----------------------------// + +#define PREFIX "\x04[SourceComms++]\x01 " + +//GLOBAL DEFINES +#define MAX_TIME_MULTI 30 // maximum mass-target punishment length +#define SESSION_MUTE_FALLBACK 120 * 60 // session mute will expire after this if it hasn't already (fallback) + +#define NOW 0 +#define TYPE_TEMP_SHIFT 10 + +#define DISPLAY_SIZE 64 +#define REASON_SIZE 192 +#define MAX_REASONS 32 +#define MAX_TIMES 32 + +enum /* List menu */ +{ + curTarget, + curIndex, + viewingMute, + viewingGag, + viewingList, + PeskyPanels, +}; + +enum DatabaseState /* Database connection state */ +{ + DatabaseState_None = 0, + DatabaseState_Wait, + DatabaseState_Connecting, + DatabaseState_Connected, +} +DatabaseState g_DatabaseState; + +enum State /* ConfigState */ +{ + ConfigStateNone = 0, + ConfigStateConfig, + ConfigStateReasons, + ConfigStateTimes, + ConfigStateServers, +} +State ConfigState; + +/* Cvar handle */ +ConVar CvarHostIp; +ConVar CvarPort; + +/* Database handle */ +Database g_hDatabase; +Database SQLiteDB; + +char + ServerIp[SBPP_SERVER_IP_LENGTH] + , ConfiguredServerIp[SBPP_SERVER_IP_LENGTH] + , ServerPort[7] + , DatabasePrefix[10] = "sb" +#if defined LOG_QUERIES + , logQuery[256] /* Log Stuff */ +#endif + , g_sName[MAXPLAYERS + 1][MAX_NAME_LENGTH] + , g_sSteamIDs[MAXPLAYERS + 1][MAX_AUTHID_LENGTH] + , g_sPlayerIP[MAXPLAYERS + 1][16] + , g_sReasonDisplays[MAX_REASONS][DISPLAY_SIZE] + , g_sReasonKey[MAX_REASONS][REASON_SIZE] + , g_sTimeDisplays[MAX_TIMES][DISPLAY_SIZE]; + +float RetryTime = 15.0; + +bool + g_bLate + , g_bPlayerAuthentified[MAXPLAYERS + 1] // Bots and players with invalid Steam ID format will always be FALSE + , g_bPlayerStatus[MAXPLAYERS + 1] // Player block check status + , g_bPlayerVerified[MAXPLAYERS + 1]; // Player has been verified into the database + +int + iNumReasons + , iNumTimes + , g_iTimeMinutes[MAX_TIMES] + , g_iPeskyPanels[MAXPLAYERS + 1][PeskyPanels] + , g_iConnectLock = 0 + , g_iSequence = 0 + , DefaultTime = 30 + , DisUBImCheck = 0 + , ConsoleImmunity = 0 + , ConfigMaxLength = 0 + , ConfigWhiteListOnly = 0 + , serverID = 0 + , g_iUserIDs[MAXPLAYERS + 1]; + +SMCParser ConfigParser; + +GlobalForward g_hFwd_OnPlayerPunished + , g_hFwd_OnPlayerUnpunished; + +Handle g_hGagExpireTimer[MAXPLAYERS + 1] = { null, ... } + , g_hMuteExpireTimer[MAXPLAYERS + 1] = { null, ... }; + +bType g_MuteType[MAXPLAYERS + 1]; +char + g_sMuteAdminName[MAXPLAYERS + 1][MAX_NAME_LENGTH] + , g_sMuteReason[MAXPLAYERS + 1][256] + , g_sMuteAdminAuth[MAXPLAYERS + 1][64]; +int + g_iMuteTime[MAXPLAYERS + 1] + , g_iMuteLength[MAXPLAYERS + 1] // in sec + , g_iMuteLevel[MAXPLAYERS + 1]; // immunity level of admin + + +bType g_GagType[MAXPLAYERS + 1]; +char + g_sGagAdminName[MAXPLAYERS + 1][MAX_NAME_LENGTH] + , g_sGagReason[MAXPLAYERS + 1][256] + , g_sGagAdminAuth[MAXPLAYERS + 1][64]; +int + g_iGagTime[MAXPLAYERS + 1] + , g_iGagLength[MAXPLAYERS + 1] // in sec + , g_iGagLevel[MAXPLAYERS + 1]; // immunity level of admin + +ArrayList g_hServersWhiteList = null; + +TopMenu hTopMenu = null; + +public Plugin myinfo = +{ + name = "SourceBans++: SourceComms", + author = "Alex, SourceBans++ Dev Team", + description = "Advanced punishments management for the Source engine in SourceBans style", + version = SBPPComms_VERSION, + url = "https://sbpp.github.io" +}; + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + g_bLate = late; + + CreateNative("SourceComms_SetClientMute", Native_SetClientMute); + CreateNative("SourceComms_SetClientGag", Native_SetClientGag); + CreateNative("SourceComms_GetClientMuteType", Native_GetClientMuteType); + CreateNative("SourceComms_GetClientGagType", Native_GetClientGagType); + + g_hFwd_OnPlayerPunished = CreateGlobalForward("SourceComms_OnBlockAdded", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_String); + g_hFwd_OnPlayerUnpunished = CreateGlobalForward("SourceComms_OnBlockRemoved", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_String); + + RegPluginLibrary("sourcecomms++"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("common.phrases"); + LoadTranslations("sbpp_comms.phrases"); + + TopMenu hTemp = null; + if (LibraryExists("adminmenu") && ((hTemp = GetAdminTopMenu()) != INVALID_HANDLE)) + OnAdminMenuReady(hTemp); + + CvarHostIp = FindConVar("hostip"); + CvarPort = FindConVar("hostport"); + g_hServersWhiteList = new ArrayList(); + + CreateConVar("sourcecomms_version", SBPPComms_VERSION, _, FCVAR_SPONLY | FCVAR_REPLICATED | FCVAR_NOTIFY); + AddCommandListener(CommandCallback, "sm_gag"); + AddCommandListener(CommandCallback, "sm_mute"); + AddCommandListener(CommandCallback, "sm_silence"); + AddCommandListener(CommandCallback, "sm_ungag"); + AddCommandListener(CommandCallback, "sm_unmute"); + AddCommandListener(CommandCallback, "sm_unsilence"); + RegServerCmd("sc_fw_block", FWBlock, "Blocking player comms by command from sourceban web site"); + RegServerCmd("sc_fw_ungag", FWUngag, "Ungagging player by command from sourceban web site"); + RegServerCmd("sc_fw_unmute", FWUnmute, "Unmuting player by command from sourceban web site"); + RegConsoleCmd("sm_comms", CommandComms, "Shows player communications status"); + + HookEvent("player_changename", Event_OnPlayerName, EventHookMode_Post); + + #if defined LOG_QUERIES + BuildPath(Path_SM, logQuery, sizeof(logQuery), "logs/sourcecomms-q.log"); + #endif + + #if defined DEBUG + PrintToServer("Sourcecomms plugin loading. Version %s", SBPPComms_VERSION); + #endif + + // Catch config error + if (!SQL_CheckConfig(DATABASE)) + { + SetFailState("Database failure: could not find database config: %s", DATABASE); + return; + } + ReadConfig(); + DB_Connect(); + InitializeBackupDB(); + + if (g_bLate) + { + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client)) + OnClientConnected(client); + } + } + + g_bLate = false; +} + +public void OnLibraryRemoved(const char[] name) +{ + if (strcmp(name, "adminmenu", false) == 0) + hTopMenu = null; +} + +public void OnConfigsExecuted() +{ + ReadConfig(); +} + +public void OnMapStart() +{ + ReadConfig(); +} + +public void OnMapEnd() +{ + // Clean up on map end just so we can start a fresh connection when we need it later. + // Also it is necessary for using SQL_SetCharset + if (g_hDatabase) + delete g_hDatabase; + + g_hDatabase = null; +} + + +// CLIENT CONNECTION FUNCTIONS // +public void OnClientDisconnect(int client) +{ + CloseMuteExpireTimer(client); + CloseGagExpireTimer(client); + + g_sSteamIDs[client][0] = '\0'; + g_sPlayerIP[client][0] = '\0'; + g_sName[client][0] = '\0'; + g_iUserIDs[client] = -1; +} + +public bool OnClientConnect(int client, char[] rejectmsg, int maxlen) +{ + /* Passing false as the backend validation status return the unverified Steam ID */ + /* Prevent players with active punishements to avoid their comms block. */ + /* They force invalid status response from the Steam validation */ + char sSteamID[MAX_AUTHID_LENGTH]; + GetClientAuthId(client, AuthId_Steam2, sSteamID, sizeof(sSteamID), false); + FormatEx(g_sSteamIDs[client], sizeof(g_sSteamIDs[]), "%s", sSteamID); + + char sIP[16]; + GetClientIP(client, sIP, sizeof(sIP)); + FormatEx(g_sPlayerIP[client], sizeof(g_sPlayerIP[]), "%s", sIP); + FormatEx(g_sName[client], sizeof(g_sName[]), "%N", client); + g_iUserIDs[client] = GetClientUserId(client); + g_bPlayerVerified[client] = false; + + if (IsInvalidSteamID(client)) + g_bPlayerAuthentified[client] = false; + else + g_bPlayerAuthentified[client] = true; + + return true; +} + +public void OnClientConnected(int client) +{ + if (!g_bPlayerAuthentified[client]) + return; + + MarkClientAsUnMuted(client); + MarkClientAsUnGagged(client); +} + +public void OnClientPostAdminCheck(int client) +{ + if (!g_bPlayerAuthentified[client]) + return; + + /* Player is not ingame yet, recheck is needed */ + if (!IsClientInGame(client)) + ClientRecheck(client); + else + VerifyBlock(client); +} + +public void VerifyBlock(int client) +{ + /* Can't connect to DB */ + if (!DB_Connect()) + { + #if defined DEBUG + LogMessage("Can't connect to the DB, %s block check status will be true.", g_sName[client]); + #endif + g_bPlayerStatus[client] = true; + return; + } + + // if plugin was late loaded + if (g_bLate && client > 0 && IsClientInGame(client)) + { + if (BaseComm_IsClientMuted(client)) + MarkClientAsMuted(client); + + if (BaseComm_IsClientGagged(client)) + MarkClientAsGagged(client); + } + + char sClAuthYZEscaped[sizeof(g_sSteamIDs[]) * 2 + 1]; + g_hDatabase.Escape(g_sSteamIDs[client][8], sClAuthYZEscaped, sizeof(sClAuthYZEscaped)); + + char Query[4096]; + FormatEx(Query, sizeof(Query), + "SELECT (c.ends - UNIX_TIMESTAMP()) AS remaining, \ + c.length, c.type, c.created, c.reason, a.user, \ + IF (a.immunity>=g.immunity, a.immunity, IFNULL(g.immunity,0)) AS immunity, \ + c.aid, c.sid, a.authid \ + FROM %s_comms AS c \ + LEFT JOIN %s_admins AS a ON a.aid = c.aid \ + LEFT JOIN %s_srvgroups AS g ON g.name = a.srv_group \ + WHERE RemoveType IS NULL \ + AND c.authid REGEXP '^STEAM_[0-9]:%s$' \ + AND (length = '0' OR ends > UNIX_TIMESTAMP())", + DatabasePrefix, DatabasePrefix, DatabasePrefix, sClAuthYZEscaped); + #if defined LOG_QUERIES + LogToFile(logQuery, "VerifyBlock for: %s. QUERY: %s", g_sSteamIDs[client], Query); + #endif + g_hDatabase.Query(Query_VerifyBlock, Query, g_iUserIDs[client], DBPrio_High); +} + +// OTHER CLIENT CODE // + +public Action Event_OnPlayerName(Handle event, const char[] name, bool dontBroadcast) +{ + int client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client > 0 && IsClientInGame(client)) + GetEventString(event, "newname", g_sName[client], sizeof(g_sName[])); + return Plugin_Continue; +} + +public void BaseComm_OnClientMute(int client, bool muteState) +{ + if (client > 0 && client <= MaxClients) + { + if (muteState) + { + if (g_MuteType[client] == bNot) + { + MarkClientAsMuted(client, _, _, _, _, _, "Muted through BaseComm natives"); + SavePunishment(_, client, TYPE_MUTE, _, "Muted through BaseComm natives"); + } + } + else + { + if (g_MuteType[client] > bNot) + { + MarkClientAsUnMuted(client); + } + } + } +} + +public void BaseComm_OnClientGag(int client, bool gagState) +{ + if (client > 0 && client <= MaxClients) + { + if (gagState) + { + if (g_GagType[client] == bNot) + { + MarkClientAsGagged(client, _, _, _, _, _, "Gagged through BaseComm natives"); + SavePunishment(_, client, TYPE_GAG, _, "Gagged through BaseComm natives"); + } + } + else + { + if (g_GagType[client] > bNot) + { + MarkClientAsUnGagged(client); + } + } + } +} + +// COMMAND CODE // + +public Action CommandComms(int client, int args) +{ + if (!client) + { + ReplyToCommand(client, "%s%t", PREFIX, "CommandComms_na"); + return Plugin_Continue; + } + + if (args == 0) + { + if (g_MuteType[client] > bNot || g_GagType[client] > bNot) + AdminMenu_ListTarget(client, client, 0); + else + ReplyToCommand(client, "%s%t", PREFIX, "CommandComms_nb"); + } + + if (args == 1) + { + char sArg[MAX_NAME_LENGTH]; + GetCmdArg(1, sArg, sizeof(sArg)); + int target = FindTarget(client, sArg, false, true); + + if (target == -1) + return Plugin_Handled; + else + { + if (!IsClientInGame(target)) + { + ReplyToCommand(client, "%s%t", PREFIX, "Target is not in game"); + return Plugin_Handled; + } + + if (!CanUserTarget(client, target)) + { + PrintToChat(client, "%s%t", PREFIX, "Unable to target"); + return Plugin_Handled; + } + + if (g_MuteType[target] > bNot || g_GagType[target] > bNot) + AdminMenu_ListTarget(client, target, 0); + else + ReplyToCommand(client, "%s%t", PREFIX, "CommandComms_nc", target); + } + } + + if (args > 1) + { + ReplyToCommand(client, "%sUsage: sm_comms <#userid|name>", PREFIX); + return Plugin_Handled; + } + + return Plugin_Handled; +} + +public Action FWBlock(int args) +{ + char arg_string[256]; + char sArg[3][64]; + GetCmdArgString(arg_string, sizeof(arg_string)); + + int type, length; + if (ExplodeString(arg_string, " ", sArg, 3, 64) != 3 || !StringToIntEx(sArg[0], type) || type < 1 || type > 3 || !StringToIntEx(sArg[1], length)) + { + LogError("Wrong usage of sc_fw_block"); + return Plugin_Stop; + } + + LogMessage("Received block command from web: steam %s, type %d, length %d", sArg[2], type, length); + + char clientAuth[MAX_AUTHID_LENGTH]; + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i) && !IsFakeClient(i)) + { + // https://github.com/sbpp/sourcebans-pp/issues/913 + // Block from Web are received with [U:0-1:xxxxx] format + // Passing Auth as false allow us to remove IsClientAuthorized + GetClientAuthId(i, AuthId_Steam3, clientAuth, sizeof(clientAuth), false); + if (strcmp(clientAuth, sArg[2], false) == 0) + { + #if defined DEBUG + PrintToServer("Catched %s (%s) for blocking from web", clientAuth, g_sSteamIDs[i]); + #endif + + switch (type) + { + case TYPE_MUTE: + { + setMute(i, length, g_sSteamIDs[i]); + Forward_Block(0, i, TYPE_MUTE, length, "Blocked from web"); + } + case TYPE_GAG: + { + setGag(i, length, g_sSteamIDs[i]); + Forward_Block(0, i, TYPE_GAG, length, "Blocked from web"); + } + case TYPE_SILENCE: + { + setMute(i, length, g_sSteamIDs[i]); + setGag(i, length, g_sSteamIDs[i]); + Forward_Block(0, i, TYPE_SILENCE, length, "Blocked from web"); + } + } + break; + } + } + } + + return Plugin_Handled; +} + +public Action FWUngag(int args) +{ + char arg_string[256]; + char sArg[1][64]; + GetCmdArgString(arg_string, sizeof(arg_string)); + if (!ExplodeString(arg_string, " ", sArg, 1, 64)) + { + LogError("Wrong usage of sc_fw_ungag"); + return Plugin_Stop; + } + + LogMessage("Received ungag command from web: steam %s", sArg[0]); + + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i) && !IsFakeClient(i)) + { + if (strcmp(g_sSteamIDs[i], sArg[0], false) == 0) + { + #if defined DEBUG + PrintToServer("Catched %s for ungagging from web", g_sSteamIDs[i]); + #endif + + if (g_GagType[i] > bNot) + { + PerformUnGag(i); + PrintToChat(i, "%s%t", PREFIX, "FWUngag"); + LogMessage("%s is ungagged from web", g_sSteamIDs[i]); + Forward_UnBlock(0, i, TYPE_UNGAG, "Unblocked from web"); + } + else + LogError("Can't ungag %s from web, it isn't gagged", g_sSteamIDs[i]); + break; + } + } + } + return Plugin_Handled; +} + +public Action FWUnmute(int args) +{ + char arg_string[256]; + char sArg[1][64]; + GetCmdArgString(arg_string, sizeof(arg_string)); + if (!ExplodeString(arg_string, " ", sArg, 1, 64)) + { + LogError("Wrong usage of sc_fw_ungag"); + return Plugin_Stop; + } + + LogMessage("Received unmute command from web: steam %s", sArg[0]); + + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i) && !IsFakeClient(i)) + { + if (strcmp(g_sSteamIDs[i], sArg[0], false) == 0) + { + #if defined DEBUG + PrintToServer("Catched %s for unmuting from web", g_sSteamIDs[i]); + #endif + + if (g_MuteType[i] > bNot) + { + PerformUnMute(i); + PrintToChat(i, "%s%t", PREFIX, "FWUnmute"); + LogMessage("%s is unmuted from web", g_sSteamIDs[i]); + Forward_UnBlock(0, i, TYPE_UNMUTE, "Unblocked from web"); + } + else + LogError("Can't unmute %s from web, it isn't muted", g_sSteamIDs[i]); + break; + } + } + } + return Plugin_Handled; +} + + +public Action CommandCallback(int client, const char[] command, int args) +{ + if (client && !CheckCommandAccess(client, command, ADMFLAG_CHAT)) + return Plugin_Continue; + + int type; + if (strcmp(command, "sm_gag", false) == 0) + type = TYPE_GAG; + else if (strcmp(command, "sm_mute", false) == 0) + type = TYPE_MUTE; + else if (strcmp(command, "sm_ungag", false) == 0) + type = TYPE_UNGAG; + else if (strcmp(command, "sm_unmute", false) == 0) + type = TYPE_UNMUTE; + else if (strcmp(command, "sm_silence", false) == 0) + type = TYPE_SILENCE; + else if (strcmp(command, "sm_unsilence", false) == 0) + type = TYPE_UNSILENCE; + else + return Plugin_Stop; + + if ( !args && client ) + { + AdminMenu_Target( client, type ); + return Plugin_Stop; + } + else if ( args < 1 ) + { + ReplyToCommand( client, "%sUsage: %s <#userid|name> %s", PREFIX, command, type > TYPE_SILENCE ? "[reason]" : "[time|0] [reason]" ); + return Plugin_Stop; + } + + char sBuffer[256]; + GetCmdArgString(sBuffer, sizeof(sBuffer)); + + if (type <= TYPE_SILENCE) + CreateBlock(client, _, _, type, _, sBuffer); + else + ProcessUnBlock(client, _, type, sBuffer); + + return Plugin_Stop; +} + + +// MENU CODE // + +public void OnAdminMenuReady(Handle hTemp) +{ + TopMenu topmenu = view_as(hTemp); + + /* Block us from being called twice */ + if (topmenu == hTopMenu) + return; + + /* Save the Handle */ + hTopMenu = topmenu; + + TopMenuObject MenuObject = hTopMenu.AddCategory("sourcecomm_cmds", Handle_Commands); + + if (MenuObject == INVALID_TOPMENUOBJECT) + return; + + hTopMenu.AddItem("sourcecomm_gag", Handle_MenuGag, MenuObject, "sm_gag", ADMFLAG_CHAT); + hTopMenu.AddItem("sourcecomm_ungag", Handle_MenuUnGag, MenuObject, "sm_ungag", ADMFLAG_CHAT); + hTopMenu.AddItem("sourcecomm_mute", Handle_MenuMute, MenuObject, "sm_mute", ADMFLAG_CHAT); + hTopMenu.AddItem("sourcecomm_unmute", Handle_MenuUnMute, MenuObject, "sm_unmute", ADMFLAG_CHAT); + hTopMenu.AddItem("sourcecomm_silence", Handle_MenuSilence, MenuObject, "sm_silence", ADMFLAG_CHAT); + hTopMenu.AddItem("sourcecomm_unsilence", Handle_MenuUnSilence, MenuObject, "sm_unsilence", ADMFLAG_CHAT); + hTopMenu.AddItem("sourcecomm_list", Handle_MenuList, MenuObject, "sm_commlist", ADMFLAG_CHAT); +} + +public int Handle_Commands(TopMenu menu, TopMenuAction action, TopMenuObject object_id, int param1, char[] buffer, int maxlength) +{ + switch (action) + { + case TopMenuAction_DisplayOption: + Format(buffer, maxlength, "%T", "AdminMenu_Main", param1); + case TopMenuAction_DisplayTitle: + Format(buffer, maxlength, "%T", "AdminMenu_Select_Main", param1); + } + return 0; +} + +public int Handle_MenuGag(TopMenu menu, TopMenuAction action, TopMenuObject object_id, int param1, char[] buffer, int maxlength) +{ + switch (action) + { + case TopMenuAction_DisplayOption: + Format(buffer, maxlength, "%T", "AdminMenu_Gag", param1); + case TopMenuAction_SelectOption: + AdminMenu_Target(param1, TYPE_GAG); + } + return 0; +} + +public int Handle_MenuUnGag(TopMenu menu, TopMenuAction action, TopMenuObject object_id, int param1, char[] buffer, int maxlength) +{ + switch (action) + { + case TopMenuAction_DisplayOption: + Format(buffer, maxlength, "%T", "AdminMenu_UnGag", param1); + case TopMenuAction_SelectOption: + AdminMenu_Target(param1, TYPE_UNGAG); + } + return 0; +} + +public int Handle_MenuMute(TopMenu menu, TopMenuAction action, TopMenuObject object_id, int param1, char[] buffer, int maxlength) +{ + switch (action) + { + case TopMenuAction_DisplayOption: + Format(buffer, maxlength, "%T", "AdminMenu_Mute", param1); + case TopMenuAction_SelectOption: + AdminMenu_Target(param1, TYPE_MUTE); + } + return 0; +} + +public int Handle_MenuUnMute(TopMenu menu, TopMenuAction action, TopMenuObject object_id, int param1, char[] buffer, int maxlength) +{ + switch (action) + { + case TopMenuAction_DisplayOption: + Format(buffer, maxlength, "%T", "AdminMenu_UnMute", param1); + case TopMenuAction_SelectOption: + AdminMenu_Target(param1, TYPE_UNMUTE); + } + return 0; +} + +public int Handle_MenuSilence(TopMenu menu, TopMenuAction action, TopMenuObject object_id, int param1, char[] buffer, int maxlength) +{ + switch (action) + { + case TopMenuAction_DisplayOption: + Format(buffer, maxlength, "%T", "AdminMenu_Silence", param1); + case TopMenuAction_SelectOption: + AdminMenu_Target(param1, TYPE_SILENCE); + } + return 0; +} + +public int Handle_MenuUnSilence(TopMenu menu, TopMenuAction action, TopMenuObject object_id, int param1, char[] buffer, int maxlength) +{ + switch (action) + { + case TopMenuAction_DisplayOption: + Format(buffer, maxlength, "%T", "AdminMenu_UnSilence", param1); + case TopMenuAction_SelectOption: + AdminMenu_Target(param1, TYPE_UNSILENCE); + } + return 0; +} + +public int Handle_MenuList(TopMenu menu, TopMenuAction action, TopMenuObject object_id, int param1, char[] buffer, int maxlength) +{ + switch (action) + { + case TopMenuAction_DisplayOption: + Format(buffer, maxlength, "%T", "AdminMenu_List", param1); + case TopMenuAction_SelectOption: + { + g_iPeskyPanels[param1][viewingList] = false; + AdminMenu_List(param1, 0); + } + } + return 0; +} + +void AdminMenu_Target(int client, int type) +{ + char Title[192], Option[32]; + switch (type) + { + case TYPE_GAG: + Format(Title, sizeof(Title), "%T", "AdminMenu_Select_Gag", client); + case TYPE_MUTE: + Format(Title, sizeof(Title), "%T", "AdminMenu_Select_Mute", client); + case TYPE_SILENCE: + Format(Title, sizeof(Title), "%T", "AdminMenu_Select_Silence", client); + case TYPE_UNGAG: + Format(Title, sizeof(Title), "%T", "AdminMenu_Select_Ungag", client); + case TYPE_UNMUTE: + Format(Title, sizeof(Title), "%T", "AdminMenu_Select_Unmute", client); + case TYPE_UNSILENCE: + Format(Title, sizeof(Title), "%T", "AdminMenu_Select_Unsilence", client); + } + + Menu hMenu = new Menu(MenuHandler_MenuTarget); // Common menu - players list. Almost full for blocking, and almost empty for unblocking + hMenu.SetTitle(Title); + hMenu.ExitBackButton = true; + + int iClients; + if (type <= 3) // Mute, gag, silence + { + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i) && !IsFakeClient(i)) + { + switch (type) + { + case TYPE_MUTE: + if (g_MuteType[i] > bNot) + continue; + case TYPE_GAG: + if (g_GagType[i] > bNot) + continue; + case TYPE_SILENCE: + if (g_MuteType[i] > bNot || g_GagType[i] > bNot) + continue; + } + iClients++; + strcopy(Title, sizeof(Title), g_sName[i]); + AdminMenu_GetPunishPhrase(client, i, Title, sizeof(Title)); + Format(Option, sizeof(Option), "%d %d", g_iUserIDs[i], type); + hMenu.AddItem(Option, Title, (CanUserTarget(client, i) ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED)); + } + } + } + else // UnMute, ungag, unsilence + { + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i) && !IsFakeClient(i)) + { + switch (type) + { + case TYPE_UNMUTE: + { + if (g_MuteType[i] > bNot) + { + iClients++; + strcopy(Title, sizeof(Title), g_sName[i]); + Format(Option, sizeof(Option), "%d %d", g_iUserIDs[i], type); + hMenu.AddItem(Option, Title, (CanUserTarget(client, i) ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED)); + } + } + case TYPE_UNGAG: + { + if (g_GagType[i] > bNot) + { + iClients++; + strcopy(Title, sizeof(Title), g_sName[i]); + Format(Option, sizeof(Option), "%d %d", g_iUserIDs[i], type); + hMenu.AddItem(Option, Title, (CanUserTarget(client, i) ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED)); + } + } + case TYPE_UNSILENCE: + { + if (g_MuteType[i] > bNot && g_GagType[i] > bNot) + { + iClients++; + strcopy(Title, sizeof(Title), g_sName[i]); + Format(Option, sizeof(Option), "%d %d", g_iUserIDs[i], type); + hMenu.AddItem(Option, Title, (CanUserTarget(client, i) ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED)); + } + } + } + } + } + } + if (!iClients) + { + switch (type) + { + case TYPE_UNMUTE: + Format(Title, sizeof(Title), "%T", "AdminMenu_Option_Mute_Empty", client); + case TYPE_UNGAG: + Format(Title, sizeof(Title), "%T", "AdminMenu_Option_Gag_Empty", client); + case TYPE_UNSILENCE: + Format(Title, sizeof(Title), "%T", "AdminMenu_Option_Silence_Empty", client); + default: + Format(Title, sizeof(Title), "%T", "AdminMenu_Option_Empty", client); + } + AddMenuItem(hMenu, "0", Title, ITEMDRAW_DISABLED); + } + + hMenu.Display(client, MENU_TIME_FOREVER); +} + +public int MenuHandler_MenuTarget(Menu menu, MenuAction action, int param1, int param2) +{ + switch (action) + { + case MenuAction_End: + delete menu; + case MenuAction_Cancel: + { + if (param2 == MenuCancel_ExitBack && hTopMenu != null) + hTopMenu.Display(param1, TopMenuPosition_LastCategory); + } + case MenuAction_Select: + { + char Option[32], Temp[2][8]; + menu.GetItem(param2, Option, sizeof(Option)); + ExplodeString(Option, " ", Temp, 2, 8); + int target = GetClientOfUserId(StringToInt(Temp[0])); + + if (Bool_ValidMenuTarget(param1, target)) + { + int type = StringToInt(Temp[1]); + if (type <= TYPE_SILENCE) + AdminMenu_Duration(param1, target, type); + else + ProcessUnBlock(param1, target, type); + } + } + } + return 0; +} + +void AdminMenu_Duration(int client, int target, int type) +{ + Menu hMenu = new Menu(MenuHandler_MenuDuration); + char sBuffer[192], sTemp[64]; + Format(sBuffer, sizeof(sBuffer), "%T", "AdminMenu_Title_Durations", client); + hMenu.SetTitle(sBuffer); + hMenu.ExitBackButton = true; + + for (int i = 0; i <= iNumTimes; i++) + { + if (IsAllowedBlockLength(client, g_iTimeMinutes[i])) + { + Format(sTemp, sizeof(sTemp), "%d %d %d", g_iUserIDs[target], type, i); // TargetID TYPE_BLOCK index_of_Time + hMenu.AddItem(sTemp, g_sTimeDisplays[i]); + } + } + + hMenu.Display(client, MENU_TIME_FOREVER); +} + +public int MenuHandler_MenuDuration(Menu menu, MenuAction action, int param1, int param2) +{ + switch (action) + { + case MenuAction_End: + delete menu; + case MenuAction_Cancel: + { + if (param2 == MenuCancel_ExitBack && hTopMenu != INVALID_HANDLE) + hTopMenu.Display(param1, TopMenuPosition_LastCategory); + } + case MenuAction_Select: + { + char sOption[32], sTemp[3][8]; + menu.GetItem(param2, sOption, sizeof(sOption)); + ExplodeString(sOption, " ", sTemp, 3, 8); + // TargetID TYPE_BLOCK index_of_Time + int target = GetClientOfUserId(StringToInt(sTemp[0])); + + if (Bool_ValidMenuTarget(param1, target)) + { + int type = StringToInt(sTemp[1]); + int lengthIndex = StringToInt(sTemp[2]); + + if (iNumReasons) // we have reasons to show + AdminMenu_Reason(param1, target, type, lengthIndex); + else + CreateBlock(param1, target, g_iTimeMinutes[lengthIndex], type); + } + } + } + return 0; +} + +void AdminMenu_Reason(int client, int target, int type, int lengthIndex) +{ + Menu hMenu = new Menu(MenuHandler_MenuReason); + char sBuffer[192], sTemp[64]; + Format(sBuffer, sizeof(sBuffer), "%T", "AdminMenu_Title_Reasons", client); + hMenu.SetTitle(sBuffer); + hMenu.ExitBackButton = true; + + for (int i = 0; i <= iNumReasons; i++) + { + Format(sTemp, sizeof(sTemp), "%d %d %d %d", g_iUserIDs[target], type, i, lengthIndex); // TargetID TYPE_BLOCK ReasonIndex LenghtIndex + hMenu.AddItem(sTemp, g_sReasonDisplays[i]); + } + + hMenu.Display(client, MENU_TIME_FOREVER); +} + +public int MenuHandler_MenuReason(Menu menu, MenuAction action, int param1, int param2) +{ + switch (action) + { + case MenuAction_End: + delete menu; + case MenuAction_Cancel: + { + if (param2 == MenuCancel_ExitBack && hTopMenu != INVALID_HANDLE) + hTopMenu.Display(param1, TopMenuPosition_LastCategory); + } + case MenuAction_Select: + { + char sOption[64], sTemp[4][8]; + menu.GetItem(param2, sOption, sizeof(sOption)); + ExplodeString(sOption, " ", sTemp, 4, 8); + // TargetID TYPE_BLOCK ReasonIndex LenghtIndex + int target = GetClientOfUserId(StringToInt(sTemp[0])); + + if (Bool_ValidMenuTarget(param1, target)) + { + int type = StringToInt(sTemp[1]); + int reasonIndex = StringToInt(sTemp[2]); + int lengthIndex = StringToInt(sTemp[3]); + int length; + if (lengthIndex >= 0 && lengthIndex <= iNumTimes) + length = g_iTimeMinutes[lengthIndex]; + else + { + length = DefaultTime; + LogError("Wrong length index in menu - using default time"); + } + + CreateBlock(param1, target, length, type, g_sReasonKey[reasonIndex]); + } + } + } + return 0; +} + +void AdminMenu_List(int client, int index) +{ + char sTitle[192], sOption[32]; + Format(sTitle, sizeof(sTitle), "%T", "AdminMenu_Select_List", client); + int iClients; + + Menu hMenu = new Menu(MenuHandler_MenuList); + hMenu.SetTitle(sTitle); + + if (!g_iPeskyPanels[client][viewingList]) + hMenu.ExitBackButton = true; + + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i) && !IsFakeClient(i) && (g_MuteType[i] > bNot || g_GagType[i] > bNot)) + { + iClients++; + strcopy(sTitle, sizeof(sTitle), g_sName[i]); + AdminMenu_GetPunishPhrase(client, i, sTitle, sizeof(sTitle)); + Format(sOption, sizeof(sOption), "%d", g_iUserIDs[i]); + hMenu.AddItem(sOption, sTitle); + } + } + + if (!iClients) + { + Format(sTitle, sizeof(sTitle), "%T", "ListMenu_Option_Empty", client); + hMenu.AddItem("0", sTitle, ITEMDRAW_DISABLED); + } + + hMenu.DisplayAt(client, index, MENU_TIME_FOREVER); +} + +public int MenuHandler_MenuList(Menu menu, MenuAction action, int param1, int param2) +{ + switch (action) + { + case MenuAction_End: + delete menu; + case MenuAction_Cancel: + { + if (!g_iPeskyPanels[param1][viewingList] && param2 == MenuCancel_ExitBack && hTopMenu != INVALID_HANDLE) + hTopMenu.Display(param1, TopMenuPosition_LastCategory); + } + case MenuAction_Select: + { + char sOption[32]; + menu.GetItem(param2, sOption, sizeof(sOption)); + int target = GetClientOfUserId(StringToInt(sOption)); + + if (Bool_ValidMenuTarget(param1, target)) + AdminMenu_ListTarget(param1, target, GetMenuSelectionPosition()); + else + AdminMenu_List(param1, GetMenuSelectionPosition()); + } + } + return 0; +} + +void AdminMenu_ListTarget(int client, int target, int index, int viewMute = 0, int viewGag = 0) +{ + int userid = g_iUserIDs[target]; + Menu hMenu = CreateMenu(MenuHandler_MenuListTarget); + + char sBuffer[192], sOption[32]; + + hMenu.SetTitle(g_sName[target]); + hMenu.Pagination = MENU_NO_PAGINATION; + hMenu.ExitButton = true; + hMenu.ExitBackButton = false; + + if (g_MuteType[target] > bNot) + { + Format(sBuffer, sizeof(sBuffer), "%T", "ListMenu_Option_Mute", client); + Format(sOption, sizeof(sOption), "0 %d %d %b %b", userid, index, viewMute, viewGag); + hMenu.AddItem(sOption, sBuffer); + + if (viewMute) + { + Format(sBuffer, sizeof(sBuffer), "%T", "ListMenu_Option_Admin", client, g_sMuteAdminName[target]); + hMenu.AddItem("", sBuffer, ITEMDRAW_DISABLED); + + char sMuteTemp[192], _sMuteTime[192]; + Format(sMuteTemp, sizeof(sMuteTemp), "%T", "ListMenu_Option_Duration", client); + switch (g_MuteType[target]) + { + case bPerm: + Format(sBuffer, sizeof(sBuffer), "%s%T", sMuteTemp, "ListMenu_Option_Duration_Perm", client); + case bTime: + Format(sBuffer, sizeof(sBuffer), "%s%T", sMuteTemp, "ListMenu_Option_Duration_Time", client, g_iMuteLength[target]); + case bSess: + Format(sBuffer, sizeof(sBuffer), "%s%T", sMuteTemp, "ListMenu_Option_Duration_Temp", client); + default: + Format(sBuffer, sizeof(sBuffer), "error"); + } + hMenu.AddItem("", sBuffer, ITEMDRAW_DISABLED); + + FormatTime(_sMuteTime, sizeof(_sMuteTime), NULL_STRING, g_iMuteTime[target]); + Format(sBuffer, sizeof(sBuffer), "%T", "ListMenu_Option_Issue", client, _sMuteTime); + hMenu.AddItem("", sBuffer, ITEMDRAW_DISABLED); + + Format(sMuteTemp, sizeof(sMuteTemp), "%T", "ListMenu_Option_Expire", client); + switch (g_MuteType[target]) + { + case bTime: + { + FormatTime(_sMuteTime, sizeof(_sMuteTime), NULL_STRING, (g_iMuteTime[target] + g_iMuteLength[target] * 60)); + Format(sBuffer, sizeof(sBuffer), "%s%T", sMuteTemp, "ListMenu_Option_Expire_Time", client, _sMuteTime); + } + case bPerm: + Format(sBuffer, sizeof(sBuffer), "%s%T", sMuteTemp, "ListMenu_Option_Expire_Perm", client); + case bSess: + Format(sBuffer, sizeof(sBuffer), "%s%T", sMuteTemp, "ListMenu_Option_Expire_Temp_Reconnect", client); + default: + Format(sBuffer, sizeof(sBuffer), "error"); + } + + hMenu.AddItem("", sBuffer, ITEMDRAW_DISABLED); + + if (strlen(g_sMuteReason[target]) > 0) + { + Format(sBuffer, sizeof(sBuffer), "%T", "ListMenu_Option_Reason", client); + Format(sOption, sizeof(sOption), "1 %d %d %b %b", userid, index, viewMute, viewGag); + hMenu.AddItem(sOption, sBuffer); + } + else + { + Format(sBuffer, sizeof(sBuffer), "%T", "ListMenu_Option_Reason_None", client); + hMenu.AddItem("", sBuffer, ITEMDRAW_DISABLED); + } + } + } + + if (g_GagType[target] > bNot) + { + Format(sBuffer, sizeof(sBuffer), "%T", "ListMenu_Option_Gag", client); + Format(sOption, sizeof(sOption), "2 %d %d %b %b", userid, index, viewMute, viewGag); + hMenu.AddItem(sOption, sBuffer); + + if (viewGag) + { + Format(sBuffer, sizeof(sBuffer), "%T", "ListMenu_Option_Admin", client, g_sGagAdminName[target]); + hMenu.AddItem("", sBuffer, ITEMDRAW_DISABLED); + + char sGagTemp[192], _sGagTime[192]; + Format(sGagTemp, sizeof(sGagTemp), "%T", "ListMenu_Option_Duration", client); + + switch (g_GagType[target]) + { + case bPerm: + Format(sBuffer, sizeof(sBuffer), "%s%T", sGagTemp, "ListMenu_Option_Duration_Perm", client); + case bTime: + Format(sBuffer, sizeof(sBuffer), "%s%T", sGagTemp, "ListMenu_Option_Duration_Time", client, g_iGagLength[target]); + case bSess: + Format(sBuffer, sizeof(sBuffer), "%s%T", sGagTemp, "ListMenu_Option_Duration_Temp", client); + default: + Format(sBuffer, sizeof(sBuffer), "error"); + } + + AddMenuItem(hMenu, "", sBuffer, ITEMDRAW_DISABLED); + + FormatTime(_sGagTime, sizeof(_sGagTime), NULL_STRING, g_iGagTime[target]); + Format(sBuffer, sizeof(sBuffer), "%T", "ListMenu_Option_Issue", client, _sGagTime); + AddMenuItem(hMenu, "", sBuffer, ITEMDRAW_DISABLED); + + Format(sGagTemp, sizeof(sGagTemp), "%T", "ListMenu_Option_Expire", client); + + switch (g_GagType[target]) + { + case bTime: + { + FormatTime(_sGagTime, sizeof(_sGagTime), NULL_STRING, (g_iGagTime[target] + g_iGagLength[target] * 60)); + Format(sBuffer, sizeof(sBuffer), "%s%T", sGagTemp, "ListMenu_Option_Expire_Time", client, _sGagTime); + } + case bPerm:Format(sBuffer, sizeof(sBuffer), "%s%T", sGagTemp, "ListMenu_Option_Expire_Perm", client); + case bSess:Format(sBuffer, sizeof(sBuffer), "%s%T", sGagTemp, "ListMenu_Option_Expire_Temp_Reconnect", client); + default:Format(sBuffer, sizeof(sBuffer), "error"); + } + + hMenu.AddItem("", sBuffer, ITEMDRAW_DISABLED); + + if (strlen(g_sGagReason[target]) > 0) + { + Format(sBuffer, sizeof(sBuffer), "%T", "ListMenu_Option_Reason", client); + Format(sOption, sizeof(sOption), "3 %d %d %b %b", userid, index, viewMute, viewGag); + hMenu.AddItem(sOption, sBuffer); + } + else + { + Format(sBuffer, sizeof(sBuffer), "%T", "ListMenu_Option_Reason_None", client); + hMenu.AddItem("", sBuffer, ITEMDRAW_DISABLED); + } + } + } + + g_iPeskyPanels[client][curIndex] = index; + g_iPeskyPanels[client][curTarget] = target; + g_iPeskyPanels[client][viewingGag] = viewGag; + g_iPeskyPanels[client][viewingMute] = viewMute; + hMenu.Display(client, MENU_TIME_FOREVER); +} + +public int MenuHandler_MenuListTarget(Menu menu, MenuAction action, int param1, int param2) +{ + switch (action) + { + case MenuAction_End: + delete menu; + case MenuAction_Cancel: + { + if (param2 == MenuCancel_ExitBack) + AdminMenu_List(param1, g_iPeskyPanels[param1][curIndex]); + } + case MenuAction_Select: + { + char sOption[64], sTemp[5][8]; + menu.GetItem(param2, sOption, sizeof(sOption)); + ExplodeString(sOption, " ", sTemp, 5, 8); + + int target = GetClientOfUserId(StringToInt(sTemp[1])); + if (param1 == target || Bool_ValidMenuTarget(param1, target)) + { + switch (StringToInt(sTemp[0])) + { + case 0: + AdminMenu_ListTarget(param1, target, StringToInt(sTemp[2]), !(StringToInt(sTemp[3])), 0); + case 1, 3: + AdminMenu_ListTargetReason(param1, target, g_iPeskyPanels[param1][viewingMute], g_iPeskyPanels[param1][viewingGag]); + case 2: + AdminMenu_ListTarget(param1, target, StringToInt(sTemp[2]), 0, !(StringToInt(sTemp[4]))); + } + } + else + AdminMenu_List(param1, StringToInt(sTemp[2])); + + } + } + return 0; +} + +void AdminMenu_ListTargetReason(int client, int target, int showMute, int showGag) +{ + char sTemp[192], sBuffer[192]; + Panel hPanel = new Panel(); + hPanel.SetTitle(g_sName[target]); + hPanel.DrawItem(" ", ITEMDRAW_SPACER | ITEMDRAW_RAWLINE); + + if (showMute) + { + Format(sTemp, sizeof(sTemp), "%T", "ReasonPanel_Punishment_Mute", client); + switch (g_MuteType[target]) + { + case bPerm: + Format(sBuffer, sizeof(sBuffer), "%s%T", sTemp, "ReasonPanel_Perm", client); + case bTime: + Format(sBuffer, sizeof(sBuffer), "%s%T", sTemp, "ReasonPanel_Time", client, g_iMuteLength[target]); + case bSess: + Format(sBuffer, sizeof(sBuffer), "%s%T", sTemp, "ReasonPanel_Temp", client); + default: + Format(sBuffer, sizeof(sBuffer), "error"); + } + hPanel.DrawText(sBuffer); + + Format(sBuffer, sizeof(sBuffer), "%T", "ReasonPanel_Reason", client, g_sMuteReason[target]); + hPanel.DrawText(sBuffer); + } + else if (showGag) + { + Format(sTemp, sizeof(sTemp), "%T", "ReasonPanel_Punishment_Gag", client); + switch (g_GagType[target]) + { + case bPerm: + Format(sBuffer, sizeof(sBuffer), "%s%T", sTemp, "ReasonPanel_Perm", client); + case bTime: + Format(sBuffer, sizeof(sBuffer), "%s%T", sTemp, "ReasonPanel_Time", client, g_iGagLength[target]); + case bSess: + Format(sBuffer, sizeof(sBuffer), "%s%T", sTemp, "ReasonPanel_Temp", client); + default: + Format(sBuffer, sizeof(sBuffer), "error"); + } + hPanel.DrawText(sBuffer); + + Format(sBuffer, sizeof(sBuffer), "%T", "ReasonPanel_Reason", client, g_sGagReason[target]); + hPanel.DrawText(sBuffer); + } + + hPanel.DrawItem(" ", ITEMDRAW_SPACER | ITEMDRAW_RAWLINE); + hPanel.CurrentKey = 10; + Format(sBuffer, sizeof(sBuffer), "%T", "ReasonPanel_Back", client); + hPanel.DrawItem(sBuffer); + hPanel.Send(client, PanelHandler_ListTargetReason, MENU_TIME_FOREVER); + delete hPanel; +} + +public int PanelHandler_ListTargetReason(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + AdminMenu_ListTarget(param1, g_iPeskyPanels[param1][curTarget], + g_iPeskyPanels[param1][curIndex], + g_iPeskyPanels[param1][viewingMute], + g_iPeskyPanels[param1][viewingGag]); + } + return 0; +} + + +// SQL CALLBACKS // + +public void GotDatabase(Database db, const char[] error, any data) +{ + #if defined DEBUG + PrintToServer("GotDatabase(data: %d, lock: %d, g_h: %d, db: %d)", data, g_iConnectLock, g_hDatabase, db); + #endif + + // If this happens to be an old connection request, ignore it. + if (data != g_iConnectLock || g_hDatabase) + { + if (db) + delete db; + return; + } + + g_iConnectLock = 0; + g_DatabaseState = DatabaseState_Connected; + g_hDatabase = db; + + // See if the connection is valid. If not, don't un-mark the caches + // as needing rebuilding, in case the next connection request works. + if (!g_hDatabase) + { + LogError("Connecting to database failed: %s", error); + return; + } + + // Set character set to UTF8MB4 in the database + // Use SQL_SetCharset to ensure charset is set synchronously before any operations + if (!db.SetCharset("utf8mb4")) + { + LogError("Failed to set database charset to utf8mb4"); + // Fallback to async method + char query[128]; + Format(query, sizeof(query), "SET NAMES utf8mb4"); + db.Query(Query_ErrorCheck, query); + } + + // Process queue + SQLiteDB.Query(Query_ProcessQueue, + "SELECT id, steam_id, time, start_time, reason, name, admin_id, admin_ip, type \ + FROM queue2"); + + // Force recheck players + ForcePlayersRecheck(); +} + +public void Query_AddBlockInsert(Database db, DBResultSet results, const char[] error, DataPack dataPack) +{ + dataPack.Reset(); + + char reason[256]; + int iTarget = GetClientOfUserId(dataPack.ReadCell()); + + if (!iTarget) { + iTarget = -1; + } + + int length = dataPack.ReadCell(); + int type = dataPack.ReadCell(); + dataPack.ReadString(reason, sizeof(reason)); + + if (DB_Conn_Lost(results) || error[0]) + { + LogError("Query_AddBlockInsert failed: %s", error); + + char name[MAX_NAME_LENGTH], auth[MAX_AUTHID_LENGTH], adminAuth[MAX_AUTHID_LENGTH], adminIp[16]; + dataPack.ReadString(name, sizeof(name)); + dataPack.ReadString(auth, sizeof(auth)); + dataPack.ReadString(adminAuth, sizeof(adminAuth)); + dataPack.ReadString(adminIp, sizeof(adminIp)); + + InsertTempBlock(length, type, name, auth, reason, adminAuth, adminIp); + } + + delete dataPack; +} + +public void Query_UnBlockSelect(Database db, DBResultSet results, const char[] error, DataPack dataPack) +{ + char adminAuth[MAX_AUTHID_LENGTH], targetAuth[MAX_AUTHID_LENGTH]; + char reason[256]; + + dataPack.Reset(); + int adminUserID = dataPack.ReadCell(); + int targetUserID = dataPack.ReadCell(); + int type = dataPack.ReadCell(); // not in use unless DEBUG + dataPack.ReadString(adminAuth, sizeof(adminAuth)); + dataPack.ReadString(targetAuth, sizeof(targetAuth)); + dataPack.ReadString(reason, sizeof(reason)); + + int admin = GetClientOfUserId(adminUserID); + int target = GetClientOfUserId(targetUserID); + + #if defined DEBUG + PrintToServer("Query_UnBlockSelect(adminUID: %d/%d, targetUID: %d/%d, type: %d, adminAuth: %s, targetAuth: %s, reason: %s)", + adminUserID, admin, targetUserID, target, type, adminAuth, targetAuth, reason); + #endif + + char targetName[MAX_NAME_LENGTH]; + strcopy(targetName, MAX_NAME_LENGTH, target && IsClientInGame(target) ? g_sName[target] : targetAuth); //FIXME + + bool hasErrors = false; + // If error is not an empty string the query failed + if (DB_Conn_Lost(results) || error[0] != '\0') + { + LogError("Query_UnBlockSelect failed: %s", error); + if (admin && IsClientInGame(admin)) + { + PrintToChat(admin, "%s%T", PREFIX, "Unblock Select Failed", admin, targetAuth); + PrintToConsole(admin, "%s%T", PREFIX, "Unblock Select Failed", admin, targetAuth); + } + else + { + PrintToServer("%s%T", PREFIX, "Unblock Select Failed", LANG_SERVER, targetAuth); + } + hasErrors = true; + } + + // If there was no results then a ban does not exist for that id + if (!DB_Conn_Lost(results) && !results.RowCount) + { + if (admin && IsClientInGame(admin)) + { + PrintToChat(admin, "%s%t", PREFIX, "No blocks found", targetAuth); + PrintToConsole(admin, "%s%t", PREFIX, "No blocks found", targetAuth); + } + else + { + PrintToServer("%s%T", PREFIX, "No blocks found", LANG_SERVER, targetAuth); + } + hasErrors = true; + } + + if (hasErrors) + { + #if defined DEBUG + PrintToServer("Calling TempUnBlock from Query_UnBlockSelect"); + #endif + + TempUnBlock(dataPack); // Datapack closed inside. + return; + } + else + { + bool b_success = false; + // Get the values from the founded blocks. + while (results.MoreRows) + { + // Oh noes! What happened?! + if (!results.FetchRow()) + continue; + + int bid = results.FetchInt(0); + int iAID = results.FetchInt(1); + int cAID = results.FetchInt(2); + int cImmunity = results.FetchInt(3); + int cType = results.FetchInt(4); + + #if defined DEBUG + PrintToServer("Fetched from DB: bid %d, iAID: %d, cAID: %d, cImmunity: %d, cType: %d", bid, iAID, cAID, cImmunity, cType); + // WHO WE ARE? + PrintToServer("WHO WE ARE CHECKING!"); + if (iAID == cAID) + PrintToServer("we are block author"); + if (!admin) + PrintToServer("we are console (possibly)"); + if (AdmHasFlag(admin)) + PrintToServer("we have special flag"); + if (GetAdmImmunity(admin) > cImmunity) + PrintToServer("we have %d immunity and block has %d. we cool", GetAdmImmunity(admin), cImmunity); + #endif + + // Checking - has we access to unblock? + if (iAID == cAID || (!admin && strcmp(adminAuth, "STEAM_ID_SERVER", false) == 0) || AdmHasFlag(admin) || (DisUBImCheck == 0 && (GetAdmImmunity(admin) > cImmunity))) + { + // Ok! we have rights to unblock + b_success = true; + // UnMute/UnGag, Show & log activity + if (target && IsClientInGame(target)) + { + switch (cType) + { + case TYPE_MUTE: + { + PerformUnMute(target); + LogAction(admin, target, "\"%L\" unmuted \"%L\" (reason \"%s\")", admin, target, reason); + Forward_UnBlock(admin, target, TYPE_UNMUTE, reason); + } + //------------------------------------------------------------------------------------------------- + case TYPE_GAG: + { + PerformUnGag(target); + LogAction(admin, target, "\"%L\" ungagged \"%L\" (reason \"%s\")", admin, target, reason); + Forward_UnBlock(admin, target, TYPE_UNGAG, reason); + } + } + } + + DataPack newDataPack = new DataPack(); + newDataPack.WriteCell(adminUserID); + newDataPack.WriteCell(cType); + newDataPack.WriteString(g_sName[target]); + newDataPack.WriteString(targetAuth); + + char unbanReason[sizeof(reason) * 2 + 1]; + db.Escape(reason, unbanReason, sizeof(unbanReason)); + + char query[2048]; + Format(query, sizeof(query), + "UPDATE %s_comms \ + SET RemovedBy = %d, \ + RemoveType = 'U', \ + RemovedOn = UNIX_TIMESTAMP(), \ + ureason = '%s' \ + WHERE bid = %d", + DatabasePrefix, iAID, unbanReason, bid); + #if defined LOG_QUERIES + LogToFile(logQuery, "Query_UnBlockSelect. QUERY: %s", query); + #endif + db.Query(Query_UnBlockUpdate, query, newDataPack); + } + else + { + // sorry, we don't have permission to unblock! + #if defined DEBUG + PrintToServer("No permissions to unblock in Query_UnBlockSelect"); + #endif + switch (cType) + { + case TYPE_MUTE: + { + if (admin && IsClientInGame(admin)) + { + PrintToChat(admin, "%s%t", PREFIX, "No permission unmute", targetName); + PrintToConsole(admin, "%s%t", PREFIX, "No permission unmute", targetName); + } + LogAction(admin, target, "\"%L\" tried (and didn't have permission) to unmute %s (reason \"%s\")", admin, targetAuth, reason); + } + //------------------------------------------------------------------------------------------------- + case TYPE_GAG: + { + if (admin && IsClientInGame(admin)) + { + PrintToChat(admin, "%s%t", PREFIX, "No permission ungag", targetName); + PrintToConsole(admin, "%s%t", PREFIX, "No permission ungag", targetName); + } + LogAction(admin, target, "\"%L\" tried (and didn't have permission) to ungag %s (reason \"%s\")", admin, targetAuth, reason); + } + } + } + } + + if (b_success && target && IsClientInGame(target)) + { + #if defined DEBUG + PrintToServer("Showing activity to server in Query_UnBlockSelect"); + #endif + ShowActivityToServer(admin, type, _, _, g_sName[target], _); + + if (type == TYPE_UNSILENCE) + { + // check result for possible combination with temp and time punishments (temp was skipped in code above) + // type is in 3rd position in datapack + dataPack.Reset(); + dataPack.ReadCell(); + dataPack.ReadCell(); + + if (g_MuteType[target] > bNot) + { + dataPack.WriteCell(TYPE_UNMUTE); + TempUnBlock(dataPack); + return; + } + else if (g_GagType[target] > bNot) + { + dataPack.WriteCell(TYPE_UNGAG); + TempUnBlock(dataPack); + return; + } + } + } + } + + if (dataPack != null) + delete dataPack; +} + +public void Query_UnBlockUpdate(Database db, DBResultSet results, const char[] error, DataPack dataPack) +{ + int admin, type; + char targetName[MAX_NAME_LENGTH], targetAuth[MAX_AUTHID_LENGTH]; + + dataPack.Reset(); + admin = GetClientOfUserId(dataPack.ReadCell()); + type = dataPack.ReadCell(); + dataPack.ReadString(targetName, sizeof(targetName)); + dataPack.ReadString(targetAuth, sizeof(targetAuth)); + delete dataPack; + + if (DB_Conn_Lost(results) || error[0] != '\0') + { + LogError("Query_UnBlockUpdate failed: %s", error); + if (admin && IsClientInGame(admin)) + { + PrintToChat(admin, "%s%t", PREFIX, "Unblock insert failed"); + PrintToConsole(admin, "%s%t", PREFIX, "Unblock insert failed"); + } + return; + } + + switch (type) + { + case TYPE_MUTE: + { + LogAction(admin, -1, "\"%L\" removed mute for %s from DB", admin, targetAuth); + if (admin && IsClientInGame(admin)) + { + PrintToChat(admin, "%s%t", PREFIX, "successfully unmuted", targetName); + PrintToConsole(admin, "%s%t", PREFIX, "successfully unmuted", targetName); + } + else + { + PrintToServer("%s%T", PREFIX, "successfully unmuted", LANG_SERVER, targetName); + } + } + //------------------------------------------------------------------------------------------------- + case TYPE_GAG: + { + LogAction(admin, -1, "\"%L\" removed gag for %s from DB", admin, targetAuth); + if (admin && IsClientInGame(admin)) { + PrintToChat(admin, "%s%t", PREFIX, "successfully ungagged", targetName); + PrintToConsole(admin, "%s%t", PREFIX, "successfully ungagged", targetName); + } + else + { + PrintToServer("%s%T", PREFIX, "successfully ungagged", LANG_SERVER, targetName); + } + } + } +} + +// ProcessQueueCallback is called as the result of selecting all the rows from the queue table +public void Query_ProcessQueue(Database db, DBResultSet results, const char[] error, any data) +{ + if (results == null || error[0]) + { + LogError("Query_ProcessQueue failed: %s", error); + return; + } + + char auth[MAX_AUTHID_LENGTH]; + char name[MAX_NAME_LENGTH]; + char reason[256]; + char adminAuth[MAX_AUTHID_LENGTH], adminIp[16]; + char query[4096]; + + while (results.MoreRows) + { + // Oh noes! What happened?! + if (!results.FetchRow()) + continue; + + char sAuthEscaped[sizeof(auth) * 2 + 1]; + char banName[MAX_NAME_LENGTH * 2 + 1]; + char banReason[sizeof(reason) * 2 + 1]; + char sAdmAuthEscaped[sizeof(adminAuth) * 2 + 1]; + char sAdmAuthYZEscaped[sizeof(adminAuth) * 2 + 1]; + + // if we get to here then there are rows in the queue pending processing + //steam_id TEXT, time INTEGER, start_time INTEGER, reason TEXT, name TEXT, admin_id TEXT, admin_ip TEXT, type INTEGER + int id = results.FetchInt(0); + results.FetchString(1, auth, sizeof(auth)); + int time = results.FetchInt(2); + int startTime = results.FetchInt(3); + results.FetchString(4, reason, sizeof(reason)); + results.FetchString(5, name, sizeof(name)); + results.FetchString(6, adminAuth, sizeof(adminAuth)); + results.FetchString(7, adminIp, sizeof(adminIp)); + int type = results.FetchInt(8); + + if (DB_Connect()) { + db.Escape(auth, sAuthEscaped, sizeof(sAuthEscaped)); + db.Escape(name, banName, sizeof(banName)); + db.Escape(reason, banReason, sizeof(banReason)); + db.Escape(adminAuth, sAdmAuthEscaped, sizeof(sAdmAuthEscaped)); + db.Escape(adminAuth[8], sAdmAuthYZEscaped, sizeof(sAdmAuthYZEscaped)); + } + else + continue; + // all blocks should be entered into db! + + FormatEx(query, sizeof(query), + "INSERT INTO %s_comms (authid, name, created, ends, length, reason, aid, adminIp, sid, type) \ + VALUES ('%s', '%s', %d, %d, %d, '%s', \ + IFNULL((SELECT aid FROM %s_admins WHERE authid = '%s' OR authid REGEXP '^STEAM_[0-9]:%s$'), '0'), \ + '%s', %d, %d)", + DatabasePrefix, sAuthEscaped, banName, startTime, (startTime + (time * 60)), (time * 60), banReason, DatabasePrefix, sAdmAuthEscaped, sAdmAuthYZEscaped, adminIp, serverID, type); + #if defined LOG_QUERIES + LogToFile(logQuery, "Query_ProcessQueue. QUERY: %s", query); + #endif + db.Query(Query_AddBlockFromQueue, query, id); + } +} + +public void Query_AddBlockFromQueue(Database db, DBResultSet results, const char[] error, any data) +{ + char query[512]; + if (error[0] == '\0') + { + // The insert was successful so delete the record from the queue + FormatEx(query, sizeof(query), + "DELETE FROM queue2 \ + WHERE id = %d", + data); + #if defined LOG_QUERIES + LogToFile(logQuery, "Query_AddBlockFromQueue. QUERY: %s", query); + #endif + SQLiteDB.Query(Query_ErrorCheck, query); + } +} + +public void Query_ErrorCheck(Database db, DBResultSet results, const char[] error, any data) +{ + if (DB_Conn_Lost(results) || error[0]) + LogError("%T (%s)", "Failed to query database", LANG_SERVER, error); +} + +public void Query_VerifyBlock(Database db, DBResultSet results, const char[] error, any userid) +{ + int client = GetClientOfUserId(userid); + + #if defined DEBUG + PrintToServer("Query_VerifyBlock(userid: %d, client: %d)", userid, client); + #endif + + if (!client) + return; + + /* Failure happen. Do retry with delay */ + if (DB_Conn_Lost(results)) + { + LogError("Query_VerifyBlock failed: %s", error); + ClientRecheck(client); + return; + } + + //SELECT (c.ends - UNIX_TIMESTAMP()) as remaining, c.length, c.type, c.created, c.reason, a.user, + //IF (a.immunity>=g.immunity, a.immunity, IFNULL(g.immunity,0)) as immunity, c.aid, c.sid, c.authid + //FROM %s_comms c LEFT JOIN %s_admins a ON a.aid=c.aid LEFT JOIN %s_srvgroups g ON g.name = a.srv_group + //WHERE c.authid REGEXP '^STEAM_[0-9]:%s$' AND (length = '0' OR ends > UNIX_TIMESTAMP()) AND RemoveType IS NULL", + if (results.RowCount > 0) + { + while (results.FetchRow()) + { + if (NotApplyToThisServer(results.FetchInt(8))) + continue; + + char sAdmName[MAX_NAME_LENGTH], sAdmAuth[64]; + char sReason[256]; + int remaining_time = results.FetchInt(0); + int length = results.FetchInt(1); + int type = results.FetchInt(2); + int time = results.FetchInt(3); + results.FetchString(4, sReason, sizeof(sReason)); + results.FetchString(5, sAdmName, sizeof(sAdmName)); + int immunity = results.FetchInt(6); + int aid = results.FetchInt(7); + results.FetchString(9, sAdmAuth, sizeof(sAdmAuth)); + + // Block from CONSOLE (aid=0) and we have `console immunity` value in config + if (!aid && ConsoleImmunity > immunity) + immunity = ConsoleImmunity; + + #if defined DEBUG + PrintToServer("Fetched from DB: remaining %d, length %d, type %d", remaining_time, length, type); + #endif + + switch (type) + { + case TYPE_MUTE: + { + //Set mute type based on length + if (length > 0) + g_MuteType[client] = bTime; + else if (length == 0) + g_MuteType[client] = bPerm; + else + g_MuteType[client] = bSess; + + //Perform mute/unmute + if (g_MuteType[client] == bSess) + { + PerformUnMute(client); + } + else if (g_MuteType[client] > bSess) + { + PerformMute(client, time, length / 60, sAdmName, sAdmAuth, immunity, sReason, remaining_time); + PrintToChat(client, "%s%t", PREFIX, "Muted on connect"); + } + } + case TYPE_GAG: + { + //Set gag type based on length + if (length > 0) + g_GagType[client] = bTime; + else if (length == 0) + g_GagType[client] = bPerm; + else + g_GagType[client] = bSess; + + //Perform gag/ungag + if (g_GagType[client] == bSess) + { + PerformUnGag(client); + } + else if (g_GagType[client] > bSess) + { + PerformGag(client, time, length / 60, sAdmName, sAdmAuth, immunity, sReason, remaining_time); + PrintToChat(client, "%s%t", PREFIX, "Gagged on connect"); + } + } + } + } + } + + g_bPlayerVerified[client] = true; + g_bPlayerStatus[client] = true; +} + + +// TIMER CALL BACKS // + +public Action Timer_ClientRecheck(Handle timer, int userid) +{ + int client = GetClientOfUserId(userid); + + if (!client) + return Plugin_Stop; + + #if defined DEBUG + PrintToServer("Timer_ClientRecheck(userid: %d)", userid); + #endif + + OnClientPostAdminCheck(client); + + return Plugin_Continue; +} + +public Action Timer_MuteExpire(Handle timer, DataPack dataPack) +{ + dataPack.Reset(); + g_hMuteExpireTimer[dataPack.ReadCell()] = INVALID_HANDLE; + + int client = GetClientOfUserId(dataPack.ReadCell()); + if (!client) + return Plugin_Continue; + + #if defined DEBUG + PrintToServer("Mute expired for %s", g_sSteamIDs[client]); + #endif + + PrintToChat(client, "%s%t", PREFIX, "Mute expired"); + + MarkClientAsUnMuted(client); + if (IsClientInGame(client)) + BaseComm_SetClientMute(client, false); + return Plugin_Continue; +} + +public Action Timer_GagExpire(Handle timer, DataPack dataPack) +{ + dataPack.Reset(); + g_hGagExpireTimer[dataPack.ReadCell()] = null; + + int client = GetClientOfUserId(dataPack.ReadCell()); + if (!client) + return Plugin_Continue; + + #if defined DEBUG + PrintToServer("Gag expired for %s", g_sSteamIDs[client]); + #endif + + PrintToChat(client, "%s%t", PREFIX, "Gag expired"); + + MarkClientAsUnGagged(client); + if (IsClientInGame(client)) + BaseComm_SetClientGag(client, false); + return Plugin_Continue; +} + +public Action Timer_StopWait(Handle timer, any data) +{ + g_DatabaseState = DatabaseState_None; + DB_Connect(); + return Plugin_Continue; +} + +// PARSER // + +static void InitializeConfigParser() +{ + if (ConfigParser == INVALID_HANDLE) + { + ConfigParser = new SMCParser(); + ConfigParser.OnEnterSection = ReadConfig_NewSection; + ConfigParser.OnKeyValue = ReadConfig_KeyValue; + ConfigParser.OnLeaveSection = ReadConfig_EndSection; + } +} + +static void InternalReadConfig(const char[] path) +{ + ConfigState = ConfigStateNone; + + SMCError err = ConfigParser.ParseFile(path); + + if (err != SMCError_Okay) + { + char buffer[64]; + LogError("%s", SMC_GetErrorString(err, buffer, sizeof(buffer)) ? buffer : "Fatal parse error"); + } +} + +public SMCResult ReadConfig_NewSection(SMCParser smc, const char[] name, bool opt_quotes) +{ + if (name[0]) + { + if (strcmp("Config", name, false) == 0) + { + ConfigState = ConfigStateConfig; + } + else if (strcmp("CommsReasons", name, false) == 0) + { + ConfigState = ConfigStateReasons; + } + else if (strcmp("CommsTimes", name, false) == 0) + { + ConfigState = ConfigStateTimes; + } + else if (strcmp("ServersWhiteList", name, false) == 0) + { + ConfigState = ConfigStateServers; + } + } + + return SMCParse_Continue; +} + +public SMCResult ReadConfig_KeyValue(SMCParser smc, const char[] key, const char[] value, bool key_quotes, bool value_quotes) +{ + if (!key[0]) + return SMCParse_Continue; + + switch (ConfigState) + { + case ConfigStateConfig: + { + if (strcmp("DatabasePrefix", key, false) == 0) + { + strcopy(DatabasePrefix, sizeof(DatabasePrefix), value); + + if (DatabasePrefix[0] == '\0') + { + DatabasePrefix = "sb"; + } + } + else if (strcmp("ServerIP", key, false) == 0) + { + char configuredIp[64]; + strcopy(configuredIp, sizeof(configuredIp), value); + TrimString(configuredIp); + + if (SBPP_IsValidServerIpOverride(configuredIp)) + { + strcopy(ConfiguredServerIp, sizeof(ConfiguredServerIp), configuredIp); + } + else + { + ConfiguredServerIp[0] = '\0'; + LogError("Ignoring invalid ServerIP override; expected an IPv4 address"); + } + } + else if (strcmp("RetryTime", key, false) == 0) + { + RetryTime = StringToFloat(value); + if (RetryTime < 15.0) + { + RetryTime = 15.0; + } + else if (RetryTime > 60.0) + { + RetryTime = 60.0; + } + } + else if (strcmp("ServerID", key, false) == 0) + { + serverID = StringToInt(value); + + // get our sb_id value if we have one + ConVar sbId = FindConVar("sb_id"); + if (sbId != null && sbId.IntValue != -1) + { + serverID = sbId.IntValue; + } + + // if it's not valid, make it 0 + // we consider -1 valid here + if (serverID < -1) + { + serverID = 0; + } + } + else if (strcmp("DefaultTime", key, false) == 0) + { + DefaultTime = StringToInt(value); + if (DefaultTime < 0) + { + DefaultTime = -1; + } + if (DefaultTime == 0) + { + DefaultTime = 30; + } + } + else if (strcmp("DisableUnblockImmunityCheck", key, false) == 0) + { + DisUBImCheck = StringToInt(value); + if (DisUBImCheck != 1) + { + DisUBImCheck = 0; + } + } + else if (strcmp("ConsoleImmunity", key, false) == 0) + { + ConsoleImmunity = StringToInt(value); + if (ConsoleImmunity < 0 || ConsoleImmunity > 100) + { + ConsoleImmunity = 0; + } + } + else if (strcmp("MaxLength", key, false) == 0) + { + ConfigMaxLength = StringToInt(value); + } + else if (strcmp("OnlyWhiteListServers", key, false) == 0) + { + ConfigWhiteListOnly = StringToInt(value); + if (ConfigWhiteListOnly != 1) + { + ConfigWhiteListOnly = 0; + } + } + } + case ConfigStateReasons: + { + Format(g_sReasonKey[iNumReasons], REASON_SIZE, "%s", key); + Format(g_sReasonDisplays[iNumReasons], DISPLAY_SIZE, "%s", value); + #if defined DEBUG + PrintToServer("Loaded reason. index %d, key \"%s\", display_text \"%s\"", iNumReasons, g_sReasonKey[iNumReasons], g_sReasonDisplays[iNumReasons]); + #endif + iNumReasons++; + } + case ConfigStateTimes: + { + Format(g_sTimeDisplays[iNumTimes], DISPLAY_SIZE, "%s", value); + g_iTimeMinutes[iNumTimes] = StringToInt(key); + #if defined DEBUG + PrintToServer("Loaded time. index %d, time %d minutes, display_text \"%s\"", iNumTimes, g_iTimeMinutes[iNumTimes], g_sTimeDisplays[iNumTimes]); + #endif + iNumTimes++; + } + case ConfigStateServers: + { + if (strcmp("id", key, false) == 0) + { + int srvID = StringToInt(value); + if (srvID >= 0) + { + g_hServersWhiteList.Push(srvID); + #if defined DEBUG + PrintToServer("Loaded white list server id %d", srvID); + #endif + } + } + } + } + return SMCParse_Continue; +} + +public SMCResult ReadConfig_EndSection(SMCParser smc) +{ + return SMCParse_Continue; +} + +// STOCK FUNCTIONS // +stock void setGag(int client, int length, const char[] clientAuth) +{ + if (g_GagType[client] == bNot) + { + PerformGag(client, _, length / 60, _, _, _, _); + PrintToChat(client, "%s%t", PREFIX, "Gagged on connect"); + LogMessage("%s is gagged from web", clientAuth); + } +} + +stock void setMute(int client, int length, const char[] clientAuth) +{ + if (g_MuteType[client] == bNot) + { + PerformMute(client, _, length / 60, _, _, _, _); + PrintToChat(client, "%s%t", PREFIX, "Muted on connect"); + LogMessage("%s is muted from web", clientAuth); + } +} + +stock void ClientRecheck(int client) +{ + CreateTimer(1.0, Timer_ClientRecheck, g_iUserIDs[client], TIMER_FLAG_NO_MAPCHANGE); +} + +stock bool IsInvalidSteamID(int client) +{ + if (strncmp(g_sSteamIDs[client][6], "ID_", 3) == 0 || g_sSteamIDs[client][0] == 'B' || IsFakeClient(client)) + return true; + else + return false; +} + +stock bool DB_Connect() +{ + #if defined DEBUG + PrintToServer("DB_Connect(handle %d, state %d, lock %d)", g_hDatabase, g_DatabaseState, g_iConnectLock); + #endif + + if (g_hDatabase) + { + return true; + } + + if (g_DatabaseState == DatabaseState_Wait) // 100500 connections in a minute is bad idea.. + { + return false; + } + + if (g_DatabaseState != DatabaseState_Connecting) + { + g_DatabaseState = DatabaseState_Connecting; + g_iConnectLock = ++g_iSequence; + // Connect using the "sourcebans" section, or the "default" section if "sourcebans" does not exist + Database.Connect(GotDatabase, DATABASE, g_iConnectLock); + } + + return false; +} + +stock bool DB_Conn_Lost(DBResultSet db) +{ + if (db == null) + { + if (g_hDatabase != null) + { + LogError("Lost connection to DB. Reconnect after delay."); + delete g_hDatabase; + g_hDatabase = null; + } + + if (g_DatabaseState != DatabaseState_Wait) + { + g_DatabaseState = DatabaseState_Wait; + CreateTimer(RetryTime, Timer_StopWait, _, TIMER_FLAG_NO_MAPCHANGE); + } + + return true; + } + + return false; +} + +stock void InitializeBackupDB() +{ + char error[256]; + SQLiteDB = SQLite_UseDatabase("sourcecomms-queue", error, sizeof(error)); + if (SQLiteDB == INVALID_HANDLE) + { + SetFailState(error); + } + + SQLiteDB.Query(Query_ErrorCheck, + "CREATE TABLE IF NOT EXISTS queue2 ( \ + id INTEGER PRIMARY KEY, \ + steam_id TEXT, \ + time INTEGER, \ + start_time INTEGER, \ + reason TEXT, \ + name TEXT, \ + admin_id TEXT, \ + admin_ip TEXT, \ + type INTEGER)"); +} + +stock void CreateBlock(int client, int targetId = 0, int length = -1, int type, const char[] sReason = "", const char[] sArgs = "") +{ + #if defined DEBUG + PrintToServer("CreateBlock(admin: %d, target: %d, length: %d, type: %d, reason: %s, args: %s)", client, targetId, length, type, sReason, sArgs); + #endif + + int target_list[MAXPLAYERS], target_count; + bool tn_is_ml; + char target_name[MAX_NAME_LENGTH]; + char reason[256]; + bool skipped = false; + + // checking args + if (targetId) + { + target_list[0] = targetId; + target_count = 1; + tn_is_ml = false; + strcopy(target_name, sizeof(target_name), g_sName[targetId]); + strcopy(reason, sizeof(reason), sReason); + } + else if (strlen(sArgs)) + { + char sArg[3][192]; + + if (ExplodeString(sArgs, "\"", sArg, 3, 192, true) == 3 && strlen(sArg[0]) == 0) // exploding by quotes + { + char sTempArg[2][192]; + TrimString(sArg[2]); + sArg[0] = sArg[1]; // target name + ExplodeString(sArg[2], " ", sTempArg, 2, 192, true); // get length and reason + sArg[1] = sTempArg[0]; // lenght + sArg[2] = sTempArg[1]; // reason + } + else + { + ExplodeString(sArgs, " ", sArg, 3, 192, true); // exploding by spaces + } + + // Get the target, find target returns a message on failure so we do not + if ((target_count = ProcessTargetString( + sArg[0], + client, + target_list, + MAXPLAYERS, + COMMAND_FILTER_NO_BOTS, + target_name, + sizeof(target_name), + tn_is_ml)) <= 0) + { + ReplyToTargetError(client, target_count); + return; + } + + // Get the block length + if (!StringToIntEx(sArg[1], length)) // not valid number in second argument + { + length = DefaultTime; + Format(reason, sizeof(reason), "%s %s", sArg[1], sArg[2]); + } + else + { + strcopy(reason, sizeof(reason), sArg[2]); + } + + // Strip spaces and quotes from reason + TrimString(reason); + StripQuotes(reason); + + if (!IsAllowedBlockLength(client, length, target_count)) + { + ReplyToCommand(client, "%s%t", PREFIX, "no access"); + return; + } + } + else + { + return; + } + + int admImmunity = GetAdmImmunity(client); + char adminAuth[MAX_AUTHID_LENGTH]; + + if (client && IsClientInGame(client)) + { + strcopy(adminAuth, sizeof(adminAuth), g_sSteamIDs[client]); + } + else + { + // setup dummy adminAuth and adminIp for server + strcopy(adminAuth, sizeof(adminAuth), "STEAM_ID_SERVER"); + } + + for (int i = 0; i < target_count; i++) + { + int target = target_list[i]; + + #if defined DEBUG + PrintToServer("Processing block for %s", g_sSteamIDs[target]); + #endif + + switch (type) + { + case TYPE_MUTE: + { + if (!BaseComm_IsClientMuted(target)) + { + #if defined DEBUG + PrintToServer("%s not muted. Mute him, creating unmute timer and add record to DB", g_sSteamIDs[target]); + #endif + + PerformMute(target, _, length, g_sName[client], adminAuth, admImmunity, reason); + PrintToChat(target, "%s%t", PREFIX, "Muted on connect"); + + LogAction(client, target, "\"%L\" muted \"%L\" (minutes \"%d\") (reason \"%s\")", client, target, length, reason); + } + else + { + #if defined DEBUG + PrintToServer("%s already muted", g_sSteamIDs[target]); + #endif + + ReplyToCommand(client, "%s%t", PREFIX, "Player already muted", g_sName[target]); + + skipped = true; + continue; + } + } + //------------------------------------------------------------------------------------------------- + case TYPE_GAG: + { + if (!BaseComm_IsClientGagged(target)) + { + #if defined DEBUG + PrintToServer("%s not gagged. Gag him, creating ungag timer and add record to DB", g_sSteamIDs[target]); + #endif + + PerformGag(target, _, length, g_sName[client], adminAuth, admImmunity, reason); + PrintToChat(target, "%s%t", PREFIX, "Gagged on connect"); + + LogAction(client, target, "\"%L\" gagged \"%L\" (minutes \"%d\") (reason \"%s\")", client, target, length, reason); + } + else + { + #if defined DEBUG + PrintToServer("%s already gagged", g_sSteamIDs[target]); + #endif + + ReplyToCommand(client, "%s%t", PREFIX, "Player already gagged", g_sName[target]); + + skipped = true; + continue; + } + } + //------------------------------------------------------------------------------------------------- + case TYPE_SILENCE: + { + if (!BaseComm_IsClientGagged(target) && !BaseComm_IsClientMuted(target)) + { + #if defined DEBUG + PrintToServer("%s not silenced. Silence him, creating ungag & unmute timers and add records to DB", g_sSteamIDs[target]); + #endif + + PerformMute(target, _, length, g_sName[client], adminAuth, admImmunity, reason); + PerformGag(target, _, length, g_sName[client], adminAuth, admImmunity, reason); + PrintToChat(target, "%s%t", PREFIX, "Muted on connect"); + PrintToChat(target, "%s%t", PREFIX, "Gagged on connect"); + + LogAction(client, target, "\"%L\" silenced \"%L\" (minutes \"%d\") (reason \"%s\")", client, target, length, reason); + } + else + { + #if defined DEBUG + PrintToServer("%s already gagged or/and muted", g_sSteamIDs[target]); + #endif + + ReplyToCommand(client, "%s%t", PREFIX, "Player already silenced", g_sName[target]); + + skipped = true; + continue; + } + } + } + + Forward_Block(client, target, type, length, reason); + + if (target && !skipped) + SavePunishment(client, target, type, length, reason); + } + if (target_count > 1 || !skipped) + ShowActivityToServer(client, type, length, reason, target_name, tn_is_ml); + + return; +} + +stock void ProcessUnBlock(int client, int targetId = 0, int type, char[] sReason = "") +{ + #if defined DEBUG + PrintToServer("ProcessUnBlock(admin: %d, target: %d, type: %d, reason: %s)", client, targetId, type, sReason); + #endif + + int target_list[MAXPLAYERS], target_count; + bool tn_is_ml; + char target_name[MAX_NAME_LENGTH]; + char reason[256]; + + if (targetId) + { + target_list[0] = targetId; + target_count = 1; + tn_is_ml = false; + strcopy(target_name, sizeof(target_name), g_sName[targetId]); + strcopy(reason, sizeof(reason), sReason); + } + else + { + char sBuffer[256]; + char sArg[3][192]; + GetCmdArgString(sBuffer, sizeof(sBuffer)); + + if (ExplodeString(sBuffer, "\"", sArg, 3, 192, true) == 3 && strlen(sArg[0]) == 0) + { + TrimString(sArg[2]); + sArg[0] = sArg[1]; // target name + sArg[1] = sArg[2]; // reason; sArg[2] - not in use + } + else + { + ExplodeString(sBuffer, " ", sArg, 2, 192, true); + } + strcopy(reason, sizeof(reason), sArg[1]); + // Strip spaces and quotes from reason + TrimString(reason); + StripQuotes(reason); + + // Get the target, find target returns a message on failure so we do not + if ((target_count = ProcessTargetString( + sArg[0], + client, + target_list, + MAXPLAYERS, + COMMAND_FILTER_NO_BOTS, + target_name, + sizeof(target_name), + tn_is_ml)) <= 0) + { + ReplyToTargetError(client, target_count); + return; + } + } + + char adminAuth[MAX_AUTHID_LENGTH]; + char targetAuth[MAX_AUTHID_LENGTH]; + + if (client && IsClientConnected(client)) + { + strcopy(adminAuth, sizeof(adminAuth), g_sSteamIDs[client]); + } + else + { + // setup dummy adminAuth and adminIp for server + strcopy(adminAuth, sizeof(adminAuth), "STEAM_ID_SERVER"); + } + + if (target_count > 1) + { + #if defined DEBUG + PrintToServer("ProcessUnBlock - targets_count > 1"); + #endif + + for (int i = 0; i < target_count; i++) + { + int target = target_list[i]; + + if (!target || !IsClientConnected(target)) + continue; + + if (!g_bPlayerAuthentified[target] || !g_bPlayerStatus[target]) + { + // The target has not been blocks verify. It must be completed before you can unblock anyone. + ReplyToCommand(client, "%s%t", PREFIX, "Player Comms Not Verified", g_sName[target]); + continue; // skip + } + + switch (type) + { + case TYPE_UNMUTE: + { + if (g_MuteType[target] == bTime || g_MuteType[target] == bPerm) + continue; + } + case TYPE_UNGAG: + { + if (g_GagType[target] == bTime || g_GagType[target] == bPerm) + continue; + } + case TYPE_UNSILENCE: + { + if ((g_MuteType[target] == bTime || g_MuteType[target] == bPerm) && + (g_GagType[target] == bTime || g_GagType[target] == bPerm)) + continue; + } + } + + ProcessUnBlock(client, target, type, reason); + } + + #if defined DEBUG + PrintToServer("Showing activity to server in ProcessUnBlock for targets_count > 1"); + #endif + ShowActivityToServer(client, type + TYPE_TEMP_SHIFT, _, _, target_name, tn_is_ml); + } + else + { + char typeWHERE[100]; + int target = target_list[0]; + + if (!target || !IsClientInGame(target)) + return; + + strcopy(targetAuth, sizeof(targetAuth), g_sSteamIDs[target]); + + switch (type) + { + case TYPE_UNMUTE: + { + if (!BaseComm_IsClientMuted(target)) + { + ReplyToCommand(client, "%s%t", PREFIX, "Player not muted"); + return; + } + else + FormatEx(typeWHERE, sizeof(typeWHERE), "c.type = '%d'", TYPE_MUTE); + } + //------------------------------------------------------------------------------------------------- + case TYPE_UNGAG: + { + if (!BaseComm_IsClientGagged(target)) + { + ReplyToCommand(client, "%s%t", PREFIX, "Player not gagged"); + return; + } + else + FormatEx(typeWHERE, sizeof(typeWHERE), "c.type = '%d'", TYPE_GAG); + } + //------------------------------------------------------------------------------------------------- + case TYPE_UNSILENCE: + { + if (!BaseComm_IsClientMuted(target) || !BaseComm_IsClientGagged(target)) + { + ReplyToCommand(client, "%s%t", PREFIX, "Player not silenced"); + return; + } + else + FormatEx(typeWHERE, sizeof(typeWHERE), "(c.type = '%d' OR c.type = '%d')", TYPE_MUTE, TYPE_GAG); + } + } + + // Pack everything into a data pack so we can retain it + DataPack dataPack = new DataPack(); + dataPack.WriteCell(GetClientUserId2(client)); + dataPack.WriteCell(g_iUserIDs[target]); + dataPack.WriteCell(type); + dataPack.WriteString(adminAuth); + dataPack.WriteString(targetAuth); + dataPack.WriteString(reason); + + // Check current player status. If player has temporary punishment - don't get info from DB + if (DB_Connect()) + { + char sAdminAuthEscaped[sizeof(adminAuth) * 2 + 1]; + char sAdminAuthYZEscaped[sizeof(adminAuth) * 2 + 1]; + char sTargetAuthEscaped[sizeof(targetAuth) * 2 + 1]; + char sTargetAuthYZEscaped[sizeof(targetAuth) * 2 + 1]; + + g_hDatabase.Escape(adminAuth, sAdminAuthEscaped, sizeof(sAdminAuthEscaped)); + g_hDatabase.Escape(adminAuth[8], sAdminAuthYZEscaped, sizeof(sAdminAuthYZEscaped)); + g_hDatabase.Escape(targetAuth, sTargetAuthEscaped, sizeof(sTargetAuthEscaped)); + g_hDatabase.Escape(targetAuth[8], sTargetAuthYZEscaped, sizeof(sTargetAuthYZEscaped)); + + char query[4096]; + Format(query, sizeof(query), + "SELECT c.bid, \ + IFNULL((SELECT aid FROM %s_admins WHERE authid = '%s' OR authid REGEXP '^STEAM_[0-9]:%s$'), '0') as iaid, \ + c.aid, \ + IF (a.immunity>=g.immunity, a.immunity, IFNULL(g.immunity,0)) as immunity, \ + c.type \ + FROM %s_comms AS c \ + LEFT JOIN %s_admins AS a ON a.aid = c.aid \ + LEFT JOIN %s_srvgroups AS g ON g.name = a.srv_group \ + WHERE RemoveType IS NULL \ + AND (c.authid = '%s' OR c.authid REGEXP '^STEAM_[0-9]:%s$') \ + AND (length = '0' OR ends > UNIX_TIMESTAMP()) \ + AND %s", + DatabasePrefix, sAdminAuthEscaped, sAdminAuthYZEscaped, DatabasePrefix, DatabasePrefix, DatabasePrefix, sTargetAuthEscaped, sTargetAuthYZEscaped, typeWHERE); + + #if defined LOG_QUERIES + LogToFile(logQuery, "ProcessUnBlock. QUERY: %s", query); + #endif + + g_hDatabase.Query(Query_UnBlockSelect, query, dataPack); + } + else + { + #if defined DEBUG + PrintToServer("Calling TempUnBlock from ProcessUnBlock"); + #endif + + if (TempUnBlock(dataPack)) + ShowActivityToServer(client, type + TYPE_TEMP_SHIFT, _, _, g_sName[target], _); + } + } +} + +stock bool TempUnBlock(DataPack dataPack) +{ + char adminAuth[MAX_AUTHID_LENGTH], targetAuth[MAX_AUTHID_LENGTH]; + char reason[256]; + + dataPack.Reset(); + int adminUserID = dataPack.ReadCell(); + int targetUserID = dataPack.ReadCell(); + int type = dataPack.ReadCell(); + dataPack.ReadString(adminAuth, sizeof(adminAuth)); + dataPack.ReadString(targetAuth, sizeof(targetAuth)); + dataPack.ReadString(reason, sizeof(reason)); + delete dataPack; // Need to close datapack + + #if defined DEBUG + PrintToServer("TempUnBlock(adminUID: %d, targetUID: %d, type: %d, adminAuth: %s, targetAuth: %s, reason: %s)", adminUserID, targetUserID, type, adminAuth, targetAuth, reason); + #endif + + int admin = GetClientOfUserId(adminUserID); + int target = GetClientOfUserId(targetUserID); + if (!target) + return false; // target has gone away + + int AdmImmunity = GetAdmImmunity(admin); + bool AdmImCheck = (DisUBImCheck == 0 + && ((type == TYPE_UNMUTE && AdmImmunity >= g_iMuteLevel[target]) + || (type == TYPE_UNGAG && AdmImmunity >= g_iGagLevel[target]) + || (type == TYPE_UNSILENCE && AdmImmunity >= g_iMuteLevel[target] + && AdmImmunity >= g_iGagLevel[target]) + ) + ); + + #if defined DEBUG + PrintToServer("WHO WE ARE CHECKING!"); + if (!admin) + PrintToServer("we are console (possibly)"); + if (AdmHasFlag(admin)) + PrintToServer("we have special flag"); + #endif + + // Check access for unblock without db changes (temporary unblock) + bool bHasPermission = (!admin && strcmp(adminAuth, "STEAM_ID_SERVER", false) == 0) || AdmHasFlag(admin) || AdmImCheck; + // can, if we are console or have special flag. else - deep checking by issuer authid + if (!bHasPermission) { + switch (type) + { + case TYPE_UNMUTE: + { + bHasPermission = strcmp(adminAuth, g_sMuteAdminAuth[target], false) == 0; + } + case TYPE_UNGAG: + { + bHasPermission = strcmp(adminAuth, g_sGagAdminAuth[target], false) == 0; + } + case TYPE_UNSILENCE: + { + bHasPermission = strcmp(adminAuth, g_sMuteAdminAuth[target], false) == 0 && strcmp(adminAuth, g_sGagAdminAuth[target], false) == 0; + } + } + } + + if (bHasPermission) + { + switch (type) + { + case TYPE_UNMUTE: + { + PerformUnMute(target); + LogAction(admin, target, "\"%L\" temporary unmuted \"%L\" (reason \"%s\")", admin, target, reason); + } + //------------------------------------------------------------------------------------------------- + case TYPE_UNGAG: + { + PerformUnGag(target); + LogAction(admin, target, "\"%L\" temporary ungagged \"%L\" (reason \"%s\")", admin, target, reason); + } + //------------------------------------------------------------------------------------------------- + case TYPE_UNSILENCE: + { + PerformUnMute(target); + PerformUnGag(target); + LogAction(admin, target, "\"%L\" temporary unsilenced \"%L\" (reason \"%s\")", admin, target, reason); + } + default: + { + return false; + } + } + return true; + } + else + { + if (admin && IsClientInGame(admin)) + { + PrintToChat(admin, "%s%t", PREFIX, "No db error unlock perm"); + PrintToConsole(admin, "%s%t", PREFIX, "No db error unlock perm"); + } + return false; + } +} + +stock void InsertTempBlock(int length, int type, const char[] name, const char[] auth, const char[] reason, const char[] adminAuth, const char[] adminIp) +{ + LogMessage("Saving punishment for %s into queue", auth); + + char banName[MAX_NAME_LENGTH * 2 + 1]; + char banReason[256 * 2 + 1]; + char sAuthEscaped[MAX_AUTHID_LENGTH * 2 + 1]; + char sAdminAuthEscaped[MAX_AUTHID_LENGTH * 2 + 1]; + char sQuery[4096], sQueryVal[2048]; + char sQueryMute[2048], sQueryGag[2048]; + + // escaping everything + SQLiteDB.Escape(name, banName, sizeof(banName)); + SQLiteDB.Escape(reason, banReason, sizeof(banReason)); + SQLiteDB.Escape(auth, sAuthEscaped, sizeof(sAuthEscaped)); + SQLiteDB.Escape(adminAuth, sAdminAuthEscaped, sizeof(sAdminAuthEscaped)); + + // steam_id time start_time reason name admin_id admin_ip + FormatEx(sQueryVal, sizeof(sQueryVal), + "'%s', %d, %d, '%s', '%s', '%s', '%s'", + sAuthEscaped, length, GetTime(), banReason, banName, sAdminAuthEscaped, adminIp); + + switch (type) + { + case TYPE_MUTE:FormatEx(sQueryMute, sizeof(sQueryMute), "(%s, %d)", sQueryVal, type); + case TYPE_GAG:FormatEx(sQueryGag, sizeof(sQueryGag), "(%s, %d)", sQueryVal, type); + case TYPE_SILENCE: + { + FormatEx(sQueryMute, sizeof(sQueryMute), "(%s, %d)", sQueryVal, TYPE_MUTE); + FormatEx(sQueryGag, sizeof(sQueryGag), "(%s, %d)", sQueryVal, TYPE_GAG); + } + } + + FormatEx(sQuery, sizeof(sQuery), + "INSERT INTO queue2 (steam_id, time, start_time, reason, name, admin_id, admin_ip, type) VALUES %s%s%s", + sQueryMute, type == TYPE_SILENCE ? ", " : "", sQueryGag); + + #if defined LOG_QUERIES + LogToFile(logQuery, "InsertTempBlock. QUERY: %s", sQuery); + #endif + + SQLiteDB.Query(Query_ErrorCheck, sQuery); +} + +stock void ServerInfo() +{ + SBPP_ResolveServerIp(CvarHostIp, ConfiguredServerIp, ServerIp, sizeof(ServerIp)); + CvarPort.GetString(ServerPort, sizeof(ServerPort)); +} + +stock void ReadConfig() +{ + InitializeConfigParser(); + + if (ConfigParser == null) + { + return; + } + + ConfiguredServerIp[0] = '\0'; + + char ConfigFile1[PLATFORM_MAX_PATH], ConfigFile2[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, ConfigFile1, sizeof(ConfigFile1), "configs/sourcebans/sourcebans.cfg"); + BuildPath(Path_SM, ConfigFile2, sizeof(ConfigFile2), "configs/sourcebans/sourcecomms.cfg"); + + if (FileExists(ConfigFile1)) + { + PrintToServer("%sLoading configs/sourcebans/sourcebans.cfg config file", PREFIX); + InternalReadConfig(ConfigFile1); + } + else + { + SetFailState("FATAL *** ERROR *** can't find %s", ConfigFile1); + } + if (FileExists(ConfigFile2)) + { + PrintToServer("%sLoading configs/sourcecomms.cfg config file", PREFIX); + iNumReasons = 0; + iNumTimes = 0; + InternalReadConfig(ConfigFile2); + if (iNumReasons) + iNumReasons--; + if (iNumTimes) + iNumTimes--; + if (serverID == 0) + { + LogError("You must set valid `ServerID` value in sourcebans.cfg!"); + if (ConfigWhiteListOnly) + { + LogError("ServersWhiteList feature disabled!"); + ConfigWhiteListOnly = 0; + } + } + } + else + { + SetFailState("FATAL *** ERROR *** can't find %s", ConfigFile2); + } + #if defined DEBUG + PrintToServer("Loaded DefaultTime value: %d", DefaultTime); + PrintToServer("Loaded DisableUnblockImmunityCheck value: %d", DisUBImCheck); + #endif + + ServerInfo(); +} + + +// some more + +void AdminMenu_GetPunishPhrase(int client, int target, char[] name, int length) +{ + char Buffer[192]; + if (g_MuteType[target] > bNot && g_GagType[target] > bNot) + Format(Buffer, sizeof(Buffer), "%T", "AdminMenu_Display_Silenced", client, name); + else if (g_MuteType[target] > bNot) + Format(Buffer, sizeof(Buffer), "%T", "AdminMenu_Display_Muted", client, name); + else if (g_GagType[target] > bNot) + Format(Buffer, sizeof(Buffer), "%T", "AdminMenu_Display_Gagged", client, name); + else + Format(Buffer, sizeof(Buffer), "%T", "AdminMenu_Display_None", client, name); + + strcopy(name, length, Buffer); +} + +bool Bool_ValidMenuTarget(int client, int target) +{ + if (target <= 0) + { + if (client) + PrintToChat(client, "%s%t", PREFIX, "AdminMenu_Not_Available"); + else + ReplyToCommand(client, "%s%t", PREFIX, "AdminMenu_Not_Available"); + + return false; + } + else if (!CanUserTarget(client, target)) + { + if (client) + PrintToChat(client, "%s%t", PREFIX, "Command_Target_Not_Targetable"); + else + ReplyToCommand(client, "%s%t", PREFIX, "Command_Target_Not_Targetable"); + + return false; + } + + return true; +} + +stock bool IsAllowedBlockLength(int admin, int length, int target_count = 1) +{ + if (target_count == 1) + { + // Restriction disabled, all allowed for console, all allowed for admins with special flag + if (!ConfigMaxLength || !admin || AdmHasFlag(admin)) + return true; + + //return false if one of these statements evaluates to true; otherwise, return true + return !(!length || length > ConfigMaxLength); + } + else + { + if (length < 0) //'session punishments allowed for mass-targeting' + return true; + + //return false if one of these statements evaluates to true; otherwise, return true + return !(!length || length > MAX_TIME_MULTI || length > DefaultTime); + } +} + +stock bool AdmHasFlag(int admin) +{ + return admin && CheckCommandAccess(admin, "", UNBLOCK_FLAG, true); +} + +stock int GetAdmImmunity(int admin) +{ + if (admin == 0) + return 0; + + AdminId aid = GetUserAdmin(admin); + if (aid == INVALID_ADMIN_ID) + return 0; + + int iImmunity = GetAdminImmunityLevel(aid); + + // int iImmunity = aid.ImmunityLevel; + // int iGroupCount = aid.GroupCount; + // if (iGroupCount > 0) + // { + // int iGroupImmunity; + // char szDummy[4]; // for AdminId.GetGroup() + + // for (int iGroupID; iGroupID < iGroupCount; iGroupID++) + // { + // iGroupImmunity = (aid.GetGroup(iGroupID, szDummy, sizeof(szDummy))).ImmunityLevel; + // if (iGroupImmunity > iImmunity) + // iImmunity = iGroupImmunity; + // } + // } + + return iImmunity; +} + +stock int GetClientUserId2(int client) +{ + return client ? g_iUserIDs[client] : 0; // 0 is for CONSOLE +} + +stock void ForcePlayersRecheck() +{ + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i) && g_bPlayerAuthentified[i] && !g_bPlayerVerified[i]) + { + #if defined DEBUG + PrintToServer("Creating Recheck timer for %s", g_sSteamIDs[i]); + #endif + OnClientPostAdminCheck(i); + } + } +} + +stock bool NotApplyToThisServer(int srvID) +{ + return ConfigWhiteListOnly && FindValueInArray(g_hServersWhiteList, srvID) == -1; +} + +stock void MarkClientAsUnMuted(int target) +{ + g_MuteType[target] = bNot; + g_iMuteTime[target] = 0; + g_iMuteLength[target] = 0; + g_iMuteLevel[target] = -1; + g_sMuteAdminName[target][0] = '\0'; + g_sMuteReason[target][0] = '\0'; + g_sMuteAdminAuth[target][0] = '\0'; +} + +stock void MarkClientAsUnGagged(int target) +{ + g_GagType[target] = bNot; + g_iGagTime[target] = 0; + g_iGagLength[target] = 0; + g_iGagLevel[target] = -1; + g_sGagAdminName[target][0] = '\0'; + g_sGagReason[target][0] = '\0'; + g_sGagAdminAuth[target][0] = '\0'; +} + +stock void MarkClientAsMuted(int target, int time = NOW, int length = -1, const char[] adminName = "CONSOLE", const char[] adminAuth = "STEAM_ID_SERVER", int adminImmunity = 0, const char[] reason = "") +{ + if (time) + g_iMuteTime[target] = time; + else + g_iMuteTime[target] = GetTime(); + + g_iMuteLength[target] = length; + g_iMuteLevel[target] = adminImmunity ? adminImmunity : ConsoleImmunity; + strcopy(g_sMuteAdminName[target], sizeof(g_sMuteAdminName[]), adminName); + strcopy(g_sMuteReason[target], sizeof(g_sMuteReason[]), reason); + strcopy(g_sMuteAdminAuth[target], sizeof(g_sMuteAdminAuth[]), adminAuth); + + if (length > 0) + g_MuteType[target] = bTime; + else if (length == 0) + g_MuteType[target] = bPerm; + else + g_MuteType[target] = bSess; +} + +stock void MarkClientAsGagged(int target, int time = NOW, int length = -1, const char[] adminName = "CONSOLE", const char[] adminAuth = "STEAM_ID_SERVER", int adminImmunity = 0, const char[] reason = "") +{ + if (time) + g_iGagTime[target] = time; + else + g_iGagTime[target] = GetTime(); + + g_iGagLength[target] = length; + g_iGagLevel[target] = adminImmunity ? adminImmunity : ConsoleImmunity; + strcopy(g_sGagAdminName[target], sizeof(g_sGagAdminName[]), adminName); + strcopy(g_sGagReason[target], sizeof(g_sGagReason[]), reason); + strcopy(g_sGagAdminAuth[target], sizeof(g_sGagAdminAuth[]), adminAuth); + + if (length > 0) + g_GagType[target] = bTime; + else if (length == 0) + g_GagType[target] = bPerm; + else + g_GagType[target] = bSess; +} + +stock void CloseMuteExpireTimer(int target) +{ + if (g_hMuteExpireTimer[target] != INVALID_HANDLE && CloseHandle(g_hMuteExpireTimer[target])) + g_hMuteExpireTimer[target] = INVALID_HANDLE; +} + +stock void CloseGagExpireTimer(int target) +{ + if (g_hGagExpireTimer[target] != INVALID_HANDLE && CloseHandle(g_hGagExpireTimer[target])) + g_hGagExpireTimer[target] = INVALID_HANDLE; +} + +stock void CreateMuteExpireTimer(int target, int remainingTime = 0) +{ + if (g_iMuteLength[target] > 0) + { + DataPack dataPack; + + if (remainingTime) + g_hMuteExpireTimer[target] = CreateDataTimer(float(remainingTime), Timer_MuteExpire, dataPack, TIMER_FLAG_NO_MAPCHANGE); + else + g_hMuteExpireTimer[target] = CreateDataTimer(float(g_iMuteLength[target] * 60), Timer_MuteExpire, dataPack, TIMER_FLAG_NO_MAPCHANGE); + + dataPack.WriteCell(target); + dataPack.WriteCell(g_iUserIDs[target]); + } +} + +stock void CreateGagExpireTimer(int target, int remainingTime = 0) +{ + if (g_iGagLength[target] > 0) + { + DataPack dataPack; + + if (remainingTime) + g_hGagExpireTimer[target] = CreateDataTimer(float(remainingTime), Timer_GagExpire, dataPack, TIMER_FLAG_NO_MAPCHANGE); + else + g_hGagExpireTimer[target] = CreateDataTimer(float(g_iGagLength[target] * 60), Timer_GagExpire, dataPack, TIMER_FLAG_NO_MAPCHANGE); + + dataPack.WriteCell(target); + dataPack.WriteCell(g_iUserIDs[target]); + } +} + +stock void PerformUnMute(int target) +{ + MarkClientAsUnMuted(target); + BaseComm_SetClientMute(target, false); + CloseMuteExpireTimer(target); +} + +stock void PerformUnGag(int target) +{ + MarkClientAsUnGagged(target); + BaseComm_SetClientGag(target, false); + CloseGagExpireTimer(target); +} + +stock void PerformMute(int target, int time = NOW, int length = -1, const char[] adminName = "CONSOLE", const char[] adminAuth = "STEAM_ID_SERVER", int adminImmunity = 0, const char[] reason = "", int remaining_time = 0) +{ + MarkClientAsMuted(target, time, length, adminName, adminAuth, adminImmunity, reason); + BaseComm_SetClientMute(target, true); + CreateMuteExpireTimer(target, remaining_time); +} + +stock void PerformGag(int target, int time = NOW, int length = -1, const char[] adminName = "CONSOLE", const char[] adminAuth = "STEAM_ID_SERVER", int adminImmunity = 0, const char[] reason = "", int remaining_time = 0) +{ + MarkClientAsGagged(target, time, length, adminName, adminAuth, adminImmunity, reason); + BaseComm_SetClientGag(target, true); + CreateGagExpireTimer(target, remaining_time); +} + +stock void SavePunishment(int admin = 0, int target, int type, int length = -1, const char[] reason = "") +{ + if (type < TYPE_MUTE || type > TYPE_SILENCE) + return; + + if (!g_bPlayerAuthentified[target] || !g_bPlayerStatus[target]) + { + // The target has not been blocks verify. Can't save it in db. + ReplyToCommand(admin, "%s%t", PREFIX, "Player Comms Not Verified", g_sName[target]); + return; + } + + // target information + char targetAuth[MAX_AUTHID_LENGTH]; + if (IsClientInGame(target)) + { + strcopy(targetAuth, sizeof(targetAuth), g_sSteamIDs[target]); + } + else + { + return; + } + + char adminIp[16]; + char adminAuth[MAX_AUTHID_LENGTH]; + if (admin && IsClientInGame(admin)) + { + strcopy(adminAuth, sizeof(adminAuth), g_sSteamIDs[admin]); + strcopy(adminIp, sizeof(adminIp), g_sPlayerIP[admin]); + } + else + { + // setup dummy adminAuth and adminIp for server + strcopy(adminAuth, sizeof(adminAuth), "STEAM_ID_SERVER"); + strcopy(adminIp, sizeof(adminIp), ServerIp); + } + + char sName[MAX_NAME_LENGTH]; + strcopy(sName, sizeof(sName), g_sName[target]); + + if (DB_Connect()) + { + // Accepts length in minutes, writes to db in seconds! In all over places in plugin - length is in minutes. + char banName[MAX_NAME_LENGTH * 2 + 1]; + char banReason[256 * 2 + 1]; + char sAuthidEscaped[MAX_AUTHID_LENGTH * 2 + 1]; + char sAdminAuthIdEscaped[MAX_AUTHID_LENGTH * 2 + 1]; + char sAdminAuthIdYZEscaped[MAX_AUTHID_LENGTH * 2 + 1]; + char sQuery[4096], sQueryAdm[512], sQueryVal[1024]; + char sQueryMute[1024], sQueryGag[1024]; + sQueryMute[0] = 0; + sQueryGag[0] = 0; + + // escaping everything + g_hDatabase.Escape(sName, banName, sizeof(banName)); + g_hDatabase.Escape(reason, banReason, sizeof(banReason)); + g_hDatabase.Escape(targetAuth, sAuthidEscaped, sizeof(sAuthidEscaped)); + g_hDatabase.Escape(adminAuth, sAdminAuthIdEscaped, sizeof(sAdminAuthIdEscaped)); + g_hDatabase.Escape(adminAuth[8], sAdminAuthIdYZEscaped, sizeof(sAdminAuthIdYZEscaped)); + + // bid authid name created ends lenght reason aid adminip sid removedBy removedType removedon type ureason + FormatEx(sQueryAdm, sizeof(sQueryAdm), + "IFNULL((SELECT aid FROM %s_admins WHERE authid = '%s' OR authid REGEXP '^STEAM_[0-9]:%s$'), 0)", + DatabasePrefix, sAdminAuthIdEscaped, sAdminAuthIdYZEscaped); + + if (length >= 0) + { + // authid name, created, ends, length, reason, aid, adminIp, sid + FormatEx(sQueryVal, sizeof(sQueryVal), + "'%s', '%s', UNIX_TIMESTAMP(), UNIX_TIMESTAMP() + %d, %d, '%s', %s, '%s', %d", + sAuthidEscaped, banName, length * 60, length * 60, banReason, sQueryAdm, adminIp, serverID); + } + else // Session mutes + { + // authid name, created, ends, length, reason, aid, adminIp, sid + FormatEx(sQueryVal, sizeof(sQueryVal), + "'%s', '%s', UNIX_TIMESTAMP(), UNIX_TIMESTAMP() + %d, %d, '%s', %s, '%s', %d", + sAuthidEscaped, banName, SESSION_MUTE_FALLBACK, -1, banReason, sQueryAdm, adminIp, serverID); + } + + switch (type) + { + case TYPE_GAG: + FormatEx(sQueryGag, sizeof(sQueryGag), "(%s, %d)", sQueryVal, type); + case TYPE_MUTE: + FormatEx(sQueryMute, sizeof(sQueryMute), "(%s, %d)", sQueryVal, type); + case TYPE_SILENCE: + { + FormatEx(sQueryMute, sizeof(sQueryMute), "(%s, %d)", sQueryVal, TYPE_MUTE); + FormatEx(sQueryGag, sizeof(sQueryGag), "(%s, %d)", sQueryVal, TYPE_GAG); + } + } + + // litle magic - one query for all actions (mute, gag or silence) + FormatEx(sQuery, sizeof(sQuery), + "INSERT INTO %s_comms (authid, name, created, ends, length, reason, aid, adminIp, sid, type) VALUES %s%s%s", + DatabasePrefix, sQueryMute, type == TYPE_SILENCE ? ", " : "", sQueryGag); + + #if defined LOG_QUERIES + LogToFile(logQuery, "SavePunishment. QUERY: %s", sQuery); + #endif + + // all data cached before calling asynchronous functions + DataPack dataPack = new DataPack(); + dataPack.WriteCell(g_iUserIDs[target]); + dataPack.WriteCell(length); + dataPack.WriteCell(type); + dataPack.WriteString(reason); + dataPack.WriteString(sName); + dataPack.WriteString(targetAuth); + dataPack.WriteString(adminAuth); + dataPack.WriteString(adminIp); + + g_hDatabase.Query(Query_AddBlockInsert, sQuery, dataPack, DBPrio_High); + } + else + InsertTempBlock(length, type, sName, targetAuth, reason, adminAuth, adminIp); +} + +stock void ShowActivityToServer(int admin, int type, int length = 0, char[] reason = "", char[] targetName, bool ml = false) +{ + #if defined DEBUG + PrintToServer("ShowActivityToServer(admin: %d, type: %d, length: %d, reason: %s, name: %s, ml: %b", + admin, type, length, reason, targetName, ml); + #endif + + char actionName[32], translationName[64]; + switch (type) + { + case TYPE_MUTE: + { + if (length > 0) + strcopy(actionName, sizeof(actionName), "Muted"); + else if (length == 0) + strcopy(actionName, sizeof(actionName), "Permamuted"); + else // temp block + strcopy(actionName, sizeof(actionName), "Temp muted"); + } + //------------------------------------------------------------------------------------------------- + case TYPE_GAG: + { + if (length > 0) + strcopy(actionName, sizeof(actionName), "Gagged"); + else if (length == 0) + strcopy(actionName, sizeof(actionName), "Permagagged"); + else //temp block + strcopy(actionName, sizeof(actionName), "Temp gagged"); + } + //------------------------------------------------------------------------------------------------- + case TYPE_SILENCE: + { + if (length > 0) + strcopy(actionName, sizeof(actionName), "Silenced"); + else if (length == 0) + strcopy(actionName, sizeof(actionName), "Permasilenced"); + else //temp block + strcopy(actionName, sizeof(actionName), "Temp silenced"); + } + //------------------------------------------------------------------------------------------------- + case TYPE_UNMUTE: + { + strcopy(actionName, sizeof(actionName), "Unmuted"); + } + //------------------------------------------------------------------------------------------------- + case TYPE_UNGAG: + { + strcopy(actionName, sizeof(actionName), "Ungagged"); + } + //------------------------------------------------------------------------------------------------- + case TYPE_TEMP_UNMUTE: + { + strcopy(actionName, sizeof(actionName), "Temp unmuted"); + } + //------------------------------------------------------------------------------------------------- + case TYPE_TEMP_UNGAG: + { + strcopy(actionName, sizeof(actionName), "Temp ungagged"); + } + //------------------------------------------------------------------------------------------------- + case TYPE_TEMP_UNSILENCE: + { + strcopy(actionName, sizeof(actionName), "Temp unsilenced"); + } + //------------------------------------------------------------------------------------------------- + default: + { + return; + } + } + + Format(translationName, sizeof(translationName), "%s %s", actionName, reason[0] == '\0' ? "player" : "player reason"); + #if defined DEBUG + PrintToServer("translation name: %s", translationName); + #endif + + if (length > 0) + { + if (ml) + ShowActivity2(admin, PREFIX, "%t", translationName, targetName, length, reason); + else + ShowActivity2(admin, PREFIX, "%t", translationName, "_s", targetName, length, reason); + } + else + { + if (ml) + ShowActivity2(admin, PREFIX, "%t", translationName, targetName, reason); + else + ShowActivity2(admin, PREFIX, "%t", translationName, "_s", targetName, reason); + } +} + +stock void Forward_Block(int admin, int target, int type, int length, const char[] reason) +{ + Call_StartForward(g_hFwd_OnPlayerPunished); + Call_PushCell(admin); + Call_PushCell(target); + Call_PushCell(length); + Call_PushCell(type); + Call_PushString(reason); + Call_Finish(); +} + +stock void Forward_UnBlock(int admin, int target, int type, const char[] reason) +{ + Call_StartForward(g_hFwd_OnPlayerUnpunished); + Call_PushCell(admin); + Call_PushCell(target); + Call_PushCell(type); + Call_PushString(reason); + Call_Finish(); +} + +// Natives // +public int Native_SetClientMute(Handle hPlugin, int numParams) +{ + int target = GetNativeCell(1); + if (target < 1 || target > MaxClients) + { + ThrowNativeError(SP_ERROR_NATIVE, "Invalid client index %d", target); + return false; + } + + if (!IsClientInGame(target)) + { + ThrowNativeError(SP_ERROR_NATIVE, "Client %d is not in game", target); + return false; + } + + bool muteState = GetNativeCell(2); + int muteLength = GetNativeCell(3); + + if (muteState && muteLength == 0) + { + ThrowNativeError(SP_ERROR_NATIVE, "Permanent mute is not allowed!"); + return false; + } + + bool bSaveToDB = GetNativeCell(4); + if (!muteState && bSaveToDB) + { + ThrowNativeError(SP_ERROR_NATIVE, "Removing punishments from DB is not allowed!"); + return false; + } + + char sReason[256]; + GetNativeString(5, sReason, sizeof(sReason)); + + if (muteState) + { + if (g_MuteType[target] > bNot) + { + return false; + } + + PerformMute(target, _, muteLength, _, _, _, sReason); + Forward_Block(0, target, TYPE_MUTE, muteLength, sReason); + + if (bSaveToDB) + SavePunishment(_, target, TYPE_MUTE, muteLength, sReason); + } + else + { + if (g_MuteType[target] == bNot) + { + return false; + } + + PerformUnMute(target); + Forward_UnBlock(0, target, TYPE_UNMUTE, sReason); + } + + return true; +} + +public int Native_SetClientGag(Handle hPlugin, int numParams) +{ + int target = GetNativeCell(1); + if (target < 1 || target > MaxClients) + { + ThrowNativeError(SP_ERROR_NATIVE, "Invalid client index %d", target); + return false; + } + + if (!IsClientInGame(target)) + { + ThrowNativeError(SP_ERROR_NATIVE, "Client %d is not in game", target); + return false; + } + + bool gagState = GetNativeCell(2); + int gagLength = GetNativeCell(3); + if (gagState && gagLength == 0) + { + ThrowNativeError(SP_ERROR_NATIVE, "Permanent gag is not allowed!"); + return false; + } + + bool bSaveToDB = GetNativeCell(4); + if (!gagState && bSaveToDB) + { + ThrowNativeError(SP_ERROR_NATIVE, "Removing punishments from DB is not allowed!"); + return false; + } + + char sReason[256]; + GetNativeString(5, sReason, sizeof(sReason)); + + if (gagState) + { + if (g_GagType[target] > bNot) + { + return false; + } + + PerformGag(target, _, gagLength, _, _, _, sReason); + + if (bSaveToDB) + SavePunishment(_, target, TYPE_GAG, gagLength, sReason); + } + else + { + if (g_GagType[target] == bNot) + { + return false; + } + + PerformUnGag(target); + Forward_UnBlock(0, target, TYPE_UNGAG, sReason); + } + + return true; +} + +public int Native_GetClientMuteType(Handle hPlugin, int numParams) +{ + int target = GetNativeCell(1); + if (target < 1 || target > MaxClients) + { + ThrowNativeError(SP_ERROR_NATIVE, "Invalid client index %d", target); + return bNot; + } + + if (!IsClientInGame(target)) + { + ThrowNativeError(SP_ERROR_NATIVE, "Client %d is not in game", target); + return bNot; + } + + return g_MuteType[target]; +} + +public int Native_GetClientGagType(Handle hPlugin, int numParams) +{ + int target = GetNativeCell(1); + if (target < 1 || target > MaxClients) + { + ThrowNativeError(SP_ERROR_NATIVE, "Invalid client index %d", target); + return bNot; + } + + if (!IsClientInGame(target)) + { + ThrowNativeError(SP_ERROR_NATIVE, "Client %d is not in game", target); + return bNot; + } + + return g_GagType[target]; +} +//Yarr! diff --git a/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/sbpp_main.sp b/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/sbpp_main.sp new file mode 100644 index 0000000..e56b346 --- /dev/null +++ b/libalternator/tests/corpus/sbpp/game/addons/sourcemod/scripting/sbpp_main.sp @@ -0,0 +1,3225 @@ +// ************************************************************************* +// This file is part of SourceBans++. +// +// Copyright (C) 2014-2024 SourceBans++ Dev Team +// +// SourceBans++ is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, per version 3 of the License. +// +// SourceBans++ is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with SourceBans++. If not, see . +// +// This file is based off work(s) covered by the following copyright(s): +// +// SourceBans 1.4.11 +// Copyright (C) 2007-2015 SourceBans Team - Part of GameConnect +// Licensed under GNU GPL version 3, or later. +// Page: - +// +// ************************************************************************* + +#pragma semicolon 1 + +#include +#include +#include + +#undef REQUIRE_PLUGIN +#include + +#pragma newdecls required + +//GLOBAL DEFINES +#define DISABLE_ADDBAN 1 +#define DISABLE_UNBAN 2 + +#define FLAG_LETTERS_SIZE 26 + +#define AUTO_ADD_SERVER_DISABLED 0 +#define AUTO_ADD_SERVER_WITH_RCON 2 + +//#define DEBUG + +enum State/* ConfigState */ +{ + ConfigStateNone = 0, + ConfigStateConfig, + ConfigStateReasons, + ConfigStateHacking, + ConfigStateTime +} + +State ConfigState; + +#define Prefix "\x04[SourceBans++]\x01 " + +/* Admin Stuff */ +AdminCachePart loadPart; + +AdminFlag g_FlagLetters[FLAG_LETTERS_SIZE]; + +/* Cvar handle */ +ConVar CvarHostIp; +ConVar CvarPort; + +ConVar sb_id; + +/* Database handle */ +Database DB; +Database SQLiteDB; + +char + ServerIp[SBPP_SERVER_IP_LENGTH] + , ConfiguredServerIp[SBPP_SERVER_IP_LENGTH] + , ServerIpEscaped[SBPP_SERVER_IP_LENGTH * 2 + 1] + , ServerPort[7] + , DatabasePrefix[10] = "sb" + , WebsiteAddress[128] + , groupsLoc[128] /* Admin KeyValues */ + , adminsLoc[128] + , overridesLoc[128] + , logFile[256] /* Log Stuff */ + , g_sSteamIDs[MAXPLAYERS + 1][MAX_AUTHID_LENGTH] + , g_sName[MAXPLAYERS + 1][MAX_NAME_LENGTH] + , g_sPlayerIP[MAXPLAYERS + 1][16]; + +float RetryTime = 15.0; + +bool + loadAdmins /* Admin Stuff*/ + , loadGroups + , loadOverrides + , LateLoaded + , g_bConnecting = false + , requireSiteLogin = false /* Require a lastvisited from SB site */ + , backupConfig = true + , enableAdmins = true + , PlayerStatus[MAXPLAYERS + 1]; /* Player ban check status */ + +int + g_BanTarget[MAXPLAYERS + 1] = { -1, ... } + , g_BanTime[MAXPLAYERS + 1] = { -1, ... } + , g_BanTargetUserId[MAXPLAYERS + 1] = { -1, ... } + , AutoAdd = 0 + , curLoading + , serverID = -1 + , ProcessQueueTime = 5 + , g_ownReasons[MAXPLAYERS + 1] = { false, ... } /* Own Chat Reason */ + , CommandDisable /* Disable of addban and unban */ + , g_iUserIDs[MAXPLAYERS + 1]; + + +SMCParser ConfigParser; + +GlobalForward g_hFwd_OnBanAdded + , g_hFwd_OnReportAdded + , g_hFwd_OnClientPreAdminCheck + , g_hFwd_OnClientPostAdminCheck; + +Handle PlayerRecheck[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; /* Timer handle */ + +DataPack PlayerDataPack[MAXPLAYERS + 1] = { null, ... }; + +TopMenu hTopMenu = null; + +Menu + TimeMenuHandle + , ReasonMenuHandle + , HackingMenuHandle; + +public Plugin myinfo = +{ + name = "SourceBans++: Main Plugin", + author = "SourceBans Development Team, SourceBans++ Dev Team", + description = "Advanced ban management for the Source engine", + version = SB_VERSION, + url = "https://sbpp.github.io" +}; + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + RegPluginLibrary("sourcebans++"); + + CreateNative("SBBanPlayer", Native_SBBanPlayer); + CreateNative("SBPP_BanPlayer", Native_SBBanPlayer); + CreateNative("SBPP_BanPlayerBySteamId", Native_SBPP_BanPlayerBySteamId); + CreateNative("SBPP_ReportPlayer", Native_SBReportPlayer); + + g_hFwd_OnBanAdded = CreateGlobalForward("SBPP_OnBanPlayer", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_String); + g_hFwd_OnReportAdded = CreateGlobalForward("SBPP_OnReportPlayer", ET_Ignore, Param_Cell, Param_Cell, Param_String); + g_hFwd_OnClientPreAdminCheck = CreateGlobalForward("SBPP_OnClientPreAdminCheck", ET_Ignore, Param_Cell); + g_hFwd_OnClientPostAdminCheck = CreateGlobalForward("SBPP_OnClientPostAdminCheck", ET_Ignore, Param_Cell); + + LateLoaded = late; + + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("common.phrases"); + LoadTranslations("plugin.basecommands"); + LoadTranslations("sbpp_main.phrases"); + LoadTranslations("basebans.phrases"); + loadAdmins = loadGroups = loadOverrides = false; + + CvarHostIp = FindConVar("hostip"); + CvarPort = FindConVar("hostport"); + CreateConVar("sb_version", SB_VERSION, _, FCVAR_SPONLY | FCVAR_REPLICATED | FCVAR_NOTIFY); + RegServerCmd("sm_rehash", sm_rehash, "Reload SQL admins"); + RegAdminCmd("sm_ban", CommandBan, ADMFLAG_BAN, "sm_ban <#userid|name> [reason]", "sourcebans"); + RegAdminCmd("sm_banip", CommandBanIp, ADMFLAG_BAN, "sm_banip