diff --git a/Source/control.cpp b/Source/control.cpp index 0f9a1d496..aecb162e1 100644 --- a/Source/control.cpp +++ b/Source/control.cpp @@ -48,6 +48,7 @@ #include "utils/format_int.hpp" #include "utils/language.h" #include "utils/log.hpp" +#include "utils/parse_int.hpp" #include "utils/sdl_geometry.h" #include "utils/stdcompat/optional.hpp" #include "utils/str_case.hpp" @@ -388,9 +389,9 @@ std::string TextCmdArena(const string_view parameter) return ret; } - int arenaNumber = atoi(parameter.data()); - _setlevels arenaLevel = static_cast<_setlevels>(arenaNumber - 1 + SL_FIRST_ARENA); - if (arenaNumber < 0 || !IsArenaLevel(arenaLevel)) { + const ParseIntResult parsedParam = ParseInt(parameter, /*min=*/0); + const _setlevels arenaLevel = parsedParam.ok() ? static_cast<_setlevels>(parsedParam.value - 1 + SL_FIRST_ARENA) : _setlevels::SL_NONE; + if (!IsArenaLevel(arenaLevel)) { StrAppend(ret, _("Invalid arena-number. Valid numbers are:")); AppendArenaOverview(ret); return ret; @@ -413,10 +414,11 @@ std::string TextCmdArenaPot(const string_view parameter) StrAppend(ret, _("Arenas are only supported in multiplayer.")); return ret; } + int numPots = ParseInt(parameter, /*min=*/1).value_or(1); Player &myPlayer = *MyPlayer; - for (int potNumber = std::max(1, atoi(parameter.data())); potNumber > 0; potNumber--) { + for (int potNumber = numPots; potNumber > 0; potNumber--) { Item item {}; InitializeItem(item, IDI_ARENAPOT); GenerateNewSeed(item); diff --git a/Source/debug.cpp b/Source/debug.cpp index 59355a775..f8e6ab0ed 100644 --- a/Source/debug.cpp +++ b/Source/debug.cpp @@ -33,6 +33,7 @@ #include "utils/file_util.h" #include "utils/language.h" #include "utils/log.hpp" +#include "utils/parse_int.hpp" #include "utils/str_case.hpp" #include "utils/str_cat.hpp" #include "utils/str_split.hpp" @@ -192,8 +193,11 @@ std::string DebugCmdTakeGoldCheat(const string_view parameter) std::string DebugCmdWarpToLevel(const string_view parameter) { Player &myPlayer = *MyPlayer; - auto level = atoi(parameter.data()); - if (level < 0 || level > (gbIsHellfire ? 24 : 16)) + const ParseIntResult parsedParam = ParseInt(parameter, /*min=*/0); + if (!parsedParam.ok()) + return "Specify the level number"; + const int level = parsedParam.value; + if (level > (gbIsHellfire ? 24 : 16)) return StrCat("Level ", level, " is not known. Do you want to write a mod?"); if (!setlevel && myPlayer.isOnLevel(level)) return StrCat("I did nothing but fulfilled your wish. You are already at level ", level, "."); @@ -214,7 +218,10 @@ std::string DebugCmdLoadQuestMap(const string_view parameter) return ret; } - auto level = atoi(parameter.data()); + const ParseIntResult parsedParam = ParseInt(parameter, /*min=*/0); + if (!parsedParam.ok()) + return "Specify the level number"; + const int level = parsedParam.value; if (level < 1) return "Map id must be 1 or higher"; if (setlevel && setlvlnum == level) @@ -245,15 +252,24 @@ std::string DebugCmdLoadMap(const string_view parameter) case 0: TestMapPath = StrCat(arg, ".dun"); break; - case 1: - mapType = atoi(std::string(arg).c_str()); - break; - case 2: - spawn.x = atoi(std::string(arg).c_str()); - break; - case 3: - spawn.y = atoi(std::string(arg).c_str()); - break; + case 1: { + const ParseIntResult parsedArg = ParseInt(arg, /*min=*/0); + if (!parsedArg.ok()) + return "Failed to parse argument 1 as integer"; + mapType = parsedArg.value; + } break; + case 2: { + const ParseIntResult parsedArg = ParseInt(arg); + if (!parsedArg.ok()) + return "Failed to parse argument 2 as integer"; + spawn.x = parsedArg.value; + } break; + case 3: { + const ParseIntResult parsedArg = ParseInt(arg); + if (!parsedArg.ok()) + return "Failed to parse argument 3 as integer"; + spawn.y = parsedArg.value; + } break; } count++; } @@ -398,15 +414,23 @@ std::string DebugCmdResetLevel(const string_view parameter) auto it = args.begin(); if (it == args.end()) return "What level do you want to visit?"; - auto level = atoi(std::string(*it).c_str()); - if (level < 0 || level > (gbIsHellfire ? 24 : 16)) + int level; + { + const ParseIntResult parsedArg = ParseInt(*it, /*min=*/0); + if (!parsedArg.ok()) + return "Failed to parse argument 1 as integer"; + level = parsedArg.value; + } + if (level > (gbIsHellfire ? 24 : 16)) return StrCat("Level ", level, " is not known. Do you want to write an extension mod?"); myPlayer._pLvlVisited[level] = false; DeltaClearLevel(level); if (++it != args.end()) { - const auto seed = static_cast(std::stoul(std::string(*it))); - glSeedTbl[level] = seed; + const ParseIntResult parsedArg = ParseInt(*it); + if (!parsedArg.ok()) + return "Failed to parse argument 2 as uint32_t"; + glSeedTbl[level] = parsedArg.value; } if (myPlayer.isOnLevel(level)) @@ -489,7 +513,10 @@ std::string DebugCmdQuest(const string_view parameter) return "Happy questing"; } - int questId = atoi(parameter.data()); + const ParseIntResult parsedArg = ParseInt(parameter, /*min=*/0); + if (!parsedArg.ok()) + return "Failed to parse argument as integer"; + const int questId = parsedArg.value; if (questId >= MAXQUESTS) return StrCat("Quest ", questId, " is not known. Do you want to write a mod?"); @@ -506,7 +533,7 @@ std::string DebugCmdQuest(const string_view parameter) std::string DebugCmdLevelUp(const string_view parameter) { - int levels = std::max(1, atoi(parameter.data())); + const int levels = ParseInt(parameter, /*min=*/1).value_or(1); for (int i = 0; i < levels; i++) NetSendCmd(true, CMD_CHEAT_EXPERIENCE); return "New experience leads to new insights."; @@ -534,7 +561,10 @@ std::string DebugCmdMinStats(const string_view parameter) std::string DebugCmdSetSpellsLevel(const string_view parameter) { - uint8_t level = static_cast(std::max(0, atoi(parameter.data()))); + const ParseIntResult parsedArg = ParseInt(parameter); + if (!parsedArg.ok()) + return "Failed to parse argument as uint8_t"; + const uint8_t level = parsedArg.value; for (uint8_t i = static_cast(SpellID::Firebolt); i < MAX_SPELLS; i++) { if (GetSpellBookLevel(static_cast(i)) != -1) { NetSendCmdParam2(true, CMD_CHANGE_SPELL_LEVEL, i, level); @@ -562,8 +592,12 @@ std::string DebugCmdChangeHealth(const string_view parameter) Player &myPlayer = *MyPlayer; int change = -1; - if (!parameter.empty()) - change = atoi(parameter.data()); + if (!parameter.empty()) { + const ParseIntResult parsedArg = ParseInt(parameter); + if (!parsedArg.ok()) + return "Failed to parse argument as integer"; + change = parsedArg.value; + } if (change == 0) return "Health hasn't changed."; @@ -581,8 +615,12 @@ std::string DebugCmdChangeMana(const string_view parameter) Player &myPlayer = *MyPlayer; int change = -1; - if (!parameter.empty()) - change = atoi(parameter.data()); + if (!parameter.empty()) { + const ParseIntResult parsedArg = ParseInt(parameter); + if (!parsedArg.ok()) + return "Failed to parse argument as integer"; + change = parsedArg.value; + } if (change == 0) return "Mana hasn't changed."; @@ -659,7 +697,10 @@ std::string DebugCmdSpawnUniqueMonster(const string_view parameter) std::string name; int count = 1; for (string_view arg : SplitByChar(parameter, ' ')) { - const int num = atoi(std::string(arg).c_str()); + const ParseIntResult parsedArg = ParseInt(arg); + if (!parsedArg.ok()) + return "Failed to parse argument as integer"; + const int num = parsedArg.value; if (num > 0) { count = num; break; @@ -746,7 +787,10 @@ std::string DebugCmdSpawnMonster(const string_view parameter) std::string name; int count = 1; for (string_view arg : SplitByChar(parameter, ' ')) { - const int num = atoi(std::string(arg).c_str()); + const ParseIntResult parsedArg = ParseInt(arg); + if (!parsedArg.ok()) + return "Failed to parse argument as integer"; + const int num = parsedArg.value; if (num > 0) { count = num; break; @@ -920,7 +964,10 @@ std::string DebugCmdQuestInfo(const string_view parameter) return ret; } - int questId = atoi(parameter.data()); + const ParseIntResult parsedArg = ParseInt(parameter, /*min=*/0); + if (!parsedArg.ok()) + return "Failed to parse argument as integer"; + const int questId = parsedArg.value; if (questId >= MAXQUESTS) return StrCat("Quest ", questId, " is not known. Do you want to write a mod?"); @@ -930,7 +977,14 @@ std::string DebugCmdQuestInfo(const string_view parameter) std::string DebugCmdPlayerInfo(const string_view parameter) { - int playerId = atoi(parameter.data()); + if (parameter.empty()) { + return StrCat("Provide a player ID between 0 and ", Players.size() - 1); + } + const ParseIntResult parsedArg = ParseInt(parameter); + if (!parsedArg.ok()) { + return "Failed to parse argument as size_t in range"; + } + const size_t playerId = parsedArg.value; if (static_cast(playerId) >= Players.size()) return "My friend, we need a valid playerId."; Player &player = Players[playerId]; diff --git a/Source/diablo.cpp b/Source/diablo.cpp index 48e33ce8a..a0457682b 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -79,6 +79,7 @@ #include "utils/console.h" #include "utils/display.h" #include "utils/language.h" +#include "utils/parse_int.hpp" #include "utils/paths.h" #include "utils/stdcompat/string_view.hpp" #include "utils/str_cat.hpp" @@ -949,13 +950,18 @@ void PrintHelpOption(string_view flags, string_view description) diablo_quit(0); } -void PrintFlagsRequiresArgument(string_view flag) +void PrintFlagMessage(string_view flag, string_view message) { printInConsole(flag); - printInConsole(" requires an argument"); + printInConsole(message); printNewlineInConsole(); } +void PrintFlagRequiresArgument(string_view flag) +{ + PrintFlagMessage(flag, " requires an argument"); +} + void DiabloParseFlags(int argc, char **argv) { #ifdef _DEBUG @@ -980,44 +986,54 @@ void DiabloParseFlags(int argc, char **argv) diablo_quit(0); } else if (arg == "--data-dir") { if (i + 1 == argc) { - PrintFlagsRequiresArgument("--data-dir"); + PrintFlagRequiresArgument("--data-dir"); diablo_quit(64); } paths::SetBasePath(argv[++i]); } else if (arg == "--save-dir") { if (i + 1 == argc) { - PrintFlagsRequiresArgument("--save-dir"); + PrintFlagRequiresArgument("--save-dir"); diablo_quit(64); } paths::SetPrefPath(argv[++i]); } else if (arg == "--config-dir") { if (i + 1 == argc) { - PrintFlagsRequiresArgument("--config-dir"); + PrintFlagRequiresArgument("--config-dir"); diablo_quit(64); } paths::SetConfigPath(argv[++i]); } else if (arg == "--lang") { if (i + 1 == argc) { - PrintFlagsRequiresArgument("--lang"); + PrintFlagRequiresArgument("--lang"); diablo_quit(64); } forceLocale = argv[++i]; #ifndef DISABLE_DEMOMODE } else if (arg == "--demo") { if (i + 1 == argc) { - PrintFlagsRequiresArgument("--demo"); + PrintFlagRequiresArgument("--demo"); diablo_quit(64); } - demoNumber = SDL_atoi(argv[++i]); + ParseIntResult parsedParam = ParseInt(argv[++i]); + if (!parsedParam.ok()) { + PrintFlagMessage("--demo", " must be a number"); + diablo_quit(64); + } + demoNumber = parsedParam.value; gbShowIntro = false; } else if (arg == "--timedemo") { timedemo = true; } else if (arg == "--record") { if (i + 1 == argc) { - PrintFlagsRequiresArgument("--record"); + PrintFlagRequiresArgument("--record"); + diablo_quit(64); + } + ParseIntResult parsedParam = ParseInt(argv[++i]); + if (!parsedParam.ok()) { + PrintFlagMessage("--record", " must be a number"); diablo_quit(64); } - recordNumber = SDL_atoi(argv[++i]); + recordNumber = parsedParam.value; } else if (arg == "--create-reference") { createDemoReference = true; #else diff --git a/Source/pfile.cpp b/Source/pfile.cpp index 7f25e28f9..a3bb1b4e3 100644 --- a/Source/pfile.cpp +++ b/Source/pfile.cpp @@ -24,6 +24,7 @@ #include "utils/endian.hpp" #include "utils/file_util.h" #include "utils/language.h" +#include "utils/parse_int.hpp" #include "utils/paths.h" #include "utils/stdcompat/abs.hpp" #include "utils/stdcompat/string_view.hpp" @@ -284,8 +285,10 @@ void CreateDetailDiffs(string_view prefix, string_view memoryMapFile, CompareInf auto it = counter.find(counterAsString); if (it != counter.end()) return it->second; - int countFromMapFile = std::stoi(counterAsString); - return CompareCounter { countFromMapFile, countFromMapFile }; + const ParseIntResult countFromMapFile = ParseInt(counterAsString); + if (!countFromMapFile.ok()) + app_fatal(StrCat("Failed to parse ", counterAsString, " as int")); + return CompareCounter { countFromMapFile.value, countFromMapFile.value }; }; auto addDiff = [&](const std::string &diffKey) { auto it = foundDiffs.find(diffKey); @@ -363,7 +366,10 @@ void CreateDetailDiffs(string_view prefix, string_view memoryMapFile, CompareInf if (command == "R" || command == "LT" || command == "LC" || command == "LC_LE") { const auto bitsAsString = std::string(*++it); const auto comment = std::string(*++it); - size_t bytes = static_cast(std::stoi(bitsAsString) / 8); + const ParseIntResult parsedBytes = ParseInt(bitsAsString); + if (!parsedBytes.ok()) + app_fatal(StrCat("Failed to parse ", bitsAsString, " as size_t")); + const size_t bytes = static_cast(parsedBytes.value / 8); if (command == "LT") { int32_t valueReference = read32BitInt(compareInfoReference, false); @@ -389,7 +395,10 @@ void CreateDetailDiffs(string_view prefix, string_view memoryMapFile, CompareInf string_view comment = *++it; CompareCounter count = getCounter(countAsString); - size_t bytes = static_cast(std::stoi(bitsAsString) / 8); + const ParseIntResult parsedBytes = ParseInt(bitsAsString); + if (!parsedBytes.ok()) + app_fatal(StrCat("Failed to parse ", bitsAsString, " as size_t")); + const size_t bytes = static_cast(parsedBytes.value / 8); for (int i = 0; i < count.max(); i++) { count.checkIfDataExists(i, compareInfoReference, compareInfoActual); if (!compareBytes(bytes)) { diff --git a/Source/utils/parse_int.hpp b/Source/utils/parse_int.hpp new file mode 100644 index 000000000..d26178779 --- /dev/null +++ b/Source/utils/parse_int.hpp @@ -0,0 +1,51 @@ +#pragma once + +#if __cplusplus >= 201703L +#include +#include +#endif + +#include "utils/stdcompat/string_view.hpp" + +namespace devilution { + +enum class ParseIntStatus { + Ok, + ParseError, + OutOfRange +}; + +template +struct ParseIntResult { + ParseIntStatus status; + IntT value = 0; + + [[nodiscard]] bool ok() const + { + return status == ParseIntStatus::Ok; + } + + template + [[nodiscard]] IntT value_or(T defaultValue) const // NOLINT(readability-identifier-naming) + { + return ok() ? value : static_cast(defaultValue); + } +}; + +template +ParseIntResult ParseInt( + string_view str, IntT min = std::numeric_limits::min(), + IntT max = std::numeric_limits::max()) +{ + IntT value; + const std::from_chars_result result = std::from_chars(str.data(), str.data() + str.size(), value); + if (result.ec == std::errc::invalid_argument) + return ParseIntResult { ParseIntStatus::ParseError }; + if (result.ec == std::errc::result_out_of_range || value < min || value > max) + return ParseIntResult { ParseIntStatus::OutOfRange }; + if (result.ec != std::errc()) + return ParseIntResult { ParseIntStatus::ParseError }; + return ParseIntResult { ParseIntStatus::Ok, value }; +} + +} // namespace devilution