diff --git a/CMakeLists.txt b/CMakeLists.txt index b58b3dd..aa76ba0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.4.1) -set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD 20) add_definitions(-DTESTING) @@ -15,6 +15,8 @@ list(FILTER TEST_SOURCES INCLUDE REGEX "(.*_test\\.cpp)|(test_.*\\.cpp)$") #message("TEST_SOURCES: ${TEST_SOURCES}") if(MSVC) + add_compile_options(/std:c++latest) + # From https://stackoverflow.com/questions/10113017/setting-the-msvc-runtime-in-cmake # Default to statically-linked runtime. if("${MSVC_RUNTIME}" STREQUAL "") @@ -58,7 +60,6 @@ add_library( # Sets the name of the library. # Provides a relative path to your source file(s). ${SOURCES} ) -# TODO: Coverage stuff should not be done unconditionally SET(GCC_COVERAGE_COMPILE_FLAGS "-Wall -fprofile-arcs -ftest-coverage -g -O0") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${GCC_COVERAGE_COMPILE_FLAGS}") @@ -105,5 +106,6 @@ add_subdirectory(${CMAKE_BINARY_DIR}/googletest-src # Now simply link against gtest or gtest_main or gmock_main as needed. add_executable(runUnitTests ${TEST_SOURCES}) target_link_libraries(runUnitTests gmock_main) +target_link_libraries(runUnitTests ssl crypto) target_link_libraries(runUnitTests psicash) #add_test(NAME example_test COMMAND runUnitTests) diff --git a/README.md b/README.md index 02df61c..924954a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ This library relies on the native environment to provide an HTTP request callbac There is an _example_ implementation in the Android wrapper project. But note that it _does not support proxied requests_, which may be necessary depending on the environment. (E.g., it probably doesn't matter on iOS, since our app only supported full-device VPN. But on Windows the app mostly uses a local proxy, so the HTTP Requester must support proxying.) +Note that the requester _must_ do HTTPS certificate validation. + ### Thread Safety All datastore reads and writes are mutexed. So, the only consistency guarantee is for individual data accesses. This means that multiple data accesses might get data from different states; for example, between getting the balance and getting the purchases list, there might have been a purchase, which would alter the balance. We will state this in positive terms as: "you will always get the very latest value". This is fine for our use cases at this time, but we might want to add "get a consistent set of data" in the future. @@ -34,3 +36,38 @@ If Android Studio is saying that a new `.cpp` file is not part of the project, g If you get a `SIGABRT` error in JNI code: You have probably triggered a JNI exception (that hasn't been cleared). It's possible that it's expected or acceptable and you just need to clear it, but it's more likely that it's a bug. If you get a `SIGSEGV` error when hitting a breakpoint in JNI code: Yeah, beats me. I get it on MacOS but not Windows. + +## State diagram + +``` + +-----------+ + | | + | no tokens +<----------------------------+ + | | | + +-----+-----+ | + | | + + | + RefreshState+NewTracker | + + | + | | + v | ++--------+--------+ + +| | ResetUser() +| Tracker tokens | + +| | | ++--------+--------+ | + | | + + | + Login | + + | + v +-----+------------------------+ ++--------+--------+ token expiry | | +| +------------------->+ IsAccount() && !HasTokens() | +| Account tokens | | | +| +------------------->+ "logged out" | ++--------------+--+ AccountLogout() | | + ^ +--+---------------------------+ + | | + +--------------------------+ + AccountLogin() +``` diff --git a/build-windows.bat b/build-windows.bat index 05afbc4..efb7ee2 100644 --- a/build-windows.bat +++ b/build-windows.bat @@ -12,15 +12,15 @@ chdir build REM Make the MSVC project -cmake -G "Visual Studio 14 2015" .. +cmake -G "Visual Studio 16 2019" -A Win32 .. if "%ERRORLEVEL%" == "1" exit /B 1 REM Build for Debug and MinSizeRel -call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin\vcvars32.bat" +call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars32.bat" if "%ERRORLEVEL%" == "1" exit /B 1 -msbuild.exe -p:Configuration=Debug -p:PlatformToolset=v140_xp -p:PreferredToolArchitecture=x86 psicash.vcxproj +msbuild.exe -p:Configuration=Debug -p:PlatformToolset=v140 -p:PreferredToolArchitecture=x86 -p:Platform=x86 -p:PlatformTarget=x86 psicash.vcxproj if "%ERRORLEVEL%" == "1" exit /B 1 -msbuild.exe -p:Configuration=MinSizeRel -p:PlatformToolset=v140_xp -p:PreferredToolArchitecture=x86 psicash.vcxproj +msbuild.exe -p:Configuration=MinSizeRel -p:PlatformToolset=v140 -p:PreferredToolArchitecture=x86 -p:Platform=x86 -p:PlatformTarget=x86 psicash.vcxproj if "%ERRORLEVEL%" == "1" exit /B 1 REM Resulting libs (and pdb) are in build/Debug and build/MinSizeRel @@ -32,3 +32,6 @@ REM TODO: put exported include files into an "include" directory and modify buil robocopy /V . dist-windows datetime.hpp error.hpp url.hpp psicash.hpp robocopy /V vendor\ dist-windows\vendor /S git describe --always --long --dirty --tags > dist-windows/git.txt + +del /F /S /Q build 1>nul +rmdir /S /Q build diff --git a/datastore.cpp b/datastore.cpp index 292411b..2f5b0b2 100644 --- a/datastore.cpp +++ b/datastore.cpp @@ -26,8 +26,9 @@ using json = nlohmann::json; +namespace psicash { + using namespace std; -using namespace psicash; using namespace error; static string FilePath(const string& file_root, const string& suffix); @@ -52,46 +53,63 @@ Error Datastore::Init(const string& file_root, const string& suffix) { #define MUST_BE_INITIALIZED if (!initialized_) { return MakeCriticalError("must only be called on an initialized datastore"); } -Error Datastore::Clear(const string& file_path) { +Error Datastore::Reset(const string& file_path, json new_value) { SYNCHRONIZE(mutex_); paused_ = false; - auto empty_json = json::object(); - if (auto err = FileStore(paused_, file_path, empty_json)) { + if (auto err = FileStore(paused_, file_path, new_value)) { return PassError(err); } - json_ = empty_json; + json_ = new_value; return error::nullerr; } -Error Datastore::Clear(const string& file_root, const string& suffix) { - return PassError(Clear(FilePath(file_root, suffix))); +Error Datastore::Reset(const string& file_root, const string& suffix, json new_value) { + return PassError(Reset(FilePath(file_root, suffix), new_value)); } -Error Datastore::Clear() { +Error Datastore::Reset(json new_value) { SYNCHRONIZE(mutex_); MUST_BE_INITIALIZED; - return PassError(Clear(file_path_)); + return PassError(Reset(file_path_, new_value)); } -void Datastore::PauseWrites() { +bool Datastore::PauseWrites() { SYNCHRONIZE(mutex_); + auto was_paused = paused_; paused_ = true; + return !was_paused; } -Error Datastore::UnpauseWrites() { +Error Datastore::UnpauseWrites(bool commit) { SYNCHRONIZE(mutex_); MUST_BE_INITIALIZED; if (!paused_) { return nullerr; } paused_ = false; - return PassError(FileStore(paused_, file_path_, json_)); + if (commit) { + return PassError(FileStore(paused_, file_path_, json_)); + } + + // Revert to what's on disk + auto res = FileLoad(file_path_); + if (!res) { + return PassError(res.error()); + } + json_ = *res; + return nullerr; +} + +error::Result Datastore::Get() const { + SYNCHRONIZE(mutex_); + MUST_BE_INITIALIZED; + return json_; } -Error Datastore::Set(const json& in) { +Error Datastore::Set(const json::json_pointer& p, json v) { SYNCHRONIZE(mutex_); MUST_BE_INITIALIZED; - json_.update(in); + json_[p] = v; return PassError(FileStore(paused_, file_path_, json_)); } @@ -143,6 +161,14 @@ static Result FileLoad(const string& file_path) { // We'll continue on with the rest of the logic, which will read the new empty file. } + uint64_t file_size = 0; + if (auto err = utils::FileSize(file_path, file_size)) { + return WrapError(err, utils::Stringer("unable to get file size; errno=", errno)); + } + if (file_size == 0) { + return MakeCriticalError("file size is zero"); + } + ifstream f; f.open(file_path, ios::in | ios::binary); if (!f) { @@ -214,3 +240,5 @@ static Error FileStore(bool paused, const string& file_path, const json& json) { return nullerr; } + +} // namespace psicash diff --git a/datastore.hpp b/datastore.hpp index c095ddb..6d49d30 100644 --- a/datastore.hpp +++ b/datastore.hpp @@ -50,60 +50,66 @@ class Datastore { /// Returns false if there's an unrecoverable error (such as an inability to use the filesystem). error::Error Init(const std::string& file_root, const std::string& suffix); - /// Clears the in-memory structure and the persistent file. + /// Resets the in-memory structure and the persistent file, setting it to `new_value` + /// (which may be an empty object). /// Calling this does not change the initialized state. If the datastore was already /// initialized with a different file_root+suffix, then the result is undefined. - error::Error Clear(const std::string& file_root, const std::string& suffix); + error::Error Reset(const std::string& file_root, const std::string& suffix, json new_value); - /// Clears the in-memory structure and the persistent file. + /// Reset the in-memory structure and the persistent file, setting it to `new_value` + /// (which may be an empty object). /// Calling this does not change the initialized state. /// Init() must have already been called, successfully. - error::Error Clear(); + error::Error Reset(json new_value); /// Stops writing of updates to disk until UnpauseWrites is called. - void PauseWrites(); - /// Unpauses writing and causes an immediate write. - error::Error UnpauseWrites(); + /// Returns false if writing was already paused (so this call did nothing). + bool PauseWrites(); + /// Unpauses writing. If commit is true, it writes the changes immediately; if false + /// it discards the changes. + error::Error UnpauseWrites(bool commit); /// Returns the value, or an error indicating the failure reason. template - nonstd::expected Get(const char* key) const { + nonstd::expected Get(const json::json_pointer& p) const { try { // Not returning inside the synchronize block to avoid compiler warning about // "control reached end of non-void function without returning a value". T val; SYNCHRONIZE_BLOCK(mutex_) { + // Not using MUST_BE_INITIALIZED so we don't need it in the header. if (!initialized_) { return nonstd::make_unexpected(kDatastoreUninitialized); } - if (json_.find(key) == json_.end()) { + + if (p.empty() || !json_.contains(p)) { return nonstd::make_unexpected(kNotFound); } - val = json_[key].get(); + val = json_.at(p).get(); } return val; } - catch (json::type_error& e) { + catch (json::type_error&) { return nonstd::make_unexpected(kTypeMismatch); } - catch (json::out_of_range& e) { + catch (json::out_of_range&) { // This should be avoided by the explicit check above. But we'll be safe. return nonstd::make_unexpected(kNotFound); } } - /// To set a single key-value: `set({{"k1", "v1"}})`. - /// To set multiple key-values: `set({{"k1", "v1"}, {"k2", "v2"}})`. - /// NOTE: If you use too few curly braces, you'll accidentally create arrays instead of objects. + error::Result Get() const; + + // Sets the value v in the datastore at path p. /// NOTE: Set is not atomic. If the file operation fails, the intermediate object will still be /// updated. We may want this to be otherwise in the future, but for now I think that it's preferable. /// Returns false if the file operation failed. - error::Error Set(const json& in); + error::Error Set(const json::json_pointer& p, json v); protected: - /// Helper for the public Clear methods - error::Error Clear(const std::string& file_path); + /// Helper for the public Reset methods + error::Error Reset(const std::string& file_path, json new_value); private: mutable std::recursive_mutex mutex_; diff --git a/datastore_test.cpp b/datastore_test.cpp index fff12d5..599f812 100644 --- a/datastore_test.cpp +++ b/datastore_test.cpp @@ -46,10 +46,11 @@ TEST_F(TestDatastore, InitSimple) TEST_F(TestDatastore, InitCorrupt) { auto temp_dir = GetTempDir(); - WriteBadData(temp_dir.c_str(), ds_suffix); + auto ok = WriteBadData(temp_dir.c_str(), true); + ASSERT_TRUE(ok); Datastore ds; - auto err = ds.Init(temp_dir.c_str(), ds_suffix); + auto err = ds.Init(temp_dir.c_str(), GetSuffix(true)); ASSERT_TRUE(err); ASSERT_GT(err.ToString().length(), 0); } @@ -79,10 +80,11 @@ TEST_F(TestDatastore, CheckPersistence) ASSERT_FALSE(err); string want = "v"; - err = ds->Set({{"k", want}}); + auto k = "/k"_json_pointer; + err = ds->Set(k, want); ASSERT_FALSE(err); - auto got = ds->Get("k"); + auto got = ds->Get(k); ASSERT_TRUE(got); ASSERT_EQ(*got, want); @@ -94,14 +96,14 @@ TEST_F(TestDatastore, CheckPersistence) err = ds->Init(temp_dir.c_str(), ds_suffix); ASSERT_FALSE(err); - got = ds->Get("k"); + got = ds->Get(k); ASSERT_TRUE(got); ASSERT_EQ(*got, want); delete ds; } -TEST_F(TestDatastore, Clear) +TEST_F(TestDatastore, Reset) { auto temp_dir = GetTempDir(); @@ -110,10 +112,11 @@ TEST_F(TestDatastore, Clear) ASSERT_FALSE(err); string want = "v"; - err = ds->Set({{"k", want}}); + auto k = "/k"_json_pointer; + err = ds->Set(k, want); ASSERT_FALSE(err); - auto got = ds->Get("k"); + auto got = ds->Get(k); ASSERT_TRUE(got); ASSERT_EQ(*got, want); @@ -125,55 +128,78 @@ TEST_F(TestDatastore, Clear) err = ds->Init(temp_dir.c_str(), ds_suffix); ASSERT_FALSE(err); - got = ds->Get("k"); + got = ds->Get(k); ASSERT_TRUE(got); ASSERT_EQ(*got, want); delete ds; - // Clear with arguments + // Reset with arguments ds = new Datastore(); - err = ds->Clear(temp_dir.c_str(), ds_suffix); + err = ds->Reset(temp_dir.c_str(), ds_suffix, {}); ASSERT_FALSE(err) << err.ToString(); // First Get without calling Init; should get "not initialized" error - got = ds->Get("k"); + got = ds->Get(k); ASSERT_FALSE(got); ASSERT_EQ(got.error(), psicash::Datastore::kDatastoreUninitialized); + // Then initialize and try again err = ds->Init(temp_dir.c_str(), ds_suffix); ASSERT_FALSE(err); - got = ds->Get("k"); + // Key should not be found, since we haven't set it + got = ds->Get(k); ASSERT_FALSE(got); ASSERT_EQ(got.error(), psicash::Datastore::kNotFound); - err = ds->Set({{"k", want}}); + // Set it + err = ds->Set(k, want); ASSERT_FALSE(err); - got = ds->Get("k"); + // Get it for real + got = ds->Get(k); ASSERT_TRUE(got); ASSERT_EQ(*got, want); delete ds; - // Clear without arguments + // Reset without arguments ds = new Datastore(); err = ds->Init(temp_dir.c_str(), ds_suffix); ASSERT_FALSE(err); - got = ds->Get("k"); + got = ds->Get(k); ASSERT_TRUE(got); ASSERT_EQ(*got, want); - err = ds->Clear(); + err = ds->Reset({}); ASSERT_FALSE(err); - got = ds->Get("k"); + got = ds->Get(k); ASSERT_FALSE(got); ASSERT_EQ(got.error(), psicash::Datastore::kNotFound); delete ds; + + // Reset with non-empty new value + temp_dir = GetTempDir(); // use a fresh dir to avoid pollution + ds = new Datastore(); + err = ds->Init(temp_dir.c_str(), ds_suffix); + ASSERT_FALSE(err); + + got = ds->Get("/k"_json_pointer); + ASSERT_FALSE(got); + + err = ds->Reset({{"k", want}}); + ASSERT_FALSE(err); + + got = ds->Get("/k"_json_pointer); + ASSERT_TRUE(got); + ASSERT_EQ(*got, want); + + delete ds; + } TEST_F(TestDatastore, WritePause) @@ -185,22 +211,38 @@ TEST_F(TestDatastore, WritePause) ASSERT_FALSE(err); // This should persist - string pause_want1 = "pause_want1"; - err = ds->Set({{pause_want1, pause_want1}}); + auto pause_want1 = "/pause_want1"_json_pointer; + err = ds->Set(pause_want1, pause_want1.to_string()); ASSERT_FALSE(err); - // This should persist + // This should persist, as we're committing + ds->PauseWrites(); + auto pause_want2 = "/pause_want2"_json_pointer; + err = ds->Set(pause_want2, pause_want2.to_string()); + ASSERT_FALSE(err); + err = ds->UnpauseWrites(/*commit=*/true); + ASSERT_FALSE(err); + + // This should NOT persist, as we're rolling back + ds->PauseWrites(); + auto pause_want3 = "/pause_want3"_json_pointer; + err = ds->Set(pause_want3, pause_want3.to_string()); + ASSERT_FALSE(err); + err = ds->UnpauseWrites(/*commit=*/false); + ASSERT_FALSE(err); + + // Another committed value, to make sure the order of things doesn't matter ds->PauseWrites(); - string pause_want2 = "pause_want2"; - err = ds->Set({{pause_want2, pause_want2}}); + auto pause_want4 = "/pause_want4"_json_pointer; + err = ds->Set(pause_want4, pause_want4.to_string()); ASSERT_FALSE(err); - err = ds->UnpauseWrites(); + err = ds->UnpauseWrites(/*commit=*/true); ASSERT_FALSE(err); - // This should NOT persist, since we'll close before unpausing + // This should also NOT persist, since we're hitting the dtor ds->PauseWrites(); - string pause_want3 = "pause_want3"; - err = ds->Set({{pause_want3, pause_want3}}); + auto pause_want5 = "/pause_want5"_json_pointer; + err = ds->Set(pause_want5, pause_want5.to_string()); ASSERT_FALSE(err); // Close @@ -211,15 +253,22 @@ TEST_F(TestDatastore, WritePause) err = ds->Init(temp_dir.c_str(), ds_suffix); ASSERT_FALSE(err); - auto got = ds->Get(pause_want1.c_str()); + auto got = ds->Get(pause_want1); + ASSERT_TRUE(got); + ASSERT_EQ(*got, pause_want1.to_string()); + + got = ds->Get(pause_want2); ASSERT_TRUE(got); - ASSERT_EQ(*got, pause_want1); + ASSERT_EQ(*got, pause_want2.to_string()); - got = ds->Get(pause_want2.c_str()); + got = ds->Get(pause_want3); + ASSERT_FALSE(got); + + got = ds->Get(pause_want4); ASSERT_TRUE(got); - ASSERT_EQ(*got, pause_want2); + ASSERT_EQ(*got, pause_want4.to_string()); - got = ds->Get(pause_want3.c_str()); + got = ds->Get(pause_want5); ASSERT_FALSE(got); delete ds; @@ -231,51 +280,38 @@ TEST_F(TestDatastore, SetSimple) auto err = ds.Init(GetTempDir().c_str(), ds_suffix); ASSERT_FALSE(err); + auto k = "/k"_json_pointer; string want = "v"; - err = ds.Set({{"k", want}}); + err = ds.Set(k, want); ASSERT_FALSE(err); - auto got = ds.Get("k"); + auto got = ds.Get(k); ASSERT_TRUE(got); ASSERT_EQ(*got, want); } -TEST_F(TestDatastore, SetMulti) -{ - Datastore ds; - auto err = ds.Init(GetTempDir().c_str(), ds_suffix); - ASSERT_FALSE(err); - - const char *key1 = "key1", *key2 = "key2"; - string want1 = "want1", want2 = "want2"; - err = ds.Set({{key1, want1}, {key2, want2}}); - ASSERT_FALSE(err); - - auto got = ds.Get(key1); - ASSERT_TRUE(got); - ASSERT_EQ(*got, want1); - - got = ds.Get(key2); - ASSERT_TRUE(got); - ASSERT_EQ(*got, want2); -} - TEST_F(TestDatastore, SetDeep) { Datastore ds; auto err = ds.Init(GetTempDir().c_str(), ds_suffix); ASSERT_FALSE(err); - const char *key1 = "key1", *key2 = "key2"; + auto key1 = "/key1"_json_pointer, key2 = "/key2"_json_pointer; string want = "want"; - err = ds.Set({{key1, {{key2, want}}}}); + err = ds.Set(key1/key2, want); ASSERT_FALSE(err); + // Try to get key1 and then get key2 from it auto gotShallow = ds.Get(key1); ASSERT_TRUE(gotShallow); string gotDeep = gotShallow->at(key2).get(); ASSERT_EQ(gotDeep, want); + + // Then try to get /key1/key2 directly + auto gotDeep2 = ds.Get(key1/key2); + ASSERT_TRUE(gotDeep2); + ASSERT_EQ(gotDeep2, want); } TEST_F(TestDatastore, SetAndClear) @@ -285,19 +321,19 @@ TEST_F(TestDatastore, SetAndClear) ASSERT_FALSE(err); map want = {{"a", "a"}, {"b", "b"}}; - err = ds.Set({{"k", want}}); + auto k = "/k"_json_pointer; + err = ds.Set(k, want); ASSERT_FALSE(err); - auto got = ds.Get>("k"); + auto got = ds.Get>(k); ASSERT_TRUE(got); ASSERT_EQ(got->size(), want.size()); want.clear(); - err = ds.Set({{"k", want}}); + err = ds.Set(k, want); ASSERT_FALSE(err); - // This used to fail when Datastore was using json.merge_patch instead of json.update. - got = ds.Get>("k"); + got = ds.Get>(k); ASSERT_TRUE(got); ASSERT_EQ(got->size(), 0); } @@ -312,8 +348,8 @@ TEST_F(TestDatastore, SetTypes) // Start with string string wantString = "v"; - const char *wantStringKey = "wantStringKey"; - err = ds.Set({{wantStringKey, wantString}}); + auto wantStringKey = "/wantStringKey"_json_pointer; + err = ds.Set(wantStringKey, wantString); ASSERT_FALSE(err); auto gotString = ds.Get(wantStringKey); @@ -322,8 +358,8 @@ TEST_F(TestDatastore, SetTypes) // bool bool wantBool = true; - const char *wantBoolKey = "wantBoolKey"; - err = ds.Set({{wantBoolKey, wantBool}}); + auto wantBoolKey = "/wantBoolKey"_json_pointer; + err = ds.Set(wantBoolKey, wantBool); ASSERT_FALSE(err); auto gotBool = ds.Get(wantBoolKey); @@ -332,8 +368,8 @@ TEST_F(TestDatastore, SetTypes) // int int wantInt = 5273482; - const char *wantIntKey = "wantIntKey"; - err = ds.Set({{wantIntKey, wantInt}}); + auto wantIntKey = "/wantIntKey"_json_pointer; + err = ds.Set(wantIntKey, wantInt); ASSERT_FALSE(err); auto gotInt = ds.Get(wantIntKey); @@ -349,8 +385,8 @@ TEST_F(TestDatastore, TypeMismatch) // string string wantString = "v"; - const char *wantStringKey = "wantStringKey"; - err = ds.Set({{wantStringKey, wantString}}); + auto wantStringKey = "/wantStringKey"_json_pointer; + err = ds.Set(wantStringKey, wantString); ASSERT_FALSE(err); auto gotString = ds.Get(wantStringKey); @@ -359,8 +395,8 @@ TEST_F(TestDatastore, TypeMismatch) // bool bool wantBool = true; - const char *wantBoolKey = "wantBoolKey"; - err = ds.Set({{wantBoolKey, wantBool}}); + auto wantBoolKey = "/wantBoolKey"_json_pointer; + err = ds.Set(wantBoolKey, wantBool); ASSERT_FALSE(err); auto gotBool = ds.Get(wantBoolKey); @@ -369,8 +405,8 @@ TEST_F(TestDatastore, TypeMismatch) // int int wantInt = 5273482; - const char *wantIntKey = "wantIntKey"; - err = ds.Set({{wantIntKey, wantInt}}); + auto wantIntKey = "/wantIntKey"_json_pointer; + err = ds.Set(wantIntKey, wantInt); ASSERT_FALSE(err); auto gotInt = ds.Get(wantIntKey); @@ -394,7 +430,7 @@ TEST_F(TestDatastore, TypeMismatch) //ASSERT_FALSE(got_fail_4); // NOTE: This doesn't actually fail. There must be a successful implicit conversion. // It's not an error to set one type to a key and then replace it with another type - err = ds.Set({{wantStringKey, wantBool}}); + err = ds.Set(wantStringKey, wantBool); ASSERT_FALSE(err); } @@ -405,14 +441,40 @@ TEST_F(TestDatastore, GetSimple) ASSERT_FALSE(err); string want = "v"; - err = ds.Set({{"k", want}}); + err = ds.Set("/k"_json_pointer, want); ASSERT_FALSE(err); - auto got = ds.Get("k"); + auto got = ds.Get("/k"_json_pointer); ASSERT_TRUE(got); ASSERT_EQ(*got, want); } +TEST_F(TestDatastore, GetDeep) +{ + // This is a copy of SetDeep + + Datastore ds; + auto err = ds.Init(GetTempDir().c_str(), ds_suffix); + ASSERT_FALSE(err); + + auto key1 = "/key1"_json_pointer, key2 = "/key2"_json_pointer; + string want = "want"; + err = ds.Set(key1/key2, want); + ASSERT_FALSE(err); + + // Try to get key1 and then get key2 from it + auto gotShallow = ds.Get(key1); + ASSERT_TRUE(gotShallow); + + string gotDeep = gotShallow->at(key2).get(); + ASSERT_EQ(gotDeep, want); + + // Then try to get /key1/key2 directly + auto gotDeep2 = ds.Get(key1/key2); + ASSERT_TRUE(gotDeep2); + ASSERT_EQ(gotDeep2, want); +} + TEST_F(TestDatastore, GetNotFound) { Datastore ds; @@ -420,15 +482,84 @@ TEST_F(TestDatastore, GetNotFound) ASSERT_FALSE(err); string want = "v"; - err = ds.Set({{"k", want}}); + auto k = "/k"_json_pointer; + err = ds.Set(k, want); ASSERT_FALSE(err); - auto got = ds.Get("k"); + auto got = ds.Get(k); ASSERT_TRUE(got); ASSERT_EQ(*got, want); // Bad key - auto nope = ds.Get("nope"); + auto nope = ds.Get("/nope"_json_pointer); ASSERT_FALSE(nope); ASSERT_EQ(nope.error(), psicash::Datastore::kNotFound); } + +TEST_F(TestDatastore, GetFullDS) +{ + // Testing Get() with no params, that returns the full datastore json + + Datastore ds; + + // Error before Init + auto j = ds.Get(); + ASSERT_FALSE(j); + + auto err = ds.Init(GetTempDir().c_str(), ds_suffix); + ASSERT_FALSE(err); + + j = ds.Get(); + ASSERT_TRUE(j); + ASSERT_TRUE(j->empty()); + + string want = "v"; + err = ds.Set("/k"_json_pointer, want); + ASSERT_FALSE(err); + + j = ds.Get(); + ASSERT_TRUE(j); + ASSERT_EQ(j->at("k").get(), want); +} + + +/* +This was a failed attempt to trigger a datastore corruption error we sometimes see. +To run, FileStore and FileLoad need to be exported. +TEST_F(TestDatastore, Errors) +{ + Datastore ds; + + auto err = ds.Init(GetTempDir().c_str(), ds_suffix); + ASSERT_FALSE(err); + + for (size_t i = 0; i < 1000; i++) { + auto j = ds.Get(); + ASSERT_TRUE(j); + + string want = "v"; + err = ds.Set("/k"_json_pointer, want); + ASSERT_FALSE(err); + } + + auto datastore_filename = DatastoreFilepath(GetTempDir(), true); + + auto load_res = FileLoad(datastore_filename); + ASSERT_TRUE(load_res); + + auto error = FileStore(false, datastore_filename, json::object()); + ASSERT_FALSE(error) << error.ToString(); + load_res = FileLoad(datastore_filename); + ASSERT_TRUE(load_res); + + error = FileStore(false, datastore_filename, json()); + ASSERT_FALSE(error) << error.ToString(); + load_res = FileLoad(datastore_filename); + ASSERT_TRUE(load_res); + + error = FileStore(false, datastore_filename, nullptr); + ASSERT_FALSE(error) << error.ToString(); + load_res = FileLoad(datastore_filename); + ASSERT_TRUE(load_res); +} +*/ diff --git a/datetime.cpp b/datetime.cpp index 0c72aa0..3813b7f 100644 --- a/datetime.cpp +++ b/datetime.cpp @@ -55,6 +55,10 @@ datetime::TimePoint NormalizeTimePoint(const datetime::Clock::time_point& tp) { } bool FromString(const char* parseSpecifier, const string& s, TimePoint& tp) { + if (s.empty()) { + return false; + } + TimePoint temp; istringstream ss(s); ss.imbue(std::locale::classic()); @@ -96,6 +100,10 @@ bool DateTime::IsZero() const { return time_point_ == kTimePointZero; } +string DateTime::ToString() const { + return ToISO8601(); +} + string DateTime::ToISO8601() const { ostringstream ss; ss.imbue(std::locale::classic()); diff --git a/datetime.hpp b/datetime.hpp index 9713a1d..d396477 100644 --- a/datetime.hpp +++ b/datetime.hpp @@ -45,6 +45,8 @@ class DateTime { // Returns true if this DateTime is the zero value. bool IsZero() const; + std::string ToString() const; + // These only support the "Z" timezone format. std::string ToISO8601() const; bool FromISO8601(const std::string& s); diff --git a/error.hpp b/error.hpp index ab5efdb..3491e92 100644 --- a/error.hpp +++ b/error.hpp @@ -100,8 +100,8 @@ const Error nullerr; */ /// Result holds an error-or-value. For usage, see nonstd::expected at -/// https://github.com/martinmoene/expected-lite -/// or actual current usage. +/// https://github.com/martinmoene/expected-lite/ or actual current usage. +/// To access the error (when `!res`), call `res.error()`. template class Result : public nonstd::expected { public: diff --git a/psicash.cpp b/psicash.cpp index f3dfb91..8ac6e02 100644 --- a/psicash.cpp +++ b/psicash.cpp @@ -43,11 +43,6 @@ using namespace error; namespace psicash { -const char* const kEarnerTokenType = "earner"; -const char* const kSpenderTokenType = "spender"; -const char* const kIndicatorTokenType = "indicator"; -const char* const kAccountTokenType = "account"; - const char* const kTransactionIDZero = ""; namespace prod { @@ -56,14 +51,16 @@ static constexpr const char* kAPIServerHostname = "api.psi.cash"; static constexpr int kAPIServerPort = 443; } namespace dev { -static constexpr const char* kAPIServerScheme = "https"; -static constexpr const char* kAPIServerHostname = "dev-api.psi.cash"; -static constexpr int kAPIServerPort = 443; -/* +#define LOCAL_TEST 0 +#if LOCAL_TEST static constexpr const char* kAPIServerScheme = "http"; static constexpr const char* kAPIServerHostname = "localhost"; static constexpr int kAPIServerPort = 51337; -*/ +#else +static constexpr const char* kAPIServerScheme = "https"; +static constexpr const char* kAPIServerHostname = "api.dev.psi.cash"; +static constexpr int kAPIServerPort = 443; +#endif } static constexpr const char* kAPIServerVersion = "v1"; @@ -71,6 +68,8 @@ static constexpr const char* kLandingPageParamKey = "psicash"; static constexpr const char* kMethodGET = "GET"; static constexpr const char* kMethodPOST = "POST"; +static constexpr const char* kDateHeaderKey = "Date"; + // // PsiCash class implementation // @@ -91,7 +90,8 @@ PsiCash::~PsiCash() { } Error PsiCash::Init(const string& user_agent, const string& file_store_root, - MakeHTTPRequestFn make_http_request_fn, bool test) { + MakeHTTPRequestFn make_http_request_fn, bool force_reset, + bool test) { test_ = test; if (test) { server_scheme_ = dev::kAPIServerScheme; @@ -112,6 +112,10 @@ Error PsiCash::Init(const string& user_agent, const string& file_store_root, return MakeCriticalError("file_store_root is required"); } + if (force_reset) { + user_data_->Clear(file_store_root, test); + } + // May still be null. make_http_request_fn_ = std::move(make_http_request_fn); @@ -123,13 +127,34 @@ Error PsiCash::Init(const string& user_agent, const string& file_store_root, return error::nullerr; } +#define MUST_BE_INITIALIZED if (!Initialized()) { return MakeCriticalError("PsiCash is uninitialized"); } + bool PsiCash::Initialized() const { return initialized_; } -Error PsiCash::Reset(const string& file_store_root, bool test) { - auto temp_user_data = std::make_unique(); - return PassError(temp_user_data->Clear(file_store_root, test)); +Error PsiCash::ResetUser() { + return PassError(user_data_->DeleteUserData(/*is_logged_out_account=*/false)); +} + +Error PsiCash::MigrateTrackerTokens(const map& tokens) { + MUST_BE_INITIALIZED; + + AuthTokens auth_tokens; + for (const auto& it : tokens) { + auth_tokens[it.first].id = it.second; + // leave expiry null + } + + UserData::WritePauser pauser(*user_data_); + // Ignoring return values while writing is paused. + // Blow away any user state, as the newly migrated tokens are overwriting it. + (void)ResetUser(); + (void)user_data_->SetAuthTokens(auth_tokens, /*is_account=*/false, /*account_username=*/""); + if (auto err = pauser.Commit()) { + return WrapError(err, "user data write failed"); + } + return nullerr; } void PsiCash::SetHTTPRequestFn(MakeHTTPRequestFn make_http_request_fn) { @@ -137,28 +162,54 @@ void PsiCash::SetHTTPRequestFn(MakeHTTPRequestFn make_http_request_fn) { } Error PsiCash::SetRequestMetadataItem(const string& key, const string& value) { + MUST_BE_INITIALIZED; return PassError(user_data_->SetRequestMetadataItem(key, value)); } +Error PsiCash::SetLocale(const string& locale) { + MUST_BE_INITIALIZED; + return PassError(user_data_->SetLocale(locale)); +} + // // Stored info accessors // -TokenTypes PsiCash::ValidTokenTypes() const { - TokenTypes tt; +bool PsiCash::HasTokens() const { + MUST_BE_INITIALIZED; + // Trackers and Accounts both require the same token types (for now). + // (Accounts will also have the "logout" type, but it isn't strictly needed for sane operation.) + vector required_token_types = {kEarnerTokenType, kSpenderTokenType, kIndicatorTokenType}; auto auth_tokens = user_data_->GetAuthTokens(); for (const auto& it : auth_tokens) { - tt.push_back(it.first); + auto found = std::find(required_token_types.begin(), required_token_types.end(), it.first); + if (found != required_token_types.end()) { + required_token_types.erase(found); + } } - return tt; + return required_token_types.empty(); } +/// If the user has no tokens, most actions are disallowed. (This can include being in +/// the is-logged-out-account state.) +#define TOKENS_REQUIRED if (!HasTokens()) { return MakeCriticalError("user has insufficient tokens"); } + bool PsiCash::IsAccount() const { + if (user_data_->GetIsLoggedOutAccount()) { + return true; + } return user_data_->GetIsAccount(); } +nonstd::optional PsiCash::AccountUsername() const { + if (user_data_->GetIsLoggedOutAccount() || !user_data_->GetIsAccount()) { + return nullopt; + } + return user_data_->GetAccountUsername(); +} + int64_t PsiCash::Balance() const { return user_data_->GetBalance(); } @@ -280,7 +331,10 @@ error::Result PsiCash::RemovePurchases(const vector& i return removed_purchases; } -Result PsiCash::ModifyLandingPage(const string& url_string) const { +/// Adds a params package to the URL which includes the user's earner token (if there is one). +/// @param query_param_only If true, the params will only be added to the query parameters +/// part of the URL, rather than first attempting to add it to the hash/fragment. +Result PsiCash::AddEarnerTokenToURL(const string& url_string, bool query_param_only) const { URL url; auto err = url.Parse(url_string); if (err) { @@ -289,12 +343,13 @@ Result PsiCash::ModifyLandingPage(const string& url_string) const { json psicash_data; psicash_data["v"] = 1; + psicash_data["timestamp"] = datetime::DateTime::Now().ToISO8601(); auto auth_tokens = user_data_->GetAuthTokens(); if (auth_tokens.count(kEarnerTokenType) == 0) { psicash_data["tokens"] = nullptr; } else { - psicash_data["tokens"] = auth_tokens[kEarnerTokenType]; + psicash_data["tokens"] = CommaDelimitTokens({kEarnerTokenType}); } if (test_) { @@ -319,12 +374,13 @@ Result PsiCash::ModifyLandingPage(const string& url_string) const { auto encoded_json = URL::Encode(base64::TrimPadding(base64::B64Encode(json_data)), false); // Our preference is to put the our data into the URL's fragment/hash/anchor, - // because we'd prefer the data not be sent to the server. + // because we'd prefer the data not be sent to the server nor included in the referrer + // header to third-party page resources. // But if there already is a fragment value then we'll put our data into the query parameters. // (Because altering the fragment is more likely to have negative consequences // for the page than adding a query parameter that will be ignored.) - if (url.fragment_.empty()) { + if (!query_param_only && url.fragment_.empty()) { // When setting in the fragment, we use "#!psicash=etc". The ! prevents the // fragment from accidentally functioning as a jump-to anchor on a landing page // (where we don't control element IDs, etc.). @@ -339,17 +395,60 @@ Result PsiCash::ModifyLandingPage(const string& url_string) const { return url.ToString(); } +Result PsiCash::ModifyLandingPage(const string& url_string) const { + // All of our landing pages are arrived at via the redirector service we run. We want + // to send our token package to the redirector, so that it can decide if and how to + // include it in the final site URL. So we have to send it via a query parameter. + return AddEarnerTokenToURL(url_string, true); +} + Result PsiCash::GetBuyPsiURL() const { - // This is just a special case of the landing page format, EXCEPT that tokens MUST be - // present, or else it's an error. - auto auth_tokens = user_data_->GetAuthTokens(); - if (auth_tokens.count(kEarnerTokenType) == 0) { - return MakeNoncriticalError("no earner token available"); + TOKENS_REQUIRED; + return AddEarnerTokenToURL(test_ ? "https://dev-psicash.myshopify.com/" : "https://buy.psi.cash/", false); +} + +std::string PsiCash::GetUserSiteURL(UserSiteURLType url_type, bool webview) const { + URL url; + url.scheme_host_path_ = test_ ? "https://dev-my.psi.cash" : "https://my.psi.cash"; + + switch (url_type) { + case UserSiteURLType::AccountSignup: + url.scheme_host_path_ += "/signup"; + break; + + case UserSiteURLType::ForgotAccount: + url.scheme_host_path_ += "/forgot"; + break; + + case UserSiteURLType::AccountManagement: + default: + // Just the root domain + break; } - return ModifyLandingPage("https://buy.psi.cash/"); + + url.query_ = "utm_source=" + URL::Encode(user_agent_, false); + url.query_ += "&locale=" + URL::Encode(user_data_->GetLocale(), false); + + if (!user_data_->GetAccountUsername().empty()) { + auto encoded_username = URL::Encode(user_data_->GetAccountUsername(), false); + // IE has a URL limit of 2083 characters, so if the username is too long (or encodes + // to too long), then we're going to omit this parameter). It is better to omit the + // username than to pre-fill an incorrect username or have broken UTF-8 characters. + if (encoded_username.length() < 2000) { + url.query_ += "&username=" + encoded_username; + } + } + + if (webview) { + url.query_ += "&webview=true"; + } + + return url.ToString(); } Result PsiCash::GetRewardedActivityData() const { + TOKENS_REQUIRED; + json psicash_data; psicash_data["v"] = 1; @@ -358,7 +457,7 @@ Result PsiCash::GetRewardedActivityData() const { if (auth_tokens.empty()) { return MakeCriticalError("earner token missing; can't create webhoook data"); } else { - psicash_data["tokens"] = auth_tokens[kEarnerTokenType]; + psicash_data["tokens"] = auth_tokens[kEarnerTokenType].id; } // Get the metadata (sponsor ID, etc.) @@ -388,7 +487,8 @@ json PsiCash::GetDiagnosticInfo() const { json j = json::object(); j["test"] = test_; - j["validTokenTypes"] = ValidTokenTypes(); + j["isLoggedOutAccount"] = user_data_->GetIsLoggedOutAccount(); + j["validTokenTypes"] = user_data_->ValidTokenTypes(); j["isAccount"] = IsAccount(); j["balance"] = Balance(); j["serverTimeDiff"] = user_data_->GetServerTimeDiff().count(); // in milliseconds @@ -431,12 +531,26 @@ json PsiCash::GetRequestMetadata(int attempt) const { // HTTPResult.error will always be empty on a non-error return. Result PsiCash::MakeHTTPRequestWithRetry( const std::string& method, const std::string& path, bool include_auth_tokens, - const std::vector>& query_params) + const std::vector>& query_params, + const optional& body) { + MUST_BE_INITIALIZED; + if (!make_http_request_fn_) { throw std::runtime_error("make_http_request_fn_ must be set before requests are attempted"); } + string body_string; + if (body) { + try { + body_string = body->dump(-1, ' ', true); + } + catch (json::exception& e) { + return MakeCriticalError( + utils::Stringer("body json dump failed: ", e.what(), "; id:", e.id)); + } + } + const int max_attempts = 3; HTTPResult http_result; @@ -447,7 +561,7 @@ Result PsiCash::MakeHTTPRequestWithRetry( } auto req_params = BuildRequestParams( - method, path, include_auth_tokens, query_params, i + 1, {}); + method, path, include_auth_tokens, query_params, i + 1, {}, body_string); if (!req_params) { return WrapError(req_params.error(), "BuildRequestParams failed"); } @@ -461,9 +575,10 @@ Result PsiCash::MakeHTTPRequestWithRetry( } // We just got a fresh server timestamp, so set the server time diff - if (!http_result.date.empty()) { + auto date_header = utils::FindHeaderValue(http_result.headers, kDateHeaderKey); + if (!date_header.empty()) { datetime::DateTime server_datetime; - if (server_datetime.FromRFC7231(http_result.date)) { + if (server_datetime.FromRFC7231(date_header)) { // We don't care about the return value at this point. (void)user_data_->SetServerTimeDiff(server_datetime); } @@ -471,11 +586,15 @@ Result PsiCash::MakeHTTPRequestWithRetry( } if (http_result.code < 0) { - // Something happened that prevented the request from nominally succeeding. Don't retry. + // Something happened that prevented the request from nominally succeeding. + // If the native code indicates that this is a "recoverable error" (such as + // the network interruption error we see on iOS sometimes), then we will retry. if (http_result.code == HTTPResult::RECOVERABLE_ERROR) { - return MakeNoncriticalError(("Request resulted in noncritical error: "s + http_result.error)); + continue; } - return MakeCriticalError(("Request resulted in critical error: "s + http_result.error)); + + // Unrecoverable error; don't retry. + return MakeCriticalError("Request resulted in critical error: "s + http_result.error); } if (IsServerError(http_result.code)) { @@ -487,7 +606,14 @@ Result PsiCash::MakeHTTPRequestWithRetry( return http_result; } - // We exceeded our retry limit. Return the last result received, which will be 500-ish. + // We exceeded our retry limit. + + if (http_result.code < 0) { + // A critical error would have returned above, so this is a non-critical error + return MakeNoncriticalError("Request resulted in noncritical error: "s + http_result.error); + } + + // Return the last result (which is a 5xx server error) return http_result; } @@ -495,7 +621,8 @@ Result PsiCash::MakeHTTPRequestWithRetry( Result PsiCash::BuildRequestParams( const std::string& method, const std::string& path, bool include_auth_tokens, const std::vector>& query_params, int attempt, - const std::map& additional_headers) const { + const std::map& additional_headers, + const std::string& body) const { HTTPParams params; @@ -507,17 +634,11 @@ Result PsiCash::BuildRequestParams( params.query = query_params; params.headers = additional_headers; + params.headers["Accept"] = "application/json"; params.headers["User-Agent"] = user_agent_; if (include_auth_tokens) { - string s; - for (const auto& at : user_data_->GetAuthTokens()) { - if (!s.empty()) { - s += ","; - } - s += at.second; - } - params.headers["X-PsiCash-Auth"] = s; + params.headers["X-PsiCash-Auth"] = CommaDelimitTokens({}); } auto metadata = GetRequestMetadata(attempt); @@ -530,16 +651,36 @@ Result PsiCash::BuildRequestParams( utils::Stringer("metadata json dump failed: ", e.what(), "; id:", e.id)); } + params.body = body; + if (!body.empty()) { + params.headers["Content-Type"] = "application/json; charset=utf-8"; + } + return params; } +/// Returns our auth tokens in comma-delimited format. If types is `{}`, all tokens will +/// be included; otherwise only tokens of the types specified will be included. +std::string PsiCash::CommaDelimitTokens(const std::vector& types) const { + vector tokens; + for (const auto& at : user_data_->GetAuthTokens()) { + if (types.empty() || std::find(types.begin(), types.end(), at.first) != types.end()) { + tokens.push_back(at.second.id); + } + } + return utils::Join(tokens, ","); +} + // Get new tracker tokens from the server. This effectively gives us a new identity. Result PsiCash::NewTracker() { + MUST_BE_INITIALIZED; + auto result = MakeHTTPRequestWithRetry( kMethodPOST, "/tracker", false, - {} + {{"instanceID", user_data_->GetInstanceID()}}, + nullopt // body ); if (!result) { return WrapError(result.error(), "MakeHTTPRequestWithRetry failed"); @@ -570,10 +711,11 @@ Result PsiCash::NewTracker() { // Set our new data in a single write. UserData::WritePauser pauser(*user_data_); - (void)user_data_->SetAuthTokens(auth_tokens, false); + (void)user_data_->SetIsLoggedOutAccount(false); + (void)user_data_->SetAuthTokens(auth_tokens, /*is_account=*/false, /*account_username=*/""); (void)user_data_->SetBalance(0); - if (auto err = pauser.Unpause()) { - return WrapError(err, "SetAuthTokens failed"); + if (auto err = pauser.Commit()) { + return WrapError(err, "user data write failed"); } return Status::Success; @@ -582,16 +724,48 @@ Result PsiCash::NewTracker() { } return MakeCriticalError(utils::Stringer( - "request returned unexpected result code: ", result->code)); + "request returned unexpected result code: ", result->code, "; ", + result->body, "; ", json(result->headers).dump())); } -Result PsiCash::RefreshState(const std::vector& purchase_classes) { +Result PsiCash::RefreshState(bool local_only, const std::vector& purchase_classes) { + if (local_only) { + // Our "local only" refresh involves checking tokens for expiry and potentially + // shifting into a logged-out state. + + // This call is offline, but we might be currently connected, so the reconnect_required + // considerations still apply. + bool reconnect_required = false; + + auto local_now = datetime::DateTime::Now(); + for (const auto& it : user_data_->GetAuthTokens()) { + if (it.second.server_time_expiry + && user_data_->ServerTimeToLocal(*it.second.server_time_expiry) < local_now) { + // If any tokens are expired, we consider ourselves to not have a proper set + + // If we're transitioning to a logged out state and there are active + // authorizations (applied to the current tunnel), then we need to + // reconnect to remove them. + // TODO: this line/logic is duplicated below; consider a helper to encapsulate + reconnect_required = !GetAuthorizations(true).empty(); + + if (auto err = user_data_->DeleteUserData(IsAccount())) { + return WrapError(err, "DeleteUserData failed"); + } + + break; + } + } + + return PsiCash::RefreshStateResponse{ Status::Success, reconnect_required }; + } + return RefreshState(purchase_classes, true); } // RefreshState helper that makes recursive calls (to allow for NewTracker and then // RefreshState requests). -Result PsiCash::RefreshState( +Result PsiCash::RefreshState( const std::vector& purchase_classes, bool allow_recursion) { /* Logic flow overview: @@ -606,18 +780,15 @@ Result PsiCash::RefreshState( 6. If there are still no valid tokens, then things are horribly wrong. Return error. */ - if (!initialized_) { - return MakeCriticalError("PsiCash is uninitialized"); - } + MUST_BE_INITIALIZED; auto auth_tokens = user_data_->GetAuthTokens(); if (auth_tokens.empty()) { // No tokens. - - if (user_data_->GetIsAccount()) { - // This is/was a logged-in account. We can't just get a new tracker. + if (IsAccount()) { + // This is a logged-in or logged-out account. We can't just get a new tracker. // The app will have to force a login for the user to do anything. - return Status::Success; + return PsiCash::RefreshStateResponse{ Status::Success, false }; } if (!allow_recursion) { @@ -634,7 +805,7 @@ Result PsiCash::RefreshState( } if (*new_tracker_result != Status::Success) { - return *new_tracker_result; + return PsiCash::RefreshStateResponse{ *new_tracker_result, false }; } // Note: NewTracker calls SetAuthTokens and SetBalance. @@ -650,11 +821,15 @@ Result PsiCash::RefreshState( query_items.emplace_back("class", purchase_class); } + // If LastTransactionID is empty, we'll get all transactions. + query_items.emplace_back("lastTransactionID", user_data_->GetLastTransactionID()); + auto result = MakeHTTPRequestWithRetry( kMethodGET, "/refresh-state", true, - query_items + query_items, + nullopt // body ); if (!result) { return WrapError(result.error(), "MakeHTTPRequestWithRetry failed"); @@ -666,6 +841,8 @@ Result PsiCash::RefreshState( utils::Stringer("result has no body; code: ", result->code)); } + bool reconnect_required = false; + try { // We're going to be setting a bunch of UserData values, so let's wait until we're done // to write them all to disk. @@ -674,11 +851,18 @@ Result PsiCash::RefreshState( auto j = json::parse(result->body); auto valid_token_types = j["TokensValid"].get>(); - user_data_->CullAuthTokens(valid_token_types); + (void)user_data_->CullAuthTokens(valid_token_types); // If any of our tokens were valid, then the IsAccount value from the // server is authoritative. Otherwise we'll respect our existing value. - if (!valid_token_types.empty() && j["IsAccount"].is_boolean()) { + bool any_valid_token = false; + for (const auto& vtt : valid_token_types) { + if (vtt.second) { + any_valid_token = true; + break; + } + } + if (any_valid_token && j["IsAccount"].is_boolean()) { // If we have moved from being an account to not being an account, // something is very wrong. auto prev_is_account = IsAccount(); @@ -687,11 +871,15 @@ Result PsiCash::RefreshState( return MakeCriticalError("invalid is-account state"); } - user_data_->SetIsAccount(is_account); + (void)user_data_->SetIsAccount(is_account); + } + + if (j["AccountUsername"].is_string()) { + (void)user_data_->SetAccountUsername(j["AccountUsername"].get()); } if (j["Balance"].is_number_integer()) { - user_data_->SetBalance(j["Balance"].get()); + (void)user_data_->SetBalance(j["Balance"].get()); } // We only try to use the PurchasePrices if we supplied purchase classes to the request @@ -702,17 +890,43 @@ Result PsiCash::RefreshState( // representation of PurchasePrice. We won't assume that the representation used by the // server is the same (nor that it won't change independent of our representation). for (const auto& pp : j["PurchasePrices"]) { + auto transaction_class = pp["Class"].get(); + purchase_prices.push_back(PurchasePrice{ - pp["Class"].get(), + transaction_class, pp["Distinguisher"].get(), pp["Price"].get() }); } - user_data_->SetPurchasePrices(purchase_prices); + (void)user_data_->SetPurchasePrices(purchase_prices); } - if (auto err = pauser.Unpause()) { + if (j["Purchases"].is_array()) { + for (const auto& p : j["Purchases"]) { + auto purchase_res = PurchaseFromJSON(p); + if (!purchase_res) { + return WrapError(purchase_res.error(), "failed to deserialize purchases"); + } + + // Authorizations are applied to tunnel connections, which requires a reconnect + reconnect_required = reconnect_required || purchase_res->authorization; + + (void)user_data_->AddPurchase(*purchase_res); + } + } + + // If the account tokens just expired, then we need to go into a logged-out state. + if (IsAccount() && !HasTokens()) { + // If we're transitioning to a logged out state and there are active + // authorizations (applied to the current tunnel), then we need to + // reconnect to remove them. + reconnect_required = reconnect_required || !GetAuthorizations(true).empty(); + + (void)user_data_->DeleteUserData(true); + } + + if (auto err = pauser.Commit()) { return WrapError(err, "UserData write failed"); } } @@ -723,42 +937,45 @@ Result PsiCash::RefreshState( if (IsAccount()) { // For accounts there's nothing else we can do, regardless of the state of token validity. - return Status::Success; + return PsiCash::RefreshStateResponse{ Status::Success, reconnect_required }; } - if (!ValidTokenTypes().empty()) { + if (HasTokens()) { // We have a good tracker state. - return Status::Success; + return PsiCash::RefreshStateResponse{ Status::Success, reconnect_required }; } // We started out with tracker tokens, but they're all invalid. + // Note that this shouldn't happen -- we "know" that Tracker tokens don't + // expire -- but we'll still try to recover if we haven't already recursed. if (!allow_recursion) { return MakeCriticalError("failed to obtain valid tracker tokens (b)"); } return RefreshState(purchase_classes, true); - } else if (result->code == kHTTPStatusUnauthorized) { + } + else if (result->code == kHTTPStatusUnauthorized) { // This can only happen if the tokens we sent didn't all belong to same user. // This really should never happen. We're not checking the return value, as there // isn't a sane response to a failure at this point. (void)user_data_->Clear(); - return Status::InvalidTokens; - } else if (IsServerError(result->code)) { - return Status::ServerError; + return PsiCash::RefreshStateResponse{ Status::InvalidTokens, false }; + } + else if (IsServerError(result->code)) { + return PsiCash::RefreshStateResponse{ Status::ServerError, false }; } return MakeCriticalError(utils::Stringer( - "request returned unexpected result code: ", result->code)); + "request returned unexpected result code: ", result->code, "; ", + result->body, "; ", json(result->headers).dump())); } Result PsiCash::NewExpiringPurchase( const string& transaction_class, const string& distinguisher, const int64_t expected_price) { - if (!initialized_) { - return MakeCriticalError("PsiCash is uninitialized"); - } + TOKENS_REQUIRED; auto result = MakeHTTPRequestWithRetry( kMethodPOST, @@ -769,17 +986,14 @@ Result PsiCash::NewExpiringPurchase( {"distinguisher", distinguisher}, // Note the conversion from positive to negative: price to amount. {"expectedAmount", to_string(-expected_price)} - } + }, + nullopt // body ); if (!result) { return WrapError(result.error(), "MakeHTTPRequestWithRetry failed"); } - string transaction_id, authorization_encoded, transaction_type; - datetime::DateTime server_expiry; - - // Set our new data in a single write. - UserData::WritePauser pauser(*user_data_); + optional purchase; // These statuses require the response body to be parsed if (result->code == kHTTPStatusOK || @@ -794,36 +1008,40 @@ Result PsiCash::NewExpiringPurchase( try { auto j = json::parse(result->body); - // Many response fields are optional (depending on the presence of the indicator token) + // Set our new data in a single write. + // Note that any early return will cause updates to roll back. + UserData::WritePauser pauser(*user_data_); - if (j["Balance"].is_number_integer()) { + // Balance is present for all non-error responses + if (j.at("Balance").is_number_integer()) { // We don't care about the return value of this right now - (void)user_data_->SetBalance(j["Balance"].get()); + (void)user_data_->SetBalance(j.at("Balance").get()); } - if (j["TransactionID"].is_string()) { - transaction_id = j["TransactionID"].get(); - } + if (result->code == kHTTPStatusOK) { + auto parse_res = PurchaseFromJSON(j, "expiring-purchase"); + if (!parse_res) { + return WrapError(parse_res.error(), "failed to parse purchase from response JSON"); + } - if (j["Authorization"].is_string()) { - authorization_encoded = j["Authorization"].get(); - } + purchase = *parse_res; - if (j["TransactionResponse"]["Type"].is_string()) { - transaction_type = j["TransactionResponse"]["Type"].get(); - } + if (!purchase->server_time_expiry) { + // Purchase expiry is optional, but we're specifically making a New**Expiring**Purchase + return MakeCriticalError("response did not provide valid expiry"); + } + + // Not checking authorization, as it doesn't apply to all expiring purchases - if (j["TransactionResponse"]["Values"]["Expires"].is_string()) { - string expiry_string = j["TransactionResponse"]["Values"]["Expires"].get(); - if (!server_expiry.FromISO8601(expiry_string)) { - return MakeCriticalError( - "failed to parse TransactionResponse.Values.Expires; got "s + - expiry_string); + if (auto err = user_data_->AddPurchase(*purchase)) { + return WrapError(err, "AddPurchase failed"); } + } - // Unused fields - //auto transaction_amount = j.at("TransactionAmount").get(); + if (auto err = pauser.Commit()) { + return WrapError(err, "UserData write failed"); + } } catch (json::exception& e) { return MakeCriticalError( @@ -831,88 +1049,196 @@ Result PsiCash::NewExpiringPurchase( } } - if (result->code == kHTTPStatusOK) { - if (transaction_type != "expiring-purchase") { - return MakeCriticalError( - ("response contained incorrect TransactionResponse.Type; want 'expiring-purchase', got "s + - transaction_type)); - } - if (transaction_id.empty()) { - return MakeCriticalError("response did not provide valid TransactionID"); - } - if (server_expiry.IsZero()) { - // Purchase expiry is optional, but we're specifically making a New**Expiring**Purchase - return MakeCriticalError( - "response did not provide valid TransactionResponse.Values.Expires"); - } - // Not checking authorization, as it doesn't apply to all expiring purchases - - optional authOptional = nullopt; - if (!authorization_encoded.empty()) { - auto decodeAuthResult = DecodeAuthorization(authorization_encoded); - if (!decodeAuthResult) { - // Authorization can be optional, but inability to decode suggests - // something is very wrong. - return WrapError(decodeAuthResult.error(), "failed to decode Purchase Authorization"); - } - authOptional = *decodeAuthResult; - } - - Purchase purchase = { - transaction_id, - transaction_class, - distinguisher, - server_expiry.IsZero() ? nullopt : make_optional( - server_expiry), - server_expiry.IsZero() ? nullopt : make_optional( - server_expiry), - authOptional - }; - - user_data_->UpdatePurchaseLocalTimeExpiry(purchase); - - if (auto err = user_data_->AddPurchase(purchase)) { - return WrapError(err, "AddPurchase failed"); - } + optional response; - if (auto err = pauser.Unpause()) { - return WrapError(err, "UserData write failed"); - } - - return PsiCash::NewExpiringPurchaseResponse{ + if (result->code == kHTTPStatusOK) { + response = PsiCash::NewExpiringPurchaseResponse{ Status::Success, purchase }; } else if (result->code == kHTTPStatusTooManyRequests) { - return PsiCash::NewExpiringPurchaseResponse{ + response = PsiCash::NewExpiringPurchaseResponse{ Status::ExistingTransaction }; } else if (result->code == kHTTPStatusPaymentRequired) { - return PsiCash::NewExpiringPurchaseResponse{ + response = PsiCash::NewExpiringPurchaseResponse{ Status::InsufficientBalance }; } else if (result->code == kHTTPStatusConflict) { - return PsiCash::NewExpiringPurchaseResponse{ + response = PsiCash::NewExpiringPurchaseResponse{ Status::TransactionAmountMismatch }; } else if (result->code == kHTTPStatusNotFound) { - return PsiCash::NewExpiringPurchaseResponse{ + response = PsiCash::NewExpiringPurchaseResponse{ Status::TransactionTypeNotFound }; } else if (result->code == kHTTPStatusUnauthorized) { - return PsiCash::NewExpiringPurchaseResponse{ + response = PsiCash::NewExpiringPurchaseResponse{ Status::InvalidTokens }; } else if (IsServerError(result->code)) { - return PsiCash::NewExpiringPurchaseResponse{ + response = PsiCash::NewExpiringPurchaseResponse{ Status::ServerError }; } + else { + return MakeCriticalError(utils::Stringer( + "request returned unexpected result code: ", result->code, "; ", + result->body, "; ", json(result->headers).dump())); + } - return MakeCriticalError(utils::Stringer( - "request returned unexpected result code: ", result->code)); + assert(response); + return *response; } +Result PsiCash::AccountLogout() { + TOKENS_REQUIRED; + + if (!IsAccount()) { + return MakeNoncriticalError("user is not account"); + } + + // Authorizations are applied to psiphond connections, so the presence of an active + // one means we will need to reconnect after logging out. + bool reconnect_required = !GetAuthorizations(true).empty(); + + Error httpErr; + auto result = MakeHTTPRequestWithRetry( + kMethodPOST, + "/logout", + true, // include auth tokens + {}, + nullopt // body + ); + if (!result) { + httpErr = result.error(); + } + else if (result->code != kHTTPStatusOK) { + httpErr = MakeNoncriticalError(utils::Stringer("logout request failed; code:", result->code, "; body:", result->body)); + } + // Even if an error occurred, we still want to do the local logout, so carry on. + + auto localErr = user_data_->DeleteUserData(true); + + // The localErr is a more significant failure, so check it first. + if (localErr) { + return WrapError(localErr, "local AccountLogout failed"); + } + /* + // We are not returning an error if the remote request failed. We have already + // affected the local logout, and we'll have to rely on the next login from this + // device to invalidate the tokens on the server. + else if (httpErr) { + return WrapError(httpErr, "MakeHTTPRequestWithRetry failed"); + } + */ + + return PsiCash::AccountLogoutResponse{ reconnect_required }; +} + +error::Result PsiCash::AccountLogin( + const std::string& utf8_username, + const std::string& utf8_password) { + MUST_BE_INITIALIZED; + + static const vector token_types = {kEarnerTokenType, kSpenderTokenType, kIndicatorTokenType, kLogoutTokenType}; + static const string token_types_str = utils::Join(token_types, ","); + + // If we have tracker tokens, include them to (attempt to) merge the balance. + string old_tokens; + if (!IsAccount() && HasTokens()) { + old_tokens = CommaDelimitTokens({}); + } + + json body = + { + {"username", utf8_username}, + {"password", utf8_password}, + {"instanceID", user_data_->GetInstanceID()}, + {"tokenTypes", token_types_str}, + {"oldTokens", old_tokens} + }; + + auto result = MakeHTTPRequestWithRetry( + kMethodPOST, + "/login", + false, // tokens for tracker merge are provided via the request body + {}, // query params + body + ); + if (!result) { + return WrapError(result.error(), "MakeHTTPRequestWithRetry failed"); + } + + if (result->code == kHTTPStatusOK) { + // Delete whatever local user data may be present. If it was a tracker, it has + // been merged now (or can't be); if it was an account, we should interpret the + // login as a desire to no longer be logged in with the previous account. + if (auto err = ResetUser()) { + return PassError(err); + } + + if (result->body.empty()) { + return MakeCriticalError( + utils::Stringer("result has no body; code: ", result->code)); + } + + AuthTokens auth_tokens; + optional last_tracker_merge; + try { + auto j = json::parse(result->body); + auth_tokens = j["Tokens"].get(); + + if (!j.at("TrackerMerged").is_null()) { + auto tracker_merges_remaining = j["TrackerMergesRemaining"].get(); + auto tracker_merged = j["TrackerMerged"].get(); + last_tracker_merge = tracker_merged && tracker_merges_remaining == 0; + } + } + catch (json::exception& e) { + return MakeCriticalError( + utils::Stringer("json parse failed: ", e.what(), "; id:", e.id)); + } + + // Sanity check + if (auth_tokens.size() < token_types.size()) { + return MakeCriticalError( + utils::Stringer("bad number of tokens received: ", auth_tokens.size())); + } + + // Set our new data in a single write. + UserData::WritePauser pauser(*user_data_); + (void)user_data_->SetIsLoggedOutAccount(false); + (void)user_data_->SetAuthTokens(auth_tokens, /*is_account=*/true, /*utf8_username=*/utf8_username); + if (auto err = pauser.Commit()) { + return WrapError(err, "user data write failed"); + } + + return PsiCash::AccountLoginResponse{ + Status::Success, + last_tracker_merge + }; + } + else if (result->code == kHTTPStatusUnauthorized) { + return PsiCash::AccountLoginResponse{ + Status::InvalidCredentials + }; + } + else if (result->code == kHTTPStatusBadRequest) { + return PsiCash::AccountLoginResponse{ + Status::BadRequest + }; + } + else if (IsServerError(result->code)) { + return PsiCash::AccountLoginResponse{ + Status::ServerError + }; + } + + return MakeCriticalError(utils::Stringer( + "request returned unexpected result code: ", result->code, "; ", + result->body, "; ", json(result->headers).dump())); +} // Enable JSON de/serializing of PurchasePrice. // See https://github.com/nlohmann/json#basic-usage @@ -937,19 +1263,22 @@ void from_json(const json& j, PurchasePrice& pp) { // Enable JSON de/serializing of Purchase. // See https://github.com/nlohmann/json#basic-usage +// NOTE: This is only for datastore purposes, and not for server responses. bool operator==(const Purchase& lhs, const Purchase& rhs) { return lhs.transaction_class == rhs.transaction_class && lhs.distinguisher == rhs.distinguisher && lhs.server_time_expiry == rhs.server_time_expiry && //lhs.local_time_expiry == rhs.local_time_expiry && // Don't include the derived local time in the comparison - lhs.authorization == rhs.authorization; + lhs.authorization == rhs.authorization && + lhs.server_time_created == rhs.server_time_created; } void to_json(json& j, const Purchase& p) { j = json{ - {"id", p.id}, - {"class", p.transaction_class}, - {"distinguisher", p.distinguisher}}; + {"id", p.id}, + {"class", p.transaction_class}, + {"distinguisher", p.distinguisher}, + {"serverTimeCreated", p.server_time_created}}; if (p.authorization) { j["authorization"] = *p.authorization; @@ -992,6 +1321,80 @@ void from_json(const json& j, Purchase& p) { } else { p.local_time_expiry = j.at("localTimeExpiry").get(); } + + // This field was not added until later versions of the datastore, so may not be present. + if (j.contains("serverTimeCreated")) { + p.server_time_created = j.at("serverTimeCreated").get(); + } else { + // Default it to a very long time ago. + p.server_time_created = datetime::DateTime(datetime::TimePoint(datetime::DurationFromInt64(1))); + } +} + +/// Builds a purchase from server response JSON. +error::Result PsiCash::PurchaseFromJSON(const json& j, const string& expected_type/*=""*/) const { + string transaction_id, transaction_class, transaction_distinguisher, authorization_encoded, transaction_type; + datetime::DateTime server_expiry, server_created; + try { + if (!expected_type.empty() && expected_type != j.at("/TransactionResponse/Type"_json_pointer).get()) { + return MakeCriticalError("expected type mismatch; want '"s + expected_type + "'; got '" + j.at("/TransactionResponse/Type"_json_pointer).get() + "'"); + } + + transaction_id = j.at("TransactionID").get(); + transaction_class = j.at("Class").get(); + transaction_distinguisher = j.at("Distinguisher").get(); + + if (!server_created.FromISO8601(j.at("Created").get())) { + return MakeCriticalError("failed to parse Created; got "s + j.at("Created").get()); + } + + if (j.at("Authorization").is_string()) { + authorization_encoded = j["Authorization"].get(); + } + + // NOTE: The presence of this field depends on the type. Right now we only have + // expiring purchases, but that may change in the future. + if (j.at("/TransactionResponse/Values/Expires"_json_pointer).is_string()) { + auto expiry_string = j["/TransactionResponse/Values/Expires"_json_pointer].get(); + if (!server_expiry.FromISO8601(expiry_string)) { + return MakeCriticalError("failed to parse TransactionResponse.Values.Expires; got "s + expiry_string); + } + } + + // Unused fields + //auto transaction_amount = j.at("TransactionAmount").get(); + } + catch (json::exception& e) { + return MakeCriticalError( + utils::Stringer("json parse failed: ", e.what(), "; id:", e.id)); + } + + optional authOptional = nullopt; + if (!authorization_encoded.empty()) { + auto decodeAuthResult = DecodeAuthorization(authorization_encoded); + if (!decodeAuthResult) { + // Authorization can be optional, but inability to decode suggests + // something is very wrong. + return WrapError(decodeAuthResult.error(), "failed to decode Purchase Authorization"); + } + authOptional = *decodeAuthResult; + } + + Purchase purchase = { + transaction_id, + server_created, + transaction_class, + transaction_distinguisher, + server_expiry.IsZero() ? nullopt : make_optional( + server_expiry), + server_expiry.IsZero() ? nullopt : make_optional( + server_expiry), + authOptional + }; + + user_data_->UpdatePurchaseLocalTimeExpiry(purchase); + + return purchase; } // Enable JSON de/serializing of Authorization. diff --git a/psicash.hpp b/psicash.hpp index c63aab8..b949135 100644 --- a/psicash.hpp +++ b/psicash.hpp @@ -63,6 +63,8 @@ struct HTTPParams { // name-value pairs: [ ["class", "speed-boost"], ["expectedAmount", "-10000"], ... ] std::vector> query; + // body must be omitted if empty + std::string body; }; // The result from MakeHTTPRequestFn: struct HTTPResult { @@ -77,8 +79,8 @@ struct HTTPResult { // The contents of the response body, if any. std::string body; - // The value of the response Date header. - std::string date; + // The response headers. + std::map> headers; // Any error message relating to an unsuccessful network attempt; // must be empty if the request succeeded (regardless of status code). @@ -87,16 +89,10 @@ struct HTTPResult { HTTPResult() : code(CRITICAL_ERROR) {} }; // This is the signature for the HTTP Requester callback provided by the native consumer. +// The requester _must_ do HTTPS certificate validation. +// In the case of a partial response, a `RECOVERABLE_ERROR` should be returned. using MakeHTTPRequestFn = std::function; -// These are the possible token types. -extern const char* const kEarnerTokenType; -extern const char* const kSpenderTokenType; -extern const char* const kIndicatorTokenType; -extern const char* const kAccountTokenType; - -using TokenTypes = std::vector; - struct PurchasePrice { std::string transaction_class; std::string distinguisher; @@ -130,6 +126,7 @@ extern const char* const kTransactionIDZero; // The "zero value" for a Transacti struct Purchase { TransactionID id; + datetime::DateTime server_time_created; std::string transaction_class; std::string distinguisher; nonstd::optional server_time_expiry; @@ -153,6 +150,8 @@ enum class Status { TransactionAmountMismatch, TransactionTypeNotFound, InvalidTokens, + InvalidCredentials, + BadRequest, ServerError }; @@ -164,26 +163,32 @@ class PsiCash { PsiCash(const PsiCash&) = delete; PsiCash& operator=(PsiCash const&) = delete; - /// Must be called once, before any other methods except Reset (or behaviour is undefined). + /// Must be called once, before any other methods (or behaviour is undefined). /// `user_agent` is required and must be non-empty. /// `file_store_root` is required and must be non-empty. `"."` can be used for the cwd. /// `make_http_request_fn` may be null and set later with SetHTTPRequestFn. /// Returns false if there's an unrecoverable error (such as an inability to use the /// filesystem). + /// If `force_reset` is true, the datastore will be completely wiped out and reset. /// If `test` is true, then the test server will be used, and other testing interfaces /// will be available. Should only be used for testing. /// When uninitialized, data accessors will return zero values, and operations (e.g., /// RefreshState and NewExpiringPurchase) will return errors. error::Error Init(const std::string& user_agent, const std::string& file_store_root, - MakeHTTPRequestFn make_http_request_fn, bool test=false); - - /// Resets the PsiCash datastore. Init() must be called after this method is used. - /// Returns an error if the reset failed, likely indicating a filesystem problem. - error::Error Reset(const std::string& file_store_root, bool test=false); + MakeHTTPRequestFn make_http_request_fn, bool force_reset, bool test); /// Returns true if the library has been successfully initialized (i.e., Init called). bool Initialized() const; + /// Resets PsiCash data for the current user (Tracker or Account). This will typically + /// be called when wanting to revert to a Tracker from a previously logged in Account. + error::Error ResetUser(); + + /// Forces the given Tracker tokens to be set in the datastore. Must be called after + /// Init(). RefreshState() must be called after method (and shouldn't be be called + /// before this method, although behaviour will be okay). + error::Error MigrateTrackerTokens(const std::map& tokens); + /// Can be used for updating the HTTP requester function pointer. void SetHTTPRequestFn(MakeHTTPRequestFn make_http_request_fn); @@ -191,17 +196,26 @@ class PsiCash { /// client_version, client_region, sponsor_id, and propagation_channel_id. error::Error SetRequestMetadataItem(const std::string& key, const std::string& value); + /// Set current UI locale. + error::Error SetLocale(const std::string& locale); + // // Stored info accessors // - /// Returns the stored valid token types. Like ["spender", "indicator"]. - /// Will be empty if no tokens are available. - TokenTypes ValidTokenTypes() const; + /// Returns true if there are sufficient tokens for this library to function on behalf + /// of a user. False otherwise. + /// If this is false and `IsAccount()` is true, then the user is a logged-out account + /// and needs to log in to continue. If this is false and `IsAccount()` is false, + /// `RefreshState()` needs to be called to get new Tracker tokens. + bool HasTokens() const; /// Returns the stored info about whether the user is a Tracker or an Account. bool IsAccount() const; + /// Returns the username of the logged-in account, if in a logged-in-account state. + nonstd::optional AccountUsername() const; + /// Returns the stored user balance. int64_t Balance() const; @@ -209,10 +223,11 @@ class PsiCash { /// Will be empty if no purchase prices are available. PurchasePrices GetPurchasePrices() const; - /// Returns the set of active purchases, if any. + /// Returns all purchases in the local datastore, if any. This may include expired + /// purchases. Purchases GetPurchases() const; - /// Returns the set of active purchases that are not expired, if any. + /// Returns the set of purchases that are not expired, if any. Purchases ActivePurchases() const; /// Returns all purchase authorizations. If activeOnly is true, only authorizations @@ -241,12 +256,23 @@ class PsiCash { /// Returns an error if modification is impossible. (In that case the error /// should be logged -- and added to feedback -- and home page opening should /// proceed with the original URL.) + /// Note that it does NOT return an error when there are no tokens or insufficient + /// tokens -- it just modifies the URL as best it can, setting `tokens` to null. error::Result ModifyLandingPage(const std::string& url) const; /// Utilizes stored tokens and metadata (and a configured base URL) to craft a URL /// where the user can buy PsiCash for real money. error::Result GetBuyPsiURL() const; + enum class UserSiteURLType { + AccountSignup = 0, + AccountManagement, + ForgotAccount + }; + /// Returns the `my.psi.cash` URL of the give type. + /// If `webview` is true, the URL will be appended to with `?webview=true`. + std::string GetUserSiteURL(UserSiteURLType url_type, bool webview) const; + /// Creates a data package that should be included with a webhook for a user /// action that should be rewarded (such as watching a rewarded video). /// NOTE: The resulting string will still need to be encoded for use in a URL. @@ -270,24 +296,35 @@ class PsiCash { // /** - Refreshes the client state. Retrieves info about whether the user has an - Account (vs Tracker), balance, valid token types, and purchase prices. After a - successful request, the retrieved values can be accessed with the accessor - methods. + Refreshes the client state. Retrieves info about whether the user has an Account (vs + Tracker), balance, valid token types, purchases, and purchase prices. After a + successful request, the retrieved values can be accessed with the accessor methods. If there are no tokens stored locally (e.g., if this is the first run), then new Tracker tokens will obtained. - If the user is/has an Account, then it is possible some tokens will be invalid + If the user has an Account, then it is possible some or all tokens will be invalid (they expire at different rates). Login may be necessary before spending, etc. - (It's even possible that validTokenTypes is empty -- i.e., there are no valid - tokens.) + (It's even possible that hasTokens is false.) + + If the user has an Account, then it is possible some or all tokens will be invalid + (they may expire at different rates) and multiple states are possible: + • spender, indicator, and earner tokens are all valid. + • Some token types are valid, while others are not. The client will probably want to + consider itself not-logged-in and force a login. + • No tokens are valid. + + See the flow chart in the README for a graphical representation of states. If there is no valid indicator token, then balance and purchase prices will not be retrieved, but there may be stored (possibly stale) values that can be used. Input parameters: + • local_only: If true, no network call will be made, and the refresh will utilize only + locally-stored data (i.e., only token expiry will be checked, and a transition into + a logged-out state may result). + • purchase_classes: The purchase class names for which prices should be retrieved, like `{"speed-boost"}`. If null or empty, no purchase prices will be retrieved. @@ -297,18 +334,31 @@ class PsiCash { • status: Request success indicator. See below for possible values. + • reconnect_required: If true, a reconnect is required due to the effects of this call. + There are two main scenarios where this is the case: + 1. A Speed Boost purchase was retrieved and its authorization needs to be applied to + the tunnel. + 2. Speed Boost is active when account tokens expires, so the authorization needs to + be removed from the tunnel. + Possible status codes: • Success: Call was successful. Tokens may now be available (depending on if - IsAccount is true, ValidTokenTypes should be checked, as a login may be required). + IsAccount is true, HasTokens should be checked, as a login may be required). • ServerError: The server returned 500 error response. Note that the request has already been retried internally and any further retry should not be immediate. - • InvalidTokens: Should never happen (indicates something like - local storage corruption). The local user state will be cleared. + • InvalidTokens: Should never happen (indicates something like local storage + corruption). The local user state will be cleared. */ - error::Result RefreshState(const std::vector& purchase_classes); + struct RefreshStateResponse { + Status status; + bool reconnect_required; + }; + error::Result RefreshState( + bool local_only, + const std::vector& purchase_classes); /** Makes a new transaction for an "expiring-purchase" class, such as "speed-boost". @@ -325,7 +375,9 @@ class PsiCash { Result fields: - • error: If set, the request failed utterly and no other params are valid. + • error: If set, the request failed utterly and no other params are valid. An error + result should be followed by a RefreshState call, in case the purchase succeeded on + the server side but wasn't retrieved; RefreshState will synchronize state. • status: Request success indicator. See below for possible values. @@ -349,8 +401,9 @@ class PsiCash { could not be found. The price list should be updated immediately, but it might also indicate an out-of-date app. - • InvalidTokens: The current auth tokens are invalid. - TODO: Figure out how to handle this. It shouldn't be a factor for Trackers or MVP. + • InvalidTokens: The current auth tokens are invalid. This shouldn't happen with + Trackers, but may happen for Accounts when their tokens expire. Calling RefreshState + should return the library to a sane state (logged out or reset). • ServerError: An error occurred on the server. Probably report to the user and try again later. Note that the request has already been retried internally and any @@ -360,29 +413,96 @@ class PsiCash { Status status; nonstd::optional purchase; }; - error::Result NewExpiringPurchase( const std::string& transaction_class, const std::string& distinguisher, const int64_t expected_price); + /** + Logs out a currently logged-in account. + + Result fields: + • error: If set, the request failed utterly and no other params are valid. + • reconnect_required: If true, a reconnect is required due to the effects of this call. + This typically means that a Speed Boost was active at the time of logout. + + An error will be returned in these cases: + • If the user is not an account + • If the request to the server fails + • If the local datastore cannot be updated + These errors should always be logged, but the local state may end up being logged out, + even if they do occur -- such as when the server request fails -- so checks for state + will need to occur. + NOTE: This (usually) does involve a network operation, so wrappers may want to be + asynchronous. + */ + struct AccountLogoutResponse { + bool reconnect_required; + }; + error::Result AccountLogout(); + + /** + Attempts to log the current user into an account. Will attempt to merge any available + Tracker balance. + + If success, RefreshState should be called immediately afterward. + + Input parameters: + • utf8_username: The username, encoded in UTF-8. + • utf8_password: The password, encoded in UTF-8. + + Result fields: + • error: If set, the request failed utterly and no other params are valid. + • status: Request success indicator. See below for possible values. + • last_tracker_merge: If true, a Tracker was merged into the account, and this was + the last such merge that is allowed -- the user should be informed of this. + + Possible status codes: + • Success: The credentials were correct and the login request was successful. There + are tokens available for future requests. + • InvalidCredentials: One or both of the username and password did not match a known + Account. + • BadRequest: The data sent to the server was invalid in some way. This should not + happen in normal operation. + • ServerError: An error occurred on the server. Probably report to the user and try + again later. Note that the request has already been retried internally and any + further retry should not be immediate. + */ + struct AccountLoginResponse { + Status status; + nonstd::optional last_tracker_merge; + }; + + error::Result AccountLogin( + const std::string& utf8_username, + const std::string& utf8_password); + protected: // See implementation for descriptions of non-public methods. + error::Result AddEarnerTokenToURL(const std::string& url_string, bool query_param_only) const; + nlohmann::json GetRequestMetadata(int attempt) const; error::Result MakeHTTPRequestWithRetry( const std::string& method, const std::string& path, bool include_auth_tokens, - const std::vector>& query_params); + const std::vector>& query_params, + const nonstd::optional& body); virtual error::Result BuildRequestParams( const std::string& method, const std::string& path, bool include_auth_tokens, const std::vector>& query_params, int attempt, - const std::map& additional_headers) const; + const std::map& additional_headers, + const std::string& body) const; error::Result NewTracker(); - error::Result - RefreshState(const std::vector& purchase_classes, bool allow_recursion); + error::Result RefreshState( + const std::vector& purchase_classes, bool allow_recursion); + + // If expected_type is empty, no check will be done. + error::Result PurchaseFromJSON(const nlohmann::json& j, const std::string& expected_type="") const; + + std::string CommaDelimitTokens(const std::vector& types) const; protected: bool test_; @@ -391,7 +511,7 @@ class PsiCash { std::string server_scheme_; std::string server_hostname_; int server_port_; - // This is a pointer rather than an instance to avoid including userdata.h (TODO: worthwhile?) + // This is a pointer rather than an instance to avoid including userdata.h std::unique_ptr user_data_; MakeHTTPRequestFn make_http_request_fn_; }; diff --git a/psicash_test.cpp b/psicash_test.cpp index 9071919..b60f35c 100644 --- a/psicash_test.cpp +++ b/psicash_test.cpp @@ -13,6 +13,10 @@ #include using json = nlohmann::json; +// Requires `apt install libssl-dev` +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include "vendor/httplib.h" + using namespace std; using namespace psicash; using namespace testing; @@ -21,86 +25,61 @@ constexpr int64_t MAX_STARTING_BALANCE = 100000000000LL; class TestPsiCash : public ::testing::Test, public TempDir { public: - TestPsiCash() : user_agent_("Psiphon-PsiCash-iOS") { - } + TestPsiCash() { } + + static string UserAgent() { return "Psiphon-PsiCash-iOS"; } static HTTPResult HTTPRequester(const HTTPParams& params) { - stringstream curl; - curl << "curl -s -i --max-time 5"; - curl << " -X " << params.method; + httplib::Params query_params; + for (const auto& qp : params.query) { + query_params.emplace(qp.first, qp.second); + } - for (auto it = params.headers.begin(); it != params.headers.end(); ++it) { - curl << " -H \"" << it->first << ":" << it->second << "\""; + stringstream url; + url << params.scheme << "://" << params.hostname << ":" << params.port; + httplib::Client http_client(url.str()); + + httplib::Headers headers; + for (const auto& h : params.headers) { + headers.emplace(h.first, h.second); } - curl << ' '; - curl << '"' << params.scheme << "://"; - curl << params.hostname << ":" << params.port; - curl << params.path; - - // query is an array of 2-tuple name-value arrays - for (auto it = params.query.begin(); it != params.query.end(); ++it) { - if (it == params.query.begin()) { - curl << "?"; - } else { - curl << "&"; - } + stringstream path_query; + path_query << params.path << "?" << httplib::detail::params_to_query_str(query_params); - curl << it->first << "=" << it->second; + nonstd::optional res; + if (params.method == "GET") { + res = http_client.Get(path_query.str().c_str(), headers); + } + else if (params.method == "POST") { + res = http_client.Post(path_query.str().c_str(), headers, params.body.c_str(), params.body.length(), "application/json"); + } + else if (params.method == "PUT") { + res = http_client.Put(path_query.str().c_str(), headers, params.body.c_str(), params.body.length(), "application/json"); + } + else { + throw std::invalid_argument("unsupported request method: "s + params.method); } - curl << '"'; HTTPResult result; - - auto command = curl.str(); - string output; - auto code = exec(command.c_str(), output); - if (code != 0) { - result.error = output; + if (res->error() != httplib::Error::Success) { + stringstream err; + err << "request error: " << res->error(); + result.error = err.str(); return result; } - - std::stringstream ss(output); - std::string line; - - string body, full_output; - bool done_headers = false; - while (std::getline(ss, line, '\n')) { - line = trim(line); - if (line.empty()) { - done_headers = true; - } - - full_output += line + "\n"; - - if (!done_headers) { - smatch match_pieces; - - // Look for HTTP status code value (200, etc.) - regex status_regex("^HTTP\\/\\d\\S* (\\d\\d\\d).*$", - regex_constants::ECMAScript | regex_constants::icase); - if (regex_match(line, match_pieces, status_regex)) { - result.code = stoi(match_pieces[1].str()); - } - - // Look for the Date header - regex date_regex("^Date: (.+)$", - regex_constants::ECMAScript | regex_constants::icase); - if (regex_match(line, match_pieces, date_regex)) { - result.date = match_pieces[1].str(); - } - } - - if (done_headers) { - body += line; - } + else if (!*res) { + result.error = "request failed utterly"; + return result; } - result.body = body; + result.code = (*res)->status; + result.body = (*res)->body; - if (result.code < 0) { - // Something went wrong during processing. Set the whole output as the error. - result.error = full_output; + for (const auto& h : (*res)->headers) { + // This won't cope correctly with multiple headers of the same name + vector v = {h.second}; + result.headers.emplace(h.first, v); } return result; @@ -112,9 +91,6 @@ class TestPsiCash : public ::testing::Test, public TempDir { return result; }; } - - const char* user_agent_; - }; #define MAKE_1T_REWARD(pc, count) (pc.MakeRewardRequests(TEST_CREDIT_TRANSACTION_CLASS, TEST_ONE_TRILLION_ONE_MICROSECOND_DISTINGUISHER, count)) @@ -122,17 +98,71 @@ class TestPsiCash : public ::testing::Test, public TempDir { TEST_F(TestPsiCash, InitSimple) { { - // Force Init to test=false to test that path (you should typically not do this in tests) PsiCashTester pc; - auto err = pc.Init(user_agent_, GetTempDir().c_str(), HTTPRequester, false); + ASSERT_FALSE(pc.Initialized()); + // Force Init to test=false to test that path (you should typically not do this in tests) + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), HTTPRequester, false, false); ASSERT_FALSE(err); + ASSERT_TRUE(pc.Initialized()); } { PsiCashTester pc; + ASSERT_FALSE(pc.Initialized()); // Force Init to test=true to test that path (you should typically not do this in tests) - auto err = pc.Init(user_agent_, GetTempDir().c_str(), nullptr, true); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), nullptr, false, true); + ASSERT_FALSE(err); + ASSERT_TRUE(pc.Initialized()); + } +} + +TEST_F(TestPsiCash, InitReset) { + auto temp_dir = GetTempDir(); + string expected_instance_id; + { + // Set up some state + PsiCashTester pc; + ASSERT_FALSE(pc.Initialized()); + auto err = pc.Init(TestPsiCash::UserAgent(), temp_dir.c_str(), HTTPRequester, false); + ASSERT_FALSE(err); + ASSERT_TRUE(pc.Initialized()); + + expected_instance_id = pc.user_data().GetInstanceID(); + + auto res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login) << res_login.error(); + ASSERT_EQ(res_login->status, Status::Success); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_TRUE(pc.HasTokens()); + ASSERT_TRUE(pc.AccountUsername()); + ASSERT_EQ(*pc.AccountUsername(), TEST_ACCOUNT_ONE_USERNAME); + } + { + // Check that the state persists + PsiCashTester pc; + ASSERT_FALSE(pc.Initialized()); + auto err = pc.Init(TestPsiCash::UserAgent(), temp_dir.c_str(), HTTPRequester, false); + ASSERT_FALSE(err); + ASSERT_TRUE(pc.Initialized()); + + ASSERT_EQ(pc.user_data().GetInstanceID(), expected_instance_id); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_TRUE(pc.HasTokens()); + ASSERT_TRUE(pc.AccountUsername()); + ASSERT_EQ(*pc.AccountUsername(), TEST_ACCOUNT_ONE_USERNAME); + } + { + // Init with reset, previous state should be gone + PsiCashTester pc; + ASSERT_FALSE(pc.Initialized()); + auto err = pc.Init(TestPsiCash::UserAgent(), temp_dir.c_str(), HTTPRequester, true); ASSERT_FALSE(err); + ASSERT_TRUE(pc.Initialized()); + + ASSERT_NE(pc.user_data().GetInstanceID(), expected_instance_id); + ASSERT_FALSE(pc.IsAccount()); + ASSERT_FALSE(pc.HasTokens()); + ASSERT_FALSE(pc.AccountUsername()); } } @@ -141,20 +171,23 @@ TEST_F(TestPsiCash, InitFail) { // Datastore directory that will not work auto bad_dir = GetTempDir() + "/a/b/c/d/f/g"; PsiCashTester pc; - auto err = pc.Init(user_agent_, bad_dir.c_str(), nullptr); + auto err = pc.Init(TestPsiCash::UserAgent(), bad_dir.c_str(), nullptr, false); ASSERT_TRUE(err) << bad_dir; + ASSERT_FALSE(pc.Initialized()); } { // Empty datastore directory PsiCashTester pc; - auto err = pc.Init(user_agent_, "", nullptr); + auto err = pc.Init(TestPsiCash::UserAgent(), "", nullptr, false); ASSERT_TRUE(err); + ASSERT_FALSE(pc.Initialized()); } { // Empty user agent PsiCashTester pc; - auto err = pc.Init("", GetTempDir().c_str(), nullptr); + auto err = pc.Init("", GetTempDir().c_str(), nullptr, false); ASSERT_TRUE(err); + ASSERT_FALSE(pc.Initialized()); } } @@ -166,75 +199,81 @@ TEST_F(TestPsiCash, UninitializedBehaviour) { ASSERT_EQ(pc.Balance(), 0); - auto res = pc.RefreshState({"speed-boost"}); + auto res = pc.RefreshState(false, {"speed-boost"}); ASSERT_FALSE(res); } { // Failed Init auto bad_dir = GetTempDir() + "/a/b/c/d/f/g"; PsiCashTester pc; - auto err = pc.Init(user_agent_, bad_dir.c_str(), nullptr); + auto err = pc.Init(TestPsiCash::UserAgent(), bad_dir.c_str(), nullptr, false); ASSERT_TRUE(err) << bad_dir; ASSERT_FALSE(pc.Initialized()); ASSERT_EQ(pc.Balance(), 0); - auto res = pc.RefreshState({"speed-boost"}); + auto res = pc.RefreshState(false, {"speed-boost"}); ASSERT_FALSE(res); } } -TEST_F(TestPsiCash, Reset) { - int64_t want_balance = 123; - auto temp_dir = GetTempDir(); - +TEST_F(TestPsiCash, MigrateTrackerTokens) { { - // Set a value + // Without calling RefreshState first (so no preexisting tokens); tracker. PsiCashTester pc; - auto err = pc.Init(user_agent_, temp_dir.c_str(), nullptr); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), HTTPRequester, false); ASSERT_FALSE(err); - err = pc.user_data().SetBalance(want_balance); - ASSERT_FALSE(err); + ASSERT_FALSE(pc.HasTokens()); - auto got_balance = pc.Balance(); - ASSERT_EQ(got_balance, want_balance); - } - { - // Check the value's persistence - PsiCashTester pc; - auto err = pc.Init(user_agent_, temp_dir.c_str(), nullptr); + map tokens = {{"a", "a"}, {"b", "b"}, {"c", "c"}}; + err = pc.MigrateTrackerTokens(tokens); ASSERT_FALSE(err); - - auto got_balance = pc.Balance(); - ASSERT_EQ(got_balance, want_balance); + ASSERT_EQ(pc.user_data().ValidTokenTypes().size(), 3); + ASSERT_FALSE(pc.IsAccount()); + ASSERT_FALSE(pc.AccountUsername()); } { - // Reset + // Call RefreshState first (so preexisting tokens and user data). PsiCashTester pc; - auto err = pc.Reset(temp_dir.c_str()); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), HTTPRequester, false); ASSERT_FALSE(err); - err = pc.Init(user_agent_, temp_dir.c_str(), nullptr); - ASSERT_FALSE(err); + ASSERT_FALSE(pc.HasTokens()); - auto got_balance = pc.Balance(); - ASSERT_EQ(got_balance, 0) << temp_dir; + auto res = pc.RefreshState(false, {"speed-boost"}); + ASSERT_TRUE(res) << res.error(); + ASSERT_EQ(res->status, Status::Success) << (int)res->status; + ASSERT_FALSE(pc.IsAccount()); + ASSERT_FALSE(pc.AccountUsername()); + ASSERT_TRUE(pc.HasTokens()); + ASSERT_THAT(pc.Balance(), AllOf(Ge(0), Le(MAX_STARTING_BALANCE))); + ASSERT_GE(pc.GetPurchasePrices().size(), 2); + + map tokens = {{"a", "a"}, {"b", "b"}, {"c", "c"}}; + AuthTokens auth_tokens = {{"a", {"a"}}, {"b", {"b"}}, {"c", {"c"}}}; + err = pc.MigrateTrackerTokens(tokens); + ASSERT_FALSE(err); + ASSERT_TRUE(AuthTokenSetsEqual(pc.user_data().GetAuthTokens(), auth_tokens)); + ASSERT_FALSE(pc.IsAccount()); + ASSERT_FALSE(pc.AccountUsername()); + ASSERT_EQ(pc.Balance(), 0); + ASSERT_GE(pc.GetPurchasePrices().size(), 0); } } TEST_F(TestPsiCash, SetHTTPRequestFn) { { PsiCashTester pc; - auto err = pc.Init(user_agent_, GetTempDir().c_str(), HTTPRequester); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), HTTPRequester, false); ASSERT_FALSE(err); pc.SetHTTPRequestFn(HTTPRequester); } { PsiCashTester pc; - auto err = pc.Init(user_agent_, GetTempDir().c_str(), nullptr); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), nullptr, false); ASSERT_FALSE(err); pc.SetHTTPRequestFn(HTTPRequester); } @@ -242,7 +281,7 @@ TEST_F(TestPsiCash, SetHTTPRequestFn) { TEST_F(TestPsiCash, SetRequestMetadataItem) { PsiCashTester pc; - auto err = pc.Init(user_agent_, GetTempDir().c_str(), nullptr); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), nullptr, false); ASSERT_FALSE(err); auto j = pc.user_data().GetRequestMetadata(); @@ -255,9 +294,24 @@ TEST_F(TestPsiCash, SetRequestMetadataItem) { ASSERT_EQ(j["k"], "v"); } +TEST_F(TestPsiCash, SetLocale) { + PsiCashTester pc; + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), nullptr, false); + ASSERT_FALSE(err); + + auto url = pc.GetUserSiteURL(PsiCash::UserSiteURLType::AccountSignup, false); + ASSERT_THAT(url, EndsWith("locale=")); + + err = pc.SetLocale("en-US"); + ASSERT_FALSE(err); + + url = pc.GetUserSiteURL(PsiCash::UserSiteURLType::AccountSignup, false); + ASSERT_THAT(url, HasSubstr("locale=en-US")); +} + TEST_F(TestPsiCash, IsAccount) { PsiCashTester pc; - auto err = pc.Init(user_agent_, GetTempDir().c_str(), nullptr); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), nullptr, false); ASSERT_FALSE(err); // Check the default @@ -277,33 +331,68 @@ TEST_F(TestPsiCash, IsAccount) { ASSERT_EQ(v, false); } -TEST_F(TestPsiCash, ValidTokenTypes) { - PsiCashTester pc; - auto err = pc.Init(user_agent_, GetTempDir().c_str(), nullptr); - ASSERT_FALSE(err); +TEST_F(TestPsiCash, HasTokens) { + { + PsiCashTester pc; + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), nullptr, false); + ASSERT_FALSE(err); - auto vtt = pc.ValidTokenTypes(); - ASSERT_EQ(vtt.size(), 0); + ASSERT_FALSE(pc.HasTokens()); + + // No expiry, bad types + AuthTokens at = {{"a", {"a"}}, {"b", {"b"}}, {"c", {"c"}}}; + err = pc.user_data().SetAuthTokens(at, false, ""); + ASSERT_FALSE(pc.HasTokens()); + + // No expiry, good types + at = {{"earner", {"a"}}, {"indicator", {"b"}}, {"spender", {"c"}}}; + err = pc.user_data().SetAuthTokens(at, false, ""); + ASSERT_TRUE(pc.HasTokens()); + + // Expiries in the future, bad types + auto future = datetime::DateTime::Now().Add(datetime::Duration(10000)); + at = {{"a", {"a", future}}, {"b", {"b", future}}, {"c", {"c", future}}}; + err = pc.user_data().SetAuthTokens(at, true, "username"); + ASSERT_FALSE(pc.HasTokens()); + + // Expiries in the future, good types + at = {{"indicator", {"a", future}}, {"spender", {"b", future}}, {"earner", {"c", future}}, {"logout", {"c", future}}}; + err = pc.user_data().SetAuthTokens(at, true, "username"); + ASSERT_TRUE(pc.HasTokens()); + + // One expiry in the past, good types + auto past = datetime::DateTime::Now().Sub(datetime::Duration(10000)); + at = {{"indicator", {"a", past}}, {"spender", {"b", future}}, {"earner", {"c", future}}, {"logout", {"c", future}}}; + err = pc.user_data().SetAuthTokens(at, true, "username"); + ASSERT_TRUE(pc.HasTokens()); + } + { + PsiCashTester pc; + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), HTTPRequester, false); + ASSERT_FALSE(err); + ASSERT_FALSE(pc.HasTokens()); - AuthTokens at = {{"a", "a"}, {"b", "b"}, {"c", "c"}}; - err = pc.user_data().SetAuthTokens(at, false); - vtt = pc.ValidTokenTypes(); - ASSERT_EQ(vtt.size(), 3); - for (const auto& k : vtt) { - ASSERT_EQ(at.count(k), 1); - at.erase(k); + auto res = pc.RefreshState(false, {"speed-boost"}); + ASSERT_TRUE(res) << res.error(); + ASSERT_EQ(res->status, Status::Success) << (int)res->status; + ASSERT_TRUE(pc.HasTokens()); } - ASSERT_EQ(at.size(), 0); // we should have erase all items + { + PsiCashTester pc; + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), HTTPRequester, false); + ASSERT_FALSE(err); + ASSERT_FALSE(pc.HasTokens()); - AuthTokens empty; - err = pc.user_data().SetAuthTokens(empty, false); - vtt = pc.ValidTokenTypes(); - ASSERT_EQ(vtt.size(), 0); + auto res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login) << res_login.error(); + ASSERT_EQ(res_login->status, Status::Success); + ASSERT_TRUE(pc.HasTokens()); + } } TEST_F(TestPsiCash, Balance) { PsiCashTester pc; - auto err = pc.Init(user_agent_, GetTempDir().c_str(), nullptr); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), nullptr, false); ASSERT_FALSE(err); // Check the default @@ -325,7 +414,7 @@ TEST_F(TestPsiCash, Balance) { TEST_F(TestPsiCash, GetPurchasePrices) { PsiCashTester pc; - auto err = pc.Init(user_agent_, GetTempDir().c_str(), nullptr); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), nullptr, false); ASSERT_FALSE(err); auto v = pc.GetPurchasePrices(); @@ -348,7 +437,7 @@ TEST_F(TestPsiCash, GetPurchasePrices) { TEST_F(TestPsiCash, GetPurchases) { PsiCashTester pc; - auto err = pc.Init(user_agent_, GetTempDir().c_str(), nullptr); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), nullptr, false); ASSERT_FALSE(err); auto v = pc.GetPurchases(); @@ -358,8 +447,8 @@ TEST_F(TestPsiCash, GetPurchases) { ASSERT_TRUE(auth_res); Purchases ps = { - {"id1", "tc1", "d1", datetime::DateTime::Now(), datetime::DateTime::Now(), *auth_res}, - {"id2", "tc2", "d2", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}}; + {"id1", datetime::DateTime::Now(), "tc1", "d1", datetime::DateTime::Now(), datetime::DateTime::Now(), *auth_res}, + {"id2", datetime::DateTime::Now(), "tc2", "d2", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}}; err = pc.user_data().SetPurchases(ps); ASSERT_FALSE(err); @@ -377,7 +466,7 @@ TEST_F(TestPsiCash, GetPurchases) { TEST_F(TestPsiCash, ActivePurchases) { PsiCashTester pc; - auto err = pc.Init(user_agent_, GetTempDir().c_str(), nullptr); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), nullptr, false); ASSERT_FALSE(err); auto v = pc.GetPurchases(); @@ -386,14 +475,15 @@ TEST_F(TestPsiCash, ActivePurchases) { v = pc.ActivePurchases(); ASSERT_EQ(v.size(), 0); + auto created = datetime::DateTime(); // value doesn't matter here auto before_now = datetime::DateTime::Now().Sub(datetime::Duration(54321)); auto after_now = datetime::DateTime::Now().Add(datetime::Duration(54321)); - Purchases ps = {{"id1", "tc1", "d1", before_now, nonstd::nullopt, nonstd::nullopt}, - {"id2", "tc2", "d2", after_now, nonstd::nullopt, nonstd::nullopt}, - {"id3", "tc3", "d3", before_now, nonstd::nullopt, nonstd::nullopt}, - {"id4", "tc4", "d4", after_now, nonstd::nullopt, nonstd::nullopt}, - {"id5", "tc5", "d5", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}}; + Purchases ps = {{"id1", created, "tc1", "d1", before_now, nonstd::nullopt, nonstd::nullopt}, + {"id2", created, "tc2", "d2", after_now, nonstd::nullopt, nonstd::nullopt}, + {"id3", created, "tc3", "d3", before_now, nonstd::nullopt, nonstd::nullopt}, + {"id4", created, "tc4", "d4", after_now, nonstd::nullopt, nonstd::nullopt}, + {"id5", created, "tc5", "d5", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}}; err = pc.user_data().SetPurchases(ps); ASSERT_FALSE(err); @@ -450,7 +540,7 @@ TEST_F(TestPsiCash, DecodeAuthorization) { TEST_F(TestPsiCash, GetAuthorizations) { PsiCashTester pc; - auto err = pc.Init(user_agent_, GetTempDir().c_str(), nullptr); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), nullptr, false); ASSERT_FALSE(err); auto purchases = pc.GetPurchases(); @@ -467,6 +557,7 @@ TEST_F(TestPsiCash, GetAuthorizations) { auto before_now = datetime::DateTime::Now().Sub(datetime::Duration(54321)); auto after_now = datetime::DateTime::Now().Add(datetime::Duration(54321)); + auto created = datetime::DateTime(); // doesn't matter const auto encoded1 = "eyJBdXRob3JpemF0aW9uIjp7IklEIjoiMFYzRXhUdmlBdFNxTGZOd2FpQXlHNHpaRUJJOGpIYnp5bFdNeU5FZ1JEZz0iLCJBY2Nlc3NUeXBlIjoic3BlZWQtYm9vc3QtdGVzdCIsIkV4cGlyZXMiOiIyMDE5LTAxLTE0VDE3OjIyOjIzLjE2ODc2NDEyOVoifSwiU2lnbmluZ0tleUlEIjoiUUNZTzV2clIvZGhjRDZ6M2FMQlVNeWRuZlJyZFNRL1RWYW1IUFhYeTd0TT0iLCJTaWduYXR1cmUiOiJQL2NrenloVUJoSk5RQ24zMnluM1VTdGpLencxU04xNW9MclVhTU9XaW9scXBOTTBzNVFSNURHVEVDT1FzQk13ODdQdTc1TGE1OGtJTHRIcW1BVzhDQT09In0="; const auto encoded2 = "eyJBdXRob3JpemF0aW9uIjp7IklEIjoibFRSWnBXK1d3TFJqYkpzOGxBUFVaQS8zWnhmcGdwNDFQY0dkdlI5a0RVST0iLCJBY2Nlc3NUeXBlIjoic3BlZWQtYm9vc3QtdGVzdCIsIkV4cGlyZXMiOiIyMDE5LTAxLTE0VDIxOjQ2OjMwLjcxNzI2NTkyNFoifSwiU2lnbmluZ0tleUlEIjoiUUNZTzV2clIvZGhjRDZ6M2FMQlVNeWRuZlJyZFNRL1RWYW1IUFhYeTd0TT0iLCJTaWduYXR1cmUiOiJtV1Z5Tm9ZU0pFRDNXU3I3bG1OeEtReEZza1M5ZWlXWG1lcDVvVWZBSHkwVmYrSjZaQW9WajZrN3ZVTDNrakIreHZQSTZyaVhQc3FzWENRNkx0eFdBQT09In0="; @@ -476,9 +567,9 @@ TEST_F(TestPsiCash, GetAuthorizations) { ASSERT_TRUE(auth_res1); ASSERT_TRUE(auth_res2); - purchases = {{"future_no_auth", "tc1", "d1", after_now, nonstd::nullopt, nonstd::nullopt}, - {"past_auth", "tc2", "d2", before_now, nonstd::nullopt, *auth_res1}, - {"future_auth", "tc3", "d3", after_now, nonstd::nullopt, *auth_res2}}; + purchases = {{"future_no_auth", created, "tc1", "d1", after_now, nonstd::nullopt, nonstd::nullopt}, + {"past_auth", created, "tc2", "d2", before_now, nonstd::nullopt, *auth_res1}, + {"future_auth", created, "tc3", "d3", after_now, nonstd::nullopt, *auth_res2}}; err = pc.user_data().SetPurchases(purchases); ASSERT_FALSE(err); @@ -495,7 +586,7 @@ TEST_F(TestPsiCash, GetAuthorizations) { TEST_F(TestPsiCash, GetPurchasesByAuthorizationID) { PsiCashTester pc; - auto err = pc.Init(user_agent_, GetTempDir().c_str(), nullptr); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), nullptr, false); ASSERT_FALSE(err); auto purchases = pc.GetPurchases(); @@ -507,6 +598,7 @@ TEST_F(TestPsiCash, GetPurchasesByAuthorizationID) { auto before_now = datetime::DateTime::Now().Sub(datetime::Duration(54321)); auto after_now = datetime::DateTime::Now().Add(datetime::Duration(54321)); + auto created = datetime::DateTime(); // doesn't matter const auto encoded1 = "eyJBdXRob3JpemF0aW9uIjp7IklEIjoiMFYzRXhUdmlBdFNxTGZOd2FpQXlHNHpaRUJJOGpIYnp5bFdNeU5FZ1JEZz0iLCJBY2Nlc3NUeXBlIjoic3BlZWQtYm9vc3QtdGVzdCIsIkV4cGlyZXMiOiIyMDE5LTAxLTE0VDE3OjIyOjIzLjE2ODc2NDEyOVoifSwiU2lnbmluZ0tleUlEIjoiUUNZTzV2clIvZGhjRDZ6M2FMQlVNeWRuZlJyZFNRL1RWYW1IUFhYeTd0TT0iLCJTaWduYXR1cmUiOiJQL2NrenloVUJoSk5RQ24zMnluM1VTdGpLencxU04xNW9MclVhTU9XaW9scXBOTTBzNVFSNURHVEVDT1FzQk13ODdQdTc1TGE1OGtJTHRIcW1BVzhDQT09In0="; const auto encoded2 = "eyJBdXRob3JpemF0aW9uIjp7IklEIjoibFRSWnBXK1d3TFJqYkpzOGxBUFVaQS8zWnhmcGdwNDFQY0dkdlI5a0RVST0iLCJBY2Nlc3NUeXBlIjoic3BlZWQtYm9vc3QtdGVzdCIsIkV4cGlyZXMiOiIyMDE5LTAxLTE0VDIxOjQ2OjMwLjcxNzI2NTkyNFoifSwiU2lnbmluZ0tleUlEIjoiUUNZTzV2clIvZGhjRDZ6M2FMQlVNeWRuZlJyZFNRL1RWYW1IUFhYeTd0TT0iLCJTaWduYXR1cmUiOiJtV1Z5Tm9ZU0pFRDNXU3I3bG1OeEtReEZza1M5ZWlXWG1lcDVvVWZBSHkwVmYrSjZaQW9WajZrN3ZVTDNrakIreHZQSTZyaVhQc3FzWENRNkx0eFdBQT09In0="; @@ -516,9 +608,9 @@ TEST_F(TestPsiCash, GetPurchasesByAuthorizationID) { ASSERT_TRUE(auth_res1); ASSERT_TRUE(auth_res2); - purchases = {{"future_no_auth", "tc1", "d1", after_now, nonstd::nullopt, nonstd::nullopt}, - {"past_auth", "tc2", "d2", before_now, nonstd::nullopt, *auth_res1}, - {"future_auth", "tc3", "d3", after_now, nonstd::nullopt, *auth_res2}}; + purchases = {{"future_no_auth", created, "tc1", "d1", after_now, nonstd::nullopt, nonstd::nullopt}, + {"past_auth", created, "tc2", "d2", before_now, nonstd::nullopt, *auth_res1}, + {"future_auth", created, "tc3", "d3", after_now, nonstd::nullopt, *auth_res2}}; err = pc.user_data().SetPurchases(purchases); ASSERT_FALSE(err); @@ -533,7 +625,7 @@ TEST_F(TestPsiCash, GetPurchasesByAuthorizationID) { TEST_F(TestPsiCash, NextExpiringPurchase) { PsiCashTester pc; - auto err = pc.Init(user_agent_, GetTempDir().c_str(), nullptr); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), nullptr, false); ASSERT_FALSE(err); auto v = pc.GetPurchases(); @@ -545,11 +637,12 @@ TEST_F(TestPsiCash, NextExpiringPurchase) { auto first = datetime::DateTime::Now().Sub(datetime::Duration(333)); auto second = datetime::DateTime::Now().Sub(datetime::Duration(222)); auto third = datetime::DateTime::Now().Sub(datetime::Duration(111)); + auto created = datetime::DateTime(); // doesn't matter - Purchases ps = {{"id1", "tc1", "d1", second, nonstd::nullopt, nonstd::nullopt}, - {"id2", "tc2", "d2", first, nonstd::nullopt, nonstd::nullopt}, // first to expire - {"id3", "tc3", "d3", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}, - {"id4", "tc4", "d4", third, nonstd::nullopt, nonstd::nullopt}}; + Purchases ps = {{"id1", created, "tc1", "d1", second, nonstd::nullopt, nonstd::nullopt}, + {"id2", created, "tc2", "d2", first, nonstd::nullopt, nonstd::nullopt}, // first to expire + {"id3", created, "tc3", "d3", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}, + {"id4", created, "tc4", "d4", third, nonstd::nullopt, nonstd::nullopt}}; err = pc.user_data().SetPurchases(ps); ASSERT_FALSE(err); @@ -563,9 +656,9 @@ TEST_F(TestPsiCash, NextExpiringPurchase) { ASSERT_EQ(p->id, ps[1].id); auto later_than_now = datetime::DateTime::Now().Add(datetime::Duration(54321)); - ps = {{"id1", "tc1", "d1", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}, - {"id2", "tc2", "d2", later_than_now, nonstd::nullopt, nonstd::nullopt}, // only expiring - {"id3", "tc3", "d3", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}}; + ps = {{"id1", created, "tc1", "d1", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}, + {"id2", created, "tc2", "d2", later_than_now, nonstd::nullopt, nonstd::nullopt}, // only expiring + {"id3", created, "tc3", "d3", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}}; err = pc.user_data().SetPurchases(ps); ASSERT_FALSE(err); @@ -579,8 +672,8 @@ TEST_F(TestPsiCash, NextExpiringPurchase) { ASSERT_EQ(p->id, ps[1].id); // None expiring - ps = {{"id1", "tc1", "d1", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}, - {"id2", "tc2", "d2", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}}; + ps = {{"id1", created, "tc1", "d1", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}, + {"id2", created, "tc2", "d2", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}}; err = pc.user_data().SetPurchases(ps); ASSERT_FALSE(err); @@ -595,7 +688,7 @@ TEST_F(TestPsiCash, NextExpiringPurchase) { TEST_F(TestPsiCash, ExpirePurchases) { PsiCashTester pc; - auto err = pc.Init(user_agent_, GetTempDir().c_str(), nullptr); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), nullptr, false); ASSERT_FALSE(err); auto v = pc.GetPurchases(); @@ -607,11 +700,12 @@ TEST_F(TestPsiCash, ExpirePurchases) { auto before_now = datetime::DateTime::Now().Sub(datetime::Duration(54321)); auto after_now = datetime::DateTime::Now().Add(datetime::Duration(54321)); + auto created = datetime::DateTime(); // doesn't matter - Purchases ps = {{"id1", "tc1", "d1", after_now, nonstd::nullopt, nonstd::nullopt}, - {"id2", "tc2", "d2", before_now, nonstd::nullopt, nonstd::nullopt}, - {"id3", "tc3", "d3", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}, - {"id4", "tc4", "d4", before_now, nonstd::nullopt, nonstd::nullopt}}; + Purchases ps = {{"id1", created, "tc1", "d1", after_now, nonstd::nullopt, nonstd::nullopt}, + {"id2", created, "tc2", "d2", before_now, nonstd::nullopt, nonstd::nullopt}, + {"id3", created, "tc3", "d3", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}, + {"id4", created, "tc4", "d4", before_now, nonstd::nullopt, nonstd::nullopt}}; Purchases expired = {ps[1], ps[3]}; Purchases nonexpired = {ps[0], ps[2]}; @@ -643,16 +737,18 @@ TEST_F(TestPsiCash, ExpirePurchases) { TEST_F(TestPsiCash, RemovePurchases) { PsiCashTester pc; - auto err = pc.Init(user_agent_, GetTempDir().c_str(), nullptr); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), nullptr, false); ASSERT_FALSE(err); auto v = pc.GetPurchases(); ASSERT_EQ(v.size(), 0); - Purchases ps = {{"id1", "tc1", "d1", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}, - {"id2", "tc2", "d2", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}, - {"id3", "tc3", "d3", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}, - {"id4", "tc4", "d4", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}}; + auto created = datetime::DateTime(); // doesn't matter + + Purchases ps = {{"id1", created, "tc1", "d1", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}, + {"id2", created, "tc2", "d2", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}, + {"id3", created, "tc3", "d3", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}, + {"id4", created, "tc4", "d4", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}}; vector remove_ids = {ps[1].id, ps[3].id}; Purchases remaining = {ps[0], ps[2]}; @@ -701,64 +797,129 @@ TEST_F(TestPsiCash, RemovePurchases) { ASSERT_EQ(v, remaining); } +// Returns empty string on match +string TokenPayloadsMatch(const string &got_base64, const json& want_incomplete) { + auto want = want_incomplete; + want["v"] = 1; + want["metadata"]["v"] = 1; + want["metadata"]["user_agent"] = TestPsiCash::UserAgent(); + + // Extract the timestamp we got, then remove so we can compare the rest + auto got = json::parse(base64::B64Decode(got_base64)); + datetime::DateTime got_tokens_timestamp; + if (!got_tokens_timestamp.FromISO8601(got.at("timestamp").get())) { + return "failed to extract timestamp from got_base64"; + } + got.erase("timestamp"); + + if (got != want) { + return utils::Stringer("got!=want; got: >>", got, "<<; want: >>", want, "<<"); + } + + auto now = datetime::DateTime::Now(); + auto timestamp_diff_ms = now.MillisSinceEpoch() - got_tokens_timestamp.MillisSinceEpoch(); + if (timestamp_diff_ms > 1000) { + return utils::Stringer("timestamps differ too much; now: ", now.MillisSinceEpoch(), "; got: ", got_tokens_timestamp.MillisSinceEpoch(), "; diff: ", timestamp_diff_ms); + } + + return ""; +} + TEST_F(TestPsiCash, ModifyLandingPage) { PsiCashTester pc; // Pass false for test so that we don't get "dev" and "debug" in all the params - auto err = pc.Init(user_agent_, GetTempDir().c_str(), nullptr, false); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), nullptr, false, false); ASSERT_FALSE(err); const string key_part = "psicash="; + const string and_key_part = "&" + key_part; URL url_in, url_out; // - // No metadata set + // No tokens: no error // - auto encoded_data = base64::TrimPadding(base64::B64Encode(utils::Stringer(R"({"metadata":{"user_agent":")", user_agent_, R"(","v":1},"tokens":null,"v":1})"))); - url_in = {"https://asdf.sadf.gf", "", ""}; auto res = pc.ModifyLandingPage(url_in.ToString()); ASSERT_TRUE(res); url_out.Parse(*res); ASSERT_EQ(url_out.scheme_host_path_, url_in.scheme_host_path_); - ASSERT_EQ(url_out.query_, url_in.query_); - ASSERT_EQ(url_out.fragment_, - "!"s + key_part + encoded_data); + ASSERT_EQ(url_out.fragment_, url_in.fragment_); + ASSERT_THAT(TokenPayloadsMatch(url_out.query_.substr(key_part.length()), R"({"tokens":null})"_json), IsEmpty()); + + // + // Add tokens + // + + // Some tokens, but no earner token (different code path) + AuthTokens auth_tokens = {{kSpenderTokenType, {"kSpenderTokenType"}}, + {kIndicatorTokenType, {"kIndicatorTokenType"}}}; + err = pc.user_data().SetAuthTokens(auth_tokens, false, ""); + ASSERT_FALSE(err); + url_in = {"https://asdf.sadf.gf", "", ""}; + res = pc.ModifyLandingPage(url_in.ToString()); + ASSERT_TRUE(res); + url_out.Parse(*res); + ASSERT_EQ(url_out.scheme_host_path_, url_in.scheme_host_path_); + ASSERT_EQ(url_out.fragment_, url_in.fragment_); + ASSERT_THAT(TokenPayloadsMatch(url_out.query_.substr(key_part.length()), R"({"tokens":null})"_json), IsEmpty()); + + // All tokens + auth_tokens = {{kSpenderTokenType, {"kSpenderTokenType"}}, + {kEarnerTokenType, {"kEarnerTokenType"}}, + {kIndicatorTokenType, {"kIndicatorTokenType"}}}; + err = pc.user_data().SetAuthTokens(auth_tokens, false, ""); + ASSERT_FALSE(err); + url_in = {"https://asdf.sadf.gf", "", ""}; + res = pc.ModifyLandingPage(url_in.ToString()); + ASSERT_TRUE(res); + url_out.Parse(*res); + ASSERT_EQ(url_out.scheme_host_path_, url_in.scheme_host_path_); + ASSERT_EQ(url_out.fragment_, url_in.fragment_); + ASSERT_THAT(TokenPayloadsMatch(url_out.query_.substr(key_part.length()), R"({"tokens":"kEarnerTokenType"})"_json), IsEmpty()); + + // + // No metadata set + // + + url_in = {"https://asdf.sadf.gf", "", ""}; + res = pc.ModifyLandingPage(url_in.ToString()); + ASSERT_TRUE(res) << res.error(); + url_out.Parse(*res); + ASSERT_EQ(url_out.scheme_host_path_, url_in.scheme_host_path_); + ASSERT_EQ(url_out.fragment_, url_in.fragment_); + ASSERT_THAT(TokenPayloadsMatch(url_out.query_.substr(key_part.length()), R"({"tokens":"kEarnerTokenType"})"_json), IsEmpty()); url_in = {"https://asdf.sadf.gf", "gfaf=asdf", ""}; res = pc.ModifyLandingPage(url_in.ToString()); ASSERT_TRUE(res); url_out.Parse(*res); ASSERT_EQ(url_out.scheme_host_path_, url_in.scheme_host_path_); - ASSERT_EQ(url_out.query_, url_in.query_); - ASSERT_EQ(url_out.fragment_, - "!"s + key_part + encoded_data); + ASSERT_EQ(url_out.fragment_, url_in.fragment_); + ASSERT_THAT(TokenPayloadsMatch(url_out.query_.substr((url_in.query_+and_key_part).length()), R"({"tokens":"kEarnerTokenType"})"_json), IsEmpty()); url_in = {"https://asdf.sadf.gf/asdfilj/adf", "gfaf=asdf", ""}; res = pc.ModifyLandingPage(url_in.ToString()); ASSERT_TRUE(res); url_out.Parse(*res); ASSERT_EQ(url_out.scheme_host_path_, url_in.scheme_host_path_); - ASSERT_EQ(url_out.query_, url_in.query_); - ASSERT_EQ(url_out.fragment_, - "!"s + key_part + encoded_data); + ASSERT_EQ(url_out.fragment_, url_in.fragment_); + ASSERT_THAT(TokenPayloadsMatch(url_out.query_.substr((url_in.query_+and_key_part).length()), R"({"tokens":"kEarnerTokenType"})"_json), IsEmpty()); url_in = {"https://asdf.sadf.gf/asdfilj/adf.html", "gfaf=asdf", ""}; res = pc.ModifyLandingPage(url_in.ToString()); ASSERT_TRUE(res); url_out.Parse(*res); ASSERT_EQ(url_out.scheme_host_path_, url_in.scheme_host_path_); - ASSERT_EQ(url_out.query_, url_in.query_); - ASSERT_EQ(url_out.fragment_, - "!"s + key_part + encoded_data); + ASSERT_EQ(url_out.fragment_, url_in.fragment_); + ASSERT_THAT(TokenPayloadsMatch(url_out.query_.substr((url_in.query_+and_key_part).length()), R"({"tokens":"kEarnerTokenType"})"_json), IsEmpty()); url_in = {"https://asdf.sadf.gf/asdfilj/adf.html", "", "regffd"}; res = pc.ModifyLandingPage(url_in.ToString()); ASSERT_TRUE(res); url_out.Parse(*res); ASSERT_EQ(url_out.scheme_host_path_, url_in.scheme_host_path_); - ASSERT_EQ(url_out.query_, - key_part + encoded_data); + ASSERT_THAT(TokenPayloadsMatch(url_out.query_.substr(key_part.length()), R"({"tokens":"kEarnerTokenType"})"_json), IsEmpty()); ASSERT_EQ(url_out.fragment_, url_in.fragment_); url_in = {"https://asdf.sadf.gf/asdfilj/adf.html", "adfg=asdf&vfjnk=fadjn", "regffd"}; @@ -766,8 +927,7 @@ TEST_F(TestPsiCash, ModifyLandingPage) { ASSERT_TRUE(res); url_out.Parse(*res); ASSERT_EQ(url_out.scheme_host_path_, url_in.scheme_host_path_); - ASSERT_EQ(url_out.query_, - url_in.query_ + "&" + key_part + encoded_data); + ASSERT_THAT(TokenPayloadsMatch(url_out.query_.substr((url_in.query_+and_key_part).length()), R"({"tokens":"kEarnerTokenType"})"_json), IsEmpty()); ASSERT_EQ(url_out.fragment_, url_in.fragment_); // @@ -781,84 +941,160 @@ TEST_F(TestPsiCash, ModifyLandingPage) { ASSERT_TRUE(res); url_out.Parse(*res); ASSERT_EQ(url_out.scheme_host_path_, url_in.scheme_host_path_); - ASSERT_EQ(url_out.query_, url_in.query_); - encoded_data = base64::TrimPadding(base64::B64Encode(utils::Stringer(R"({"metadata":{"k":"v","user_agent":")", user_agent_, R"(","v":1},"tokens":null,"v":1})"))); - ASSERT_EQ(url_out.fragment_, "!"s + key_part + encoded_data); + ASSERT_EQ(url_out.fragment_, url_in.fragment_); + ASSERT_THAT(TokenPayloadsMatch(url_out.query_.substr(key_part.length()), R"({"metadata":{"k":"v"},"tokens":"kEarnerTokenType"})"_json), IsEmpty()); - // With tokens + // + // Errors + // + + res = pc.ModifyLandingPage("#$%^&"); + ASSERT_FALSE(res); +} - AuthTokens auth_tokens = {{kSpenderTokenType, "kSpenderTokenType"}, - {kEarnerTokenType, "kEarnerTokenType"}, - {kIndicatorTokenType, "kIndicatorTokenType"}}; - err = pc.user_data().SetAuthTokens(auth_tokens, false); +TEST_F(TestPsiCash, GetBuyPsiURL) { + // Most of the logic for GetBuyPsiURL is in ModifyLandingPage, so we don't need to + // repeat all of those tests. + + PsiCashTester pc; + // Pass false for test so that we don't get "dev" and "debug" in all the params + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), nullptr, false, false); ASSERT_FALSE(err); - url_in = {"https://asdf.sadf.gf", "", ""}; - res = pc.ModifyLandingPage(url_in.ToString()); - ASSERT_TRUE(res); - url_out.Parse(*res); - ASSERT_EQ(url_out.scheme_host_path_, url_in.scheme_host_path_); - ASSERT_EQ(url_out.query_, url_in.query_); - encoded_data = base64::TrimPadding(base64::B64Encode(utils::Stringer(R"({"metadata":{"k":"v","user_agent":")", user_agent_, R"(","v":1},"tokens":"kEarnerTokenType","v":1})"))); - ASSERT_EQ(url_out.fragment_, "!"s + key_part + encoded_data); + + const string bang_key_part = "!psicash="; + URL url_out; + + string buy_scheme_host_path = "https://buy.psi.cash/"; + + // + // No tokens: error + // + + auto res = pc.GetBuyPsiURL(); + ASSERT_FALSE(res); + + // + // No earner token: error + // // Some tokens, but no earner token (different code path) - auth_tokens = {{kSpenderTokenType, "kSpenderTokenType"}, - {kIndicatorTokenType, "kIndicatorTokenType"}}; - err = pc.user_data().SetAuthTokens(auth_tokens, false); + AuthTokens auth_tokens = {{kSpenderTokenType, {"kSpenderTokenType"}}, + {kIndicatorTokenType, {"kIndicatorTokenType"}}}; + err = pc.user_data().SetAuthTokens(auth_tokens, false, ""); ASSERT_FALSE(err); - url_in = {"https://asdf.sadf.gf", "", ""}; - res = pc.ModifyLandingPage(url_in.ToString()); - ASSERT_TRUE(res); - url_out.Parse(*res); - ASSERT_EQ(url_out.scheme_host_path_, url_in.scheme_host_path_); - ASSERT_EQ(url_out.query_, url_in.query_); - encoded_data = base64::TrimPadding(base64::B64Encode(utils::Stringer(R"({"metadata":{"k":"v","user_agent":")", user_agent_, R"(","v":1},"tokens":null,"v":1})"))); - ASSERT_EQ(url_out.fragment_, "!"s + key_part + encoded_data); + res = pc.GetBuyPsiURL(); + ASSERT_FALSE(res); // - // Errors + // With tokens // - res = pc.ModifyLandingPage("#$%^&"); - ASSERT_FALSE(res); + auth_tokens = {{kSpenderTokenType, {"kSpenderTokenType"}}, + {kEarnerTokenType, {"kEarnerTokenType"}}, + {kIndicatorTokenType, {"kIndicatorTokenType"}}}; + err = pc.user_data().SetAuthTokens(auth_tokens, false, ""); + ASSERT_FALSE(err); + res = pc.GetBuyPsiURL(); + ASSERT_TRUE(res); + url_out.Parse(*res); + ASSERT_EQ(url_out.scheme_host_path_, buy_scheme_host_path); + ASSERT_THAT(TokenPayloadsMatch(url_out.fragment_.substr(bang_key_part.length()), R"({"tokens":"kEarnerTokenType"})"_json), IsEmpty()); +} + +TEST_F(TestPsiCash, GetUserSiteURL) { + // State that affects the URL: testing/dev flag, user agent, locale, account username. + + for (bool test : {true}) { // When accounts are live, we can use this: for (bool test : {false, true}) { + for (PsiCash::UserSiteURLType url_type : {PsiCash::UserSiteURLType::AccountSignup, PsiCash::UserSiteURLType::ForgotAccount, PsiCash::UserSiteURLType::AccountManagement}) { + for (bool webview : {false, true}) { + for (string locale : {"", "en-US", "zh"}) { + PsiCashTester pc; + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), HTTPRequester, false, test); + ASSERT_FALSE(err); + + auto res_refresh = pc.RefreshState(false, {"speed-boost"}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + + err = pc.SetLocale(locale); + ASSERT_FALSE(err); + + auto url = pc.GetUserSiteURL(url_type, webview); + + ASSERT_THAT(url, StartsWith("https://")); + ASSERT_THAT(url, HasSubstr(TestPsiCash::UserAgent())); + + if (test) { + ASSERT_THAT(url, HasSubstr("dev-")); + } + else { + ASSERT_THAT(url, Not(HasSubstr("dev-"))); + } + + if (webview) { + ASSERT_THAT(url, HasSubstr("webview")); + } + else { + ASSERT_THAT(url, Not(HasSubstr("webview"))); + } + + ASSERT_THAT(url, HasSubstr("locale="+locale)); + + ASSERT_THAT(url, Not(HasSubstr("username="))); + + // We're not going to log in to set the username, as it makes this test very slow (around a full minute) + pc.user_data().SetAccountUsername(TEST_ACCOUNT_UNICODE_USERNAME); + url = pc.GetUserSiteURL(url_type, webview); + ASSERT_THAT(url, HasSubstr("username=%E1%88%88")); + + string too_long_username; + too_long_username.resize(3000, 'x'); + pc.user_data().SetAccountUsername(too_long_username); + url = pc.GetUserSiteURL(url_type, webview); + ASSERT_THAT(url, Not(HasSubstr("username="))); + } + } + } + } } TEST_F(TestPsiCash, GetRewardedActivityData) { PsiCashTester pc; - auto err = pc.Init(user_agent_, GetTempDir().c_str(), nullptr); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), nullptr, false); ASSERT_FALSE(err); // Error with no tokens auto res = pc.GetRewardedActivityData(); ASSERT_FALSE(res); - AuthTokens auth_tokens = {{kSpenderTokenType, "kSpenderTokenType"}, - {kEarnerTokenType, "kEarnerTokenType"}, - {kIndicatorTokenType, "kIndicatorTokenType"}}; - err = pc.user_data().SetAuthTokens(auth_tokens, false); + AuthTokens auth_tokens = {{kSpenderTokenType, {"kSpenderTokenType"}}, + {kEarnerTokenType, {"kEarnerTokenType"}}, + {kIndicatorTokenType, {"kIndicatorTokenType"}}}; + err = pc.user_data().SetAuthTokens(auth_tokens, false, ""); ASSERT_FALSE(err); res = pc.GetRewardedActivityData(); ASSERT_TRUE(res); - ASSERT_EQ(*res, base64::B64Encode(utils::Stringer(R"({"metadata":{"user_agent":")", user_agent_, R"(","v":1},"tokens":"kEarnerTokenType","v":1})"))); + ASSERT_EQ(*res, base64::B64Encode(utils::Stringer(R"({"metadata":{"user_agent":")", TestPsiCash::UserAgent(), R"(","v":1},"tokens":"kEarnerTokenType","v":1})"))); err = pc.SetRequestMetadataItem("k", "v"); ASSERT_FALSE(err); res = pc.GetRewardedActivityData(); ASSERT_TRUE(res); - ASSERT_EQ(*res, base64::B64Encode(utils::Stringer(R"({"metadata":{"k":"v","user_agent":")", user_agent_, R"(","v":1},"tokens":"kEarnerTokenType","v":1})"))); + ASSERT_EQ(*res, base64::B64Encode(utils::Stringer(R"({"metadata":{"k":"v","user_agent":")", TestPsiCash::UserAgent(), R"(","v":1},"tokens":"kEarnerTokenType","v":1})"))); } TEST_F(TestPsiCash, GetDiagnosticInfo) { { // First do a simple test with test=false PsiCashTester pc; - auto err = pc.Init(user_agent_, GetTempDir().c_str(), nullptr, false); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), nullptr, false, false); ASSERT_FALSE(err); auto want = R"|({ "balance":0, "isAccount":false, + "isLoggedOutAccount":false, "purchasePrices":[], "purchases":[], "serverTimeDiff":0, @@ -872,12 +1108,13 @@ TEST_F(TestPsiCash, GetDiagnosticInfo) { { // Then do the full test with test=true PsiCashTester pc; - auto err = pc.Init(user_agent_, GetTempDir().c_str(), nullptr, true); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), nullptr, false, true); ASSERT_FALSE(err); auto want = R"|({ "balance":0, "isAccount":false, + "isLoggedOutAccount":false, "purchasePrices":[], "purchases":[], "serverTimeDiff":0, @@ -890,12 +1127,13 @@ TEST_F(TestPsiCash, GetDiagnosticInfo) { pc.user_data().SetBalance(12345); pc.user_data().SetPurchasePrices({{"tc1", "d1", 123}, {"tc2", "d2", 321}}); pc.user_data().SetPurchases( - {{"id2", "tc2", "d2", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}}); - pc.user_data().SetAuthTokens({{"a", "a"}, {"b", "b"}, {"c", "c"}}, true); + {{"id2", datetime::DateTime(), "tc2", "d2", nonstd::nullopt, nonstd::nullopt, nonstd::nullopt}}); + pc.user_data().SetAuthTokens({{"a", {"a"}}, {"b", {"b"}}, {"c", {"c"}}}, true, "username"); // pc.user_data().SetServerTimeDiff() // too hard to do reliably want = R"|({ "balance":12345, "isAccount":true, + "isLoggedOutAccount":false, "purchasePrices":[{"distinguisher":"d1","price":123,"class":"tc1"},{"distinguisher":"d2","price":321,"class":"tc2"}], "purchases":[{"class":"tc2","distinguisher":"d2"}], "serverTimeDiff":0, @@ -904,111 +1142,525 @@ TEST_F(TestPsiCash, GetDiagnosticInfo) { })|"_json; j = pc.GetDiagnosticInfo(); ASSERT_EQ(j, want); + + pc.user_data().DeleteUserData(/*is_logged_out_account=*/true); + want = R"|({ + "balance":0, + "isAccount":true, + "isLoggedOutAccount":true, + "purchasePrices":[], + "purchases":[], + "serverTimeDiff":0, + "test":true, + "validTokenTypes":[] + })|"_json; + j = pc.GetDiagnosticInfo(); + ASSERT_EQ(j, want); } } TEST_F(TestPsiCash, RefreshState) { PsiCashTester pc; - auto err = pc.Init(user_agent_, GetTempDir().c_str(), HTTPRequester); - ASSERT_FALSE(err); - - pc.user_data().Clear(); - ASSERT_TRUE(pc.ValidTokenTypes().empty()); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), HTTPRequester, false); + ASSERT_FALSE(err) << err; + ASSERT_FALSE(pc.HasTokens()); // Basic NewTracker success - auto res = pc.RefreshState({"speed-boost"}); + auto res = pc.RefreshState(false, {"speed-boost"}); ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success); + ASSERT_EQ(res->status, Status::Success) << (int)res->status; + ASSERT_FALSE(res->reconnect_required); ASSERT_FALSE(pc.IsAccount()); - ASSERT_GE(pc.ValidTokenTypes().size(), 3); + ASSERT_FALSE(pc.AccountUsername()); + ASSERT_TRUE(pc.HasTokens()); ASSERT_THAT(pc.Balance(), AllOf(Ge(0), Le(MAX_STARTING_BALANCE))); ASSERT_GE(pc.GetPurchasePrices().size(), 2); // Test with existing tracker auto want_tokens = pc.user_data().GetAuthTokens(); - res = pc.RefreshState({"speed-boost"}); + res = pc.RefreshState(false, {"speed-boost"}); ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success); + ASSERT_EQ(res->status, Status::Success); + ASSERT_FALSE(res->reconnect_required); ASSERT_FALSE(pc.IsAccount()); - ASSERT_GE(pc.ValidTokenTypes().size(), 3); + ASSERT_FALSE(pc.AccountUsername()); + ASSERT_TRUE(pc.HasTokens()); ASSERT_THAT(pc.Balance(), AllOf(Ge(0), Le(MAX_STARTING_BALANCE))); auto speed_boost_purchase_prices = pc.GetPurchasePrices(); ASSERT_GE(pc.GetPurchasePrices().size(), 2); - ASSERT_EQ(want_tokens, pc.user_data().GetAuthTokens()); + ASSERT_TRUE(AuthTokenSetsEqual(want_tokens, pc.user_data().GetAuthTokens())); // Multiple purchase classes pc.user_data().Clear(); - res = pc.RefreshState({"speed-boost", TEST_DEBIT_TRANSACTION_CLASS}); + res = pc.RefreshState(false, {"speed-boost", TEST_DEBIT_TRANSACTION_CLASS}); ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success); + ASSERT_EQ(res->status, Status::Success); + ASSERT_FALSE(res->reconnect_required); ASSERT_FALSE(pc.IsAccount()); - ASSERT_GE(pc.ValidTokenTypes().size(), 3); + ASSERT_FALSE(pc.AccountUsername()); + ASSERT_TRUE(pc.HasTokens()); ASSERT_THAT(pc.Balance(), AllOf(Ge(0), Le(MAX_STARTING_BALANCE))); ASSERT_GT(pc.GetPurchasePrices().size(), speed_boost_purchase_prices.size()); // No purchase classes pc.user_data().Clear(); - res = pc.RefreshState({}); + res = pc.RefreshState(false, {}); ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success); + ASSERT_EQ(res->status, Status::Success); + ASSERT_FALSE(res->reconnect_required); ASSERT_FALSE(pc.IsAccount()); - ASSERT_GE(pc.ValidTokenTypes().size(), 3); + ASSERT_FALSE(pc.AccountUsername()); + ASSERT_TRUE(pc.HasTokens()); ASSERT_THAT(pc.Balance(), AllOf(Ge(0), Le(MAX_STARTING_BALANCE))); ASSERT_EQ(pc.GetPurchasePrices().size(), 0); // we didn't ask for any // Purchase classes, then none; verify that previous aren't lost pc.user_data().Clear(); - res = pc.RefreshState({"speed-boost"}); // with class + res = pc.RefreshState(false, {"speed-boost"}); // with class ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success); + ASSERT_EQ(res->status, Status::Success); + ASSERT_FALSE(res->reconnect_required); speed_boost_purchase_prices = pc.GetPurchasePrices(); ASSERT_GT(speed_boost_purchase_prices.size(), 3); - res = pc.RefreshState({}); // without class + res = pc.RefreshState(false, {}); // without class ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success); + ASSERT_EQ(res->status, Status::Success); ASSERT_EQ(pc.GetPurchasePrices().size(), speed_boost_purchase_prices.size()); // Balance increase pc.user_data().Clear(); - res = pc.RefreshState({"speed-boost"}); // with class + res = pc.RefreshState(false, {"speed-boost"}); // with class ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success); + ASSERT_EQ(res->status, Status::Success); + ASSERT_FALSE(res->reconnect_required); auto starting_balance = pc.Balance(); err = MAKE_1T_REWARD(pc, 1); ASSERT_FALSE(err) << err; - res = pc.RefreshState({"speed-boost"}); // with class + res = pc.RefreshState(false, {"speed-boost"}); // with class ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success); + ASSERT_EQ(res->status, Status::Success); ASSERT_EQ(pc.Balance(), starting_balance + ONE_TRILLION); +} + +TEST_F(TestPsiCash, RefreshStateAccount) { + PsiCashTester pc; + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), HTTPRequester, false); + ASSERT_FALSE(err); // Test bad is-account state. This is a local sanity check failure that will occur - // after the request to the server sees an illegal is-account state. + // after the request to the server sees an illegal is-account state. It should + // result in a local data reset. pc.user_data().Clear(); - res = pc.RefreshState({}); - ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success); + auto res_refresh = pc.RefreshState(false, {"speed-boost"}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_FALSE(res_refresh->reconnect_required); // We're setting "isAccount" with tracker tokens. This is not okay and shouldn't happen. pc.user_data().SetIsAccount(true); - res = pc.RefreshState({}); - ASSERT_FALSE(res) << res.error(); + res_refresh = pc.RefreshState(false, {}); + ASSERT_FALSE(res_refresh); // Test is-account with no tokens pc.user_data().Clear(); // blow away existing tokens pc.user_data().SetIsAccount(true); // force is-account - res = pc.RefreshState({"speed-boost"}); // ask for purchase prices - ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success); + res_refresh = pc.RefreshState(false, {"speed-boost"}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_FALSE(res_refresh->reconnect_required); + ASSERT_TRUE(pc.IsAccount()); // should still be is-account + ASSERT_FALSE(pc.HasTokens()); // but no tokens + ASSERT_EQ(pc.Balance(), 0); + ASSERT_EQ(pc.GetPurchasePrices().size(), 0); // shouldn't get any, because no valid indicator token + + // Successful login and refresh + auto res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login) << res_login.error(); + ASSERT_EQ(res_login->status, Status::Success); + ASSERT_FALSE(res_login->last_tracker_merge); + + res_refresh = pc.RefreshState(false, {"speed-boost"}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_FALSE(res_refresh->reconnect_required); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_TRUE(pc.HasTokens()); + ASSERT_TRUE(pc.AccountUsername()); + ASSERT_EQ(*pc.AccountUsername(), TEST_ACCOUNT_ONE_USERNAME); + ASSERT_GT(pc.Balance(), 0); // Our test accounts don't have zero balance + ASSERT_GT(pc.GetPurchasePrices().size(), 0); + + // In order to test the username retrieval, we'll set it to an incorrect value + // and then refresh to overwrite it. + pc.user_data().SetAccountUsername("not-real-username"); + ASSERT_TRUE(pc.AccountUsername()); + ASSERT_EQ(*pc.AccountUsername(), "not-real-username"); + res_refresh = pc.RefreshState(false, {"speed-boost"}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_TRUE(pc.AccountUsername()); + ASSERT_EQ(*pc.AccountUsername(), TEST_ACCOUNT_ONE_USERNAME); + + // Log out and try to refresh + auto res_logout = pc.AccountLogout(); + ASSERT_TRUE(res_logout); ASSERT_TRUE(pc.IsAccount()); // should still be is-account - ASSERT_EQ(pc.ValidTokenTypes().size(), 0); // but no tokens + ASSERT_FALSE(pc.HasTokens()); + res_refresh = pc.RefreshState(false, {"speed-boost"}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_FALSE(res_refresh->reconnect_required); + ASSERT_TRUE(pc.IsAccount()); // should still be is-account + ASSERT_FALSE(pc.HasTokens()); // but no tokens + ASSERT_FALSE(pc.AccountUsername()); // and no username + ASSERT_EQ(pc.Balance(), 0); + ASSERT_EQ(pc.GetPurchasePrices().size(), 0); // shouldn't get any, because no valid indicator token + + // Force invalid tokens, forcing logged-out state + if (pc.MutatorsEnabled()) { + // Successful login and refresh + auto res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login); + ASSERT_EQ(res_login->status, Status::Success); + ASSERT_FALSE(res_login->last_tracker_merge); + + res_refresh = pc.RefreshState(false, {"speed-boost"}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_FALSE(res_refresh->reconnect_required); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_TRUE(pc.HasTokens()); + ASSERT_TRUE(pc.AccountUsername()); + ASSERT_EQ(*pc.AccountUsername(), TEST_ACCOUNT_ONE_USERNAME); + ASSERT_GT(pc.Balance(), 0); // Our test accounts don't have zero balance + ASSERT_GT(pc.GetPurchasePrices().size(), 0); + + // Try again with invalid tokens + pc.SetRequestMutators({"InvalidTokens"}); + res_refresh = pc.RefreshState(false, {"speed-boost"}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_FALSE(res_refresh->reconnect_required); + ASSERT_TRUE(pc.IsAccount()); // should still be is-account + ASSERT_FALSE(pc.HasTokens()); // but no tokens + ASSERT_FALSE(pc.AccountUsername()); // and no username + ASSERT_EQ(pc.Balance(), 0); + ASSERT_EQ(pc.GetPurchasePrices().size(), 0); // shouldn't get any, because no valid indicator token + } + + // Test Account token expiry while Authorization active, requiring tunnel reconnect + if (pc.MutatorsEnabled()) { + // Force reset to get rid of any other purchases + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), HTTPRequester, true); + ASSERT_FALSE(err); + + // Successful login and refresh + auto res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login); + ASSERT_EQ(res_login->status, Status::Success); + ASSERT_FALSE(res_login->last_tracker_merge); + + res_refresh = pc.RefreshState(false, {}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_FALSE(res_refresh->reconnect_required); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_TRUE(pc.HasTokens()); + + err = MAKE_1T_REWARD(pc, 1); + ASSERT_FALSE(err) << err; + + // Make a purchase that produces an authorization + auto purchase_result = pc.NewExpiringPurchase(TEST_DEBIT_WITH_AUTHORIZATION_TRANSACTION_CLASS, TEST_ONE_TRILLION_ONE_MINUTE_DISTINGUISHER, ONE_TRILLION); + ASSERT_TRUE(purchase_result); + ASSERT_EQ(purchase_result->status, Status::Success); + ASSERT_TRUE(purchase_result->purchase->authorization); + + // Force our tokens to be invalid, emulating expiry + pc.SetRequestMutators({"InvalidTokens"}); + res_refresh = pc.RefreshState(false, {}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_TRUE(res_refresh->reconnect_required); // need to remove the Authorization from the tunnel + ASSERT_TRUE(pc.IsAccount()); // should still be is-account + ASSERT_FALSE(pc.HasTokens()); // but no tokens + } +} + +TEST_F(TestPsiCash, RefreshStateRetrievePurchases) { + // We'll go through a set of tests twice -- once with a tracker, once with an account + + for (auto i = 0; i < 2; i++) { + Purchases expected_purchases; + + PsiCashTester pc; + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), HTTPRequester, false); + ASSERT_FALSE(err); + + if (i == 0) { + // Get a tracker + auto res_refresh = pc.RefreshState(false, {"speed-boost"}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_FALSE(res_refresh->reconnect_required); + } + else { + // Log in as an account + auto res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login); + ASSERT_EQ(res_login->status, Status::Success); + auto res_refresh = pc.RefreshState(false, {}); + ASSERT_TRUE(res_refresh); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_TRUE(pc.IsAccount()); + // Should have reset everything + ASSERT_EQ(pc.GetPurchases().size(), 0); + ASSERT_EQ(pc.user_data().GetLastTransactionID(), ""); + } + + err = MAKE_1T_REWARD(pc, 3); + ASSERT_FALSE(err) << err; + + // We have no purchases yet + ASSERT_EQ(pc.GetPurchases().size(), 0); + + // Make a couple sufficiently long-lived purchases + auto purchase_result = pc.NewExpiringPurchase(TEST_DEBIT_TRANSACTION_CLASS, TEST_ONE_TRILLION_TEN_SECOND_DISTINGUISHER, ONE_TRILLION); + ASSERT_TRUE(purchase_result); + ASSERT_EQ(purchase_result->status, Status::Success) << static_cast(purchase_result->status); + expected_purchases.push_back(*purchase_result->purchase); + ASSERT_EQ(pc.GetPurchases(), expected_purchases) << (pc.IsAccount() ? "account: " : "tracker: ") << pc.GetPurchases().size() << " vs " << expected_purchases.size() << ": " << json(pc.GetPurchases()) << " vs " << json(expected_purchases); + ASSERT_EQ(pc.user_data().GetLastTransactionID(), purchase_result->purchase->id); + + purchase_result = pc.NewExpiringPurchase(TEST_DEBIT_TRANSACTION_CLASS, TEST_ONE_TRILLION_ONE_MINUTE_DISTINGUISHER, ONE_TRILLION); // if this lifetime is too long for other tests, we should create an 11-second distinguisher to use (kind of thing) + ASSERT_TRUE(purchase_result); + ASSERT_EQ(purchase_result->status, Status::Success) << static_cast(purchase_result->status); + expected_purchases.push_back(*purchase_result->purchase); + ASSERT_EQ(pc.GetPurchases(), expected_purchases); + ASSERT_EQ(pc.user_data().GetLastTransactionID(), purchase_result->purchase->id); + + // "Lose" our purchases, but not the LastTransactionID, so no retrieval will occur + pc.user_data().SetPurchases({}); + ASSERT_EQ(pc.GetPurchases().size(), 0); + ASSERT_EQ(pc.user_data().GetLastTransactionID(), expected_purchases.back().id); + + // Refresh + auto res_refresh = pc.RefreshState(false, {"speed-boost"}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + + // We didn't get the purchases back + ASSERT_EQ(pc.GetPurchases().size(), 0); + ASSERT_FALSE(res_refresh->reconnect_required); + + // Clear the LastTransactionID value and try again + pc.user_data().SetLastTransactionID(""); + res_refresh = pc.RefreshState(false, {"speed-boost"}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + + // We got our purchases back + ASSERT_EQ(pc.GetPurchases(), expected_purchases) << pc.GetPurchases().size() << " vs " << expected_purchases.size() << ": " << json(pc.GetPurchases()) << " vs " << json(expected_purchases); + ASSERT_EQ(pc.user_data().GetLastTransactionID(), expected_purchases.back().id); + ASSERT_FALSE(res_refresh->reconnect_required); // Not speed-boost, so no reconnect required + + // Clear the purchases and set the LastTransactionID to garbage, which will also trigger a full retrieval + pc.user_data().SetPurchases({}); + pc.user_data().SetLastTransactionID(""); + ASSERT_EQ(pc.GetPurchases().size(), 0); + ASSERT_EQ(pc.user_data().GetLastTransactionID(), ""s); + res_refresh = pc.RefreshState(false, {"speed-boost"}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + + // We got our purchases back + ASSERT_EQ(pc.GetPurchases(), expected_purchases); + ASSERT_EQ(pc.user_data().GetLastTransactionID(), expected_purchases.back().id); + ASSERT_FALSE(res_refresh->reconnect_required); // Not speed-boost, so no reconnect required + + // Lose the second purchase, but not the first + pc.user_data().SetPurchases({}); + pc.user_data().SetLastTransactionID(""); + pc.user_data().AddPurchase(expected_purchases[0]); + ASSERT_EQ(pc.GetPurchases().size(), 1); + ASSERT_EQ(pc.user_data().GetLastTransactionID(), expected_purchases[0].id); + res_refresh = pc.RefreshState(false, {"speed-boost"}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + + // We got our purchases back + ASSERT_EQ(pc.GetPurchases(), expected_purchases); + ASSERT_EQ(pc.user_data().GetLastTransactionID(), expected_purchases.back().id); + ASSERT_FALSE(res_refresh->reconnect_required); // Not speed-boost, so no reconnect required + + // Make a purchase that produces an authorization, so we can test the reconnect-required flag + purchase_result = pc.NewExpiringPurchase(TEST_DEBIT_WITH_AUTHORIZATION_TRANSACTION_CLASS, TEST_ONE_TRILLION_ONE_MINUTE_DISTINGUISHER, ONE_TRILLION); + ASSERT_TRUE(purchase_result); + ASSERT_EQ(purchase_result->status, Status::Success) << static_cast(purchase_result->status); + ASSERT_TRUE(purchase_result->purchase->authorization); + expected_purchases.push_back(*purchase_result->purchase); + ASSERT_EQ(pc.GetPurchases(), expected_purchases); + ASSERT_EQ(pc.user_data().GetLastTransactionID(), purchase_result->purchase->id); + + // Refresh state, but won't have retrieved anything. + res_refresh = pc.RefreshState(false, {"speed-boost"}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_FALSE(res_refresh->reconnect_required); + + // Clear the purchases and set the LastTransactionID to garbage, which will also trigger a full retrieval + pc.user_data().SetPurchases({}); + pc.user_data().SetLastTransactionID(""); + ASSERT_EQ(pc.GetPurchases().size(), 0); + ASSERT_EQ(pc.user_data().GetLastTransactionID(), ""s); + res_refresh = pc.RefreshState(false, {"speed-boost"}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + // Should have got our Speed Boost, so reconnect is required + ASSERT_TRUE(res_refresh->reconnect_required); + + // Account-only tests + if (i == 1) { + ASSERT_GT(expected_purchases.size(), 0); + ASSERT_EQ(pc.GetPurchases(), expected_purchases); + + // Logout + auto res_logout = pc.AccountLogout(); + ASSERT_TRUE(res_logout); + ASSERT_TRUE(pc.IsAccount()); // should still be is-account + ASSERT_FALSE(pc.HasTokens()); + ASSERT_EQ(pc.GetPurchases().size(), 0); + // Log back in + auto res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login); + ASSERT_EQ(res_login->status, Status::Success); + // Retrieve our purchases + auto res_refresh = pc.RefreshState(false, {}); + ASSERT_TRUE(res_refresh); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_EQ(pc.GetPurchases(), expected_purchases) << pc.GetPurchases().size() << " vs " << expected_purchases.size() << ": " << json(pc.GetPurchases()) << " vs " << json(expected_purchases); + } + } +} + +TEST_F(TestPsiCash, RefreshStateLocalOnly) { + PsiCashTester pc; + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), HTTPRequester, false); + ASSERT_FALSE(err) << err; + ASSERT_FALSE(pc.HasTokens()); + + const bool REMOTE = false, LOCAL_ONLY = true; + + auto expire_tokens_fn = [&] { + auto past = datetime::DateTime::Now().Sub(datetime::Duration(10000)); + auto auth_tokens = pc.user_data().GetAuthTokens(); + for (auto& at : auth_tokens) { + at.second.server_time_expiry = past; + } + auto err = pc.user_data().SetAuthTokens(auth_tokens, true, *pc.AccountUsername()); + ASSERT_FALSE(err); + }; + + // If called local-only before there's any tokens, it does nothing. + auto res_refresh = pc.RefreshState(LOCAL_ONLY, {}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_FALSE(res_refresh->reconnect_required); + ASSERT_FALSE(pc.HasTokens()); + ASSERT_FALSE(pc.IsAccount()); + ASSERT_FALSE(pc.AccountUsername()); + + // Basic NewTracker success + res_refresh = pc.RefreshState(REMOTE, {"speed-boost"}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success) << (int)res_refresh->status; + ASSERT_FALSE(res_refresh->reconnect_required); + ASSERT_FALSE(pc.IsAccount()); + ASSERT_FALSE(pc.AccountUsername()); + ASSERT_TRUE(pc.HasTokens()); ASSERT_THAT(pc.Balance(), AllOf(Ge(0), Le(MAX_STARTING_BALANCE))); - ASSERT_EQ(pc.GetPurchasePrices().size(), - 0); // shouldn't get any, because no valid indicator token + ASSERT_GE(pc.GetPurchasePrices().size(), 2); + + // Test with tracker + res_refresh = pc.RefreshState(LOCAL_ONLY, {}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_FALSE(res_refresh->reconnect_required); + ASSERT_FALSE(pc.IsAccount()); + ASSERT_FALSE(pc.AccountUsername()); + ASSERT_TRUE(pc.HasTokens()); + ASSERT_THAT(pc.Balance(), AllOf(Ge(0), Le(MAX_STARTING_BALANCE))); + auto speed_boost_purchase_prices = pc.GetPurchasePrices(); + ASSERT_GE(pc.GetPurchasePrices().size(), 2); + + // Get account tokens, to expire them + auto res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login); + ASSERT_EQ(res_login->status, Status::Success); + res_refresh = pc.RefreshState(false, {}); + ASSERT_TRUE(res_refresh); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_TRUE(pc.HasTokens()); + ASSERT_TRUE(pc.AccountUsername()); + ASSERT_THAT(*pc.AccountUsername(), Not(IsEmpty())); + res_refresh = pc.RefreshState(REMOTE, {"speed-boost"}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success) << (int)res_refresh->status; + + res_refresh = pc.RefreshState(true, {"speed-boost"}); // local-only + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + + // Force the account tokens to be expired + expire_tokens_fn(); + // Refresh local + res_refresh = pc.RefreshState(LOCAL_ONLY, {}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_FALSE(res_refresh->reconnect_required); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_FALSE(pc.HasTokens()); // should be gone + ASSERT_FALSE(pc.AccountUsername()); // should be gone + + // Log in again, get authorization + res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login); + ASSERT_EQ(res_login->status, Status::Success); + res_refresh = pc.RefreshState(false, {}); + ASSERT_TRUE(res_refresh); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_TRUE(pc.HasTokens()); + ASSERT_TRUE(pc.AccountUsername()); + ASSERT_THAT(*pc.AccountUsername(), Not(IsEmpty())); + res_refresh = pc.RefreshState(REMOTE, {"speed-boost"}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success) << (int)res_refresh->status; + // Get some credit + err = MAKE_1T_REWARD(pc, 1); + ASSERT_FALSE(err) << err; + // Buy something that gives an authorization + auto purchase_result = pc.NewExpiringPurchase(TEST_DEBIT_WITH_AUTHORIZATION_TRANSACTION_CLASS, TEST_ONE_TRILLION_ONE_MINUTE_DISTINGUISHER, ONE_TRILLION); + ASSERT_TRUE(purchase_result); + ASSERT_EQ(purchase_result->status, Status::Success); + ASSERT_TRUE(purchase_result->purchase->authorization); + // Force tokens to be expired + expire_tokens_fn(); + // Refresh local + res_refresh = pc.RefreshState(LOCAL_ONLY, {}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_TRUE(res_refresh->reconnect_required); // now true + ASSERT_TRUE(pc.IsAccount()); + ASSERT_FALSE(pc.HasTokens()); // should be gone + ASSERT_FALSE(pc.AccountUsername()); // should be gone } TEST_F(TestPsiCash, RefreshStateMutators) { PsiCashTester pc; - auto err = pc.Init(user_agent_, GetTempDir().c_str(), HTTPRequester); - ASSERT_FALSE(err); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), HTTPRequester, false); + ASSERT_FALSE(err) << err; if (!pc.MutatorsEnabled()) { // Can't proceed with these tests @@ -1017,9 +1669,9 @@ TEST_F(TestPsiCash, RefreshStateMutators) { // Tracker with invalid tokens pc.user_data().Clear(); - auto res = pc.RefreshState({}); + auto res = pc.RefreshState(false, {}); ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success); + ASSERT_EQ(res->status, Status::Success); auto prev_tokens = pc.user_data().GetAuthTokens(); ASSERT_GE(prev_tokens.size(), 3); err = pc.user_data().SetBalance(12345); // force a nonzero balance @@ -1027,36 +1679,38 @@ TEST_F(TestPsiCash, RefreshStateMutators) { ASSERT_GT(pc.Balance(), 0); // We have tokens; force the server to consider them invalid pc.SetRequestMutators({"InvalidTokens"}); - res = pc.RefreshState({}); + res = pc.RefreshState(false, {}); ASSERT_TRUE(res) << res.error(); + ASSERT_EQ(res->status, Status::Success); // We should have brand new tokens now. auto next_tokens = pc.user_data().GetAuthTokens(); ASSERT_GE(next_tokens.size(), 3); - ASSERT_NE(prev_tokens, next_tokens); + ASSERT_FALSE(AuthTokenSetsEqual(prev_tokens, next_tokens)); // And a reset balance ASSERT_THAT(pc.Balance(), AllOf(Ge(0), Le(MAX_STARTING_BALANCE))); // Account with invalid tokens pc.user_data().Clear(); - res = pc.RefreshState({}); + res = pc.RefreshState(false, {}); ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success); + ASSERT_EQ(res->status, Status::Success); ASSERT_GE(pc.user_data().GetAuthTokens().size(), 3); // We're setting "isAccount" with tracker tokens. This is not okay, but the server is // going to blindly consider them invalid anyway. pc.user_data().SetIsAccount(true); // We have tokens; force the server to consider them invalid pc.SetRequestMutators({"InvalidTokens"}); - res = pc.RefreshState({}); + res = pc.RefreshState(false, {}); ASSERT_TRUE(res) << res.error(); + ASSERT_EQ(res->status, Status::Success); // Accounts won't get new tokens by refreshing, so now we should have none ASSERT_GE(pc.user_data().GetAuthTokens().size(), 0); // Tracker with always-invalid tokens (impossible to get valid ones) pc.user_data().Clear(); - res = pc.RefreshState({}); + res = pc.RefreshState(false, {}); ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success); + ASSERT_EQ(res->status, Status::Success); ASSERT_GE(pc.user_data().GetAuthTokens().size(), 3); // We have tokens; force the server to consider them invalid pc.SetRequestMutators({ @@ -1064,9 +1718,9 @@ TEST_F(TestPsiCash, RefreshStateMutators) { "", // NewTracker "InvalidTokens", // RefreshState }); - res = pc.RefreshState({}); + res = pc.RefreshState(false, {}); // Should have failed utterly - ASSERT_FALSE(res) << static_cast(*res); + ASSERT_FALSE(res) << static_cast(res->status); // Should be no tokens ASSERT_EQ(pc.user_data().GetAuthTokens().size(), 0); ASSERT_EQ(pc.Balance(), 0); @@ -1076,8 +1730,8 @@ TEST_F(TestPsiCash, RefreshStateMutators) { pc.user_data().Clear(); // First request is NewTracker, sleep for 11 secs pc.SetRequestMutators({"Timeout:11"}); - res = pc.RefreshState({}); - ASSERT_FALSE(res) << static_cast(*res); + res = pc.RefreshState(false, {}); + ASSERT_FALSE(res) << static_cast(res->status); // Should be no tokens ASSERT_EQ(pc.user_data().GetAuthTokens().size(), 0); ASSERT_EQ(pc.Balance(), 0); @@ -1085,25 +1739,25 @@ TEST_F(TestPsiCash, RefreshStateMutators) { // No server response for RefreshState pc.user_data().Clear(); // Do an initial request to get Tracker tokens - res = pc.RefreshState({}); + res = pc.RefreshState(false, {}); ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success); + ASSERT_EQ(res->status, Status::Success); auto auth_tokens = pc.user_data().GetAuthTokens(); ASSERT_GE(auth_tokens.size(), 3); // Because we have tokens the first request will be RefreshState; sleep for 11 secs pc.SetRequestMutators({"Timeout:11"}); - res = pc.RefreshState({}); - ASSERT_FALSE(res) << static_cast(*res); + res = pc.RefreshState(false, {}); + ASSERT_FALSE(res) << static_cast(res->status); // Tokens should be unchanged - ASSERT_EQ(auth_tokens, pc.user_data().GetAuthTokens()); + ASSERT_TRUE(AuthTokenSetsEqual(auth_tokens, pc.user_data().GetAuthTokens())); // NewTracker response with no data // Blow away any existing tokens to force internal NewTracker. pc.user_data().Clear(); // First request is NewTracker; force an empty response pc.SetRequestMutators({"Response:code=200,body=none"}); - res = pc.RefreshState({}); - ASSERT_FALSE(res) << static_cast(*res); + res = pc.RefreshState(false, {}); + ASSERT_FALSE(res) << static_cast(res->status); // Should be no tokens ASSERT_EQ(pc.user_data().GetAuthTokens().size(), 0); ASSERT_EQ(pc.Balance(), 0); @@ -1111,25 +1765,25 @@ TEST_F(TestPsiCash, RefreshStateMutators) { // Empty server response for RefreshState pc.user_data().Clear(); // Do an initial request to get Tracker tokens - res = pc.RefreshState({}); + res = pc.RefreshState(false, {}); ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success); + ASSERT_EQ(res->status, Status::Success); auth_tokens = pc.user_data().GetAuthTokens(); ASSERT_GE(auth_tokens.size(), 3); // Because we have tokens the first request will be RefreshState; force an empty response pc.SetRequestMutators({"Response:code=200,body=none"}); - res = pc.RefreshState({}); - ASSERT_FALSE(res) << static_cast(*res); + res = pc.RefreshState(false, {}); + ASSERT_FALSE(res) << static_cast(res->status); // Tokens should be unchanged - ASSERT_EQ(auth_tokens, pc.user_data().GetAuthTokens()); + ASSERT_TRUE(AuthTokenSetsEqual(auth_tokens, pc.user_data().GetAuthTokens())); // NewTracker response with bad JSON // Blow away any existing tokens to force internal NewTracker. pc.user_data().Clear(); // First request is NewTracker; force a response with bad JSON pc.SetRequestMutators({"BadJSON:200"}); - res = pc.RefreshState({}); - ASSERT_FALSE(res) << static_cast(*res); + res = pc.RefreshState(false, {}); + ASSERT_FALSE(res) << static_cast(res->status); // Should be no tokens ASSERT_EQ(pc.user_data().GetAuthTokens().size(), 0); ASSERT_EQ(pc.Balance(), 0); @@ -1137,26 +1791,26 @@ TEST_F(TestPsiCash, RefreshStateMutators) { // RefreshState response with bad JSON pc.user_data().Clear(); // Do an initial request to get Tracker tokens - res = pc.RefreshState({}); + res = pc.RefreshState(false, {}); ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success); + ASSERT_EQ(res->status, Status::Success); auth_tokens = pc.user_data().GetAuthTokens(); ASSERT_GE(auth_tokens.size(), 3); // Because we have tokens the first request will be RefreshState; force a response with bad JSON pc.SetRequestMutators({"BadJSON:200"}); - res = pc.RefreshState({}); - ASSERT_FALSE(res) << static_cast(*res); + res = pc.RefreshState(false, {}); + ASSERT_FALSE(res) << static_cast(res->status); // Tokens should be unchanged - ASSERT_EQ(auth_tokens, pc.user_data().GetAuthTokens()); + ASSERT_TRUE(AuthTokenSetsEqual(auth_tokens, pc.user_data().GetAuthTokens())); // 1 NewTracker response is 500 (retry succeeds) // Blow away any existing tokens to force internal NewTracker. pc.user_data().Clear(); // First request is NewTracker; pc.SetRequestMutators({"Response:code=500"}); - res = pc.RefreshState({}); + res = pc.RefreshState(false, {}); ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success) << static_cast(*res); + ASSERT_EQ(res->status, Status::Success) << static_cast(res->status); ASSERT_GE(pc.user_data().GetAuthTokens().size(), 3); // 2 NewTracker responses are 500 (retry succeeds) @@ -1164,9 +1818,9 @@ TEST_F(TestPsiCash, RefreshStateMutators) { pc.user_data().Clear(); // First request is NewTracker pc.SetRequestMutators({"Response:code=500", "Response:code=500"}); - res = pc.RefreshState({}); + res = pc.RefreshState(false, {}); ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success) << static_cast(*res); + ASSERT_EQ(res->status, Status::Success) << static_cast(res->status); ASSERT_GE(pc.user_data().GetAuthTokens().size(), 3); // 3 NewTracker responses are 500 (retry fails) @@ -1174,9 +1828,9 @@ TEST_F(TestPsiCash, RefreshStateMutators) { pc.user_data().Clear(); // First request is NewTracker pc.SetRequestMutators({"Response:code=500", "Response:code=500", "Response:code=500"}); - res = pc.RefreshState({}); + res = pc.RefreshState(false, {}); ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::ServerError) << static_cast(*res); + ASSERT_EQ(res->status, Status::ServerError) << static_cast(res->status); // Should be no tokens ASSERT_EQ(pc.user_data().GetAuthTokens().size(), 0); ASSERT_EQ(pc.Balance(), 0); @@ -1185,64 +1839,64 @@ TEST_F(TestPsiCash, RefreshStateMutators) { // Blow away any existing tokens to force internal NewTracker. pc.user_data().Clear(); // Do an initial request to get Tracker tokens - res = pc.RefreshState({}); + res = pc.RefreshState(false, {}); ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success); + ASSERT_EQ(res->status, Status::Success); auth_tokens = pc.user_data().GetAuthTokens(); ASSERT_GE(auth_tokens.size(), 3); // Because we have tokens the first request will be RefreshState pc.SetRequestMutators({"Response:code=500"}); - res = pc.RefreshState({}); + res = pc.RefreshState(false, {}); ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success) << static_cast(*res); + ASSERT_EQ(res->status, Status::Success) << static_cast(res->status); ASSERT_GE(pc.user_data().GetAuthTokens().size(), 3); // 2 RefreshState responses are 500 (retry succeeds) // Blow away any existing tokens to force internal NewTracker. pc.user_data().Clear(); // Do an initial request to get Tracker tokens - res = pc.RefreshState({}); + res = pc.RefreshState(false, {}); ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success); + ASSERT_EQ(res->status, Status::Success); auth_tokens = pc.user_data().GetAuthTokens(); ASSERT_GE(auth_tokens.size(), 3); // Because we have tokens the first request will be RefreshState pc.SetRequestMutators({"Response:code=500", "Response:code=500"}); - res = pc.RefreshState({}); + res = pc.RefreshState(false, {}); ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success) << static_cast(*res); + ASSERT_EQ(res->status, Status::Success) << static_cast(res->status); ASSERT_GE(pc.user_data().GetAuthTokens().size(), 3); // 3 RefreshState responses are 500 (retry fails) // Blow away any existing tokens to force internal NewTracker. pc.user_data().Clear(); // Do an initial request to get Tracker tokens - res = pc.RefreshState({}); + res = pc.RefreshState(false, {}); ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success); + ASSERT_EQ(res->status, Status::Success); auth_tokens = pc.user_data().GetAuthTokens(); ASSERT_GE(auth_tokens.size(), 3); // Because we have tokens the first request will be RefreshState pc.SetRequestMutators({"Response:code=500", "Response:code=500", "Response:code=500"}); - res = pc.RefreshState({}); + res = pc.RefreshState(false, {}); ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::ServerError) << static_cast(*res); + ASSERT_EQ(res->status, Status::ServerError) << static_cast(res->status); // Tokens should be unchanged - ASSERT_EQ(auth_tokens, pc.user_data().GetAuthTokens()); + ASSERT_TRUE(AuthTokenSetsEqual(auth_tokens, pc.user_data().GetAuthTokens())); // RefreshState response with status code indicating invalid tokens pc.user_data().Clear(); // Do an initial request to get Tracker tokens - res = pc.RefreshState({}); + res = pc.RefreshState(false, {}); ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success); + ASSERT_EQ(res->status, Status::Success); auth_tokens = pc.user_data().GetAuthTokens(); ASSERT_GE(auth_tokens.size(), 3); // Because we have tokens the first request will be RefreshState pc.SetRequestMutators({"Response:code=401"}); - res = pc.RefreshState({}); + res = pc.RefreshState(false, {}); ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::InvalidTokens) << static_cast(*res); + ASSERT_EQ(res->status, Status::InvalidTokens) << static_cast(res->status); // UserData should be cleared ASSERT_EQ(pc.user_data().GetAuthTokens().size(), 0); @@ -1251,8 +1905,8 @@ TEST_F(TestPsiCash, RefreshStateMutators) { pc.user_data().Clear(); // First request is NewTracker pc.SetRequestMutators({"Response:code=666"}); - res = pc.RefreshState({}); - ASSERT_FALSE(res) << static_cast(*res); + res = pc.RefreshState(false, {}); + ASSERT_FALSE(res) << static_cast(res->status); // Should be no tokens ASSERT_EQ(pc.user_data().GetAuthTokens().size(), 0); ASSERT_EQ(pc.Balance(), 0); @@ -1260,39 +1914,61 @@ TEST_F(TestPsiCash, RefreshStateMutators) { // RefreshState response with unknown status code pc.user_data().Clear(); // Do an initial request to get Tracker tokens - res = pc.RefreshState({}); + res = pc.RefreshState(false, {}); ASSERT_TRUE(res) << res.error(); - ASSERT_EQ(*res, Status::Success); + ASSERT_EQ(res->status, Status::Success); auth_tokens = pc.user_data().GetAuthTokens(); ASSERT_GE(auth_tokens.size(), 3); // Because we have tokens the first request will be RefreshState pc.SetRequestMutators({"Response:code=666"}); - res = pc.RefreshState({}); - ASSERT_FALSE(res) << static_cast(*res); + res = pc.RefreshState(false, {}); + ASSERT_FALSE(res) << static_cast(res->status); // Tokens should be unchanged - ASSERT_EQ(auth_tokens, pc.user_data().GetAuthTokens()); + ASSERT_TRUE(AuthTokenSetsEqual(auth_tokens, pc.user_data().GetAuthTokens())); +} + +TEST_F(TestPsiCash, RefreshStateOffline) { + PsiCashTester pc; + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), HTTPRequester, false); + ASSERT_FALSE(err) << err; + + bool request_attempted = false; + const MakeHTTPRequestFn noHTTPRequester = [&request_attempted](const HTTPParams&) -> HTTPResult { + request_attempted = true; + return HTTPResult(); + }; + const MakeHTTPRequestFn errorHTTPRequester = [&request_attempted](const HTTPParams&) -> HTTPResult { + auto res = HTTPResult(); + res.code = HTTPResult::RECOVERABLE_ERROR; + res.error = "test"; + return res; + }; + + + pc.SetHTTPRequestFn(noHTTPRequester); + } TEST_F(TestPsiCash, NewExpiringPurchase) { PsiCashTester pc; - auto err = pc.Init(user_agent_, GetTempDir().c_str(), HTTPRequester); - ASSERT_FALSE(err); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), HTTPRequester, false); + ASSERT_FALSE(err) << err; // Simple success pc.user_data().Clear(); - auto refresh_result = pc.RefreshState({}); + auto refresh_result = pc.RefreshState(false, {}); ASSERT_TRUE(refresh_result) << refresh_result.error(); - ASSERT_EQ(*refresh_result, Status::Success); + ASSERT_EQ(refresh_result->status, Status::Success); auto initial_balance = pc.Balance(); err = MAKE_1T_REWARD(pc, 1); ASSERT_FALSE(err) << err; - refresh_result = pc.RefreshState({}); + refresh_result = pc.RefreshState(false, {}); ASSERT_TRUE(refresh_result) << refresh_result.error(); - ASSERT_EQ(*refresh_result, Status::Success); + ASSERT_EQ(refresh_result->status, Status::Success); ASSERT_EQ(pc.Balance(), initial_balance + ONE_TRILLION); // Note: this puchase will be valid for 1 second auto purchase_result = pc.NewExpiringPurchase(TEST_DEBIT_TRANSACTION_CLASS, TEST_ONE_TRILLION_ONE_SECOND_DISTINGUISHER, ONE_TRILLION); - ASSERT_TRUE(purchase_result); + ASSERT_TRUE(purchase_result) << purchase_result.error(); ASSERT_EQ(purchase_result->status, Status::Success); ASSERT_TRUE(purchase_result->purchase); ASSERT_GT(purchase_result->purchase->id.size(), 0); @@ -1304,6 +1980,7 @@ TEST_F(TestPsiCash, NewExpiringPurchase) { auto local_now = datetime::DateTime::Now(); ASSERT_NEAR(purchase_result->purchase->local_time_expiry->MillisSinceEpoch(), local_now.MillisSinceEpoch(), 5000); ASSERT_NEAR(purchase_result->purchase->server_time_expiry->MillisSinceEpoch(), local_now.MillisSinceEpoch(), 5000) << "Try resyncing your local clock"; + ASSERT_NEAR(purchase_result->purchase->server_time_created.MillisSinceEpoch(), local_now.MillisSinceEpoch(), 5000) << "Try resyncing your local clock"; // Check UserData -- purchase should still be valid ASSERT_EQ(pc.user_data().GetLastTransactionID(), purchase_result->purchase->id); auto purchases = pc.GetPurchases(); @@ -1343,9 +2020,9 @@ TEST_F(TestPsiCash, NewExpiringPurchase) { initial_balance = pc.Balance(); err = MAKE_1T_REWARD(pc, 3); ASSERT_FALSE(err) << err; - refresh_result = pc.RefreshState({}); + refresh_result = pc.RefreshState(false, {}); ASSERT_TRUE(refresh_result) << refresh_result.error(); - ASSERT_EQ(*refresh_result, Status::Success); + ASSERT_EQ(refresh_result->status, Status::Success); ASSERT_EQ(pc.Balance(), initial_balance + 3*ONE_TRILLION); purchase_result = pc.NewExpiringPurchase(TEST_DEBIT_TRANSACTION_CLASS, TEST_ONE_TRILLION_ONE_MICROSECOND_DISTINGUISHER, ONE_TRILLION); ASSERT_TRUE(purchase_result); @@ -1413,9 +2090,9 @@ TEST_F(TestPsiCash, NewExpiringPurchase) { // Failure: insufficient balance pc.user_data().Clear(); - refresh_result = pc.RefreshState({}); + refresh_result = pc.RefreshState(false, {}); ASSERT_TRUE(refresh_result) << refresh_result.error(); - ASSERT_EQ(*refresh_result, Status::Success); + ASSERT_EQ(refresh_result->status, Status::Success); ASSERT_THAT(pc.Balance(), AllOf(Ge(0), Le(MAX_STARTING_BALANCE))); // We have no credit for this purchase_result = pc.NewExpiringPurchase(TEST_DEBIT_TRANSACTION_CLASS, TEST_ONE_TRILLION_ONE_MICROSECOND_DISTINGUISHER, ONE_TRILLION); @@ -1434,11 +2111,77 @@ TEST_F(TestPsiCash, NewExpiringPurchase) { purchase_result = pc.NewExpiringPurchase(TEST_DEBIT_TRANSACTION_CLASS, "invalid-distinguisher", ONE_TRILLION); ASSERT_TRUE(purchase_result); ASSERT_EQ(purchase_result->status, Status::TransactionTypeNotFound) << static_cast(purchase_result->status); + + // Using an account + auto res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login); + ASSERT_EQ(res_login->status, Status::Success); + refresh_result = pc.RefreshState(false, {}); + ASSERT_TRUE(refresh_result); + ASSERT_EQ(refresh_result->status, Status::Success); + ASSERT_TRUE(pc.IsAccount()); + initial_balance = pc.Balance(); + err = MAKE_1T_REWARD(pc, 1); + ASSERT_FALSE(err) << err; + refresh_result = pc.RefreshState(false, {}); + ASSERT_TRUE(refresh_result); + ASSERT_EQ(refresh_result->status, Status::Success); + ASSERT_EQ(pc.Balance(), initial_balance + ONE_TRILLION); + purchase_result = pc.NewExpiringPurchase(TEST_DEBIT_TRANSACTION_CLASS, TEST_ONE_TRILLION_ONE_SECOND_DISTINGUISHER, ONE_TRILLION); + ASSERT_TRUE(purchase_result); + ASSERT_EQ(purchase_result->status, Status::Success); + ASSERT_EQ(pc.Balance(), initial_balance); +} + +TEST_F(TestPsiCash, NewExpiringPurchasePauserCommitBug) { + // Bug test: When a kHTTPStatusTooManyRequests response (or any non-success, but + // especially that one) was received, the updated balance received in the response + // would be written to the datastore, but the WritePauser would not be committed, so + // the change would be lost and the UI wouldn't update until a RefreshState request + // was made. + + PsiCashTester pc; + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), HTTPRequester, false); + ASSERT_FALSE(err); + auto res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login); + ASSERT_EQ(res_login->status, Status::Success); + auto refresh_result = pc.RefreshState(false, {}); + ASSERT_TRUE(refresh_result); + ASSERT_EQ(refresh_result->status, Status::Success); + ASSERT_TRUE(pc.IsAccount()); + auto balance = pc.Balance(); + err = MAKE_1T_REWARD(pc, 2); // make sure we have enough balance for two purchases + ASSERT_FALSE(err) << err; + refresh_result = pc.RefreshState(false, {}); + ASSERT_TRUE(refresh_result); + ASSERT_EQ(refresh_result->status, Status::Success); + ASSERT_EQ(pc.Balance(), balance + ONE_TRILLION + ONE_TRILLION); + + // Make a purchase that's valid long enough for us to have a conflict. + balance = pc.Balance(); + auto purchase_result = pc.NewExpiringPurchase(TEST_DEBIT_TRANSACTION_CLASS, TEST_ONE_TRILLION_TEN_SECOND_DISTINGUISHER, ONE_TRILLION); + ASSERT_TRUE(purchase_result) << purchase_result.error(); + ASSERT_EQ(purchase_result->status, Status::Success); + ASSERT_EQ(pc.Balance(), balance - ONE_TRILLION); + + // Mess with the balance so we can see that it gets updated by the 429 purchase attempt + balance = pc.Balance(); + pc.user_data().SetBalance(1); + ASSERT_EQ(pc.Balance(), 1); + + // Now make a conflicting purchase + purchase_result = pc.NewExpiringPurchase(TEST_DEBIT_TRANSACTION_CLASS, TEST_ONE_TRILLION_TEN_SECOND_DISTINGUISHER, ONE_TRILLION); + ASSERT_TRUE(purchase_result); + ASSERT_EQ(purchase_result->status, Status::ExistingTransaction); + + // Make sure the balance was updated + ASSERT_EQ(pc.Balance(), balance); } TEST_F(TestPsiCash, NewExpiringPurchaseMutators) { PsiCashTester pc; - auto err = pc.Init(user_agent_, GetTempDir().c_str(), HTTPRequester); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), HTTPRequester, false); ASSERT_FALSE(err); if (!pc.MutatorsEnabled()) { @@ -1447,9 +2190,9 @@ TEST_F(TestPsiCash, NewExpiringPurchaseMutators) { } // Failure: invalid tokens - auto refresh_result = pc.RefreshState({}); + auto refresh_result = pc.RefreshState(false, {}); ASSERT_TRUE(refresh_result) << refresh_result.error(); - ASSERT_EQ(*refresh_result, Status::Success); + ASSERT_EQ(refresh_result->status, Status::Success); pc.SetRequestMutators({"InvalidTokens"}); auto purchase_result = pc.NewExpiringPurchase(TEST_DEBIT_TRANSACTION_CLASS, TEST_ONE_TRILLION_ONE_MICROSECOND_DISTINGUISHER, ONE_TRILLION); ASSERT_TRUE(purchase_result); @@ -1475,7 +2218,7 @@ TEST_F(TestPsiCash, NewExpiringPurchaseMutators) { ASSERT_FALSE(err) << err; pc.SetRequestMutators({"Response:code=500"}); purchase_result = pc.NewExpiringPurchase(TEST_DEBIT_TRANSACTION_CLASS, TEST_ONE_TRILLION_ONE_MICROSECOND_DISTINGUISHER, ONE_TRILLION); - ASSERT_TRUE(purchase_result); + ASSERT_TRUE(purchase_result) << purchase_result.error(); ASSERT_EQ(purchase_result->status, Status::Success); // Success: Two 500 response (sucessful retry) @@ -1502,16 +2245,649 @@ TEST_F(TestPsiCash, NewExpiringPurchaseMutators) { TEST_F(TestPsiCash, HTTPRequestBadResult) { PsiCashTester pc; - auto err = pc.Init(user_agent_, GetTempDir().c_str(), nullptr); + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), nullptr, false); ASSERT_FALSE(err); // This isn't a "bad" result, exactly, but we'll force an error code and message. - auto want_error_message = "my error message"s; + auto want_error_message = "my RECOVERABLE_ERROR message"s; HTTPResult errResult; errResult.code = HTTPResult::RECOVERABLE_ERROR; errResult.error = want_error_message; pc.SetHTTPRequestFn(FakeHTTPRequester(errResult)); - auto refresh_result = pc.RefreshState({}); + auto refresh_result = pc.RefreshState(false, {}); ASSERT_FALSE(refresh_result); - ASSERT_NE(refresh_result.error().ToString().find(want_error_message), string::npos); + ASSERT_NE(refresh_result.error().ToString().find(want_error_message), string::npos) << refresh_result.error().ToString(); + + want_error_message = "my CRITICAL_ERROR message"s; + errResult.code = HTTPResult::CRITICAL_ERROR; + errResult.error = want_error_message; + pc.SetHTTPRequestFn(FakeHTTPRequester(errResult)); + refresh_result = pc.RefreshState(false, {}); + ASSERT_FALSE(refresh_result); + ASSERT_NE(refresh_result.error().ToString().find(want_error_message), string::npos) << refresh_result.error().ToString(); +} + +TEST_F(TestPsiCash, AccountLoginSimple) { + // The initial internal release doesn't have Logout, so the tests need to be constrained. + + PsiCashTester pc; + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), HTTPRequester, false); + ASSERT_FALSE(err); + + // Empty username and password + auto res_login = pc.AccountLogin("", ""); + ASSERT_TRUE(res_login) << res_login.error(); + ASSERT_EQ(res_login->status, Status::BadRequest); + ASSERT_FALSE(pc.IsAccount()); + ASSERT_FALSE(pc.HasTokens()); + ASSERT_FALSE(pc.AccountUsername()); + + // Bad username + auto rand = utils::RandomID(); // ensure we don't match a real user + res_login = pc.AccountLogin(rand, "this is a bad password"); + ASSERT_TRUE(res_login) << res_login.error(); + ASSERT_EQ(res_login->status, Status::InvalidCredentials); + ASSERT_FALSE(pc.IsAccount()); + ASSERT_FALSE(pc.HasTokens()); + ASSERT_FALSE(pc.AccountUsername()); + + // Good username, bad password + res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, "this is a bad password"); + ASSERT_TRUE(res_login); + ASSERT_EQ(res_login->status, Status::InvalidCredentials); + ASSERT_FALSE(pc.IsAccount()); + ASSERT_FALSE(pc.HasTokens()); + ASSERT_FALSE(pc.AccountUsername()); + + // Good credentials + res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login) << res_login.error(); + ASSERT_EQ(res_login->status, Status::Success); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_TRUE(pc.AccountUsername()); + ASSERT_EQ(*pc.AccountUsername(), TEST_ACCOUNT_ONE_USERNAME); + ASSERT_EQ(pc.user_data().ValidTokenTypes().size(), 4); + ASSERT_THAT(pc.user_data().ValidTokenTypes(), AllOf(Contains("spender"), Contains("earner"), Contains("indicator"), Contains("logout"))); + ASSERT_EQ(pc.Balance(), 0); // we haven't called RefreshState yet + auto prev_earner_token = pc.user_data().GetAuthTokens()["earner"].id; + + auto res_refresh = pc.RefreshState(false, {}); + ASSERT_TRUE(res_refresh); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_GT(pc.Balance(), 0); // Our test accounts don't have zero balance + + // Try to log in again with bad creds + res_login = pc.AccountLogin(rand, "this is a bad password"); + ASSERT_TRUE(res_login) << res_login.error(); + ASSERT_EQ(res_login->status, Status::InvalidCredentials); + // Login state should not have changed + ASSERT_TRUE(pc.IsAccount()); + ASSERT_TRUE(pc.AccountUsername()); + ASSERT_EQ(*pc.AccountUsername(), TEST_ACCOUNT_ONE_USERNAME); + ASSERT_EQ(pc.user_data().ValidTokenTypes().size(), 4); + ASSERT_THAT(pc.user_data().ValidTokenTypes(), AllOf(Contains("spender"), Contains("earner"), Contains("indicator"), Contains("logout"))); + + // Good username, bad password + res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, "this is a bad password"); + ASSERT_TRUE(res_login); + ASSERT_EQ(res_login->status, Status::InvalidCredentials); + // Login state should not have changed + ASSERT_TRUE(pc.IsAccount()); + ASSERT_TRUE(pc.AccountUsername()); + ASSERT_EQ(*pc.AccountUsername(), TEST_ACCOUNT_ONE_USERNAME); + ASSERT_EQ(pc.user_data().ValidTokenTypes().size(), 4); + ASSERT_THAT(pc.user_data().ValidTokenTypes(), AllOf(Contains("spender"), Contains("earner"), Contains("indicator"), Contains("logout"))); + + // Log in again with the same account + res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login); + ASSERT_EQ(res_login->status, Status::Success); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_TRUE(pc.AccountUsername()); + ASSERT_EQ(*pc.AccountUsername(), TEST_ACCOUNT_ONE_USERNAME); + ASSERT_EQ(pc.user_data().ValidTokenTypes().size(), 4); + ASSERT_THAT(pc.user_data().ValidTokenTypes(), AllOf(Contains("spender"), Contains("earner"), Contains("indicator"), Contains("logout"))); + ASSERT_NE(pc.user_data().GetAuthTokens()["earner"].id, prev_earner_token); // should get a different token + ASSERT_EQ(pc.Balance(), 0); // we haven't yet done a RefreshState + + // Different account, good credentials + res_login = pc.AccountLogin(TEST_ACCOUNT_TWO_USERNAME, TEST_ACCOUNT_TWO_PASSWORD); + ASSERT_TRUE(res_login); + ASSERT_EQ(res_login->status, Status::Success); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_TRUE(pc.AccountUsername()); + ASSERT_EQ(*pc.AccountUsername(), TEST_ACCOUNT_TWO_USERNAME); + ASSERT_EQ(pc.user_data().ValidTokenTypes().size(), 4); + ASSERT_THAT(pc.user_data().ValidTokenTypes(), AllOf(Contains("spender"), Contains("earner"), Contains("indicator"), Contains("logout"))); + ASSERT_NE(pc.user_data().GetAuthTokens()["earner"].id, prev_earner_token); + ASSERT_EQ(pc.Balance(), 0); // we haven't yet done a RefreshState + + res_refresh = pc.RefreshState(false, {}); + ASSERT_TRUE(res_refresh); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_GT(pc.Balance(), 0); // Our test accounts don't have zero balance + + // Different account, non-ASCII username and password + res_login = pc.AccountLogin(TEST_ACCOUNT_UNICODE_USERNAME, TEST_ACCOUNT_UNICODE_PASSWORD); + ASSERT_TRUE(res_login) << res_login.error(); + ASSERT_EQ(res_login->status, Status::Success); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_TRUE(pc.AccountUsername()); + ASSERT_EQ(*pc.AccountUsername(), TEST_ACCOUNT_UNICODE_USERNAME); + ASSERT_EQ(pc.user_data().ValidTokenTypes().size(), 4); + ASSERT_THAT(pc.user_data().ValidTokenTypes(), AllOf(Contains("spender"), Contains("earner"), Contains("indicator"), Contains("logout"))); + ASSERT_NE(pc.user_data().GetAuthTokens()["earner"].id, prev_earner_token); + ASSERT_EQ(pc.Balance(), 0); // we haven't yet done a RefreshState + + res_refresh = pc.RefreshState(false, {}); + ASSERT_TRUE(res_refresh); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_GT(pc.Balance(), 0); // Our test accounts don't have zero balance +} + +TEST_F(TestPsiCash, AccountLoginMerge) { + PsiCashTester pc; + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), HTTPRequester, false); + ASSERT_FALSE(err); + + auto res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login); + ASSERT_EQ(res_login->status, Status::Success); + ASSERT_FALSE(res_login->last_tracker_merge); + + // Post-login RefreshState is required + auto res_refresh = pc.RefreshState(false, {}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_TRUE(pc.HasTokens()); + + auto account_starting_balance = pc.Balance(); + + // Log out and reset so we can get a tracker + auto res_logout = pc.AccountLogout(); + ASSERT_TRUE(res_logout); + err = pc.ResetUser(); + ASSERT_FALSE(err); + + // Get a new tracker + res_refresh = pc.RefreshState(false, {}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_FALSE(pc.IsAccount()); + ASSERT_TRUE(pc.HasTokens()); + ASSERT_THAT(pc.Balance(), AllOf(Ge(0), Le(MAX_STARTING_BALANCE))); + + // Get some balance with which to make purchases + err = MAKE_1T_REWARD(pc, 3); + ASSERT_FALSE(err) << err; + + // Make a purchase + auto purchase_result = pc.NewExpiringPurchase(TEST_DEBIT_TRANSACTION_CLASS, TEST_ONE_TRILLION_TEN_SECOND_DISTINGUISHER, ONE_TRILLION); + ASSERT_TRUE(purchase_result); + ASSERT_EQ(purchase_result->status, Status::Success); + auto expected_purchases = pc.GetPurchases(); + ASSERT_THAT(expected_purchases, SizeIs(1)); + + auto tracker_balance = pc.Balance(); + auto expected_balance = account_starting_balance + tracker_balance; + ASSERT_GT(expected_balance, account_starting_balance); // If it's not greater, then we're not really testing it + + // Log in, with bad username; should be no change to tracker + res_login = pc.AccountLogin("badusername", TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login); + ASSERT_EQ(res_login->status, Status::InvalidCredentials); + res_refresh = pc.RefreshState(false, {}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_FALSE(pc.IsAccount()); + ASSERT_TRUE(pc.HasTokens()); + ASSERT_EQ(pc.Balance(), tracker_balance); + + // Log in, with bad password; should be no change to tracker + res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, "badpassword"); + ASSERT_TRUE(res_login); + ASSERT_EQ(res_login->status, Status::InvalidCredentials); + res_refresh = pc.RefreshState(false, {}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_FALSE(pc.IsAccount()); + ASSERT_TRUE(pc.HasTokens()); + ASSERT_EQ(pc.Balance(), tracker_balance); + + // Log in, with merge + res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login); + ASSERT_EQ(res_login->status, Status::Success); + ASSERT_TRUE(res_login->last_tracker_merge); + ASSERT_FALSE(*res_login->last_tracker_merge); // this tracker has near-infinite merges + + res_refresh = pc.RefreshState(false, {}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_TRUE(pc.HasTokens()); + + // The tracker merge should have given the account the tracker's balance... + ASSERT_EQ(pc.Balance(), expected_balance); + // ...and the tracker's purchases, which should have been retrieved + ASSERT_EQ(pc.GetPurchases(), expected_purchases); + + // Force a "last tracker merge" + if (pc.MutatorsEnabled()) { + account_starting_balance = pc.Balance(); + + // Log out and reset so we can get a tracker + auto res_logout = pc.AccountLogout(); + ASSERT_TRUE(res_logout); + err = pc.ResetUser(); + ASSERT_FALSE(err); + + // Get a new tracker + res_refresh = pc.RefreshState(false, {}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_FALSE(pc.IsAccount()); + ASSERT_TRUE(pc.HasTokens()); + ASSERT_THAT(pc.Balance(), AllOf(Ge(0), Le(MAX_STARTING_BALANCE))); + + expected_balance = account_starting_balance + pc.Balance(); + + // Log in, with merge and mutator to force "last tracker merge" + pc.SetRequestMutators({"EditBody:response,TrackerMergesRemaining=0"}); + res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login); + ASSERT_EQ(res_login->status, Status::Success); + ASSERT_TRUE(res_login->last_tracker_merge); + ASSERT_TRUE(*res_login->last_tracker_merge); // forced + + res_refresh = pc.RefreshState(false, {}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_TRUE(pc.HasTokens()); + + ASSERT_EQ(pc.Balance(), expected_balance); + } + + // + // Force invalid tracker tokens to merge (will be rejected by server) + // + + // Log out and reset so we can get a tracker + res_logout = pc.AccountLogout(); + ASSERT_TRUE(res_logout); + err = pc.ResetUser(); + ASSERT_FALSE(err); + + // Get a new tracker + res_refresh = pc.RefreshState(false, {}); + ASSERT_TRUE(res_refresh) << res_refresh.error(); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_FALSE(pc.IsAccount()); + ASSERT_TRUE(pc.HasTokens()); + ASSERT_THAT(pc.Balance(), AllOf(Ge(0), Le(MAX_STARTING_BALANCE))); + + // Log in, forcing tracker tokens to be invalid. + // Note that the tokens are not passed via the auth header, so we can't use the `"InvalidTokens"` mutator. + auto at = pc.user_data().GetAuthTokens(); + for (const auto& t : at) { + at[t.first].id = at[t.first].id + "-INVALID"; + } + pc.user_data().SetAuthTokens(at, false, ""); + + res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login); + ASSERT_EQ(res_login->status, Status::BadRequest); + ASSERT_FALSE(pc.IsAccount()); + ASSERT_TRUE(pc.HasTokens()); +} + +TEST_F(TestPsiCash, AccountLogout) { + // This also tests the combination of logging in and out + + PsiCashTester pc; + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), HTTPRequester, false); + ASSERT_FALSE(err); + + // The instance ID should not change throughout this + auto instance_id = pc.user_data().GetInstanceID(); + + // Try to log out before logging in + auto res_logout = pc.AccountLogout(); + ASSERT_FALSE(res_logout); + ASSERT_FALSE(pc.IsAccount()); + ASSERT_FALSE(pc.HasTokens()); + ASSERT_FALSE(pc.AccountUsername()); + ASSERT_EQ(pc.user_data().GetInstanceID(), instance_id); + + // RefreshState to get a tracker and try to log out again + auto res_refresh = pc.RefreshState(false, {}); + ASSERT_TRUE(res_refresh); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_EQ(pc.user_data().ValidTokenTypes().size(), 3); + ASSERT_THAT(pc.user_data().ValidTokenTypes(), AllOf(Contains("spender"), Contains("earner"), Contains("indicator"))); + ASSERT_FALSE(pc.IsAccount()); + ASSERT_EQ(pc.user_data().GetInstanceID(), instance_id); + + res_logout = pc.AccountLogout(); + ASSERT_FALSE(res_logout); // fails and has no effect + ASSERT_EQ(pc.user_data().ValidTokenTypes().size(), 3); + ASSERT_THAT(pc.user_data().ValidTokenTypes(), AllOf(Contains("spender"), Contains("earner"), Contains("indicator"))); + ASSERT_FALSE(pc.IsAccount()); + ASSERT_EQ(pc.user_data().GetInstanceID(), instance_id); + + // Log in with good credentials + auto res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login) << res_login.error(); + ASSERT_EQ(res_login->status, Status::Success); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_TRUE(pc.AccountUsername()); + ASSERT_EQ(*pc.AccountUsername(), TEST_ACCOUNT_ONE_USERNAME); + ASSERT_EQ(pc.user_data().ValidTokenTypes().size(), 4); + ASSERT_THAT(pc.user_data().ValidTokenTypes(), AllOf(Contains("spender"), Contains("earner"), Contains("indicator"), Contains("logout"))); + ASSERT_EQ(pc.Balance(), 0); // we haven't called RefreshState yet + ASSERT_EQ(pc.user_data().GetInstanceID(), instance_id); + auto prev_earner_token = pc.user_data().GetAuthTokens()["earner"].id; + + res_refresh = pc.RefreshState(false, {}); + ASSERT_TRUE(res_refresh); + ASSERT_EQ(res_refresh->status, Status::Success); + ASSERT_GT(pc.Balance(), 0); // Our test accounts don't have zero balance + ASSERT_EQ(pc.user_data().GetInstanceID(), instance_id); + + res_logout = pc.AccountLogout(); + ASSERT_TRUE(res_logout) << res_logout.error(); + ASSERT_FALSE(res_logout->reconnect_required); + // This is the state we should be in after a logout + ASSERT_TRUE(pc.IsAccount()); + ASSERT_FALSE(pc.HasTokens()); + ASSERT_FALSE(pc.AccountUsername()); + ASSERT_EQ(pc.Balance(), 0); + ASSERT_EQ(pc.user_data().GetInstanceID(), instance_id); + + // We're in the was-account-logged-in state. Try to log out again. + res_logout = pc.AccountLogout(); + ASSERT_FALSE(res_logout); // should fail + // No change to this state + ASSERT_TRUE(pc.IsAccount()); + ASSERT_FALSE(pc.HasTokens()); + ASSERT_FALSE(pc.AccountUsername()); + ASSERT_EQ(pc.Balance(), 0); + ASSERT_EQ(pc.user_data().GetInstanceID(), instance_id); + + // Log in again with the same user + res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login) << res_login.error(); + ASSERT_EQ(res_login->status, Status::Success); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_TRUE(pc.AccountUsername()); + ASSERT_EQ(*pc.AccountUsername(), TEST_ACCOUNT_ONE_USERNAME); + ASSERT_EQ(pc.user_data().ValidTokenTypes().size(), 4); + ASSERT_THAT(pc.user_data().ValidTokenTypes(), AllOf(Contains("spender"), Contains("earner"), Contains("indicator"), Contains("logout"))); + ASSERT_EQ(pc.Balance(), 0); // we haven't called RefreshState yet + ASSERT_NE(pc.user_data().GetAuthTokens()["earner"].id, prev_earner_token); // different token + ASSERT_EQ(pc.user_data().GetInstanceID(), instance_id); + prev_earner_token = pc.user_data().GetAuthTokens()["earner"].id; + + // Log out, reset, log in with a different user (without first getting a new tracker) + res_logout = pc.AccountLogout(); + ASSERT_TRUE(res_logout); + ASSERT_FALSE(res_logout->reconnect_required); + // This is the state we should be in after a logout + ASSERT_TRUE(pc.IsAccount()); + ASSERT_FALSE(pc.HasTokens()); + ASSERT_FALSE(pc.AccountUsername()); + ASSERT_EQ(pc.Balance(), 0); + ASSERT_EQ(pc.user_data().GetInstanceID(), instance_id); + + err = pc.ResetUser(); + ASSERT_FALSE(err); + ASSERT_FALSE(pc.IsAccount()); + ASSERT_FALSE(pc.HasTokens()); + ASSERT_FALSE(pc.AccountUsername()); + ASSERT_EQ(pc.Balance(), 0); + ASSERT_EQ(pc.user_data().GetInstanceID(), instance_id); + + // Log in with a different user + res_login = pc.AccountLogin(TEST_ACCOUNT_TWO_USERNAME, TEST_ACCOUNT_TWO_PASSWORD); + ASSERT_TRUE(res_login); + ASSERT_EQ(res_login->status, Status::Success); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_TRUE(pc.HasTokens()); + ASSERT_TRUE(pc.AccountUsername()); + ASSERT_EQ(*pc.AccountUsername(), TEST_ACCOUNT_TWO_USERNAME); + ASSERT_NE(pc.user_data().GetAuthTokens()["earner"].id, prev_earner_token); // different token + ASSERT_EQ(pc.user_data().GetInstanceID(), instance_id); + + res_logout = pc.AccountLogout(); + ASSERT_TRUE(res_logout); + ASSERT_FALSE(res_logout->reconnect_required); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_FALSE(pc.HasTokens()); + ASSERT_FALSE(pc.AccountUsername()); + ASSERT_EQ(pc.user_data().GetInstanceID(), instance_id); + + err = pc.ResetUser(); + ASSERT_FALSE(err); + ASSERT_EQ(pc.user_data().GetInstanceID(), instance_id); + + // Should be back to not being an account + ASSERT_FALSE(pc.IsAccount()); + ASSERT_FALSE(pc.HasTokens()); + ASSERT_FALSE(pc.AccountUsername()); + + // Ensure we can log in again + res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login); + ASSERT_EQ(res_login->status, Status::Success); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_TRUE(pc.HasTokens()); + ASSERT_TRUE(pc.AccountUsername()); + ASSERT_EQ(*pc.AccountUsername(), TEST_ACCOUNT_ONE_USERNAME); + ASSERT_EQ(pc.user_data().GetInstanceID(), instance_id); + + res_logout = pc.AccountLogout(); + ASSERT_TRUE(res_logout); + ASSERT_FALSE(res_logout->reconnect_required); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_FALSE(pc.HasTokens()); + ASSERT_FALSE(pc.AccountUsername()); + ASSERT_EQ(pc.user_data().GetInstanceID(), instance_id); + + // Logging out twice is an error + res_logout = pc.AccountLogout(); + ASSERT_FALSE(res_logout); + ASSERT_EQ(pc.user_data().GetInstanceID(), instance_id); + + // We silently fall back to a local-only logout, so we're going to specifically check + // that the remote logout is occurring (in the absence of an error). + // First, get tokens. + res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login); + ASSERT_EQ(res_login->status, Status::Success); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_TRUE(pc.HasTokens()); + // Save them for later. + auto auth_tokens = pc.user_data().GetAuthTokens(); + // Logout should invalidate the tokens remotely. + res_logout = pc.AccountLogout(); + ASSERT_TRUE(res_logout); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_FALSE(pc.HasTokens()); + // Now restore the tokens and try to use them (note that we're in a pretty broken + // state here but it should be good enough for this test). + ASSERT_FALSE(pc.user_data().SetAuthTokens(auth_tokens, true, TEST_ACCOUNT_ONE_USERNAME)); + auto purchase_result = pc.NewExpiringPurchase(TEST_DEBIT_TRANSACTION_CLASS, TEST_ONE_TRILLION_TEN_SECOND_DISTINGUISHER, ONE_TRILLION); + ASSERT_TRUE(purchase_result); + // Server should respond with InvalidTokens + ASSERT_EQ(purchase_result->status, Status::InvalidTokens); + + // Test logging out with invalid tokens. + if (pc.MutatorsEnabled()) { + // First log in normally. + res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login); + ASSERT_EQ(res_login->status, Status::Success); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_TRUE(pc.HasTokens()); + ASSERT_TRUE(pc.AccountUsername()); + ASSERT_EQ(*pc.AccountUsername(), TEST_ACCOUNT_ONE_USERNAME); + ASSERT_EQ(pc.user_data().GetInstanceID(), instance_id); + // Then log out with invalid tokens. It should succeed even though nothing was done + // server-side, and have nuked our local state. + pc.SetRequestMutators({"InvalidTokens"}); + res_logout = pc.AccountLogout(); + ASSERT_TRUE(res_logout); + ASSERT_FALSE(res_logout->reconnect_required); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_FALSE(pc.HasTokens()); + ASSERT_FALSE(pc.AccountUsername()); + ASSERT_EQ(pc.user_data().GetInstanceID(), instance_id); + } +} + +TEST_F(TestPsiCash, AccountLogoutNeedReconnect) { + PsiCashTester pc; + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), HTTPRequester, false); + ASSERT_FALSE(err); + + // Log in with good credentials + auto res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login) << res_login.error(); + ASSERT_EQ(res_login->status, Status::Success); + ASSERT_TRUE(pc.IsAccount()); + ASSERT_TRUE(pc.AccountUsername()); + ASSERT_EQ(*pc.AccountUsername(), TEST_ACCOUNT_ONE_USERNAME); + auto res_refresh = pc.RefreshState(false, {}); + ASSERT_TRUE(res_refresh); + ASSERT_EQ(res_refresh->status, Status::Success); + + // Ensure we have no active purchases when starting this test + ASSERT_EQ(pc.ActivePurchases().size(), 0); + + // Get some balance with which to make purchases + err = MAKE_1T_REWARD(pc, 3); + ASSERT_FALSE(err) << err; + + // Log out with no active purchases + auto res_logout = pc.AccountLogout(); + ASSERT_TRUE(res_logout) << res_logout.error(); + // No purchase, so no reconnect required + ASSERT_FALSE(res_logout->reconnect_required); + + // Log in again with the same user + res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login) << res_login.error(); + ASSERT_EQ(res_login->status, Status::Success); + res_refresh = pc.RefreshState(false, {}); + ASSERT_TRUE(res_refresh); + ASSERT_EQ(res_refresh->status, Status::Success); + + // Make a purchase that does _not_ produce an authorization + auto purchase_result = pc.NewExpiringPurchase(TEST_DEBIT_TRANSACTION_CLASS, TEST_ONE_TRILLION_TEN_SECOND_DISTINGUISHER, ONE_TRILLION); + ASSERT_TRUE(purchase_result); + ASSERT_EQ(purchase_result->status, Status::Success); + ASSERT_FALSE(purchase_result->purchase->authorization); + ASSERT_EQ(pc.ActivePurchases().size(), 1); + + // Log out, should still be no reconnect required + res_logout = pc.AccountLogout(); + ASSERT_TRUE(res_logout); + ASSERT_FALSE(res_logout->reconnect_required); + + // Log in again with the same user + res_login = pc.AccountLogin(TEST_ACCOUNT_ONE_USERNAME, TEST_ACCOUNT_ONE_PASSWORD); + ASSERT_TRUE(res_login) << res_login.error(); + ASSERT_EQ(res_login->status, Status::Success); + res_refresh = pc.RefreshState(false, {}); + ASSERT_TRUE(res_refresh); + ASSERT_EQ(res_refresh->status, Status::Success); + + // Make a purchase that _does_ produce an authorization + purchase_result = pc.NewExpiringPurchase(TEST_DEBIT_WITH_AUTHORIZATION_TRANSACTION_CLASS, TEST_ONE_TRILLION_TEN_SECOND_DISTINGUISHER, ONE_TRILLION); + ASSERT_TRUE(purchase_result); + ASSERT_EQ(purchase_result->status, Status::Success); + ASSERT_TRUE(purchase_result->purchase->authorization); + ASSERT_EQ(pc.ActivePurchases().size(), 2) << json(pc.ActivePurchases()); + + // Log out; now that we have a purchase with authorization, we should need reconnect + res_logout = pc.AccountLogout(); + ASSERT_TRUE(res_logout); + ASSERT_TRUE(res_logout->reconnect_required); +} + +TEST_F(TestPsiCash, PurchaseFromJSON) { + PsiCashTester pc; + auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), HTTPRequester, false); + ASSERT_FALSE(err); + + // Basically no diff + ASSERT_FALSE(pc.user_data().SetServerTimeDiff(datetime::DateTime::Now())); + + auto j = R"|({ + "TransactionID": "txid", + "Created": "2001-01-01T01:01:01.001Z", + "Class": "txclass", + "Distinguisher": "txdistinguisher", + "Authorization": "eyJBdXRob3JpemF0aW9uIjp7IklEIjoiMFYzRXhUdmlBdFNxTGZOd2FpQXlHNHpaRUJJOGpIYnp5bFdNeU5FZ1JEZz0iLCJBY2Nlc3NUeXBlIjoic3BlZWQtYm9vc3QtdGVzdCIsIkV4cGlyZXMiOiIyMDE5LTAxLTE0VDE3OjIyOjIzLjE2ODc2NDEyOVoifSwiU2lnbmluZ0tleUlEIjoiUUNZTzV2clIvZGhjRDZ6M2FMQlVNeWRuZlJyZFNRL1RWYW1IUFhYeTd0TT0iLCJTaWduYXR1cmUiOiJQL2NrenloVUJoSk5RQ24zMnluM1VTdGpLencxU04xNW9MclVhTU9XaW9scXBOTTBzNVFSNURHVEVDT1FzQk13ODdQdTc1TGE1OGtJTHRIcW1BVzhDQT09In0=", + "TransactionResponse": { + "Type": "expected_type", + "Values": { + "Expires": "2001-01-01T02:01:01.001Z" + } + } + })|"_json; + + // Simple success + auto res = pc.PurchaseFromJSON(j); + ASSERT_TRUE(res); + ASSERT_EQ(res->id, "txid"); + ASSERT_EQ(res->transaction_class, "txclass"); + ASSERT_EQ(res->distinguisher, "txdistinguisher"); + ASSERT_TRUE(res->authorization); + datetime::DateTime dt; + ASSERT_TRUE(dt.FromISO8601("2001-01-01T01:01:01.001Z")); + ASSERT_EQ(dt, res->server_time_created); + ASSERT_TRUE(dt.FromISO8601("2001-01-01T02:01:01.001Z")); + ASSERT_EQ(dt, *res->server_time_expiry); + ASSERT_NEAR(res->local_time_expiry->MillisSinceEpoch(), dt.MillisSinceEpoch(), 500); + + // Expected type mismatch + res = pc.PurchaseFromJSON(j, "won't match"); + ASSERT_FALSE(res); + + // Bad created date + auto prev_val = j["Created"]; + j["Created"] = "nope"; + res = pc.PurchaseFromJSON(j); + ASSERT_FALSE(res); + j["Created"] = prev_val; // put it back for following tests + + // Bad expiry date + prev_val = j["/TransactionResponse/Values/Expires"_json_pointer]; + j["/TransactionResponse/Values/Expires"_json_pointer] = "nope"; + res = pc.PurchaseFromJSON(j); + ASSERT_FALSE(res); + j["/TransactionResponse/Values/Expires"_json_pointer] = prev_val; + + // Authorization decode fail + prev_val = j["Authorization"]; + j["Authorization"] = "nope"; + res = pc.PurchaseFromJSON(j); + ASSERT_FALSE(res); + j["Authorization"] = prev_val; + + // Missing expected JSON field + prev_val = j["TransactionID"]; + j["TransactionID"] = nullptr; + res = pc.PurchaseFromJSON(j); + ASSERT_FALSE(res); + j["TransactionID"] = prev_val; + + prev_val = j["TransactionID"]; + j.erase("TransactionID"); + res = pc.PurchaseFromJSON(j); + ASSERT_FALSE(res); + j["TransactionID"] = prev_val; } diff --git a/psicash_tester.cpp b/psicash_tester.cpp index 66ad075..fd5860f 100644 --- a/psicash_tester.cpp +++ b/psicash_tester.cpp @@ -21,6 +21,7 @@ #include #include +#include #include "psicash_tester.hpp" #include "utils.hpp" #include "http_status_codes.h" @@ -47,17 +48,13 @@ PsiCashTester::~PsiCashTester() { } error::Error PsiCashTester::Init(const string& user_agent, const string& file_store_root, - MakeHTTPRequestFn make_http_request_fn) { - return Init(user_agent, file_store_root, make_http_request_fn, DEV_ENV); + MakeHTTPRequestFn make_http_request_fn, bool force_reset) { + return Init(user_agent, file_store_root, make_http_request_fn, force_reset, DEV_ENV); } error::Error PsiCashTester::Init(const string& user_agent, const string& file_store_root, - MakeHTTPRequestFn make_http_request_fn, bool test) { - return PsiCash::Init(user_agent, file_store_root, make_http_request_fn, test); -} - -error::Error PsiCashTester::Reset(const string& file_store_root) { - return PsiCash::Reset(file_store_root, DEV_ENV); + MakeHTTPRequestFn make_http_request_fn, bool force_reset, bool test) { + return PsiCash::Init(user_agent, file_store_root, make_http_request_fn, force_reset, test); } UserData& PsiCashTester::user_data() { @@ -76,7 +73,8 @@ error::Error PsiCashTester::MakeRewardRequests(const std::string& transaction_cl auto result = MakeHTTPRequestWithRetry( "POST", "/transaction", true, {{"class", transaction_class}, - {"distinguisher", distinguisher}}); + {"distinguisher", distinguisher}}, + nonstd::nullopt); if (!result) { return WrapError(result.error(), "MakeHTTPRequestWithRetry failed"); } else if (result->code != kHTTPStatusOK) { @@ -92,7 +90,8 @@ PsiCashTester::BuildRequestParams(const std::string& method, const std::string& bool include_auth_tokens, const std::vector>& query_params, int attempt, - const std::map& additional_headers) const { + const std::map& additional_headers, + const std::string& body) const { auto bonus_headers = additional_headers; if (!g_request_mutators.empty()) { auto mutator = g_request_mutators.back(); @@ -103,30 +102,30 @@ PsiCashTester::BuildRequestParams(const std::string& method, const std::string& } return PsiCash::BuildRequestParams( - method, path, include_auth_tokens, query_params, attempt, bonus_headers); + method, path, include_auth_tokens, query_params, attempt, bonus_headers, body); } bool PsiCashTester::MutatorsEnabled() { - static bool checked = false; + static bool checked = false, mutators_enabled = false; if (checked) { - return mutators_enabled_; + return mutators_enabled; } checked = true; SetRequestMutators({"CheckEnabled"}); auto result = MakeHTTPRequestWithRetry( - "GET", "/refresh-state", false, {}); + "GET", "/refresh-state", false, {}, nonstd::nullopt); if (!result) { throw std::runtime_error("MUTATOR CHECK FAILED: "s + result.error().ToString()); } - mutators_enabled_ = (result->code == kHTTPStatusAccepted); + mutators_enabled = (result->code == kHTTPStatusAccepted); - if (!mutators_enabled_) { - //cout << "SKIPPING MUTATOR TESTS; code: " << result->code << endl; + if (!mutators_enabled) { + std::cout << "SKIPPING MUTATOR TESTS; code: " << result->code << std::endl; } - return mutators_enabled_; + return mutators_enabled; } void PsiCashTester::SetRequestMutators(const std::vector& mutators) { @@ -134,6 +133,10 @@ void PsiCashTester::SetRequestMutators(const std::vector& mutators) g_request_mutators.assign(mutators.crbegin(), mutators.crend()); } +psicash::error::Result PsiCashTester::PurchaseFromJSON(const nlohmann::json& j, const std::string& expected_type) const { + return PsiCash::PurchaseFromJSON(j, expected_type); +} + } // namespace psicash #endif // NDEBUG diff --git a/psicash_tester.hpp b/psicash_tester.hpp index 15dd08c..f396b3d 100644 --- a/psicash_tester.hpp +++ b/psicash_tester.hpp @@ -24,6 +24,7 @@ #include #include #include "psicash.hpp" +#include "error.hpp" namespace testing { @@ -40,14 +41,11 @@ class PsiCashTester : public psicash::PsiCash { // Most tests should use this form of Init. It will use the global flag for `init`. psicash::error::Error Init(const std::string& user_agent, const std::string& file_store_root, - psicash::MakeHTTPRequestFn make_http_request_fn); + psicash::MakeHTTPRequestFn make_http_request_fn, bool force_reset); // If the `test` flag really must be set explicitly, use this method. psicash::error::Error Init(const std::string& user_agent, const std::string& file_store_root, - psicash::MakeHTTPRequestFn make_http_request_fn, bool test); - - // Overridden to use the global testing flag - psicash::error::Error Reset(const std::string& file_store_root); + psicash::MakeHTTPRequestFn make_http_request_fn, bool force_reset, bool test); psicash::UserData& user_data(); @@ -59,14 +57,14 @@ class PsiCashTester : public psicash::PsiCash { BuildRequestParams(const std::string& method, const std::string& path, bool include_auth_tokens, const std::vector>& query_params, int attempt, - const std::map& additional_headers) const; + const std::map& additional_headers, + const std::string& body) const; bool MutatorsEnabled(); void SetRequestMutators(const std::vector& mutators); -private: - bool mutators_enabled_; + psicash::error::Result PurchaseFromJSON(const nlohmann::json& j, const std::string& expected_type="") const; }; } // namespace testing diff --git a/test_helpers.cpp b/test_helpers.cpp index 5dbc75d..1ccc578 100644 --- a/test_helpers.cpp +++ b/test_helpers.cpp @@ -23,3 +23,31 @@ int exec(const char* cmd, std::string& output) { // pclose will be called again as the shared_ptr deleter, but that's okay return result; } + +bool AuthTokenSetsEqual(const psicash::AuthTokens& at1, const psicash::AuthTokens& at2) { + if (at1.size() != at2.size()) { + return false; + } + + for (const auto& map_it1 : at1) { + const auto& val_it2 = at2.find(map_it1.first); + if (val_it2 == at2.end()) { + return false; + } + + if (val_it2->second.id != map_it1.second.id) { + return false; + } + + if (val_it2->second.server_time_expiry.has_value() != map_it1.second.server_time_expiry.has_value()) { + return false; + } + + if (val_it2->second.server_time_expiry.has_value() + && !(*(val_it2->second.server_time_expiry) == *(map_it1.second.server_time_expiry))) { + return false; + } + } + + return true; +} diff --git a/test_helpers.hpp b/test_helpers.hpp index 3120122..1fe495f 100644 --- a/test_helpers.hpp +++ b/test_helpers.hpp @@ -7,7 +7,10 @@ #include #include #include +#include +#include #include "gtest/gtest.h" +#include "userdata.hpp" class TempDir { @@ -61,11 +64,31 @@ class TempDir return res; } - void WriteBadData(const std::string& datastore_root, const std::string& suffix) + std::string GetSuffix(bool dev) { + return dev ? ".dev" : ".prod"; + } + + std::string DatastoreFilepath(const std::string& datastore_root, bool dev) { + return datastore_root + "/psicashdatastore" + GetSuffix(dev); + } + + bool Write(const std::string& datastore_root, bool dev, const std::string& s) { + auto ds_file = DatastoreFilepath(datastore_root, dev); + std::ofstream f; + f.open(ds_file, std::ios::out | std::ios::trunc | std::ios::binary); + if (!f.is_open()) { + return false; + } + f << s; + f.close(); + return true; + } + + bool WriteBadData(const std::string& datastore_root, bool dev) { - auto ds_file = datastore_root + "/psicashdatastore" + suffix; + auto ds_file = DatastoreFilepath(datastore_root, dev); auto make_bad_file = "echo nonsense > " + ds_file; - system(make_bad_file.c_str()); + return Write(datastore_root, dev, "this is bad data"); } }; @@ -114,4 +137,6 @@ ::testing::AssertionResult VectorSetsMatch(const std::vector& expected, const return ::testing::AssertionSuccess(); } +bool AuthTokenSetsEqual(const psicash::AuthTokens& at1, const psicash::AuthTokens& at2); + #endif // PSICASHLIB_TEST_HELPERS_H diff --git a/userdata.cpp b/userdata.cpp index c80269f..aab45b2 100644 --- a/userdata.cpp +++ b/userdata.cpp @@ -28,16 +28,52 @@ using namespace std; namespace psicash { +constexpr int kCurrentDatastoreVersion = 2; + // Datastore keys -static constexpr const char* VERSION = "v"; // Preliminary version key; not yet used for anything +static constexpr const char* VERSION = "v"; +static auto kVersionPtr = "/v"_json_pointer; +// +// Instance-specific data keys +// +static auto kInstancePtr = "/instance"_json_pointer; +static const string INSTANCE_ID = "instanceID"; +static const auto kInstanceIDPtr = kInstancePtr / INSTANCE_ID; +static constexpr const char* IS_LOGGED_OUT_ACCOUNT = "isLoggedOutAccount"; +static const auto kIsLoggedOutAccountPtr = kInstancePtr / IS_LOGGED_OUT_ACCOUNT; +static constexpr const char* LOCALE = "locale"; +static const auto kLocalePtr = kInstancePtr / LOCALE; +// +// User-specific data keys +// +static auto kUserPtr = "/user"_json_pointer; static constexpr const char* SERVER_TIME_DIFF = "serverTimeDiff"; +static const auto kServerTimeDiffPtr = kUserPtr / SERVER_TIME_DIFF; static constexpr const char* AUTH_TOKENS = "authTokens"; +static const auto kAuthTokensPtr = kUserPtr / AUTH_TOKENS; static constexpr const char* BALANCE = "balance"; -static constexpr const char* IS_ACCOUNT = "IsAccount"; +static const auto kBalancePtr = kUserPtr / BALANCE; +static constexpr const char* IS_ACCOUNT = "isAccount"; +static const auto kIsAccountPtr = kUserPtr / IS_ACCOUNT; +static constexpr const char* ACCOUNT_USERNAME = "accountUsername"; +static const auto kAccountUsernamePtr = kUserPtr / ACCOUNT_USERNAME; static constexpr const char* PURCHASE_PRICES = "purchasePrices"; +static const auto kPurchasePricesPtr = kUserPtr / PURCHASE_PRICES; static constexpr const char* PURCHASES = "purchases"; +static const auto kPurchasesPtr = kUserPtr / PURCHASES; static constexpr const char* LAST_TRANSACTION_ID = "lastTransactionID"; -const char* REQUEST_METADATA = "requestMetadata"; // used in header +static const auto kLastTransactionIDPtr = kUserPtr / LAST_TRANSACTION_ID; +static const char* REQUEST_METADATA = "requestMetadata"; +const json::json_pointer kRequestMetadataPtr = kUserPtr / REQUEST_METADATA; // used in header, so not static + + +// These are the possible token types. +const char* const kEarnerTokenType = "earner"; +const char* const kSpenderTokenType = "spender"; +const char* const kIndicatorTokenType = "indicator"; +const char* const kAccountTokenType = "account"; +const char* const kLogoutTokenType = "logout"; + UserData::UserData() { } @@ -49,30 +85,99 @@ static string DataStoreSuffix(bool dev) { return dev ? ".dev" : ".prod"; } +static auto FreshDatastore() { + json ds; + ds[kVersionPtr] = kCurrentDatastoreVersion; + ds[kUserPtr] = json::object(); + ds[kInstancePtr] = json::object(); + ds[kInstanceIDPtr] = "instanceid_"s + utils::RandomID(); + + return ds; +} + error::Error UserData::Init(const string& file_store_root, bool dev) { auto err = datastore_.Init(file_store_root, DataStoreSuffix(dev)); if (err) { return PassError(err); } - err = datastore_.Set({{VERSION, 1}}); - if (err) { - return PassError(err); + auto version = datastore_.Get(kVersionPtr); + if (!version) { + err = datastore_.Reset(FreshDatastore()); + if (err) { + return PassError(err); + } + } + else if (*version == 1) { + // We need to migrate from the structure where all data was at the root + // of the object, to one where the object looks like: + // {"v":2,"user":{old data},"instance":{new stuff}} + + auto oldDS = datastore_.Get(); + if (!oldDS) { + // This should never happen. The version was successfully returned, + // so we know there's a structure there and we should have got it. + return error::MakeCriticalError("failed to retrieve v1 data"); + } + + json newDS = FreshDatastore(); + oldDS->erase("v"); + newDS[kUserPtr] = *oldDS; + + err = datastore_.Reset(newDS); + if (err) { + return PassError(err); + } + } + else if (*version != kCurrentDatastoreVersion) { + return error::MakeCriticalError( + utils::Stringer("found unexpected version number: ", *version)); } + // else we've loaded a good, current datastore return error::nullerr; } error::Error UserData::Clear(const string& file_store_root, bool dev) { - return PassError(datastore_.Clear(file_store_root, DataStoreSuffix(dev))); + return PassError(datastore_.Reset( + file_store_root, DataStoreSuffix(dev), FreshDatastore())); } error::Error UserData::Clear() { - return PassError(datastore_.Clear()); + return PassError(datastore_.Reset(FreshDatastore())); +} + +error::Error UserData::DeleteUserData(bool isLoggedOutAccount) { + WritePauser pauser(*this); + // Not checking return values, since writing is paused. + (void)datastore_.Set(kUserPtr, json::object()); + (void)SetIsLoggedOutAccount(isLoggedOutAccount); + return PassError(pauser.Commit()); +} + +std::string UserData::GetInstanceID() const { + auto v = datastore_.Get(kInstanceIDPtr); + + // This should not happen. The instance ID must be initialized when the datastore is set up. + assert(!!v); + + return *v; +} + +bool UserData::GetIsLoggedOutAccount() const { + auto v = datastore_.Get(kIsLoggedOutAccountPtr); + if (!v) { + return false; + } + return *v; +} + +error::Error UserData::SetIsLoggedOutAccount(bool v) { + return PassError(datastore_.Set(kIsLoggedOutAccountPtr, v)); } datetime::Duration UserData::GetServerTimeDiff() const { - auto v = datastore_.Get(SERVER_TIME_DIFF); + auto v = datastore_.Get(kServerTimeDiffPtr); if (!v) { return datetime::DurationFromInt64(0); } @@ -82,41 +187,134 @@ datetime::Duration UserData::GetServerTimeDiff() const { error::Error UserData::SetServerTimeDiff(const datetime::DateTime& serverTimeNow) { auto localTimeNow = datetime::DateTime::Now(); auto diff = serverTimeNow.Diff(localTimeNow); - return PassError(datastore_.Set({{SERVER_TIME_DIFF, datetime::DurationToInt64(diff)}})); + return PassError(datastore_.Set(kServerTimeDiffPtr, datetime::DurationToInt64(diff))); +} + +datetime::DateTime UserData::ServerTimeToLocal(const datetime::DateTime& server_time) const { + // server_time_diff is server-minus-local. So it's positive if server is ahead, negative if behind. + // So we have to subtract the diff from the server time to get the local time. + // Δ = s - l + // l = s - Δ + return server_time.Sub(GetServerTimeDiff()); +} + +/* There are two JSON formats that we might receive for tokens, and we'll handle them both. +NewTracker: + { + "earner": , + "spender": , + "indicator": + } + +Login: + { + "earner": { + "ID": "token", + "Expiry": "" + }, + ... + } + +Note that the NewTracker style was used in our pre-accounts datastore and the Login style +is used now, so this multi-format reading support allows for easy migration. +*/ +void from_json(const json& j, AuthTokens& v) { + for (const auto& it : j.items()) { + if (it.value().is_string()) { + // NewTracker style + v[it.key()] = TokenInfo{ it.value().get(), nonstd::nullopt }; + } + else { + // Login style + v[it.key()] = TokenInfo{ it.value().at("ID").get(), nonstd::nullopt }; + if (it.value().at("Expiry").is_string()) { + v[it.key()].server_time_expiry = it.value().at("Expiry").get(); + } + } + } +} + +// We are serializing (into the datastore) the same format used by the server's Login response +void to_json(json& j, const AuthTokens& v) { + j = json::object(); + for (const auto& it : v) { + j[it.first] = { + { "ID", it.second.id }, + { "Expiry", nullptr } + }; + if (it.second.server_time_expiry) { + j[it.first]["Expiry"] = *it.second.server_time_expiry; + } + } } AuthTokens UserData::GetAuthTokens() const { - auto v = datastore_.Get(AUTH_TOKENS); + auto v = datastore_.Get(kAuthTokensPtr); if (!v) { return AuthTokens(); } return *v; } -error::Error UserData::SetAuthTokens(const AuthTokens& v, bool is_account) { - return PassError(datastore_.Set({{AUTH_TOKENS, v}, - {IS_ACCOUNT, is_account}})); +error::Error UserData::SetAuthTokens(const AuthTokens& v, bool is_account, const std::string& utf8_username) { + WritePauser pauser(*this); + // Not checking errors while paused, as there's no error that can occur. + json json_tokens; + to_json(json_tokens, v); + (void)datastore_.Set(kAuthTokensPtr, json_tokens); + (void)datastore_.Set(kIsAccountPtr, is_account); + (void)datastore_.Set(kAccountUsernamePtr, utf8_username); + return PassError(pauser.Commit()); // write } error::Error UserData::CullAuthTokens(const std::map& valid_tokens) { - auto all_auth_tokens = GetAuthTokens(); - AuthTokens good_auth_tokens; - - // all_auth_tokens is { "earner": "ABCD0123" } and valid_tokens is { "ABCD0123": true } - for (const auto& t : all_auth_tokens) { + // There's no guarantee that the tokens in valid_tokens will be idential to the tokens + // we have stored -- although they really should be. We're going to interpret the + // absence of a stored token from valid_tokens as an indicator that it's invalid. + // (There's no much we can do about the presence of a token in valid_tokens that we + // don't have stored. We'll ignore it.) + // + // We handle _any_ invalid token as reason to blow away _all_ tokens. An incomplete + // set is effectively the same as no set at all. + + bool all_tokens_okay = true; + + // all_auth_tokens is { "earner": {ID: "ABCD0123", Expiry: <>} } and valid_tokens is { "ABCD0123": true } + for (const auto& t : GetAuthTokens()) { + bool t_ok = false; for (const auto& vtt : valid_tokens) { - if (vtt.first == t.second && vtt.second) { - good_auth_tokens[t.first] = t.second; + if (vtt.first == t.second.id && vtt.second) { + t_ok = true; break; } } + + if (!t_ok) { + all_tokens_okay = false; + break; + } } - return PassError(datastore_.Set({{AUTH_TOKENS, good_auth_tokens}})); + if (all_tokens_okay) { + // All our tokens are good, so there's nothing to do + return error::nullerr; + } + + // Clear all stored tokens + return PassError(datastore_.Set(kAuthTokensPtr, {})); +} + +psicash::TokenTypes UserData::ValidTokenTypes() const { + auto auth_tokens = GetAuthTokens(); + vector valid_token_types; + for (const auto& it : auth_tokens) { + valid_token_types.push_back(it.first); + } + return valid_token_types; } bool UserData::GetIsAccount() const { - auto v = datastore_.Get(IS_ACCOUNT); + auto v = datastore_.Get(kIsAccountPtr); if (!v) { return false; } @@ -124,11 +322,23 @@ bool UserData::GetIsAccount() const { } error::Error UserData::SetIsAccount(bool v) { - return PassError(datastore_.Set({{IS_ACCOUNT, v}})); + return PassError(datastore_.Set(kIsAccountPtr, v)); +} + +std::string UserData::GetAccountUsername() const { + auto v = datastore_.Get(kAccountUsernamePtr); + if (!v) { + return ""; + } + return *v; +} + +error::Error UserData::SetAccountUsername(const std::string& v) { + return PassError(datastore_.Set(kAccountUsernamePtr, v)); } int64_t UserData::GetBalance() const { - auto v = datastore_.Get(BALANCE); + auto v = datastore_.Get(kBalancePtr); if (!v) { return 0; } @@ -136,11 +346,11 @@ int64_t UserData::GetBalance() const { } error::Error UserData::SetBalance(int64_t v) { - return PassError(datastore_.Set({{BALANCE, v}})); + return PassError(datastore_.Set(kBalancePtr, v)); } PurchasePrices UserData::GetPurchasePrices() const { - auto v = datastore_.Get(PURCHASE_PRICES); + auto v = datastore_.Get(kPurchasePricesPtr); if (!v) { return PurchasePrices(); } @@ -148,11 +358,11 @@ PurchasePrices UserData::GetPurchasePrices() const { } error::Error UserData::SetPurchasePrices(const PurchasePrices& v) { - return PassError(datastore_.Set({{PURCHASE_PRICES, v}})); + return PassError(datastore_.Set(kPurchasePricesPtr, v)); } Purchases UserData::GetPurchases() const { - auto v = datastore_.Get(PURCHASES); + auto v = datastore_.Get(kPurchasesPtr); if (!v) { v = Purchases(); } @@ -162,26 +372,58 @@ Purchases UserData::GetPurchases() const { } error::Error UserData::SetPurchases(const Purchases& v) { - return PassError(datastore_.Set({{PURCHASES, v}})); + return PassError(datastore_.Set(kPurchasesPtr, v)); } error::Error UserData::AddPurchase(const Purchase& v) { + // We're not going to assume too much about the incoming purchase: it might be a + // duplicate, or it might not be as new as the newest purchase we already have. + + // Assumption: The purchases vector is already sorted by created date ascending. + // Assumption: The ID of our purchase argument should become our LastTransactionID. + // This will be true _even if_ there are purchases in our datastore with later + // created dates. + // - If this purchase is being added due to, say, NewExpiringPurchase, then the + // purchase is brand new. + // - If the purchase is being added due to RefreshState retreiving some newer than + // our LastTransactionID, then the argument is newer. + // - If the purchase is being added because our LastTransactionID was corrupt and + // RefreshState is giving us everything, then we need to replace it with the + // purchases we're storing, as we store them. + + // TODO: If/when we start dealing with large numbers of purchases being added, + // (e.g., when we have a large number of a non-instance-specific purchases and the + // user is doing a full retrieve), the work here should be done more efficiently + // (like in a batch). + // (Of course, we might also have to rethink our datastore at that point.) + auto purchases = GetPurchases(); - // Prevent duplicate insertion - for (const auto& p : purchases) { - if (p.id == v.id) { - return error::nullerr; + + for (auto iter = purchases.begin(); ; iter++) { + if (iter == purchases.end()) { + // We searched to the end and didn't find a duplicate or insertion point. + // Put the new purchase at the end. + purchases.insert(iter, v); + break; + } + else if (iter->id == v.id) { + // This is a duplicate. Update our local copy in case we have bad data. + *iter = v; + break; + } + else if (iter->server_time_created > v.server_time_created) { + // We have found the sorted insertion point. + purchases.insert(iter, v); + break; } } - purchases.push_back(v); - // Pause to set Purchases and LastTransactionID in one write WritePauser pauser(*this); // These don't write, so have no meaningful return (void)SetPurchases(purchases); (void)SetLastTransactionID(v.id); - return PassError(pauser.Unpause()); // write + return PassError(pauser.Commit()); // write } void UserData::UpdatePurchaseLocalTimeExpiry(Purchase& purchase) const { @@ -189,11 +431,7 @@ void UserData::UpdatePurchaseLocalTimeExpiry(Purchase& purchase) const { return; } - // server_time_diff is server-minus-local. So it's positive if server is ahead, negative if behind. - // So we have to subtract the diff from the server time to get the local time. - // Δ = s - l - // l = s - Δ - purchase.local_time_expiry = purchase.server_time_expiry->Sub(GetServerTimeDiff()); + purchase.local_time_expiry = ServerTimeToLocal(*purchase.server_time_expiry); } void UserData::UpdatePurchasesLocalTimeExpiry(Purchases& purchases) const { @@ -203,7 +441,7 @@ void UserData::UpdatePurchasesLocalTimeExpiry(Purchases& purchases) const { } TransactionID UserData::GetLastTransactionID() const { - auto v = datastore_.Get(LAST_TRANSACTION_ID); + auto v = datastore_.Get(kLastTransactionIDPtr); if (!v) { return TransactionID(); } @@ -211,11 +449,11 @@ TransactionID UserData::GetLastTransactionID() const { } error::Error UserData::SetLastTransactionID(const TransactionID& v) { - return PassError(datastore_.Set({{LAST_TRANSACTION_ID, v}})); + return PassError(datastore_.Set(kLastTransactionIDPtr, v)); } json UserData::GetRequestMetadata() const { - auto j = datastore_.Get(REQUEST_METADATA); + auto j = datastore_.Get(kRequestMetadataPtr); if (!j) { return json::object(); } @@ -223,4 +461,17 @@ json UserData::GetRequestMetadata() const { return *j; } +std::string UserData::GetLocale() const { + auto v = datastore_.Get(kLocalePtr); + if (!v) { + return ""; + } + return *v; +} + +error::Error UserData::SetLocale(const std::string& v) { + return PassError(datastore_.Set(kLocalePtr, v)); +} + + } // namespace psicash diff --git a/userdata.hpp b/userdata.hpp index 14a6ec0..c3dc8b4 100644 --- a/userdata.hpp +++ b/userdata.hpp @@ -29,9 +29,21 @@ namespace psicash { -extern const char* REQUEST_METADATA; // only for use in template method below +extern const nlohmann::json::json_pointer kRequestMetadataPtr; // only for use in template method below + +struct TokenInfo { std::string id; nonstd::optional server_time_expiry; }; +using AuthTokens = std::map; // type to token info +void to_json(nlohmann::json& j, const AuthTokens& v); +void from_json(const nlohmann::json& j, AuthTokens& v); +using TokenTypes = std::vector; + +// These are the possible token types. +extern const char* const kEarnerTokenType; +extern const char* const kSpenderTokenType; +extern const char* const kIndicatorTokenType; +extern const char* const kAccountTokenType; +extern const char* const kLogoutTokenType; -using AuthTokens = std::map; /// Storage and retrieval (and some processing) of PsiCash user data/state. /// UserData operations are threadsafe (via Datastore). @@ -55,31 +67,51 @@ class UserData { /// Init() must have already been called, successfully. error::Error Clear(); - /// Used to pause and result datastore file writing. + /// Used to pause and resume datastore file writing. + /// WritePausers can be nested -- inner instances will do nothing. class WritePauser { public: - WritePauser(UserData& user_data) : user_data_( - user_data) { user_data_.datastore_.PauseWrites(); }; - ~WritePauser() { (void)Unpause(); } // TODO: Should dtor nuke changes (implying error)? Maybe param to ctor to indicate? - error::Error Unpause() { return user_data_.datastore_.UnpauseWrites(); } + WritePauser(UserData& user_data) : actually_paused_(false), user_data_( + user_data) { actually_paused_ = user_data_.datastore_.PauseWrites(); }; + ~WritePauser() { if (actually_paused_) { (void)Rollback(); } } + error::Error Commit() { return Unpause(true); } + error::Error Rollback() { return Unpause(false); } private: + error::Error Unpause(bool commit) { auto p = actually_paused_; actually_paused_ = false; if (p) { return user_data_.datastore_.UnpauseWrites(commit); } return error::nullerr; } + bool actually_paused_; UserData& user_data_; }; public: + /// Deletes the stored user data and sets the isLoggedOutAccount flag. + error::Error DeleteUserData(bool isLoggedOutAccount); + + std::string GetInstanceID() const; + + bool GetIsLoggedOutAccount() const; + error::Error SetIsLoggedOutAccount(bool v); + datetime::Duration GetServerTimeDiff() const; error::Error SetServerTimeDiff(const datetime::DateTime& serverTimeNow); + /// Converts `server_time` to local time using the current diff + datetime::DateTime ServerTimeToLocal(const datetime::DateTime& server_time) const; /// Modifies the argument purchase. void UpdatePurchaseLocalTimeExpiry(Purchase& purchase) const; AuthTokens GetAuthTokens() const; - error::Error SetAuthTokens(const AuthTokens& v, bool is_account); + /// `utf8_username` must be set if `is_account` is true. + error::Error SetAuthTokens(const AuthTokens& v, bool is_account, const std::string& utf8_username); /// valid_token_types is of the form {"tokenvalueABCD0123": true, ...} error::Error CullAuthTokens(const std::map& valid_tokens); + psicash::TokenTypes ValidTokenTypes() const; bool GetIsAccount() const; + /// Note that setting is-account to true does _not_ populate the account username field. error::Error SetIsAccount(bool v); + std::string GetAccountUsername() const; + error::Error SetAccountUsername(const std::string& v); + int64_t GetBalance() const; error::Error SetBalance(int64_t v); @@ -87,7 +119,10 @@ class UserData { error::Error SetPurchasePrices(const PurchasePrices& v); Purchases GetPurchases() const; + /// Does not update LastTransactionID. This must only be called when storing a subset + /// of the already-existing purchases. Also, the vector must still be sorted. error::Error SetPurchases(const Purchases& v); + /// Does update LastTransactionID. error::Error AddPurchase(const Purchase& v); TransactionID GetLastTransactionID() const; @@ -99,11 +134,13 @@ class UserData { if (key.empty()) { return error::MakeCriticalError("Metadata key cannot be empty"); } - auto j = GetRequestMetadata(); - j[key] = val; - return datastore_.Set({{REQUEST_METADATA, j}}); + auto ptr = kRequestMetadataPtr / key; + return datastore_.Set(ptr, val); } + std::string GetLocale() const; + error::Error SetLocale(const std::string& v); + protected: /// Modifies the purchases in the argument. void UpdatePurchasesLocalTimeExpiry(Purchases& purchases) const; diff --git a/userdata_test.cpp b/userdata_test.cpp index 6e1f6e2..f71142a 100644 --- a/userdata_test.cpp +++ b/userdata_test.cpp @@ -1,4 +1,5 @@ #include "gtest/gtest.h" +#include "gmock/gmock.h" #include "test_helpers.hpp" #include "userdata.hpp" #include "vendor/nlohmann/json.hpp" @@ -7,6 +8,7 @@ using json = nlohmann::json; using namespace std; using namespace nonstd; using namespace psicash; +using namespace testing; constexpr auto dev = true; @@ -31,17 +33,61 @@ TEST_F(TestUserData, InitFail) ASSERT_TRUE(err); } +TEST_F(TestUserData, InitUpgrade) +{ + auto dsDir = GetTempDir(); + + // Write a v1 file. + auto ok = TempDir::Write(dsDir, dev, R"({"IsAccount":false,"authTokens":{"earner":"earnertoken","indicator":"indicatortoken","spender":"spendertoken"},"balance":125000000000,"lastTransactionID":"boosttransid","purchasePrices":[{"class":"speed-boost","distinguisher":"1hr","price":100000000000},{"class":"speed-boost","distinguisher":"2hr","price":200000000000},{"class":"speed-boost","distinguisher":"3hr","price":300000000000},{"class":"speed-boost","distinguisher":"4hr","price":400000000000},{"class":"speed-boost","distinguisher":"5hr","price":500000000000},{"class":"speed-boost","distinguisher":"6hr","price":600000000000},{"class":"speed-boost","distinguisher":"7hr","price":700000000000},{"class":"speed-boost","distinguisher":"8hr","price":800000000000},{"class":"speed-boost","distinguisher":"9hr","price":900000000000},{"class":"speed-boost","distinguisher":"24hr","price":800000000000}],"purchases":[{"authorization":{"AccessType":"speed-boost-test","Encoded":"boostauth","Expires":"2020-07-27T15:14:30.986Z","ID":"boostauthid"},"class":"speed-boost","distinguisher":"1hr","id":"boosttransid","localTimeExpiry":"2020-07-27T15:14:32.878Z","serverTimeExpiry":"2020-07-27T15:14:30.986Z"}],"requestMetadata":{"client_region":"CA","client_version":"999","propagation_channel_id":"ABCD1234","sponsor_id":"ABCD1234"},"serverTimeDiff":-2584,"v":1})"); + ASSERT_TRUE(ok) << errno; + + // Now load the file and upgrade + UserData ud; + auto err = ud.Init(dsDir.c_str(), dev); + ASSERT_FALSE(err); + + ASSERT_GT(ud.GetInstanceID().length(), 0); + ASSERT_FALSE(ud.GetIsLoggedOutAccount()); + ASSERT_EQ(ud.GetServerTimeDiff().count(), -2584); + AuthTokens want_tokens = {{"earner",{"earnertoken",nullopt}},{"indicator",{"indicatortoken",nullopt}},{"spender",{"spendertoken",nullopt}}}; + ASSERT_TRUE(AuthTokenSetsEqual(ud.GetAuthTokens(), want_tokens)); + ASSERT_FALSE(ud.GetIsAccount()); + ASSERT_EQ(ud.GetAccountUsername(), ""); + ASSERT_EQ(ud.GetBalance(), 125000000000L); + ASSERT_EQ(ud.GetPurchasePrices().size(), 10); + ASSERT_EQ(ud.GetPurchases().size(), 1); + ASSERT_EQ(ud.GetLastTransactionID(), "boosttransid"); + ASSERT_EQ(ud.GetRequestMetadata().size(), 4); +} + +TEST_F(TestUserData, InitBadVersion) +{ + auto dsDir = GetTempDir(); + + // Write a datastore file with a bad (too future) version: 999999. + auto ok = TempDir::Write(dsDir, dev, R"({"IsAccount":false,"authTokens":{"earner":"earnertoken","indicator":"indicatortoken","spender":"spendertoken"},"balance":125000000000,"lastTransactionID":"boosttransid","purchasePrices":[{"class":"speed-boost","distinguisher":"1hr","price":100000000000},{"class":"speed-boost","distinguisher":"2hr","price":200000000000},{"class":"speed-boost","distinguisher":"3hr","price":300000000000},{"class":"speed-boost","distinguisher":"4hr","price":400000000000},{"class":"speed-boost","distinguisher":"5hr","price":500000000000},{"class":"speed-boost","distinguisher":"6hr","price":600000000000},{"class":"speed-boost","distinguisher":"7hr","price":700000000000},{"class":"speed-boost","distinguisher":"8hr","price":800000000000},{"class":"speed-boost","distinguisher":"9hr","price":900000000000},{"class":"speed-boost","distinguisher":"24hr","price":800000000000}],"purchases":[{"authorization":{"AccessType":"speed-boost-test","Encoded":"boostauth","Expires":"2020-07-27T15:14:30.986Z","ID":"boostauthid"},"class":"speed-boost","distinguisher":"1hr","id":"boosttransid","localTimeExpiry":"2020-07-27T15:14:32.878Z","serverTimeExpiry":"2020-07-27T15:14:30.986Z","serverTimeCreated":"2020-07-27T15:14:30.986Z"}],"requestMetadata":{"client_region":"CA","client_version":"999","propagation_channel_id":"ABCD1234","sponsor_id":"ABCD1234"},"serverTimeDiff":-2584,"v":999999})"); + ASSERT_TRUE(ok) << errno; + + // Now load the file and upgrade + UserData ud; + auto err = ud.Init(dsDir.c_str(), dev); + ASSERT_TRUE(err); +} + TEST_F(TestUserData, Persistence) { auto want_server_time_diff_ms = 54321; auto want_server_time_diff = datetime::Duration(want_server_time_diff_ms); - AuthTokens want_auth_tokens = {{"k1", "v1"}, {"k2", "v2"}}; + auto future = datetime::DateTime::Now().Add(datetime::Duration(want_server_time_diff_ms*2)); // if this is less than want_server_time_diff_ms, the tokens will be expired already + auto past = datetime::DateTime::Now().Sub(datetime::Duration(want_server_time_diff_ms*2)); + AuthTokens want_auth_tokens = {{"k1", {"v1", nullopt}}, {"k2", {"v2", future}}, {"k3", {"v3", past}}}; bool want_is_account = true; + string want_account_username = "account-username"s; int64_t want_balance = 12345; PurchasePrices want_purchase_prices = {{"tc1", "d1", 123}, {"tc2", "d2", 321}}; Purchases want_purchases = { - {"id1", "tc1", "d1", nullopt, nullopt, nullopt}, - {"id2", "tc2", "d2", nullopt, nullopt, nullopt}}; + {"id1", datetime::DateTime(), "tc1", "d1", nullopt, nullopt, nullopt}, + {"id2", datetime::DateTime(), "tc2", "d2", nullopt, nullopt, nullopt}}; string req_metadata_key = "req_metadata_key"; string want_req_metadata_value = "want_req_metadata_value"; @@ -57,7 +103,7 @@ TEST_F(TestUserData, Persistence) err = ud.SetServerTimeDiff(shifted_now); ASSERT_FALSE(err); - err = ud.SetAuthTokens(want_auth_tokens, want_is_account); + err = ud.SetAuthTokens(want_auth_tokens, want_is_account, want_account_username); ASSERT_FALSE(err); err = ud.SetBalance(want_balance); @@ -83,11 +129,14 @@ TEST_F(TestUserData, Persistence) ASSERT_NEAR(want_server_time_diff.count(), got_server_time_diff.count(), 10); auto got_auth_tokens = ud.GetAuthTokens(); - ASSERT_EQ(got_auth_tokens, want_auth_tokens); + ASSERT_TRUE(AuthTokenSetsEqual(got_auth_tokens, want_auth_tokens)); auto got_is_account = ud.GetIsAccount(); ASSERT_EQ(got_is_account, want_is_account); + auto got_account_username = ud.GetAccountUsername(); + ASSERT_EQ(got_account_username, want_account_username); + auto got_balance = ud.GetBalance(); ASSERT_EQ(got_balance, want_balance); @@ -102,6 +151,55 @@ TEST_F(TestUserData, Persistence) } } +TEST_F(TestUserData, GetInstanceID) +{ + UserData ud; + auto err = ud.Init(GetTempDir().c_str(), dev); + ASSERT_FALSE(err); + + string prefix = "instanceid_"; + + auto v1 = ud.GetInstanceID(); + ASSERT_EQ(v1.length(), prefix.length() + 48); + ASSERT_EQ(v1.substr(0, prefix.length()), prefix); + + auto v1again = ud.GetInstanceID(); + ASSERT_EQ(v1, v1again); + + // Clear and expect to get a different ID + + ud.Clear(); + + auto v2 = ud.GetInstanceID(); + ASSERT_EQ(v2.length(), prefix.length() + 48); + ASSERT_EQ(v2.substr(0, prefix.length()), prefix); + ASSERT_NE(v1, v2); +} + +TEST_F(TestUserData, IsLoggedOutAccount) +{ + UserData ud; + auto err = ud.Init(GetTempDir().c_str(), dev); + ASSERT_FALSE(err); + + // Check default value + auto v = ud.GetIsLoggedOutAccount(); + ASSERT_EQ(v, false); + + // Set then get + bool want = true; + err = ud.SetIsLoggedOutAccount(want); + ASSERT_FALSE(err); + auto got = ud.GetIsLoggedOutAccount(); + ASSERT_EQ(got, want); + + want = false; + err = ud.SetIsLoggedOutAccount(want); + ASSERT_FALSE(err); + got = ud.GetIsLoggedOutAccount(); + ASSERT_EQ(got, want); +} + TEST_F(TestUserData, ServerTimeDiff) { UserData ud; @@ -129,6 +227,7 @@ TEST_F(TestUserData, UpdatePurchaseLocalTimeExpiry) Purchase purchase_noexpiry{ .id = "id", + .server_time_created = datetime::DateTime(), .transaction_class = "tc", .distinguisher = "d" }; @@ -136,6 +235,7 @@ TEST_F(TestUserData, UpdatePurchaseLocalTimeExpiry) ASSERT_TRUE(server_expiry.FromISO8601("2031-02-03T04:05:06.789Z")); Purchase purchase_expiry{ .id = "id", + .server_time_created = datetime::DateTime(), .transaction_class = "tc", .distinguisher = "d", .server_time_expiry = server_expiry @@ -172,31 +272,98 @@ TEST_F(TestUserData, AuthTokens) auto is_account = ud.GetIsAccount(); ASSERT_EQ(is_account, false); - // Set then get - AuthTokens want = {{"k1", "v1"}, {"k2", "v2"}}; - err = ud.SetAuthTokens(want, false); + auto past = datetime::DateTime::Now().Sub(datetime::Duration(10000)); + auto future = datetime::DateTime::Now().Add(datetime::Duration(10000)); + + AuthTokens want = {{"k1", {"v1", future}}, {"k2", {"v2", past}}}; + err = ud.SetAuthTokens(want, false, ""); ASSERT_FALSE(err); auto got_tokens = ud.GetAuthTokens(); - ASSERT_EQ(want, got_tokens); + ASSERT_TRUE(AuthTokenSetsEqual(want, got_tokens)); + is_account = ud.GetIsAccount(); ASSERT_EQ(is_account, false); + ASSERT_EQ(ud.GetAccountUsername(), ""); - err = ud.SetAuthTokens(want, true); + err = ud.SetAuthTokens(want, true, "tokens-username"); ASSERT_FALSE(err); got_tokens = ud.GetAuthTokens(); - ASSERT_EQ(want, got_tokens); + ASSERT_TRUE(AuthTokenSetsEqual(want, got_tokens)); is_account = ud.GetIsAccount(); ASSERT_EQ(is_account, true); + ASSERT_EQ(ud.GetAccountUsername(), "tokens-username"); +} + +TEST_F(TestUserData, CullAuthTokens) +{ + UserData ud; + auto err = ud.Init(GetTempDir().c_str(), dev); + ASSERT_FALSE(err); + + AuthTokens auth_tokens = {{"k1",{"v1"}},{"k2",{"v2"}},{"k3",{"v3"}},{"k4",{"v4"}},}; - // CullAuthTokens - err = ud.SetAuthTokens({{"k1","v1"},{"k2","v2"},{"k3","v3"},{"k4","v4"},}, false); + // All good + err = ud.SetAuthTokens(auth_tokens, false, ""); ASSERT_FALSE(err); - std::map valid_tokens = {{"v1",true},{"v2",false},{"v3",true}}; - want = {{"k1","v1"},{"k3","v3"}}; + std::map valid_tokens = {{"v1",true},{"v2",true},{"v3",true},{"v4",true}}; err = ud.CullAuthTokens(valid_tokens); ASSERT_FALSE(err); - got_tokens = ud.GetAuthTokens(); - ASSERT_EQ(want, got_tokens); + ASSERT_TRUE(AuthTokenSetsEqual(auth_tokens, ud.GetAuthTokens())); + + // All present, one invalid + err = ud.SetAuthTokens(auth_tokens, false, ""); + ASSERT_FALSE(err); + valid_tokens = {{"v1",true},{"v2",false},{"v3",true},{"v4",true}}; + err = ud.CullAuthTokens(valid_tokens); + ASSERT_FALSE(err); + ASSERT_THAT(ud.GetAuthTokens(), IsEmpty()); + + // All present, all invalid + err = ud.SetAuthTokens(auth_tokens, false, ""); + ASSERT_FALSE(err); + valid_tokens = {{"v1",false},{"v2",false},{"v3",false},{"v4",false}}; + err = ud.CullAuthTokens(valid_tokens); + ASSERT_FALSE(err); + ASSERT_THAT(ud.GetAuthTokens(), IsEmpty()); + + // All valid, one missing + err = ud.SetAuthTokens(auth_tokens, false, ""); + ASSERT_FALSE(err); + valid_tokens = {{"v1",true},{"v3",true},{"v4",true}}; + err = ud.CullAuthTokens(valid_tokens); + ASSERT_FALSE(err); + ASSERT_THAT(ud.GetAuthTokens(), IsEmpty()); + + // All missing + err = ud.SetAuthTokens(auth_tokens, false, ""); + ASSERT_FALSE(err); + valid_tokens = {}; + err = ud.CullAuthTokens(valid_tokens); + ASSERT_FALSE(err); + ASSERT_THAT(ud.GetAuthTokens(), IsEmpty()); +} + +TEST_F(TestUserData, ValidTokenTypes) { + UserData ud; + auto err = ud.Init(GetTempDir().c_str(), dev); + ASSERT_FALSE(err); + + ASSERT_EQ(ud.ValidTokenTypes().size(), 0); + + AuthTokens at = {{"a", {"a"}}, {"b", {"b"}}, {"c", {"c"}}}; + err = ud.SetAuthTokens(at, false, ""); + auto vtt = ud.ValidTokenTypes(); + ASSERT_EQ(vtt.size(), 3); + for (const auto& k : vtt) { + ASSERT_EQ(at.count(k), 1); + at.erase(k); + } + ASSERT_EQ(at.size(), 0); // we should have erased all items + + AuthTokens empty; + err = ud.SetAuthTokens(empty, false, ""); + vtt = ud.ValidTokenTypes(); + ASSERT_EQ(vtt.size(), 0); } TEST_F(TestUserData, IsAccount) @@ -217,6 +384,39 @@ TEST_F(TestUserData, IsAccount) ASSERT_EQ(got, want); } +TEST_F(TestUserData, AccountUsername) +{ + UserData ud; + auto err = ud.Init(GetTempDir().c_str(), dev); + ASSERT_FALSE(err); + + // Check default value + auto v = ud.GetAccountUsername(); + ASSERT_EQ(v, ""); + + // Set then get + string want = "account-username"; + err = ud.SetAccountUsername(want); + ASSERT_FALSE(err); + auto got = ud.GetAccountUsername(); + ASSERT_EQ(got, want); + + // Set via SetAuthTokens + want = "account-username"; + auto future = datetime::DateTime::Now().Add(datetime::Duration(10000)); + AuthTokens at = {{"a", {"a", future}}, {"b", {"b", future}}, {"c", {"c", future}}}; + err = ud.SetAuthTokens(at, true, want); + got = ud.GetAccountUsername(); + ASSERT_EQ(got, want); + + // With tracker tokens -- so no username + want = ""; + at = {{"a", {"a"}}, {"b", {"b"}}, {"c", {"c"}}}; + err = ud.SetAuthTokens(at, true, want); + got = ud.GetAccountUsername(); + ASSERT_EQ(got, want); +} + TEST_F(TestUserData, Balance) { UserData ud; @@ -266,12 +466,14 @@ TEST_F(TestUserData, Purchases) // Set then get auto dt1 = datetime::DateTime::Now().Add(datetime::Duration(1)); auto dt2 = datetime::DateTime::Now().Add(datetime::Duration(2)); + auto created1 = datetime::DateTime::Now().Sub(datetime::Duration(3)); + auto created2 = datetime::DateTime::Now().Sub(datetime::Duration(4)); auto auth_res1 = psicash::DecodeAuthorization("eyJBdXRob3JpemF0aW9uIjp7IklEIjoibFRSWnBXK1d3TFJqYkpzOGxBUFVaQS8zWnhmcGdwNDFQY0dkdlI5a0RVST0iLCJBY2Nlc3NUeXBlIjoic3BlZWQtYm9vc3QtdGVzdCIsIkV4cGlyZXMiOiIyMDE5LTAxLTE0VDIxOjQ2OjMwLjcxNzI2NTkyNFoifSwiU2lnbmluZ0tleUlEIjoiUUNZTzV2clIvZGhjRDZ6M2FMQlVNeWRuZlJyZFNRL1RWYW1IUFhYeTd0TT0iLCJTaWduYXR1cmUiOiJtV1Z5Tm9ZU0pFRDNXU3I3bG1OeEtReEZza1M5ZWlXWG1lcDVvVWZBSHkwVmYrSjZaQW9WajZrN3ZVTDNrakIreHZQSTZyaVhQc3FzWENRNkx0eFdBQT09In0="); ASSERT_TRUE(auth_res1); Purchases want = { - {"id1", "tc1", "d1", dt1, dt2, *auth_res1}, - {"id2", "tc2", "d2", nullopt, nullopt, nullopt}}; + {"id1", created1, "tc1", "d1", dt1, dt2, *auth_res1}, + {"id2", created2, "tc2", "d2", nullopt, nullopt, nullopt}}; err = ud.SetPurchases(want); ASSERT_FALSE(err); @@ -285,7 +487,7 @@ TEST_F(TestUserData, Purchases) err = ud.SetServerTimeDiff(server_now); ASSERT_FALSE(err); // Supply server time but not local time - want.push_back({"id3", "tc3", "d3", server_now, nullopt, nullopt}); + want.push_back({"id3", created1, "tc3", "d3", server_now, nullopt, nullopt}); err = ud.SetPurchases(want); got = ud.GetPurchases(); ASSERT_EQ(got.size(), 3); @@ -295,6 +497,8 @@ TEST_F(TestUserData, Purchases) TEST_F(TestUserData, AddPurchase) { + // This also tests Get/SetLastTransactionID (as Set isn't public) + UserData ud; auto err = ud.Init(GetTempDir().c_str(), dev); ASSERT_FALSE(err); @@ -303,47 +507,48 @@ TEST_F(TestUserData, AddPurchase) auto v = ud.GetPurchases(); ASSERT_EQ(v.size(), 0); - // Set then get - Purchases want = { - {"id1", "tc1", "d1", nullopt, nullopt, nullopt}, - {"id2", "tc2", "d2", nullopt, nullopt, nullopt}}; + // We need to check the behaviour for duplicates and out-of-chrono-order addition - err = ud.SetPurchases(want); + Purchases final_want = { + {"id0", datetime::DateTime::Now().Sub(datetime::Duration(6)), "tc0", "d0", nullopt, nullopt, nullopt}, + {"id1", datetime::DateTime::Now().Sub(datetime::Duration(5)), "tc1", "d1", nullopt, nullopt, nullopt}, + {"id2", datetime::DateTime::Now().Sub(datetime::Duration(4)), "tc2", "d2", nullopt, nullopt, nullopt}, + {"id3", datetime::DateTime::Now().Sub(datetime::Duration(3)), "tc3", "d3", nullopt, nullopt, nullopt}}; + + // Start with a subset + Purchases want = {final_want[0], final_want[2]}; + err = ud.AddPurchase(want[0]); + ASSERT_FALSE(err); + err = ud.AddPurchase(want[1]); ASSERT_FALSE(err); auto got = ud.GetPurchases(); ASSERT_EQ(got, want); + ASSERT_EQ(ud.GetLastTransactionID(), "id2"); - Purchase add = {"id3", "tc3", "d3", nullopt, nullopt, nullopt}; - err = ud.AddPurchase(add); + // Add a later purchase + want = {final_want[0], final_want[2], final_want[3]}; + err = ud.AddPurchase(final_want[3]); ASSERT_FALSE(err); got = ud.GetPurchases(); - want.push_back(add); ASSERT_EQ(got, want); ASSERT_EQ(ud.GetLastTransactionID(), "id3"); - // Try to add the same purchase again - err = ud.AddPurchase(add); + // Add a purchase in the middle + want = {final_want[0], final_want[1], final_want[2], final_want[3]}; + err = ud.AddPurchase(final_want[1]); ASSERT_FALSE(err); got = ud.GetPurchases(); ASSERT_EQ(got, want); -} + // Even though id1 is not the newest, it was added last and therefore will be the LastTransactionID. See comment in AddPurchase for details. + ASSERT_EQ(ud.GetLastTransactionID(), "id1"); -TEST_F(TestUserData, LastTransactionID) -{ - UserData ud; - auto err = ud.Init(GetTempDir().c_str(), dev); + // Add a duplicate purchase + want = {final_want[0], final_want[1], final_want[2], final_want[3]}; + err = ud.AddPurchase(final_want[2]); ASSERT_FALSE(err); - - // Check default value - auto v = ud.GetLastTransactionID(); - ASSERT_EQ(v, kTransactionIDZero); - - // Set then get - TransactionID want = "LastTransactionID"; - err = ud.SetLastTransactionID(want); - ASSERT_FALSE(err); - auto got = ud.GetLastTransactionID(); + got = ud.GetPurchases(); ASSERT_EQ(got, want); + ASSERT_EQ(ud.GetLastTransactionID(), "id2"); } TEST_F(TestUserData, Metadata) @@ -384,3 +589,23 @@ TEST_F(TestUserData, Metadata) v = ud.GetRequestMetadata(); ASSERT_TRUE(v["k"].is_null()); } + +TEST_F(TestUserData, Locale) +{ + UserData ud; + auto err = ud.Init(GetTempDir().c_str(), dev); + ASSERT_FALSE(err); + + auto v = ud.GetLocale(); + ASSERT_THAT(v, IsEmpty()); + + err = ud.SetLocale("en-US"); + ASSERT_FALSE(err); + v = ud.GetLocale(); + ASSERT_EQ(v, "en-US"); + + err = ud.SetLocale(""); + ASSERT_FALSE(err); + v = ud.GetLocale(); + ASSERT_EQ(v, ""); +} diff --git a/utils.cpp b/utils.cpp index 1ce51e1..ffe1e90 100644 --- a/utils.cpp +++ b/utils.cpp @@ -17,7 +17,12 @@ * */ +#include +#include +#include +#include #include "utils.hpp" +#include "error.hpp" #ifdef _WIN32 #include @@ -26,6 +31,9 @@ #include #endif +using namespace std; +using namespace psicash; + namespace utils { // From https://stackoverflow.com/a/33486052 @@ -34,4 +42,81 @@ bool FileExists(const std::string& filename) return access(filename.c_str(), 0) == 0; } +static const std::vector kRandomStringChars = + { + '0','1','2','3','4', + '5','6','7','8','9', + 'A','B','C','D','E','F', + 'G','H','I','J','K', + 'L','M','N','O','P', + 'Q','R','S','T','U', + 'V','W','X','Y','Z', + 'a','b','c','d','e','f', + 'g','h','i','j','k', + 'l','m','n','o','p', + 'q','r','s','t','u', + 'v','w','x','y','z' + }; + +// From https://stackoverflow.com/a/24586587 +std::string RandomID() { + static auto& chrs = "0123456789" + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + // A length of 48 and a space of 62 characters gives us: + // log(62^48) / log(2) = 285 bits of entropy + std::string::size_type length = 48; + + thread_local static std::mt19937 rg{std::random_device{}()}; + thread_local static std::uniform_int_distribution pick(0, sizeof(chrs) - 2); + + std::string s; + + s.reserve(length); + + while (length--) { + s += chrs[pick(rg)]; + } + + return s; +} + +// Note that this _only_ works for plain ASCII strings. +static string ToLowerASCII(const string& s) { + ostringstream ss; + for (auto c : s) { + ss << tolower(c); + } + return ss.str(); +} + +string FindHeaderValue(const map>& headers, const string& key) { + auto lower_key = ToLowerASCII(key); + for (const auto& entry : headers) { + if (lower_key == ToLowerASCII(entry.first)) { + return entry.second.empty() ? "" : entry.second.front(); + } + } + return ""; +} + +// Adapted from https://stackoverflow.com/a/22986486/729729 +error::Error FileSize(const string& path, uint64_t& o_size) { + o_size = 0; + + ifstream f; + f.open(path, ios::in | ios::binary); + if (!f) { + return error::MakeCriticalError(utils::Stringer("file open failed; errno=", errno)); + } + + f.ignore(numeric_limits::max()); + auto length = f.gcount(); + f.close(); + + o_size = (uint64_t)length; + return error::nullerr; +} + } diff --git a/utils.hpp b/utils.hpp index f2df370..59653aa 100644 --- a/utils.hpp +++ b/utils.hpp @@ -21,13 +21,17 @@ #define PSICASHLIB_UTILS_H #include +#include +#include #include +#include +#include "error.hpp" namespace utils { // From https://stackoverflow.com/a/25386444/729729 -// Can be used like `s = Stringer("lucky ", 42, '!'); +/// Can be used like `s = Stringer("lucky ", 42, '!'); template std::string Stringer(const T& value) { std::ostringstream oss; @@ -51,6 +55,51 @@ std::string Stringer(const T& value, const Args& ... args) { /// Tests if the given filepath+name exists. bool FileExists(const std::string& filename); +/// Gets the size of the file at the given path. +psicash::error::Error FileSize(const std::string& path, uint64_t& o_size); + +/// Generates a large random ID. +std::string RandomID(); + +/// Finds the value of the header with the given key in `headers` (case-insensitive). +/// Returns the value if found, or empty string if not found. +/// If there are multiple header values for the key, the first one is returned. +std::string FindHeaderValue(const std::map>& headers, const std::string& key); + +// From https://stackoverflow.com/a/5289170/729729 +/// note: delimiter cannot contain NUL characters +template +std::string Join(Range const& elements, const char *const delimiter) { + std::ostringstream os; + auto b = begin(elements), e = end(elements); + + if (b != e) { + std::copy(b, prev(e), std::ostream_iterator(os, delimiter)); + b = prev(e); + } + if (b != e) { + os << *b; + } + + return os.str(); +} + +// From https://stackoverflow.com/a/5289170/729729 +/// note: imput is assumed to not contain NUL characters +template +void Split(char delimiter, Output &output, Input const& input) { + using namespace std; + for (auto cur = begin(input), beg = cur; ; ++cur) { + if (cur == end(input) || *cur == delimiter || !*cur) { + output.insert(output.end(), Value(beg, cur)); + if (cur == end(input) || !*cur) + break; + else + beg = next(cur); + } + } +} + } #endif //PSICASHLIB_UTILS_H diff --git a/utils_test.cpp b/utils_test.cpp index eca8a65..763b33e 100644 --- a/utils_test.cpp +++ b/utils_test.cpp @@ -5,14 +5,35 @@ using namespace std; using namespace utils; TEST(TestStringer, SingleValue) { - auto s = Stringer("s"); - ASSERT_EQ(s, "s"); + auto s = Stringer("s"); + ASSERT_EQ(s, "s"); - s = Stringer(123); - ASSERT_EQ(s, "123"); + s = Stringer(123); + ASSERT_EQ(s, "123"); } TEST(TestStringer, MultiValue) { - auto s = Stringer("one", 2, "three", 4, '5', '!'); - ASSERT_EQ(s, "one2three45!"); + auto s = Stringer("one", 2, "three", 4, '5', '!'); + ASSERT_EQ(s, "one2three45!"); +} + +TEST(TestRandomID, Simple) { + auto s = RandomID(); + ASSERT_EQ(s.length(), 48); +} + +TEST(TestFindHeaderValue, Simple) { + map> headers; + + headers = {{"a", {"xyz"}}, {"Date", {"expected", "second"}}, {"c", {"abc", "def"}}}; + auto s = FindHeaderValue(headers, "Date"); + ASSERT_EQ(s, "expected"); + + headers = {{"date", {"expected", "second"}}, {"a", {"xyz"}}, {"c", {"abc", "def"}}}; + s = FindHeaderValue(headers, "Date"); + ASSERT_EQ(s, "expected"); + + headers = {{"a", {"xyz"}}, {"c", {"abc", "def"}}, {"DATE", {"expected", "second"}}}; + s = FindHeaderValue(headers, "Date"); + ASSERT_EQ(s, "expected"); } diff --git a/vendor/httplib.h b/vendor/httplib.h new file mode 100644 index 0000000..f8e2c12 --- /dev/null +++ b/vendor/httplib.h @@ -0,0 +1,7834 @@ +// PSIPHON: USED ONLY FOR TESTING + +// +// httplib.h +// +// Copyright (c) 2021 Yuji Hirose. All rights reserved. +// MIT License +// + +#ifndef CPPHTTPLIB_HTTPLIB_H +#define CPPHTTPLIB_HTTPLIB_H + +/* + * Configuration + */ + +#ifndef CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND +#define CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND 5 +#endif + +#ifndef CPPHTTPLIB_KEEPALIVE_MAX_COUNT +#define CPPHTTPLIB_KEEPALIVE_MAX_COUNT 5 +#endif + +#ifndef CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND +#define CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND 300 +#endif + +#ifndef CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND +#define CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND 0 +#endif + +#ifndef CPPHTTPLIB_READ_TIMEOUT_SECOND +#define CPPHTTPLIB_READ_TIMEOUT_SECOND 5 +#endif + +#ifndef CPPHTTPLIB_READ_TIMEOUT_USECOND +#define CPPHTTPLIB_READ_TIMEOUT_USECOND 0 +#endif + +#ifndef CPPHTTPLIB_WRITE_TIMEOUT_SECOND +#define CPPHTTPLIB_WRITE_TIMEOUT_SECOND 5 +#endif + +#ifndef CPPHTTPLIB_WRITE_TIMEOUT_USECOND +#define CPPHTTPLIB_WRITE_TIMEOUT_USECOND 0 +#endif + +#ifndef CPPHTTPLIB_IDLE_INTERVAL_SECOND +#define CPPHTTPLIB_IDLE_INTERVAL_SECOND 0 +#endif + +#ifndef CPPHTTPLIB_IDLE_INTERVAL_USECOND +#ifdef _WIN32 +#define CPPHTTPLIB_IDLE_INTERVAL_USECOND 10000 +#else +#define CPPHTTPLIB_IDLE_INTERVAL_USECOND 0 +#endif +#endif + +#ifndef CPPHTTPLIB_REQUEST_URI_MAX_LENGTH +#define CPPHTTPLIB_REQUEST_URI_MAX_LENGTH 8192 +#endif + +#ifndef CPPHTTPLIB_REDIRECT_MAX_COUNT +#define CPPHTTPLIB_REDIRECT_MAX_COUNT 20 +#endif + +#ifndef CPPHTTPLIB_PAYLOAD_MAX_LENGTH +#define CPPHTTPLIB_PAYLOAD_MAX_LENGTH ((std::numeric_limits::max)()) +#endif + +#ifndef CPPHTTPLIB_TCP_NODELAY +#define CPPHTTPLIB_TCP_NODELAY false +#endif + +#ifndef CPPHTTPLIB_RECV_BUFSIZ +#define CPPHTTPLIB_RECV_BUFSIZ size_t(4096u) +#endif + +#ifndef CPPHTTPLIB_COMPRESSION_BUFSIZ +#define CPPHTTPLIB_COMPRESSION_BUFSIZ size_t(16384u) +#endif + +#ifndef CPPHTTPLIB_THREAD_POOL_COUNT +#define CPPHTTPLIB_THREAD_POOL_COUNT \ + ((std::max)(8u, std::thread::hardware_concurrency() > 0 \ + ? std::thread::hardware_concurrency() - 1 \ + : 0)) +#endif + +#ifndef CPPHTTPLIB_RECV_FLAGS +#define CPPHTTPLIB_RECV_FLAGS 0 +#endif + +#ifndef CPPHTTPLIB_SEND_FLAGS +#define CPPHTTPLIB_SEND_FLAGS 0 +#endif + +/* + * Headers + */ + +#ifdef _WIN32 +#ifndef _CRT_SECURE_NO_WARNINGS +#define _CRT_SECURE_NO_WARNINGS +#endif //_CRT_SECURE_NO_WARNINGS + +#ifndef _CRT_NONSTDC_NO_DEPRECATE +#define _CRT_NONSTDC_NO_DEPRECATE +#endif //_CRT_NONSTDC_NO_DEPRECATE + +#if defined(_MSC_VER) +#ifdef _WIN64 +using ssize_t = __int64; +#else +using ssize_t = int; +#endif + +#if _MSC_VER < 1900 +#define snprintf _snprintf_s +#endif +#endif // _MSC_VER + +#ifndef S_ISREG +#define S_ISREG(m) (((m)&S_IFREG) == S_IFREG) +#endif // S_ISREG + +#ifndef S_ISDIR +#define S_ISDIR(m) (((m)&S_IFDIR) == S_IFDIR) +#endif // S_ISDIR + +#ifndef NOMINMAX +#define NOMINMAX +#endif // NOMINMAX + +#include +#include + +#include +#include + +#ifndef WSA_FLAG_NO_HANDLE_INHERIT +#define WSA_FLAG_NO_HANDLE_INHERIT 0x80 +#endif + +#ifdef _MSC_VER +#pragma comment(lib, "ws2_32.lib") +#pragma comment(lib, "crypt32.lib") +#pragma comment(lib, "cryptui.lib") +#endif + +#ifndef strcasecmp +#define strcasecmp _stricmp +#endif // strcasecmp + +using socket_t = SOCKET; +#ifdef CPPHTTPLIB_USE_POLL +#define poll(fds, nfds, timeout) WSAPoll(fds, nfds, timeout) +#endif + +#else // not _WIN32 + +#include +#include +#include +#include +#include +#ifdef __linux__ +#include +#endif +#include +#ifdef CPPHTTPLIB_USE_POLL +#include +#endif +#include +#include +#include +#include +#include + +using socket_t = int; +#define INVALID_SOCKET (-1) +#endif //_WIN32 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#include +#include +#include +#include + +#if defined(_WIN32) && defined(OPENSSL_USE_APPLINK) +#include +#endif + +#include +#include + +#if OPENSSL_VERSION_NUMBER < 0x1010100fL +#error Sorry, OpenSSL versions prior to 1.1.1 are not supported +#endif + +#if OPENSSL_VERSION_NUMBER < 0x10100000L +#include +inline const unsigned char *ASN1_STRING_get0_data(const ASN1_STRING *asn1) { + return M_ASN1_STRING_data(asn1); +} +#endif +#endif + +#ifdef CPPHTTPLIB_ZLIB_SUPPORT +#include +#endif + +#ifdef CPPHTTPLIB_BROTLI_SUPPORT +#include +#include +#endif + +/* + * Declaration + */ +namespace httplib { + +namespace detail { + +/* + * Backport std::make_unique from C++14. + * + * NOTE: This code came up with the following stackoverflow post: + * https://stackoverflow.com/questions/10149840/c-arrays-and-make-unique + * + */ + +template +typename std::enable_if::value, std::unique_ptr>::type +make_unique(Args &&... args) { + return std::unique_ptr(new T(std::forward(args)...)); +} + +template +typename std::enable_if::value, std::unique_ptr>::type +make_unique(std::size_t n) { + typedef typename std::remove_extent::type RT; + return std::unique_ptr(new RT[n]); +} + +struct ci { + bool operator()(const std::string &s1, const std::string &s2) const { + return std::lexicographical_compare(s1.begin(), s1.end(), s2.begin(), + s2.end(), + [](unsigned char c1, unsigned char c2) { + return ::tolower(c1) < ::tolower(c2); + }); + } +}; + +} // namespace detail + +using Headers = std::multimap; + +using Params = std::multimap; +using Match = std::smatch; + +using Progress = std::function; + +struct Response; +using ResponseHandler = std::function; + +struct MultipartFormData { + std::string name; + std::string content; + std::string filename; + std::string content_type; +}; +using MultipartFormDataItems = std::vector; +using MultipartFormDataMap = std::multimap; + +class DataSink { +public: + DataSink() : os(&sb_), sb_(*this) {} + + DataSink(const DataSink &) = delete; + DataSink &operator=(const DataSink &) = delete; + DataSink(DataSink &&) = delete; + DataSink &operator=(DataSink &&) = delete; + + std::function write; + std::function done; + std::function is_writable; + std::ostream os; + +private: + class data_sink_streambuf : public std::streambuf { + public: + explicit data_sink_streambuf(DataSink &sink) : sink_(sink) {} + + protected: + std::streamsize xsputn(const char *s, std::streamsize n) { + sink_.write(s, static_cast(n)); + return n; + } + + private: + DataSink &sink_; + }; + + data_sink_streambuf sb_; +}; + +using ContentProvider = + std::function; + +using ContentProviderWithoutLength = + std::function; + +using ContentProviderResourceReleaser = std::function; + +using ContentReceiverWithProgress = + std::function; + +using ContentReceiver = + std::function; + +using MultipartContentHeader = + std::function; + +class ContentReader { +public: + using Reader = std::function; + using MultipartReader = std::function; + + ContentReader(Reader reader, MultipartReader multipart_reader) + : reader_(std::move(reader)), + multipart_reader_(std::move(multipart_reader)) {} + + bool operator()(MultipartContentHeader header, + ContentReceiver receiver) const { + return multipart_reader_(std::move(header), std::move(receiver)); + } + + bool operator()(ContentReceiver receiver) const { + return reader_(std::move(receiver)); + } + + Reader reader_; + MultipartReader multipart_reader_; +}; + +using Range = std::pair; +using Ranges = std::vector; + +struct Request { + std::string method; + std::string path; + Headers headers; + std::string body; + + std::string remote_addr; + int remote_port = -1; + + // for server + std::string version; + std::string target; + Params params; + MultipartFormDataMap files; + Ranges ranges; + Match matches; + + // for client + ResponseHandler response_handler; + ContentReceiverWithProgress content_receiver; + Progress progress; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + const SSL *ssl = nullptr; +#endif + + bool has_header(const char *key) const; + std::string get_header_value(const char *key, size_t id = 0) const; + template + T get_header_value(const char *key, size_t id = 0) const; + size_t get_header_value_count(const char *key) const; + void set_header(const char *key, const char *val); + void set_header(const char *key, const std::string &val); + + bool has_param(const char *key) const; + std::string get_param_value(const char *key, size_t id = 0) const; + size_t get_param_value_count(const char *key) const; + + bool is_multipart_form_data() const; + + bool has_file(const char *key) const; + MultipartFormData get_file_value(const char *key) const; + + // private members... + size_t redirect_count_ = CPPHTTPLIB_REDIRECT_MAX_COUNT; + size_t content_length_ = 0; + ContentProvider content_provider_; + bool is_chunked_content_provider_ = false; + size_t authorization_count_ = 0; +}; + +struct Response { + std::string version; + int status = -1; + std::string reason; + Headers headers; + std::string body; + std::string location; // Redirect location + + bool has_header(const char *key) const; + std::string get_header_value(const char *key, size_t id = 0) const; + template + T get_header_value(const char *key, size_t id = 0) const; + size_t get_header_value_count(const char *key) const; + void set_header(const char *key, const char *val); + void set_header(const char *key, const std::string &val); + + void set_redirect(const char *url, int status = 302); + void set_redirect(const std::string &url, int status = 302); + void set_content(const char *s, size_t n, const char *content_type); + void set_content(const std::string &s, const char *content_type); + + void set_content_provider( + size_t length, const char *content_type, ContentProvider provider, + ContentProviderResourceReleaser resource_releaser = nullptr); + + void set_content_provider( + const char *content_type, ContentProviderWithoutLength provider, + ContentProviderResourceReleaser resource_releaser = nullptr); + + void set_chunked_content_provider( + const char *content_type, ContentProviderWithoutLength provider, + ContentProviderResourceReleaser resource_releaser = nullptr); + + Response() = default; + Response(const Response &) = default; + Response &operator=(const Response &) = default; + Response(Response &&) = default; + Response &operator=(Response &&) = default; + ~Response() { + if (content_provider_resource_releaser_) { + content_provider_resource_releaser_(content_provider_success_); + } + } + + // private members... + size_t content_length_ = 0; + ContentProvider content_provider_; + ContentProviderResourceReleaser content_provider_resource_releaser_; + bool is_chunked_content_provider_ = false; + bool content_provider_success_ = false; +}; + +class Stream { +public: + virtual ~Stream() = default; + + virtual bool is_readable() const = 0; + virtual bool is_writable() const = 0; + + virtual ssize_t read(char *ptr, size_t size) = 0; + virtual ssize_t write(const char *ptr, size_t size) = 0; + virtual void get_remote_ip_and_port(std::string &ip, int &port) const = 0; + virtual socket_t socket() const = 0; + + template + ssize_t write_format(const char *fmt, const Args &... args); + ssize_t write(const char *ptr); + ssize_t write(const std::string &s); +}; + +class TaskQueue { +public: + TaskQueue() = default; + virtual ~TaskQueue() = default; + + virtual void enqueue(std::function fn) = 0; + virtual void shutdown() = 0; + + virtual void on_idle(){}; +}; + +class ThreadPool : public TaskQueue { +public: + explicit ThreadPool(size_t n) : shutdown_(false) { + while (n) { + threads_.emplace_back(worker(*this)); + n--; + } + } + + ThreadPool(const ThreadPool &) = delete; + ~ThreadPool() override = default; + + void enqueue(std::function fn) override { + std::unique_lock lock(mutex_); + jobs_.push_back(std::move(fn)); + cond_.notify_one(); + } + + void shutdown() override { + // Stop all worker threads... + { + std::unique_lock lock(mutex_); + shutdown_ = true; + } + + cond_.notify_all(); + + // Join... + for (auto &t : threads_) { + t.join(); + } + } + +private: + struct worker { + explicit worker(ThreadPool &pool) : pool_(pool) {} + + void operator()() { + for (;;) { + std::function fn; + { + std::unique_lock lock(pool_.mutex_); + + pool_.cond_.wait( + lock, [&] { return !pool_.jobs_.empty() || pool_.shutdown_; }); + + if (pool_.shutdown_ && pool_.jobs_.empty()) { break; } + + fn = pool_.jobs_.front(); + pool_.jobs_.pop_front(); + } + + assert(true == static_cast(fn)); + fn(); + } + } + + ThreadPool &pool_; + }; + friend struct worker; + + std::vector threads_; + std::list> jobs_; + + bool shutdown_; + + std::condition_variable cond_; + std::mutex mutex_; +}; + +using Logger = std::function; + +using SocketOptions = std::function; + +inline void default_socket_options(socket_t sock) { + int yes = 1; +#ifdef _WIN32 + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&yes), + sizeof(yes)); + setsockopt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, + reinterpret_cast(&yes), sizeof(yes)); +#else +#ifdef SO_REUSEPORT + setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, reinterpret_cast(&yes), + sizeof(yes)); +#else + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&yes), + sizeof(yes)); +#endif +#endif +} + +class Server { +public: + using Handler = std::function; + + using ExceptionHandler = + std::function; + + enum class HandlerResponse { + Handled, + Unhandled, + }; + using HandlerWithResponse = + std::function; + + using HandlerWithContentReader = std::function; + + using Expect100ContinueHandler = + std::function; + + Server(); + + virtual ~Server(); + + virtual bool is_valid() const; + + Server &Get(const std::string &pattern, Handler handler); + Server &Post(const std::string &pattern, Handler handler); + Server &Post(const std::string &pattern, HandlerWithContentReader handler); + Server &Put(const std::string &pattern, Handler handler); + Server &Put(const std::string &pattern, HandlerWithContentReader handler); + Server &Patch(const std::string &pattern, Handler handler); + Server &Patch(const std::string &pattern, HandlerWithContentReader handler); + Server &Delete(const std::string &pattern, Handler handler); + Server &Delete(const std::string &pattern, HandlerWithContentReader handler); + Server &Options(const std::string &pattern, Handler handler); + + bool set_base_dir(const std::string &dir, + const std::string &mount_point = nullptr); + bool set_mount_point(const std::string &mount_point, const std::string &dir, + Headers headers = Headers()); + bool remove_mount_point(const std::string &mount_point); + Server &set_file_extension_and_mimetype_mapping(const char *ext, + const char *mime); + Server &set_file_request_handler(Handler handler); + + Server &set_error_handler(HandlerWithResponse handler); + Server &set_error_handler(Handler handler); + Server &set_exception_handler(ExceptionHandler handler); + Server &set_pre_routing_handler(HandlerWithResponse handler); + Server &set_post_routing_handler(Handler handler); + + Server &set_expect_100_continue_handler(Expect100ContinueHandler handler); + Server &set_logger(Logger logger); + + Server &set_address_family(int family); + Server &set_tcp_nodelay(bool on); + Server &set_socket_options(SocketOptions socket_options); + + Server &set_default_headers(Headers headers); + + Server &set_keep_alive_max_count(size_t count); + Server &set_keep_alive_timeout(time_t sec); + + Server &set_read_timeout(time_t sec, time_t usec = 0); + template + Server &set_read_timeout(const std::chrono::duration &duration); + + Server &set_write_timeout(time_t sec, time_t usec = 0); + template + Server &set_write_timeout(const std::chrono::duration &duration); + + Server &set_idle_interval(time_t sec, time_t usec = 0); + template + Server &set_idle_interval(const std::chrono::duration &duration); + + Server &set_payload_max_length(size_t length); + + bool bind_to_port(const char *host, int port, int socket_flags = 0); + int bind_to_any_port(const char *host, int socket_flags = 0); + bool listen_after_bind(); + + bool listen(const char *host, int port, int socket_flags = 0); + + bool is_running() const; + void stop(); + + std::function new_task_queue; + +protected: + bool process_request(Stream &strm, bool close_connection, + bool &connection_closed, + const std::function &setup_request); + + std::atomic svr_sock_; + size_t keep_alive_max_count_ = CPPHTTPLIB_KEEPALIVE_MAX_COUNT; + time_t keep_alive_timeout_sec_ = CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND; + time_t read_timeout_sec_ = CPPHTTPLIB_READ_TIMEOUT_SECOND; + time_t read_timeout_usec_ = CPPHTTPLIB_READ_TIMEOUT_USECOND; + time_t write_timeout_sec_ = CPPHTTPLIB_WRITE_TIMEOUT_SECOND; + time_t write_timeout_usec_ = CPPHTTPLIB_WRITE_TIMEOUT_USECOND; + time_t idle_interval_sec_ = CPPHTTPLIB_IDLE_INTERVAL_SECOND; + time_t idle_interval_usec_ = CPPHTTPLIB_IDLE_INTERVAL_USECOND; + size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH; + +private: + using Handlers = std::vector>; + using HandlersForContentReader = + std::vector>; + + socket_t create_server_socket(const char *host, int port, int socket_flags, + SocketOptions socket_options) const; + int bind_internal(const char *host, int port, int socket_flags); + bool listen_internal(); + + bool routing(Request &req, Response &res, Stream &strm); + bool handle_file_request(const Request &req, Response &res, + bool head = false); + bool dispatch_request(Request &req, Response &res, const Handlers &handlers); + bool + dispatch_request_for_content_reader(Request &req, Response &res, + ContentReader content_reader, + const HandlersForContentReader &handlers); + + bool parse_request_line(const char *s, Request &req); + void apply_ranges(const Request &req, Response &res, + std::string &content_type, std::string &boundary); + bool write_response(Stream &strm, bool close_connection, const Request &req, + Response &res); + bool write_response_with_content(Stream &strm, bool close_connection, + const Request &req, Response &res); + bool write_response_core(Stream &strm, bool close_connection, + const Request &req, Response &res, + bool need_apply_ranges); + bool write_content_with_provider(Stream &strm, const Request &req, + Response &res, const std::string &boundary, + const std::string &content_type); + bool read_content(Stream &strm, Request &req, Response &res); + bool + read_content_with_content_receiver(Stream &strm, Request &req, Response &res, + ContentReceiver receiver, + MultipartContentHeader multipart_header, + ContentReceiver multipart_receiver); + bool read_content_core(Stream &strm, Request &req, Response &res, + ContentReceiver receiver, + MultipartContentHeader mulitpart_header, + ContentReceiver multipart_receiver); + + virtual bool process_and_close_socket(socket_t sock); + + struct MountPointEntry { + std::string mount_point; + std::string base_dir; + Headers headers; + }; + std::vector base_dirs_; + + std::atomic is_running_; + std::map file_extension_and_mimetype_map_; + Handler file_request_handler_; + Handlers get_handlers_; + Handlers post_handlers_; + HandlersForContentReader post_handlers_for_content_reader_; + Handlers put_handlers_; + HandlersForContentReader put_handlers_for_content_reader_; + Handlers patch_handlers_; + HandlersForContentReader patch_handlers_for_content_reader_; + Handlers delete_handlers_; + HandlersForContentReader delete_handlers_for_content_reader_; + Handlers options_handlers_; + HandlerWithResponse error_handler_; + ExceptionHandler exception_handler_; + HandlerWithResponse pre_routing_handler_; + Handler post_routing_handler_; + Logger logger_; + Expect100ContinueHandler expect_100_continue_handler_; + + int address_family_ = AF_UNSPEC; + bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY; + SocketOptions socket_options_ = default_socket_options; + + Headers default_headers_; +}; + +enum class Error { + Success = 0, + Unknown, + Connection, + BindIPAddress, + Read, + Write, + ExceedRedirectCount, + Canceled, + SSLConnection, + SSLLoadingCerts, + SSLServerVerification, + UnsupportedMultipartBoundaryChars, + Compression, +}; + +inline std::string to_string(const Error error) { + switch (error) { + case Error::Success: return "Success"; + case Error::Connection: return "Connection"; + case Error::BindIPAddress: return "BindIPAddress"; + case Error::Read: return "Read"; + case Error::Write: return "Write"; + case Error::ExceedRedirectCount: return "ExceedRedirectCount"; + case Error::Canceled: return "Canceled"; + case Error::SSLConnection: return "SSLConnection"; + case Error::SSLLoadingCerts: return "SSLLoadingCerts"; + case Error::SSLServerVerification: return "SSLServerVerification"; + case Error::UnsupportedMultipartBoundaryChars: + return "UnsupportedMultipartBoundaryChars"; + case Error::Compression: return "Compression"; + case Error::Unknown: return "Unknown"; + default: break; + } + + return "Invalid"; +} + +inline std::ostream &operator<<(std::ostream &os, const Error &obj) { + os << to_string(obj); + os << " (" << static_cast::type>(obj) << ')'; + return os; +} + +class Result { +public: + Result(std::unique_ptr &&res, Error err, + Headers &&request_headers = Headers{}) + : res_(std::move(res)), err_(err), + request_headers_(std::move(request_headers)) {} + // Response + operator bool() const { return res_ != nullptr; } + bool operator==(std::nullptr_t) const { return res_ == nullptr; } + bool operator!=(std::nullptr_t) const { return res_ != nullptr; } + const Response &value() const { return *res_; } + Response &value() { return *res_; } + const Response &operator*() const { return *res_; } + Response &operator*() { return *res_; } + const Response *operator->() const { return res_.get(); } + Response *operator->() { return res_.get(); } + + // Error + Error error() const { return err_; } + + // Request Headers + bool has_request_header(const char *key) const; + std::string get_request_header_value(const char *key, size_t id = 0) const; + template + T get_request_header_value(const char *key, size_t id = 0) const; + size_t get_request_header_value_count(const char *key) const; + +private: + std::unique_ptr res_; + Error err_; + Headers request_headers_; +}; + +class ClientImpl { +public: + explicit ClientImpl(const std::string &host); + + explicit ClientImpl(const std::string &host, int port); + + explicit ClientImpl(const std::string &host, int port, + const std::string &client_cert_path, + const std::string &client_key_path); + + virtual ~ClientImpl(); + + virtual bool is_valid() const; + + Result Get(const char *path); + Result Get(const char *path, const Headers &headers); + Result Get(const char *path, Progress progress); + Result Get(const char *path, const Headers &headers, Progress progress); + Result Get(const char *path, ContentReceiver content_receiver); + Result Get(const char *path, const Headers &headers, + ContentReceiver content_receiver); + Result Get(const char *path, ContentReceiver content_receiver, + Progress progress); + Result Get(const char *path, const Headers &headers, + ContentReceiver content_receiver, Progress progress); + Result Get(const char *path, ResponseHandler response_handler, + ContentReceiver content_receiver); + Result Get(const char *path, const Headers &headers, + ResponseHandler response_handler, + ContentReceiver content_receiver); + Result Get(const char *path, ResponseHandler response_handler, + ContentReceiver content_receiver, Progress progress); + Result Get(const char *path, const Headers &headers, + ResponseHandler response_handler, ContentReceiver content_receiver, + Progress progress); + + Result Get(const char *path, const Params ¶ms, const Headers &headers, + Progress progress = nullptr); + Result Get(const char *path, const Params ¶ms, const Headers &headers, + ContentReceiver content_receiver, Progress progress = nullptr); + Result Get(const char *path, const Params ¶ms, const Headers &headers, + ResponseHandler response_handler, ContentReceiver content_receiver, + Progress progress = nullptr); + + Result Head(const char *path); + Result Head(const char *path, const Headers &headers); + + Result Post(const char *path); + Result Post(const char *path, const char *body, size_t content_length, + const char *content_type); + Result Post(const char *path, const Headers &headers, const char *body, + size_t content_length, const char *content_type); + Result Post(const char *path, const std::string &body, + const char *content_type); + Result Post(const char *path, const Headers &headers, const std::string &body, + const char *content_type); + Result Post(const char *path, size_t content_length, + ContentProvider content_provider, const char *content_type); + Result Post(const char *path, ContentProviderWithoutLength content_provider, + const char *content_type); + Result Post(const char *path, const Headers &headers, size_t content_length, + ContentProvider content_provider, const char *content_type); + Result Post(const char *path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const char *content_type); + Result Post(const char *path, const Params ¶ms); + Result Post(const char *path, const Headers &headers, const Params ¶ms); + Result Post(const char *path, const MultipartFormDataItems &items); + Result Post(const char *path, const Headers &headers, + const MultipartFormDataItems &items); + Result Post(const char *path, const Headers &headers, + const MultipartFormDataItems &items, const std::string &boundary); + + Result Put(const char *path); + Result Put(const char *path, const char *body, size_t content_length, + const char *content_type); + Result Put(const char *path, const Headers &headers, const char *body, + size_t content_length, const char *content_type); + Result Put(const char *path, const std::string &body, + const char *content_type); + Result Put(const char *path, const Headers &headers, const std::string &body, + const char *content_type); + Result Put(const char *path, size_t content_length, + ContentProvider content_provider, const char *content_type); + Result Put(const char *path, ContentProviderWithoutLength content_provider, + const char *content_type); + Result Put(const char *path, const Headers &headers, size_t content_length, + ContentProvider content_provider, const char *content_type); + Result Put(const char *path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const char *content_type); + Result Put(const char *path, const Params ¶ms); + Result Put(const char *path, const Headers &headers, const Params ¶ms); + + Result Patch(const char *path); + Result Patch(const char *path, const char *body, size_t content_length, + const char *content_type); + Result Patch(const char *path, const Headers &headers, const char *body, + size_t content_length, const char *content_type); + Result Patch(const char *path, const std::string &body, + const char *content_type); + Result Patch(const char *path, const Headers &headers, + const std::string &body, const char *content_type); + Result Patch(const char *path, size_t content_length, + ContentProvider content_provider, const char *content_type); + Result Patch(const char *path, ContentProviderWithoutLength content_provider, + const char *content_type); + Result Patch(const char *path, const Headers &headers, size_t content_length, + ContentProvider content_provider, const char *content_type); + Result Patch(const char *path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const char *content_type); + + Result Delete(const char *path); + Result Delete(const char *path, const Headers &headers); + Result Delete(const char *path, const char *body, size_t content_length, + const char *content_type); + Result Delete(const char *path, const Headers &headers, const char *body, + size_t content_length, const char *content_type); + Result Delete(const char *path, const std::string &body, + const char *content_type); + Result Delete(const char *path, const Headers &headers, + const std::string &body, const char *content_type); + + Result Options(const char *path); + Result Options(const char *path, const Headers &headers); + + bool send(Request &req, Response &res, Error &error); + Result send(const Request &req); + + size_t is_socket_open() const; + + void stop(); + + void set_default_headers(Headers headers); + + void set_address_family(int family); + void set_tcp_nodelay(bool on); + void set_socket_options(SocketOptions socket_options); + + void set_connection_timeout(time_t sec, time_t usec = 0); + template + void + set_connection_timeout(const std::chrono::duration &duration); + + void set_read_timeout(time_t sec, time_t usec = 0); + template + void set_read_timeout(const std::chrono::duration &duration); + + void set_write_timeout(time_t sec, time_t usec = 0); + template + void set_write_timeout(const std::chrono::duration &duration); + + void set_basic_auth(const char *username, const char *password); + void set_bearer_token_auth(const char *token); +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + void set_digest_auth(const char *username, const char *password); +#endif + + void set_keep_alive(bool on); + void set_follow_location(bool on); + + void set_url_encode(bool on); + + void set_compress(bool on); + + void set_decompress(bool on); + + void set_interface(const char *intf); + + void set_proxy(const char *host, int port); + void set_proxy_basic_auth(const char *username, const char *password); + void set_proxy_bearer_token_auth(const char *token); +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + void set_proxy_digest_auth(const char *username, const char *password); +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + void set_ca_cert_path(const char *ca_cert_file_path, + const char *ca_cert_dir_path = nullptr); + void set_ca_cert_store(X509_STORE *ca_cert_store); +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + void enable_server_certificate_verification(bool enabled); +#endif + + void set_logger(Logger logger); + +protected: + struct Socket { + socket_t sock = INVALID_SOCKET; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + SSL *ssl = nullptr; +#endif + + bool is_open() const { return sock != INVALID_SOCKET; } + }; + + Result send_(Request &&req); + + virtual bool create_and_connect_socket(Socket &socket, Error &error); + + // All of: + // shutdown_ssl + // shutdown_socket + // close_socket + // should ONLY be called when socket_mutex_ is locked. + // Also, shutdown_ssl and close_socket should also NOT be called concurrently + // with a DIFFERENT thread sending requests using that socket. + virtual void shutdown_ssl(Socket &socket, bool shutdown_gracefully); + void shutdown_socket(Socket &socket); + void close_socket(Socket &socket); + + bool process_request(Stream &strm, Request &req, Response &res, + bool close_connection, Error &error); + + bool write_content_with_provider(Stream &strm, const Request &req, + Error &error); + + void copy_settings(const ClientImpl &rhs); + + // Socket endoint information + const std::string host_; + const int port_; + const std::string host_and_port_; + + // Current open socket + Socket socket_; + mutable std::mutex socket_mutex_; + std::recursive_mutex request_mutex_; + + // These are all protected under socket_mutex + size_t socket_requests_in_flight_ = 0; + std::thread::id socket_requests_are_from_thread_ = std::thread::id(); + bool socket_should_be_closed_when_request_is_done_ = false; + + // Default headers + Headers default_headers_; + + // Settings + std::string client_cert_path_; + std::string client_key_path_; + + time_t connection_timeout_sec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND; + time_t connection_timeout_usec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND; + time_t read_timeout_sec_ = CPPHTTPLIB_READ_TIMEOUT_SECOND; + time_t read_timeout_usec_ = CPPHTTPLIB_READ_TIMEOUT_USECOND; + time_t write_timeout_sec_ = CPPHTTPLIB_WRITE_TIMEOUT_SECOND; + time_t write_timeout_usec_ = CPPHTTPLIB_WRITE_TIMEOUT_USECOND; + + std::string basic_auth_username_; + std::string basic_auth_password_; + std::string bearer_token_auth_token_; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + std::string digest_auth_username_; + std::string digest_auth_password_; +#endif + + bool keep_alive_ = false; + bool follow_location_ = false; + + bool url_encode_ = true; + + int address_family_ = AF_UNSPEC; + bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY; + SocketOptions socket_options_ = nullptr; + + bool compress_ = false; + bool decompress_ = true; + + std::string interface_; + + std::string proxy_host_; + int proxy_port_ = -1; + + std::string proxy_basic_auth_username_; + std::string proxy_basic_auth_password_; + std::string proxy_bearer_token_auth_token_; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + std::string proxy_digest_auth_username_; + std::string proxy_digest_auth_password_; +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + std::string ca_cert_file_path_; + std::string ca_cert_dir_path_; + + X509_STORE *ca_cert_store_ = nullptr; +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + bool server_certificate_verification_ = true; +#endif + + Logger logger_; + +private: + socket_t create_client_socket(Error &error) const; + bool read_response_line(Stream &strm, const Request &req, Response &res); + bool write_request(Stream &strm, Request &req, bool close_connection, + Error &error); + bool redirect(Request &req, Response &res, Error &error); + bool handle_request(Stream &strm, Request &req, Response &res, + bool close_connection, Error &error); + std::unique_ptr send_with_content_provider( + Request &req, + // const char *method, const char *path, const Headers &headers, + const char *body, size_t content_length, ContentProvider content_provider, + ContentProviderWithoutLength content_provider_without_length, + const char *content_type, Error &error); + Result send_with_content_provider( + const char *method, const char *path, const Headers &headers, + const char *body, size_t content_length, ContentProvider content_provider, + ContentProviderWithoutLength content_provider_without_length, + const char *content_type); + + std::string adjust_host_string(const std::string &host) const; + + virtual bool process_socket(const Socket &socket, + std::function callback); + virtual bool is_ssl() const; +}; + +class Client { +public: + // Universal interface + explicit Client(const std::string &scheme_host_port); + + explicit Client(const std::string &scheme_host_port, + const std::string &client_cert_path, + const std::string &client_key_path); + + // HTTP only interface + explicit Client(const std::string &host, int port); + + explicit Client(const std::string &host, int port, + const std::string &client_cert_path, + const std::string &client_key_path); + + ~Client(); + + bool is_valid() const; + + Result Get(const char *path); + Result Get(const char *path, const Headers &headers); + Result Get(const char *path, Progress progress); + Result Get(const char *path, const Headers &headers, Progress progress); + Result Get(const char *path, ContentReceiver content_receiver); + Result Get(const char *path, const Headers &headers, + ContentReceiver content_receiver); + Result Get(const char *path, ContentReceiver content_receiver, + Progress progress); + Result Get(const char *path, const Headers &headers, + ContentReceiver content_receiver, Progress progress); + Result Get(const char *path, ResponseHandler response_handler, + ContentReceiver content_receiver); + Result Get(const char *path, const Headers &headers, + ResponseHandler response_handler, + ContentReceiver content_receiver); + Result Get(const char *path, const Headers &headers, + ResponseHandler response_handler, ContentReceiver content_receiver, + Progress progress); + Result Get(const char *path, ResponseHandler response_handler, + ContentReceiver content_receiver, Progress progress); + + Result Get(const char *path, const Params ¶ms, const Headers &headers, + Progress progress = nullptr); + Result Get(const char *path, const Params ¶ms, const Headers &headers, + ContentReceiver content_receiver, Progress progress = nullptr); + Result Get(const char *path, const Params ¶ms, const Headers &headers, + ResponseHandler response_handler, ContentReceiver content_receiver, + Progress progress = nullptr); + + Result Head(const char *path); + Result Head(const char *path, const Headers &headers); + + Result Post(const char *path); + Result Post(const char *path, const char *body, size_t content_length, + const char *content_type); + Result Post(const char *path, const Headers &headers, const char *body, + size_t content_length, const char *content_type); + Result Post(const char *path, const std::string &body, + const char *content_type); + Result Post(const char *path, const Headers &headers, const std::string &body, + const char *content_type); + Result Post(const char *path, size_t content_length, + ContentProvider content_provider, const char *content_type); + Result Post(const char *path, ContentProviderWithoutLength content_provider, + const char *content_type); + Result Post(const char *path, const Headers &headers, size_t content_length, + ContentProvider content_provider, const char *content_type); + Result Post(const char *path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const char *content_type); + Result Post(const char *path, const Params ¶ms); + Result Post(const char *path, const Headers &headers, const Params ¶ms); + Result Post(const char *path, const MultipartFormDataItems &items); + Result Post(const char *path, const Headers &headers, + const MultipartFormDataItems &items); + Result Post(const char *path, const Headers &headers, + const MultipartFormDataItems &items, const std::string &boundary); + Result Put(const char *path); + Result Put(const char *path, const char *body, size_t content_length, + const char *content_type); + Result Put(const char *path, const Headers &headers, const char *body, + size_t content_length, const char *content_type); + Result Put(const char *path, const std::string &body, + const char *content_type); + Result Put(const char *path, const Headers &headers, const std::string &body, + const char *content_type); + Result Put(const char *path, size_t content_length, + ContentProvider content_provider, const char *content_type); + Result Put(const char *path, ContentProviderWithoutLength content_provider, + const char *content_type); + Result Put(const char *path, const Headers &headers, size_t content_length, + ContentProvider content_provider, const char *content_type); + Result Put(const char *path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const char *content_type); + Result Put(const char *path, const Params ¶ms); + Result Put(const char *path, const Headers &headers, const Params ¶ms); + Result Patch(const char *path); + Result Patch(const char *path, const char *body, size_t content_length, + const char *content_type); + Result Patch(const char *path, const Headers &headers, const char *body, + size_t content_length, const char *content_type); + Result Patch(const char *path, const std::string &body, + const char *content_type); + Result Patch(const char *path, const Headers &headers, + const std::string &body, const char *content_type); + Result Patch(const char *path, size_t content_length, + ContentProvider content_provider, const char *content_type); + Result Patch(const char *path, ContentProviderWithoutLength content_provider, + const char *content_type); + Result Patch(const char *path, const Headers &headers, size_t content_length, + ContentProvider content_provider, const char *content_type); + Result Patch(const char *path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const char *content_type); + + Result Delete(const char *path); + Result Delete(const char *path, const Headers &headers); + Result Delete(const char *path, const char *body, size_t content_length, + const char *content_type); + Result Delete(const char *path, const Headers &headers, const char *body, + size_t content_length, const char *content_type); + Result Delete(const char *path, const std::string &body, + const char *content_type); + Result Delete(const char *path, const Headers &headers, + const std::string &body, const char *content_type); + + Result Options(const char *path); + Result Options(const char *path, const Headers &headers); + + bool send(Request &req, Response &res, Error &error); + Result send(const Request &req); + + size_t is_socket_open() const; + + void stop(); + + void set_default_headers(Headers headers); + + void set_address_family(int family); + void set_tcp_nodelay(bool on); + void set_socket_options(SocketOptions socket_options); + + void set_connection_timeout(time_t sec, time_t usec = 0); + template + void + set_connection_timeout(const std::chrono::duration &duration); + + void set_read_timeout(time_t sec, time_t usec = 0); + template + void set_read_timeout(const std::chrono::duration &duration); + + void set_write_timeout(time_t sec, time_t usec = 0); + template + void set_write_timeout(const std::chrono::duration &duration); + + void set_basic_auth(const char *username, const char *password); + void set_bearer_token_auth(const char *token); +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + void set_digest_auth(const char *username, const char *password); +#endif + + void set_keep_alive(bool on); + void set_follow_location(bool on); + + void set_url_encode(bool on); + + void set_compress(bool on); + + void set_decompress(bool on); + + void set_interface(const char *intf); + + void set_proxy(const char *host, int port); + void set_proxy_basic_auth(const char *username, const char *password); + void set_proxy_bearer_token_auth(const char *token); +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + void set_proxy_digest_auth(const char *username, const char *password); +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + void enable_server_certificate_verification(bool enabled); +#endif + + void set_logger(Logger logger); + + // SSL +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + void set_ca_cert_path(const char *ca_cert_file_path, + const char *ca_cert_dir_path = nullptr); + + void set_ca_cert_store(X509_STORE *ca_cert_store); + + long get_openssl_verify_result() const; + + SSL_CTX *ssl_context() const; +#endif + +private: + std::unique_ptr cli_; + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + bool is_ssl_ = false; +#endif +}; + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +class SSLServer : public Server { +public: + SSLServer(const char *cert_path, const char *private_key_path, + const char *client_ca_cert_file_path = nullptr, + const char *client_ca_cert_dir_path = nullptr); + + SSLServer(X509 *cert, EVP_PKEY *private_key, + X509_STORE *client_ca_cert_store = nullptr); + + ~SSLServer() override; + + bool is_valid() const override; + +private: + bool process_and_close_socket(socket_t sock) override; + + SSL_CTX *ctx_; + std::mutex ctx_mutex_; +}; + +class SSLClient : public ClientImpl { +public: + explicit SSLClient(const std::string &host); + + explicit SSLClient(const std::string &host, int port); + + explicit SSLClient(const std::string &host, int port, + const std::string &client_cert_path, + const std::string &client_key_path); + + explicit SSLClient(const std::string &host, int port, X509 *client_cert, + EVP_PKEY *client_key); + + ~SSLClient() override; + + bool is_valid() const override; + + void set_ca_cert_store(X509_STORE *ca_cert_store); + + long get_openssl_verify_result() const; + + SSL_CTX *ssl_context() const; + +private: + bool create_and_connect_socket(Socket &socket, Error &error) override; + void shutdown_ssl(Socket &socket, bool shutdown_gracefully) override; + void shutdown_ssl_impl(Socket &socket, bool shutdown_socket); + + bool process_socket(const Socket &socket, + std::function callback) override; + bool is_ssl() const override; + + bool connect_with_proxy(Socket &sock, Response &res, bool &success, + Error &error); + bool initialize_ssl(Socket &socket, Error &error); + + bool load_certs(); + + bool verify_host(X509 *server_cert) const; + bool verify_host_with_subject_alt_name(X509 *server_cert) const; + bool verify_host_with_common_name(X509 *server_cert) const; + bool check_host_name(const char *pattern, size_t pattern_len) const; + + SSL_CTX *ctx_; + std::mutex ctx_mutex_; + std::once_flag initialize_cert_; + + std::vector host_components_; + + long verify_result_ = 0; + + friend class ClientImpl; +}; +#endif + +/* + * Implementation of template methods. + */ + +namespace detail { + +template +inline void duration_to_sec_and_usec(const T &duration, U callback) { + auto sec = std::chrono::duration_cast(duration).count(); + auto usec = std::chrono::duration_cast( + duration - std::chrono::seconds(sec)) + .count(); + callback(sec, usec); +} + +template +inline T get_header_value(const Headers & /*headers*/, const char * /*key*/, + size_t /*id*/ = 0, uint64_t /*def*/ = 0) {} + +template <> +inline uint64_t get_header_value(const Headers &headers, + const char *key, size_t id, + uint64_t def) { + auto rng = headers.equal_range(key); + auto it = rng.first; + std::advance(it, static_cast(id)); + if (it != rng.second) { + return std::strtoull(it->second.data(), nullptr, 10); + } + return def; +} + +} // namespace detail + +template +inline T Request::get_header_value(const char *key, size_t id) const { + return detail::get_header_value(headers, key, id, 0); +} + +template +inline T Response::get_header_value(const char *key, size_t id) const { + return detail::get_header_value(headers, key, id, 0); +} + +template +inline ssize_t Stream::write_format(const char *fmt, const Args &... args) { + const auto bufsiz = 2048; + std::array buf; + +#if defined(_MSC_VER) && _MSC_VER < 1900 + auto sn = _snprintf_s(buf.data(), bufsiz - 1, buf.size() - 1, fmt, args...); +#else + auto sn = snprintf(buf.data(), buf.size() - 1, fmt, args...); +#endif + if (sn <= 0) { return sn; } + + auto n = static_cast(sn); + + if (n >= buf.size() - 1) { + std::vector glowable_buf(buf.size()); + + while (n >= glowable_buf.size() - 1) { + glowable_buf.resize(glowable_buf.size() * 2); +#if defined(_MSC_VER) && _MSC_VER < 1900 + n = static_cast(_snprintf_s(&glowable_buf[0], glowable_buf.size(), + glowable_buf.size() - 1, fmt, + args...)); +#else + n = static_cast( + snprintf(&glowable_buf[0], glowable_buf.size() - 1, fmt, args...)); +#endif + } + return write(&glowable_buf[0], n); + } else { + return write(buf.data(), n); + } +} + +template +inline Server & +Server::set_read_timeout(const std::chrono::duration &duration) { + detail::duration_to_sec_and_usec( + duration, [&](time_t sec, time_t usec) { set_read_timeout(sec, usec); }); + return *this; +} + +template +inline Server & +Server::set_write_timeout(const std::chrono::duration &duration) { + detail::duration_to_sec_and_usec( + duration, [&](time_t sec, time_t usec) { set_write_timeout(sec, usec); }); + return *this; +} + +template +inline Server & +Server::set_idle_interval(const std::chrono::duration &duration) { + detail::duration_to_sec_and_usec( + duration, [&](time_t sec, time_t usec) { set_idle_interval(sec, usec); }); + return *this; +} + +template +inline T Result::get_request_header_value(const char *key, size_t id) const { + return detail::get_header_value(request_headers_, key, id, 0); +} + +template +inline void ClientImpl::set_connection_timeout( + const std::chrono::duration &duration) { + detail::duration_to_sec_and_usec(duration, [&](time_t sec, time_t usec) { + set_connection_timeout(sec, usec); + }); +} + +template +inline void ClientImpl::set_read_timeout( + const std::chrono::duration &duration) { + detail::duration_to_sec_and_usec( + duration, [&](time_t sec, time_t usec) { set_read_timeout(sec, usec); }); +} + +template +inline void ClientImpl::set_write_timeout( + const std::chrono::duration &duration) { + detail::duration_to_sec_and_usec( + duration, [&](time_t sec, time_t usec) { set_write_timeout(sec, usec); }); +} + +template +inline void Client::set_connection_timeout( + const std::chrono::duration &duration) { + cli_->set_connection_timeout(duration); +} + +template +inline void +Client::set_read_timeout(const std::chrono::duration &duration) { + cli_->set_read_timeout(duration); +} + +template +inline void +Client::set_write_timeout(const std::chrono::duration &duration) { + cli_->set_write_timeout(duration); +} + +/* + * Forward declarations and types that will be part of the .h file if split into + * .h + .cc. + */ + +std::pair make_range_header(Ranges ranges); + +std::pair +make_basic_authentication_header(const std::string &username, + const std::string &password, + bool is_proxy = false); + +namespace detail { + +std::string encode_query_param(const std::string &value); + +void read_file(const std::string &path, std::string &out); + +std::string trim_copy(const std::string &s); + +void split(const char *b, const char *e, char d, + std::function fn); + +bool process_client_socket(socket_t sock, time_t read_timeout_sec, + time_t read_timeout_usec, time_t write_timeout_sec, + time_t write_timeout_usec, + std::function callback); + +socket_t create_client_socket(const char *host, int port, int address_family, + bool tcp_nodelay, SocketOptions socket_options, + time_t connection_timeout_sec, + time_t connection_timeout_usec, + time_t read_timeout_sec, time_t read_timeout_usec, + time_t write_timeout_sec, + time_t write_timeout_usec, + const std::string &intf, Error &error); + +const char *get_header_value(const Headers &headers, const char *key, + size_t id = 0, const char *def = nullptr); + +std::string params_to_query_str(const Params ¶ms); + +void parse_query_text(const std::string &s, Params ¶ms); + +bool parse_range_header(const std::string &s, Ranges &ranges); + +int close_socket(socket_t sock); + +enum class EncodingType { None = 0, Gzip, Brotli }; + +EncodingType encoding_type(const Request &req, const Response &res); + +class BufferStream : public Stream { +public: + BufferStream() = default; + ~BufferStream() override = default; + + bool is_readable() const override; + bool is_writable() const override; + ssize_t read(char *ptr, size_t size) override; + ssize_t write(const char *ptr, size_t size) override; + void get_remote_ip_and_port(std::string &ip, int &port) const override; + socket_t socket() const override; + + const std::string &get_buffer() const; + +private: + std::string buffer; + size_t position = 0; +}; + +class compressor { +public: + virtual ~compressor() = default; + + typedef std::function Callback; + virtual bool compress(const char *data, size_t data_length, bool last, + Callback callback) = 0; +}; + +class decompressor { +public: + virtual ~decompressor() = default; + + virtual bool is_valid() const = 0; + + typedef std::function Callback; + virtual bool decompress(const char *data, size_t data_length, + Callback callback) = 0; +}; + +class nocompressor : public compressor { +public: + virtual ~nocompressor() = default; + + bool compress(const char *data, size_t data_length, bool /*last*/, + Callback callback) override; +}; + +#ifdef CPPHTTPLIB_ZLIB_SUPPORT +class gzip_compressor : public compressor { +public: + gzip_compressor(); + ~gzip_compressor(); + + bool compress(const char *data, size_t data_length, bool last, + Callback callback) override; + +private: + bool is_valid_ = false; + z_stream strm_; +}; + +class gzip_decompressor : public decompressor { +public: + gzip_decompressor(); + ~gzip_decompressor(); + + bool is_valid() const override; + + bool decompress(const char *data, size_t data_length, + Callback callback) override; + +private: + bool is_valid_ = false; + z_stream strm_; +}; +#endif + +#ifdef CPPHTTPLIB_BROTLI_SUPPORT +class brotli_compressor : public compressor { +public: + brotli_compressor(); + ~brotli_compressor(); + + bool compress(const char *data, size_t data_length, bool last, + Callback callback) override; + +private: + BrotliEncoderState *state_ = nullptr; +}; + +class brotli_decompressor : public decompressor { +public: + brotli_decompressor(); + ~brotli_decompressor(); + + bool is_valid() const override; + + bool decompress(const char *data, size_t data_length, + Callback callback) override; + +private: + BrotliDecoderResult decoder_r; + BrotliDecoderState *decoder_s = nullptr; +}; +#endif + +// NOTE: until the read size reaches `fixed_buffer_size`, use `fixed_buffer` +// to store data. The call can set memory on stack for performance. +class stream_line_reader { +public: + stream_line_reader(Stream &strm, char *fixed_buffer, + size_t fixed_buffer_size); + const char *ptr() const; + size_t size() const; + bool end_with_crlf() const; + bool getline(); + +private: + void append(char c); + + Stream &strm_; + char *fixed_buffer_; + const size_t fixed_buffer_size_; + size_t fixed_buffer_used_size_ = 0; + std::string glowable_buffer_; +}; + +} // namespace detail + +// ---------------------------------------------------------------------------- + +/* + * Implementation that will be part of the .cc file if split into .h + .cc. + */ + +namespace detail { + +inline bool is_hex(char c, int &v) { + if (0x20 <= c && isdigit(c)) { + v = c - '0'; + return true; + } else if ('A' <= c && c <= 'F') { + v = c - 'A' + 10; + return true; + } else if ('a' <= c && c <= 'f') { + v = c - 'a' + 10; + return true; + } + return false; +} + +inline bool from_hex_to_i(const std::string &s, size_t i, size_t cnt, + int &val) { + if (i >= s.size()) { return false; } + + val = 0; + for (; cnt; i++, cnt--) { + if (!s[i]) { return false; } + int v = 0; + if (is_hex(s[i], v)) { + val = val * 16 + v; + } else { + return false; + } + } + return true; +} + +inline std::string from_i_to_hex(size_t n) { + const char *charset = "0123456789abcdef"; + std::string ret; + do { + ret = charset[n & 15] + ret; + n >>= 4; + } while (n > 0); + return ret; +} + +inline size_t to_utf8(int code, char *buff) { + if (code < 0x0080) { + buff[0] = (code & 0x7F); + return 1; + } else if (code < 0x0800) { + buff[0] = static_cast(0xC0 | ((code >> 6) & 0x1F)); + buff[1] = static_cast(0x80 | (code & 0x3F)); + return 2; + } else if (code < 0xD800) { + buff[0] = static_cast(0xE0 | ((code >> 12) & 0xF)); + buff[1] = static_cast(0x80 | ((code >> 6) & 0x3F)); + buff[2] = static_cast(0x80 | (code & 0x3F)); + return 3; + } else if (code < 0xE000) { // D800 - DFFF is invalid... + return 0; + } else if (code < 0x10000) { + buff[0] = static_cast(0xE0 | ((code >> 12) & 0xF)); + buff[1] = static_cast(0x80 | ((code >> 6) & 0x3F)); + buff[2] = static_cast(0x80 | (code & 0x3F)); + return 3; + } else if (code < 0x110000) { + buff[0] = static_cast(0xF0 | ((code >> 18) & 0x7)); + buff[1] = static_cast(0x80 | ((code >> 12) & 0x3F)); + buff[2] = static_cast(0x80 | ((code >> 6) & 0x3F)); + buff[3] = static_cast(0x80 | (code & 0x3F)); + return 4; + } + + // NOTREACHED + return 0; +} + +// NOTE: This code came up with the following stackoverflow post: +// https://stackoverflow.com/questions/180947/base64-decode-snippet-in-c +inline std::string base64_encode(const std::string &in) { + static const auto lookup = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + std::string out; + out.reserve(in.size()); + + int val = 0; + int valb = -6; + + for (auto c : in) { + val = (val << 8) + static_cast(c); + valb += 8; + while (valb >= 0) { + out.push_back(lookup[(val >> valb) & 0x3F]); + valb -= 6; + } + } + + if (valb > -6) { out.push_back(lookup[((val << 8) >> (valb + 8)) & 0x3F]); } + + while (out.size() % 4) { + out.push_back('='); + } + + return out; +} + +inline bool is_file(const std::string &path) { + struct stat st; + return stat(path.c_str(), &st) >= 0 && S_ISREG(st.st_mode); +} + +inline bool is_dir(const std::string &path) { + struct stat st; + return stat(path.c_str(), &st) >= 0 && S_ISDIR(st.st_mode); +} + +inline bool is_valid_path(const std::string &path) { + size_t level = 0; + size_t i = 0; + + // Skip slash + while (i < path.size() && path[i] == '/') { + i++; + } + + while (i < path.size()) { + // Read component + auto beg = i; + while (i < path.size() && path[i] != '/') { + i++; + } + + auto len = i - beg; + assert(len > 0); + + if (!path.compare(beg, len, ".")) { + ; + } else if (!path.compare(beg, len, "..")) { + if (level == 0) { return false; } + level--; + } else { + level++; + } + + // Skip slash + while (i < path.size() && path[i] == '/') { + i++; + } + } + + return true; +} + +inline std::string encode_query_param(const std::string &value) { + std::ostringstream escaped; + escaped.fill('0'); + escaped << std::hex; + + for (auto c : value) { + if (std::isalnum(static_cast(c)) || c == '-' || c == '_' || + c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' || + c == ')') { + escaped << c; + } else { + escaped << std::uppercase; + escaped << '%' << std::setw(2) + << static_cast(static_cast(c)); + escaped << std::nouppercase; + } + } + + return escaped.str(); +} + +inline std::string encode_url(const std::string &s) { + std::string result; + result.reserve(s.size()); + + for (size_t i = 0; s[i]; i++) { + switch (s[i]) { + case ' ': result += "%20"; break; + case '+': result += "%2B"; break; + case '\r': result += "%0D"; break; + case '\n': result += "%0A"; break; + case '\'': result += "%27"; break; + case ',': result += "%2C"; break; + // case ':': result += "%3A"; break; // ok? probably... + case ';': result += "%3B"; break; + default: + auto c = static_cast(s[i]); + if (c >= 0x80) { + result += '%'; + char hex[4]; + auto len = snprintf(hex, sizeof(hex) - 1, "%02X", c); + assert(len == 2); + result.append(hex, static_cast(len)); + } else { + result += s[i]; + } + break; + } + } + + return result; +} + +inline std::string decode_url(const std::string &s, + bool convert_plus_to_space) { + std::string result; + + for (size_t i = 0; i < s.size(); i++) { + if (s[i] == '%' && i + 1 < s.size()) { + if (s[i + 1] == 'u') { + int val = 0; + if (from_hex_to_i(s, i + 2, 4, val)) { + // 4 digits Unicode codes + char buff[4]; + size_t len = to_utf8(val, buff); + if (len > 0) { result.append(buff, len); } + i += 5; // 'u0000' + } else { + result += s[i]; + } + } else { + int val = 0; + if (from_hex_to_i(s, i + 1, 2, val)) { + // 2 digits hex codes + result += static_cast(val); + i += 2; // '00' + } else { + result += s[i]; + } + } + } else if (convert_plus_to_space && s[i] == '+') { + result += ' '; + } else { + result += s[i]; + } + } + + return result; +} + +inline void read_file(const std::string &path, std::string &out) { + std::ifstream fs(path, std::ios_base::binary); + fs.seekg(0, std::ios_base::end); + auto size = fs.tellg(); + fs.seekg(0); + out.resize(static_cast(size)); + fs.read(&out[0], static_cast(size)); +} + +inline std::string file_extension(const std::string &path) { + std::smatch m; + static auto re = std::regex("\\.([a-zA-Z0-9]+)$"); + if (std::regex_search(path, m, re)) { return m[1].str(); } + return std::string(); +} + +inline bool is_space_or_tab(char c) { return c == ' ' || c == '\t'; } + +inline std::pair trim(const char *b, const char *e, size_t left, + size_t right) { + while (b + left < e && is_space_or_tab(b[left])) { + left++; + } + while (right > 0 && is_space_or_tab(b[right - 1])) { + right--; + } + return std::make_pair(left, right); +} + +inline std::string trim_copy(const std::string &s) { + auto r = trim(s.data(), s.data() + s.size(), 0, s.size()); + return s.substr(r.first, r.second - r.first); +} + +inline void split(const char *b, const char *e, char d, + std::function fn) { + size_t i = 0; + size_t beg = 0; + + while (e ? (b + i < e) : (b[i] != '\0')) { + if (b[i] == d) { + auto r = trim(b, e, beg, i); + if (r.first < r.second) { fn(&b[r.first], &b[r.second]); } + beg = i + 1; + } + i++; + } + + if (i) { + auto r = trim(b, e, beg, i); + if (r.first < r.second) { fn(&b[r.first], &b[r.second]); } + } +} + +inline stream_line_reader::stream_line_reader(Stream &strm, char *fixed_buffer, + size_t fixed_buffer_size) + : strm_(strm), fixed_buffer_(fixed_buffer), + fixed_buffer_size_(fixed_buffer_size) {} + +inline const char *stream_line_reader::ptr() const { + if (glowable_buffer_.empty()) { + return fixed_buffer_; + } else { + return glowable_buffer_.data(); + } +} + +inline size_t stream_line_reader::size() const { + if (glowable_buffer_.empty()) { + return fixed_buffer_used_size_; + } else { + return glowable_buffer_.size(); + } +} + +inline bool stream_line_reader::end_with_crlf() const { + auto end = ptr() + size(); + return size() >= 2 && end[-2] == '\r' && end[-1] == '\n'; +} + +inline bool stream_line_reader::getline() { + fixed_buffer_used_size_ = 0; + glowable_buffer_.clear(); + + for (size_t i = 0;; i++) { + char byte; + auto n = strm_.read(&byte, 1); + + if (n < 0) { + return false; + } else if (n == 0) { + if (i == 0) { + return false; + } else { + break; + } + } + + append(byte); + + if (byte == '\n') { break; } + } + + return true; +} + +inline void stream_line_reader::append(char c) { + if (fixed_buffer_used_size_ < fixed_buffer_size_ - 1) { + fixed_buffer_[fixed_buffer_used_size_++] = c; + fixed_buffer_[fixed_buffer_used_size_] = '\0'; + } else { + if (glowable_buffer_.empty()) { + assert(fixed_buffer_[fixed_buffer_used_size_] == '\0'); + glowable_buffer_.assign(fixed_buffer_, fixed_buffer_used_size_); + } + glowable_buffer_ += c; + } +} + +inline int close_socket(socket_t sock) { +#ifdef _WIN32 + return closesocket(sock); +#else + return close(sock); +#endif +} + +template inline ssize_t handle_EINTR(T fn) { + ssize_t res = false; + while (true) { + res = fn(); + if (res < 0 && errno == EINTR) { continue; } + break; + } + return res; +} + +inline ssize_t select_read(socket_t sock, time_t sec, time_t usec) { +#ifdef CPPHTTPLIB_USE_POLL + struct pollfd pfd_read; + pfd_read.fd = sock; + pfd_read.events = POLLIN; + + auto timeout = static_cast(sec * 1000 + usec / 1000); + + return handle_EINTR([&]() { return poll(&pfd_read, 1, timeout); }); +#else +#ifndef _WIN32 + if (sock >= FD_SETSIZE) { return 1; } +#endif + + fd_set fds; + FD_ZERO(&fds); + FD_SET(sock, &fds); + + timeval tv; + tv.tv_sec = static_cast(sec); + tv.tv_usec = static_cast(usec); + + return handle_EINTR([&]() { + return select(static_cast(sock + 1), &fds, nullptr, nullptr, &tv); + }); +#endif +} + +inline ssize_t select_write(socket_t sock, time_t sec, time_t usec) { +#ifdef CPPHTTPLIB_USE_POLL + struct pollfd pfd_read; + pfd_read.fd = sock; + pfd_read.events = POLLOUT; + + auto timeout = static_cast(sec * 1000 + usec / 1000); + + return handle_EINTR([&]() { return poll(&pfd_read, 1, timeout); }); +#else +#ifndef _WIN32 + if (sock >= FD_SETSIZE) { return 1; } +#endif + + fd_set fds; + FD_ZERO(&fds); + FD_SET(sock, &fds); + + timeval tv; + tv.tv_sec = static_cast(sec); + tv.tv_usec = static_cast(usec); + + return handle_EINTR([&]() { + return select(static_cast(sock + 1), nullptr, &fds, nullptr, &tv); + }); +#endif +} + +inline bool wait_until_socket_is_ready(socket_t sock, time_t sec, time_t usec) { +#ifdef CPPHTTPLIB_USE_POLL + struct pollfd pfd_read; + pfd_read.fd = sock; + pfd_read.events = POLLIN | POLLOUT; + + auto timeout = static_cast(sec * 1000 + usec / 1000); + + auto poll_res = handle_EINTR([&]() { return poll(&pfd_read, 1, timeout); }); + + if (poll_res > 0 && pfd_read.revents & (POLLIN | POLLOUT)) { + int error = 0; + socklen_t len = sizeof(error); + auto res = getsockopt(sock, SOL_SOCKET, SO_ERROR, + reinterpret_cast(&error), &len); + return res >= 0 && !error; + } + return false; +#else +#ifndef _WIN32 + if (sock >= FD_SETSIZE) { return false; } +#endif + + fd_set fdsr; + FD_ZERO(&fdsr); + FD_SET(sock, &fdsr); + + auto fdsw = fdsr; + auto fdse = fdsr; + + timeval tv; + tv.tv_sec = static_cast(sec); + tv.tv_usec = static_cast(usec); + + auto ret = handle_EINTR([&]() { + return select(static_cast(sock + 1), &fdsr, &fdsw, &fdse, &tv); + }); + + if (ret > 0 && (FD_ISSET(sock, &fdsr) || FD_ISSET(sock, &fdsw))) { + int error = 0; + socklen_t len = sizeof(error); + return getsockopt(sock, SOL_SOCKET, SO_ERROR, + reinterpret_cast(&error), &len) >= 0 && + !error; + } + return false; +#endif +} + +class SocketStream : public Stream { +public: + SocketStream(socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, + time_t write_timeout_sec, time_t write_timeout_usec); + ~SocketStream() override; + + bool is_readable() const override; + bool is_writable() const override; + ssize_t read(char *ptr, size_t size) override; + ssize_t write(const char *ptr, size_t size) override; + void get_remote_ip_and_port(std::string &ip, int &port) const override; + socket_t socket() const override; + +private: + socket_t sock_; + time_t read_timeout_sec_; + time_t read_timeout_usec_; + time_t write_timeout_sec_; + time_t write_timeout_usec_; +}; + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +class SSLSocketStream : public Stream { +public: + SSLSocketStream(socket_t sock, SSL *ssl, time_t read_timeout_sec, + time_t read_timeout_usec, time_t write_timeout_sec, + time_t write_timeout_usec); + ~SSLSocketStream() override; + + bool is_readable() const override; + bool is_writable() const override; + ssize_t read(char *ptr, size_t size) override; + ssize_t write(const char *ptr, size_t size) override; + void get_remote_ip_and_port(std::string &ip, int &port) const override; + socket_t socket() const override; + +private: + socket_t sock_; + SSL *ssl_; + time_t read_timeout_sec_; + time_t read_timeout_usec_; + time_t write_timeout_sec_; + time_t write_timeout_usec_; +}; +#endif + +inline bool keep_alive(socket_t sock, time_t keep_alive_timeout_sec) { + using namespace std::chrono; + auto start = steady_clock::now(); + while (true) { + auto val = select_read(sock, 0, 10000); + if (val < 0) { + return false; + } else if (val == 0) { + auto current = steady_clock::now(); + auto duration = duration_cast(current - start); + auto timeout = keep_alive_timeout_sec * 1000; + if (duration.count() > timeout) { return false; } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } else { + return true; + } + } +} + +template +inline bool +process_server_socket_core(socket_t sock, size_t keep_alive_max_count, + time_t keep_alive_timeout_sec, T callback) { + assert(keep_alive_max_count > 0); + auto ret = false; + auto count = keep_alive_max_count; + while (count > 0 && keep_alive(sock, keep_alive_timeout_sec)) { + auto close_connection = count == 1; + auto connection_closed = false; + ret = callback(close_connection, connection_closed); + if (!ret || connection_closed) { break; } + count--; + } + return ret; +} + +template +inline bool +process_server_socket(socket_t sock, size_t keep_alive_max_count, + time_t keep_alive_timeout_sec, time_t read_timeout_sec, + time_t read_timeout_usec, time_t write_timeout_sec, + time_t write_timeout_usec, T callback) { + return process_server_socket_core( + sock, keep_alive_max_count, keep_alive_timeout_sec, + [&](bool close_connection, bool &connection_closed) { + SocketStream strm(sock, read_timeout_sec, read_timeout_usec, + write_timeout_sec, write_timeout_usec); + return callback(strm, close_connection, connection_closed); + }); +} + +inline bool process_client_socket(socket_t sock, time_t read_timeout_sec, + time_t read_timeout_usec, + time_t write_timeout_sec, + time_t write_timeout_usec, + std::function callback) { + SocketStream strm(sock, read_timeout_sec, read_timeout_usec, + write_timeout_sec, write_timeout_usec); + return callback(strm); +} + +inline int shutdown_socket(socket_t sock) { +#ifdef _WIN32 + return shutdown(sock, SD_BOTH); +#else + return shutdown(sock, SHUT_RDWR); +#endif +} + +template +socket_t create_socket(const char *host, int port, int address_family, + int socket_flags, bool tcp_nodelay, + SocketOptions socket_options, + BindOrConnect bind_or_connect) { + // Get address info + struct addrinfo hints; + struct addrinfo *result; + + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_family = address_family; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = socket_flags; + hints.ai_protocol = 0; + + auto service = std::to_string(port); + + if (getaddrinfo(host, service.c_str(), &hints, &result)) { +#ifdef __linux__ + res_init(); +#endif + return INVALID_SOCKET; + } + + for (auto rp = result; rp; rp = rp->ai_next) { + // Create a socket +#ifdef _WIN32 + auto sock = + WSASocketW(rp->ai_family, rp->ai_socktype, rp->ai_protocol, nullptr, 0, + WSA_FLAG_NO_HANDLE_INHERIT | WSA_FLAG_OVERLAPPED); + /** + * Since the WSA_FLAG_NO_HANDLE_INHERIT is only supported on Windows 7 SP1 + * and above the socket creation fails on older Windows Systems. + * + * Let's try to create a socket the old way in this case. + * + * Reference: + * https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketa + * + * WSA_FLAG_NO_HANDLE_INHERIT: + * This flag is supported on Windows 7 with SP1, Windows Server 2008 R2 with + * SP1, and later + * + */ + if (sock == INVALID_SOCKET) { + sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + } +#else + auto sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); +#endif + if (sock == INVALID_SOCKET) { continue; } + +#ifndef _WIN32 + if (fcntl(sock, F_SETFD, FD_CLOEXEC) == -1) { continue; } +#endif + + if (tcp_nodelay) { + int yes = 1; + setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&yes), + sizeof(yes)); + } + + if (socket_options) { socket_options(sock); } + + if (rp->ai_family == AF_INET6) { + int no = 0; + setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast(&no), + sizeof(no)); + } + + // bind or connect + if (bind_or_connect(sock, *rp)) { + freeaddrinfo(result); + return sock; + } + + close_socket(sock); + } + + freeaddrinfo(result); + return INVALID_SOCKET; +} + +inline void set_nonblocking(socket_t sock, bool nonblocking) { +#ifdef _WIN32 + auto flags = nonblocking ? 1UL : 0UL; + ioctlsocket(sock, FIONBIO, &flags); +#else + auto flags = fcntl(sock, F_GETFL, 0); + fcntl(sock, F_SETFL, + nonblocking ? (flags | O_NONBLOCK) : (flags & (~O_NONBLOCK))); +#endif +} + +inline bool is_connection_error() { +#ifdef _WIN32 + return WSAGetLastError() != WSAEWOULDBLOCK; +#else + return errno != EINPROGRESS; +#endif +} + +inline bool bind_ip_address(socket_t sock, const char *host) { + struct addrinfo hints; + struct addrinfo *result; + + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = 0; + + if (getaddrinfo(host, "0", &hints, &result)) { return false; } + + auto ret = false; + for (auto rp = result; rp; rp = rp->ai_next) { + const auto &ai = *rp; + if (!::bind(sock, ai.ai_addr, static_cast(ai.ai_addrlen))) { + ret = true; + break; + } + } + + freeaddrinfo(result); + return ret; +} + +#if !defined _WIN32 && !defined ANDROID +#define USE_IF2IP +#endif + +#ifdef USE_IF2IP +inline std::string if2ip(const std::string &ifn) { + struct ifaddrs *ifap; + getifaddrs(&ifap); + for (auto ifa = ifap; ifa; ifa = ifa->ifa_next) { + if (ifa->ifa_addr && ifn == ifa->ifa_name) { + if (ifa->ifa_addr->sa_family == AF_INET) { + auto sa = reinterpret_cast(ifa->ifa_addr); + char buf[INET_ADDRSTRLEN]; + if (inet_ntop(AF_INET, &sa->sin_addr, buf, INET_ADDRSTRLEN)) { + freeifaddrs(ifap); + return std::string(buf, INET_ADDRSTRLEN); + } + } + } + } + freeifaddrs(ifap); + return std::string(); +} +#endif + +inline socket_t create_client_socket( + const char *host, int port, int address_family, bool tcp_nodelay, + SocketOptions socket_options, time_t connection_timeout_sec, + time_t connection_timeout_usec, time_t read_timeout_sec, + time_t read_timeout_usec, time_t write_timeout_sec, + time_t write_timeout_usec, const std::string &intf, Error &error) { + auto sock = create_socket( + host, port, address_family, 0, tcp_nodelay, std::move(socket_options), + [&](socket_t sock2, struct addrinfo &ai) -> bool { + if (!intf.empty()) { +#ifdef USE_IF2IP + auto ip = if2ip(intf); + if (ip.empty()) { ip = intf; } + if (!bind_ip_address(sock2, ip.c_str())) { + error = Error::BindIPAddress; + return false; + } +#endif + } + + set_nonblocking(sock2, true); + + auto ret = + ::connect(sock2, ai.ai_addr, static_cast(ai.ai_addrlen)); + + if (ret < 0) { + if (is_connection_error() || + !wait_until_socket_is_ready(sock2, connection_timeout_sec, + connection_timeout_usec)) { + error = Error::Connection; + return false; + } + } + + set_nonblocking(sock2, false); + + { + timeval tv; + tv.tv_sec = static_cast(read_timeout_sec); + tv.tv_usec = static_cast(read_timeout_usec); + setsockopt(sock2, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv, sizeof(tv)); + } + { + timeval tv; + tv.tv_sec = static_cast(write_timeout_sec); + tv.tv_usec = static_cast(write_timeout_usec); + setsockopt(sock2, SOL_SOCKET, SO_SNDTIMEO, (char *)&tv, sizeof(tv)); + } + + error = Error::Success; + return true; + }); + + if (sock != INVALID_SOCKET) { + error = Error::Success; + } else { + if (error == Error::Success) { error = Error::Connection; } + } + + return sock; +} + +inline void get_remote_ip_and_port(const struct sockaddr_storage &addr, + socklen_t addr_len, std::string &ip, + int &port) { + if (addr.ss_family == AF_INET) { + port = ntohs(reinterpret_cast(&addr)->sin_port); + } else if (addr.ss_family == AF_INET6) { + port = + ntohs(reinterpret_cast(&addr)->sin6_port); + } + + std::array ipstr{}; + if (!getnameinfo(reinterpret_cast(&addr), addr_len, + ipstr.data(), static_cast(ipstr.size()), nullptr, + 0, NI_NUMERICHOST)) { + ip = ipstr.data(); + } +} + +inline void get_remote_ip_and_port(socket_t sock, std::string &ip, int &port) { + struct sockaddr_storage addr; + socklen_t addr_len = sizeof(addr); + + if (!getpeername(sock, reinterpret_cast(&addr), + &addr_len)) { + get_remote_ip_and_port(addr, addr_len, ip, port); + } +} + +inline constexpr unsigned int str2tag_core(const char *s, size_t l, + unsigned int h) { + return (l == 0) ? h + : str2tag_core(s + 1, l - 1, + (h * 33) ^ static_cast(*s)); +} + +inline unsigned int str2tag(const std::string &s) { + return str2tag_core(s.data(), s.size(), 0); +} + +namespace udl { + +inline constexpr unsigned int operator"" _t(const char *s, size_t l) { + return str2tag_core(s, l, 0); +} + +} // namespace udl + +inline const char * +find_content_type(const std::string &path, + const std::map &user_data) { + auto ext = file_extension(path); + + auto it = user_data.find(ext); + if (it != user_data.end()) { return it->second.c_str(); } + + using udl::operator""_t; + + switch (str2tag(ext)) { + default: return nullptr; + case "css"_t: return "text/css"; + case "csv"_t: return "text/csv"; + case "txt"_t: return "text/plain"; + case "vtt"_t: return "text/vtt"; + case "htm"_t: + case "html"_t: return "text/html"; + + case "apng"_t: return "image/apng"; + case "avif"_t: return "image/avif"; + case "bmp"_t: return "image/bmp"; + case "gif"_t: return "image/gif"; + case "png"_t: return "image/png"; + case "svg"_t: return "image/svg+xml"; + case "webp"_t: return "image/webp"; + case "ico"_t: return "image/x-icon"; + case "tif"_t: return "image/tiff"; + case "tiff"_t: return "image/tiff"; + case "jpg"_t: + case "jpeg"_t: return "image/jpeg"; + + case "mp4"_t: return "video/mp4"; + case "mpeg"_t: return "video/mpeg"; + case "webm"_t: return "video/webm"; + + case "mp3"_t: return "audio/mp3"; + case "mpga"_t: return "audio/mpeg"; + case "weba"_t: return "audio/webm"; + case "wav"_t: return "audio/wave"; + + case "otf"_t: return "font/otf"; + case "ttf"_t: return "font/ttf"; + case "woff"_t: return "font/woff"; + case "woff2"_t: return "font/woff2"; + + case "7z"_t: return "application/x-7z-compressed"; + case "atom"_t: return "application/atom+xml"; + case "pdf"_t: return "application/pdf"; + case "js"_t: + case "mjs"_t: return "application/javascript"; + case "json"_t: return "application/json"; + case "rss"_t: return "application/rss+xml"; + case "tar"_t: return "application/x-tar"; + case "xht"_t: + case "xhtml"_t: return "application/xhtml+xml"; + case "xslt"_t: return "application/xslt+xml"; + case "xml"_t: return "application/xml"; + case "gz"_t: return "application/gzip"; + case "zip"_t: return "application/zip"; + case "wasm"_t: return "application/wasm"; + } +} + +inline const char *status_message(int status) { + switch (status) { + case 100: return "Continue"; + case 101: return "Switching Protocol"; + case 102: return "Processing"; + case 103: return "Early Hints"; + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 203: return "Non-Authoritative Information"; + case 204: return "No Content"; + case 205: return "Reset Content"; + case 206: return "Partial Content"; + case 207: return "Multi-Status"; + case 208: return "Already Reported"; + case 226: return "IM Used"; + case 300: return "Multiple Choice"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 305: return "Use Proxy"; + case 306: return "unused"; + case 307: return "Temporary Redirect"; + case 308: return "Permanent Redirect"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 402: return "Payment Required"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 406: return "Not Acceptable"; + case 407: return "Proxy Authentication Required"; + case 408: return "Request Timeout"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 411: return "Length Required"; + case 412: return "Precondition Failed"; + case 413: return "Payload Too Large"; + case 414: return "URI Too Long"; + case 415: return "Unsupported Media Type"; + case 416: return "Range Not Satisfiable"; + case 417: return "Expectation Failed"; + case 418: return "I'm a teapot"; + case 421: return "Misdirected Request"; + case 422: return "Unprocessable Entity"; + case 423: return "Locked"; + case 424: return "Failed Dependency"; + case 425: return "Too Early"; + case 426: return "Upgrade Required"; + case 428: return "Precondition Required"; + case 429: return "Too Many Requests"; + case 431: return "Request Header Fields Too Large"; + case 451: return "Unavailable For Legal Reasons"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + case 505: return "HTTP Version Not Supported"; + case 506: return "Variant Also Negotiates"; + case 507: return "Insufficient Storage"; + case 508: return "Loop Detected"; + case 510: return "Not Extended"; + case 511: return "Network Authentication Required"; + + default: + case 500: return "Internal Server Error"; + } +} + +inline bool can_compress_content_type(const std::string &content_type) { + return (!content_type.find("text/") && content_type != "text/event-stream") || + content_type == "image/svg+xml" || + content_type == "application/javascript" || + content_type == "application/json" || + content_type == "application/xml" || + content_type == "application/xhtml+xml"; +} + +inline EncodingType encoding_type(const Request &req, const Response &res) { + auto ret = + detail::can_compress_content_type(res.get_header_value("Content-Type")); + if (!ret) { return EncodingType::None; } + + const auto &s = req.get_header_value("Accept-Encoding"); + (void)(s); + +#ifdef CPPHTTPLIB_BROTLI_SUPPORT + // TODO: 'Accept-Encoding' has br, not br;q=0 + ret = s.find("br") != std::string::npos; + if (ret) { return EncodingType::Brotli; } +#endif + +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + // TODO: 'Accept-Encoding' has gzip, not gzip;q=0 + ret = s.find("gzip") != std::string::npos; + if (ret) { return EncodingType::Gzip; } +#endif + + return EncodingType::None; +} + +inline bool nocompressor::compress(const char *data, size_t data_length, + bool /*last*/, Callback callback) { + if (!data_length) { return true; } + return callback(data, data_length); +} + +#ifdef CPPHTTPLIB_ZLIB_SUPPORT +inline gzip_compressor::gzip_compressor() { + std::memset(&strm_, 0, sizeof(strm_)); + strm_.zalloc = Z_NULL; + strm_.zfree = Z_NULL; + strm_.opaque = Z_NULL; + + is_valid_ = deflateInit2(&strm_, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 31, 8, + Z_DEFAULT_STRATEGY) == Z_OK; +} + +inline gzip_compressor::~gzip_compressor() { deflateEnd(&strm_); } + +inline bool gzip_compressor::compress(const char *data, size_t data_length, + bool last, Callback callback) { + assert(is_valid_); + + do { + constexpr size_t max_avail_in = + std::numeric_limits::max(); + + strm_.avail_in = static_cast( + std::min(data_length, max_avail_in)); + strm_.next_in = const_cast(reinterpret_cast(data)); + + data_length -= strm_.avail_in; + data += strm_.avail_in; + + auto flush = (last && data_length == 0) ? Z_FINISH : Z_NO_FLUSH; + int ret = Z_OK; + + std::array buff{}; + do { + strm_.avail_out = static_cast(buff.size()); + strm_.next_out = reinterpret_cast(buff.data()); + + ret = deflate(&strm_, flush); + if (ret == Z_STREAM_ERROR) { return false; } + + if (!callback(buff.data(), buff.size() - strm_.avail_out)) { + return false; + } + } while (strm_.avail_out == 0); + + assert((flush == Z_FINISH && ret == Z_STREAM_END) || + (flush == Z_NO_FLUSH && ret == Z_OK)); + assert(strm_.avail_in == 0); + + } while (data_length > 0); + + return true; +} + +inline gzip_decompressor::gzip_decompressor() { + std::memset(&strm_, 0, sizeof(strm_)); + strm_.zalloc = Z_NULL; + strm_.zfree = Z_NULL; + strm_.opaque = Z_NULL; + + // 15 is the value of wbits, which should be at the maximum possible value + // to ensure that any gzip stream can be decoded. The offset of 32 specifies + // that the stream type should be automatically detected either gzip or + // deflate. + is_valid_ = inflateInit2(&strm_, 32 + 15) == Z_OK; +} + +inline gzip_decompressor::~gzip_decompressor() { inflateEnd(&strm_); } + +inline bool gzip_decompressor::is_valid() const { return is_valid_; } + +inline bool gzip_decompressor::decompress(const char *data, size_t data_length, + Callback callback) { + assert(is_valid_); + + int ret = Z_OK; + + do { + constexpr size_t max_avail_in = + std::numeric_limits::max(); + + strm_.avail_in = static_cast( + std::min(data_length, max_avail_in)); + strm_.next_in = const_cast(reinterpret_cast(data)); + + data_length -= strm_.avail_in; + data += strm_.avail_in; + + std::array buff{}; + while (strm_.avail_in > 0) { + strm_.avail_out = static_cast(buff.size()); + strm_.next_out = reinterpret_cast(buff.data()); + + ret = inflate(&strm_, Z_NO_FLUSH); + assert(ret != Z_STREAM_ERROR); + switch (ret) { + case Z_NEED_DICT: + case Z_DATA_ERROR: + case Z_MEM_ERROR: inflateEnd(&strm_); return false; + } + + if (!callback(buff.data(), buff.size() - strm_.avail_out)) { + return false; + } + } + + if (ret != Z_OK && ret != Z_STREAM_END) return false; + + } while (data_length > 0); + + return true; +} +#endif + +#ifdef CPPHTTPLIB_BROTLI_SUPPORT +inline brotli_compressor::brotli_compressor() { + state_ = BrotliEncoderCreateInstance(nullptr, nullptr, nullptr); +} + +inline brotli_compressor::~brotli_compressor() { + BrotliEncoderDestroyInstance(state_); +} + +inline bool brotli_compressor::compress(const char *data, size_t data_length, + bool last, Callback callback) { + std::array buff{}; + + auto operation = last ? BROTLI_OPERATION_FINISH : BROTLI_OPERATION_PROCESS; + auto available_in = data_length; + auto next_in = reinterpret_cast(data); + + for (;;) { + if (last) { + if (BrotliEncoderIsFinished(state_)) { break; } + } else { + if (!available_in) { break; } + } + + auto available_out = buff.size(); + auto next_out = buff.data(); + + if (!BrotliEncoderCompressStream(state_, operation, &available_in, &next_in, + &available_out, &next_out, nullptr)) { + return false; + } + + auto output_bytes = buff.size() - available_out; + if (output_bytes) { + callback(reinterpret_cast(buff.data()), output_bytes); + } + } + + return true; +} + +inline brotli_decompressor::brotli_decompressor() { + decoder_s = BrotliDecoderCreateInstance(0, 0, 0); + decoder_r = decoder_s ? BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT + : BROTLI_DECODER_RESULT_ERROR; +} + +inline brotli_decompressor::~brotli_decompressor() { + if (decoder_s) { BrotliDecoderDestroyInstance(decoder_s); } +} + +inline bool brotli_decompressor::is_valid() const { return decoder_s; } + +inline bool brotli_decompressor::decompress(const char *data, + size_t data_length, + Callback callback) { + if (decoder_r == BROTLI_DECODER_RESULT_SUCCESS || + decoder_r == BROTLI_DECODER_RESULT_ERROR) { + return 0; + } + + const uint8_t *next_in = (const uint8_t *)data; + size_t avail_in = data_length; + size_t total_out; + + decoder_r = BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT; + + std::array buff{}; + while (decoder_r == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT) { + char *next_out = buff.data(); + size_t avail_out = buff.size(); + + decoder_r = BrotliDecoderDecompressStream( + decoder_s, &avail_in, &next_in, &avail_out, + reinterpret_cast(&next_out), &total_out); + + if (decoder_r == BROTLI_DECODER_RESULT_ERROR) { return false; } + + if (!callback(buff.data(), buff.size() - avail_out)) { return false; } + } + + return decoder_r == BROTLI_DECODER_RESULT_SUCCESS || + decoder_r == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT; +} +#endif + +inline bool has_header(const Headers &headers, const char *key) { + return headers.find(key) != headers.end(); +} + +inline const char *get_header_value(const Headers &headers, const char *key, + size_t id, const char *def) { + auto rng = headers.equal_range(key); + auto it = rng.first; + std::advance(it, static_cast(id)); + if (it != rng.second) { return it->second.c_str(); } + return def; +} + +template +inline bool parse_header(const char *beg, const char *end, T fn) { + // Skip trailing spaces and tabs. + while (beg < end && is_space_or_tab(end[-1])) { + end--; + } + + auto p = beg; + while (p < end && *p != ':') { + p++; + } + + if (p == end) { return false; } + + auto key_end = p; + + if (*p++ != ':') { return false; } + + while (p < end && is_space_or_tab(*p)) { + p++; + } + + if (p < end) { + fn(std::string(beg, key_end), decode_url(std::string(p, end), false)); + return true; + } + + return false; +} + +inline bool read_headers(Stream &strm, Headers &headers) { + const auto bufsiz = 2048; + char buf[bufsiz]; + stream_line_reader line_reader(strm, buf, bufsiz); + + for (;;) { + if (!line_reader.getline()) { return false; } + + // Check if the line ends with CRLF. + if (line_reader.end_with_crlf()) { + // Blank line indicates end of headers. + if (line_reader.size() == 2) { break; } + } else { + continue; // Skip invalid line. + } + + // Exclude CRLF + auto end = line_reader.ptr() + line_reader.size() - 2; + + parse_header(line_reader.ptr(), end, + [&](std::string &&key, std::string &&val) { + headers.emplace(std::move(key), std::move(val)); + }); + } + + return true; +} + +inline bool read_content_with_length(Stream &strm, uint64_t len, + Progress progress, + ContentReceiverWithProgress out) { + char buf[CPPHTTPLIB_RECV_BUFSIZ]; + + uint64_t r = 0; + while (r < len) { + auto read_len = static_cast(len - r); + auto n = strm.read(buf, (std::min)(read_len, CPPHTTPLIB_RECV_BUFSIZ)); + if (n <= 0) { return false; } + + if (!out(buf, static_cast(n), r, len)) { return false; } + r += static_cast(n); + + if (progress) { + if (!progress(r, len)) { return false; } + } + } + + return true; +} + +inline void skip_content_with_length(Stream &strm, uint64_t len) { + char buf[CPPHTTPLIB_RECV_BUFSIZ]; + uint64_t r = 0; + while (r < len) { + auto read_len = static_cast(len - r); + auto n = strm.read(buf, (std::min)(read_len, CPPHTTPLIB_RECV_BUFSIZ)); + if (n <= 0) { return; } + r += static_cast(n); + } +} + +inline bool read_content_without_length(Stream &strm, + ContentReceiverWithProgress out) { + char buf[CPPHTTPLIB_RECV_BUFSIZ]; + uint64_t r = 0; + for (;;) { + auto n = strm.read(buf, CPPHTTPLIB_RECV_BUFSIZ); + if (n < 0) { + return false; + } else if (n == 0) { + return true; + } + + if (!out(buf, static_cast(n), r, 0)) { return false; } + r += static_cast(n); + } + + return true; +} + +inline bool read_content_chunked(Stream &strm, + ContentReceiverWithProgress out) { + const auto bufsiz = 16; + char buf[bufsiz]; + + stream_line_reader line_reader(strm, buf, bufsiz); + + if (!line_reader.getline()) { return false; } + + unsigned long chunk_len; + while (true) { + char *end_ptr; + + chunk_len = std::strtoul(line_reader.ptr(), &end_ptr, 16); + + if (end_ptr == line_reader.ptr()) { return false; } + if (chunk_len == ULONG_MAX) { return false; } + + if (chunk_len == 0) { break; } + + if (!read_content_with_length(strm, chunk_len, nullptr, out)) { + return false; + } + + if (!line_reader.getline()) { return false; } + + if (strcmp(line_reader.ptr(), "\r\n")) { break; } + + if (!line_reader.getline()) { return false; } + } + + if (chunk_len == 0) { + // Reader terminator after chunks + if (!line_reader.getline() || strcmp(line_reader.ptr(), "\r\n")) + return false; + } + + return true; +} + +inline bool is_chunked_transfer_encoding(const Headers &headers) { + return !strcasecmp(get_header_value(headers, "Transfer-Encoding", 0, ""), + "chunked"); +} + +template +bool prepare_content_receiver(T &x, int &status, + ContentReceiverWithProgress receiver, + bool decompress, U callback) { + if (decompress) { + std::string encoding = x.get_header_value("Content-Encoding"); + std::unique_ptr decompressor; + + if (encoding.find("gzip") != std::string::npos || + encoding.find("deflate") != std::string::npos) { +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + decompressor = detail::make_unique(); +#else + status = 415; + return false; +#endif + } else if (encoding.find("br") != std::string::npos) { +#ifdef CPPHTTPLIB_BROTLI_SUPPORT + decompressor = detail::make_unique(); +#else + status = 415; + return false; +#endif + } + + if (decompressor) { + if (decompressor->is_valid()) { + ContentReceiverWithProgress out = [&](const char *buf, size_t n, + uint64_t off, uint64_t len) { + return decompressor->decompress(buf, n, + [&](const char *buf2, size_t n2) { + return receiver(buf2, n2, off, len); + }); + }; + return callback(std::move(out)); + } else { + status = 500; + return false; + } + } + } + + ContentReceiverWithProgress out = [&](const char *buf, size_t n, uint64_t off, + uint64_t len) { + return receiver(buf, n, off, len); + }; + return callback(std::move(out)); +} + +template +bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status, + Progress progress, ContentReceiverWithProgress receiver, + bool decompress) { + return prepare_content_receiver( + x, status, std::move(receiver), decompress, + [&](const ContentReceiverWithProgress &out) { + auto ret = true; + auto exceed_payload_max_length = false; + + if (is_chunked_transfer_encoding(x.headers)) { + ret = read_content_chunked(strm, out); + } else if (!has_header(x.headers, "Content-Length")) { + ret = read_content_without_length(strm, out); + } else { + auto len = get_header_value(x.headers, "Content-Length"); + if (len > payload_max_length) { + exceed_payload_max_length = true; + skip_content_with_length(strm, len); + ret = false; + } else if (len > 0) { + ret = read_content_with_length(strm, len, std::move(progress), out); + } + } + + if (!ret) { status = exceed_payload_max_length ? 413 : 400; } + return ret; + }); +} + +inline ssize_t write_headers(Stream &strm, const Headers &headers) { + ssize_t write_len = 0; + for (const auto &x : headers) { + auto len = + strm.write_format("%s: %s\r\n", x.first.c_str(), x.second.c_str()); + if (len < 0) { return len; } + write_len += len; + } + auto len = strm.write("\r\n"); + if (len < 0) { return len; } + write_len += len; + return write_len; +} + +inline bool write_data(Stream &strm, const char *d, size_t l) { + size_t offset = 0; + while (offset < l) { + auto length = strm.write(d + offset, l - offset); + if (length < 0) { return false; } + offset += static_cast(length); + } + return true; +} + +template +inline bool write_content(Stream &strm, const ContentProvider &content_provider, + size_t offset, size_t length, T is_shutting_down, + Error &error) { + size_t end_offset = offset + length; + auto ok = true; + DataSink data_sink; + + data_sink.write = [&](const char *d, size_t l) -> bool { + if (ok) { + if (write_data(strm, d, l)) { + offset += l; + } else { + ok = false; + } + } + return ok; + }; + + data_sink.is_writable = [&](void) { return ok && strm.is_writable(); }; + + while (offset < end_offset && !is_shutting_down()) { + if (!content_provider(offset, end_offset - offset, data_sink)) { + error = Error::Canceled; + return false; + } + if (!ok) { + error = Error::Write; + return false; + } + } + + error = Error::Success; + return true; +} + +template +inline bool write_content(Stream &strm, const ContentProvider &content_provider, + size_t offset, size_t length, + const T &is_shutting_down) { + auto error = Error::Success; + return write_content(strm, content_provider, offset, length, is_shutting_down, + error); +} + +template +inline bool +write_content_without_length(Stream &strm, + const ContentProvider &content_provider, + const T &is_shutting_down) { + size_t offset = 0; + auto data_available = true; + auto ok = true; + DataSink data_sink; + + data_sink.write = [&](const char *d, size_t l) -> bool { + if (ok) { + offset += l; + if (!write_data(strm, d, l)) { ok = false; } + } + return ok; + }; + + data_sink.done = [&](void) { data_available = false; }; + + data_sink.is_writable = [&](void) { return ok && strm.is_writable(); }; + + while (data_available && !is_shutting_down()) { + if (!content_provider(offset, 0, data_sink)) { return false; } + if (!ok) { return false; } + } + return true; +} + +template +inline bool +write_content_chunked(Stream &strm, const ContentProvider &content_provider, + const T &is_shutting_down, U &compressor, Error &error) { + size_t offset = 0; + auto data_available = true; + auto ok = true; + DataSink data_sink; + + data_sink.write = [&](const char *d, size_t l) -> bool { + if (ok) { + data_available = l > 0; + offset += l; + + std::string payload; + if (compressor.compress(d, l, false, + [&](const char *data, size_t data_len) { + payload.append(data, data_len); + return true; + })) { + if (!payload.empty()) { + // Emit chunked response header and footer for each chunk + auto chunk = + from_i_to_hex(payload.size()) + "\r\n" + payload + "\r\n"; + if (!write_data(strm, chunk.data(), chunk.size())) { ok = false; } + } + } else { + ok = false; + } + } + return ok; + }; + + data_sink.done = [&](void) { + if (!ok) { return; } + + data_available = false; + + std::string payload; + if (!compressor.compress(nullptr, 0, true, + [&](const char *data, size_t data_len) { + payload.append(data, data_len); + return true; + })) { + ok = false; + return; + } + + if (!payload.empty()) { + // Emit chunked response header and footer for each chunk + auto chunk = from_i_to_hex(payload.size()) + "\r\n" + payload + "\r\n"; + if (!write_data(strm, chunk.data(), chunk.size())) { + ok = false; + return; + } + } + + static const std::string done_marker("0\r\n\r\n"); + if (!write_data(strm, done_marker.data(), done_marker.size())) { + ok = false; + } + }; + + data_sink.is_writable = [&](void) { return ok && strm.is_writable(); }; + + while (data_available && !is_shutting_down()) { + if (!content_provider(offset, 0, data_sink)) { + error = Error::Canceled; + return false; + } + if (!ok) { + error = Error::Write; + return false; + } + } + + error = Error::Success; + return true; +} + +template +inline bool write_content_chunked(Stream &strm, + const ContentProvider &content_provider, + const T &is_shutting_down, U &compressor) { + auto error = Error::Success; + return write_content_chunked(strm, content_provider, is_shutting_down, + compressor, error); +} + +template +inline bool redirect(T &cli, Request &req, Response &res, + const std::string &path, const std::string &location, + Error &error) { + Request new_req = req; + new_req.path = path; + new_req.redirect_count_ -= 1; + + if (res.status == 303 && (req.method != "GET" && req.method != "HEAD")) { + new_req.method = "GET"; + new_req.body.clear(); + new_req.headers.clear(); + } + + Response new_res; + + auto ret = cli.send(new_req, new_res, error); + if (ret) { + req = new_req; + res = new_res; + res.location = location; + } + return ret; +} + +inline std::string params_to_query_str(const Params ¶ms) { + std::string query; + + for (auto it = params.begin(); it != params.end(); ++it) { + if (it != params.begin()) { query += "&"; } + query += it->first; + query += "="; + query += encode_query_param(it->second); + } + return query; +} + +inline std::string append_query_params(const char *path, const Params ¶ms) { + std::string path_with_query = path; + const static std::regex re("[^?]+\\?.*"); + auto delm = std::regex_match(path, re) ? '&' : '?'; + path_with_query += delm + params_to_query_str(params); + return path_with_query; +} + +inline void parse_query_text(const std::string &s, Params ¶ms) { + std::set cache; + split(s.data(), s.data() + s.size(), '&', [&](const char *b, const char *e) { + std::string kv(b, e); + if (cache.find(kv) != cache.end()) { return; } + cache.insert(kv); + + std::string key; + std::string val; + split(b, e, '=', [&](const char *b2, const char *e2) { + if (key.empty()) { + key.assign(b2, e2); + } else { + val.assign(b2, e2); + } + }); + + if (!key.empty()) { + params.emplace(decode_url(key, true), decode_url(val, true)); + } + }); +} + +inline bool parse_multipart_boundary(const std::string &content_type, + std::string &boundary) { + auto pos = content_type.find("boundary="); + if (pos == std::string::npos) { return false; } + boundary = content_type.substr(pos + 9); + if (boundary.length() >= 2 && boundary.front() == '"' && + boundary.back() == '"') { + boundary = boundary.substr(1, boundary.size() - 2); + } + return !boundary.empty(); +} + +inline bool parse_range_header(const std::string &s, Ranges &ranges) try { + static auto re_first_range = std::regex(R"(bytes=(\d*-\d*(?:,\s*\d*-\d*)*))"); + std::smatch m; + if (std::regex_match(s, m, re_first_range)) { + auto pos = static_cast(m.position(1)); + auto len = static_cast(m.length(1)); + bool all_valid_ranges = true; + split(&s[pos], &s[pos + len], ',', [&](const char *b, const char *e) { + if (!all_valid_ranges) return; + static auto re_another_range = std::regex(R"(\s*(\d*)-(\d*))"); + std::cmatch cm; + if (std::regex_match(b, e, cm, re_another_range)) { + ssize_t first = -1; + if (!cm.str(1).empty()) { + first = static_cast(std::stoll(cm.str(1))); + } + + ssize_t last = -1; + if (!cm.str(2).empty()) { + last = static_cast(std::stoll(cm.str(2))); + } + + if (first != -1 && last != -1 && first > last) { + all_valid_ranges = false; + return; + } + ranges.emplace_back(std::make_pair(first, last)); + } + }); + return all_valid_ranges; + } + return false; +} catch (...) { return false; } + +class MultipartFormDataParser { +public: + MultipartFormDataParser() = default; + + void set_boundary(std::string &&boundary) { boundary_ = boundary; } + + bool is_valid() const { return is_valid_; } + + bool parse(const char *buf, size_t n, const ContentReceiver &content_callback, + const MultipartContentHeader &header_callback) { + + static const std::regex re_content_disposition( + "^Content-Disposition:\\s*form-data;\\s*name=\"(.*?)\"(?:;\\s*filename=" + "\"(.*?)\")?\\s*$", + std::regex_constants::icase); + static const std::string dash_ = "--"; + static const std::string crlf_ = "\r\n"; + + buf_.append(buf, n); // TODO: performance improvement + + while (!buf_.empty()) { + switch (state_) { + case 0: { // Initial boundary + auto pattern = dash_ + boundary_ + crlf_; + if (pattern.size() > buf_.size()) { return true; } + auto pos = buf_.find(pattern); + if (pos != 0) { return false; } + buf_.erase(0, pattern.size()); + off_ += pattern.size(); + state_ = 1; + break; + } + case 1: { // New entry + clear_file_info(); + state_ = 2; + break; + } + case 2: { // Headers + auto pos = buf_.find(crlf_); + while (pos != std::string::npos) { + // Empty line + if (pos == 0) { + if (!header_callback(file_)) { + is_valid_ = false; + return false; + } + buf_.erase(0, crlf_.size()); + off_ += crlf_.size(); + state_ = 3; + break; + } + + static const std::string header_name = "content-type:"; + const auto header = buf_.substr(0, pos); + if (start_with_case_ignore(header, header_name)) { + file_.content_type = trim_copy(header.substr(header_name.size())); + } else { + std::smatch m; + if (std::regex_match(header, m, re_content_disposition)) { + file_.name = m[1]; + file_.filename = m[2]; + } + } + + buf_.erase(0, pos + crlf_.size()); + off_ += pos + crlf_.size(); + pos = buf_.find(crlf_); + } + if (state_ != 3) { return true; } + break; + } + case 3: { // Body + { + auto pattern = crlf_ + dash_; + if (pattern.size() > buf_.size()) { return true; } + + auto pos = find_string(buf_, pattern); + + if (!content_callback(buf_.data(), pos)) { + is_valid_ = false; + return false; + } + + off_ += pos; + buf_.erase(0, pos); + } + { + auto pattern = crlf_ + dash_ + boundary_; + if (pattern.size() > buf_.size()) { return true; } + + auto pos = buf_.find(pattern); + if (pos != std::string::npos) { + if (!content_callback(buf_.data(), pos)) { + is_valid_ = false; + return false; + } + + off_ += pos + pattern.size(); + buf_.erase(0, pos + pattern.size()); + state_ = 4; + } else { + if (!content_callback(buf_.data(), pattern.size())) { + is_valid_ = false; + return false; + } + + off_ += pattern.size(); + buf_.erase(0, pattern.size()); + } + } + break; + } + case 4: { // Boundary + if (crlf_.size() > buf_.size()) { return true; } + if (buf_.compare(0, crlf_.size(), crlf_) == 0) { + buf_.erase(0, crlf_.size()); + off_ += crlf_.size(); + state_ = 1; + } else { + auto pattern = dash_ + crlf_; + if (pattern.size() > buf_.size()) { return true; } + if (buf_.compare(0, pattern.size(), pattern) == 0) { + buf_.erase(0, pattern.size()); + off_ += pattern.size(); + is_valid_ = true; + state_ = 5; + } else { + return true; + } + } + break; + } + case 5: { // Done + is_valid_ = false; + return false; + } + } + } + + return true; + } + +private: + void clear_file_info() { + file_.name.clear(); + file_.filename.clear(); + file_.content_type.clear(); + } + + bool start_with_case_ignore(const std::string &a, + const std::string &b) const { + if (a.size() < b.size()) { return false; } + for (size_t i = 0; i < b.size(); i++) { + if (::tolower(a[i]) != ::tolower(b[i])) { return false; } + } + return true; + } + + bool start_with(const std::string &a, size_t off, + const std::string &b) const { + if (a.size() - off < b.size()) { return false; } + for (size_t i = 0; i < b.size(); i++) { + if (a[i + off] != b[i]) { return false; } + } + return true; + } + + size_t find_string(const std::string &s, const std::string &pattern) const { + auto c = pattern.front(); + + size_t off = 0; + while (off < s.size()) { + auto pos = s.find(c, off); + if (pos == std::string::npos) { return s.size(); } + + auto rem = s.size() - pos; + if (pattern.size() > rem) { return pos; } + + if (start_with(s, pos, pattern)) { return pos; } + + off = pos + 1; + } + + return s.size(); + } + + std::string boundary_; + + std::string buf_; + size_t state_ = 0; + bool is_valid_ = false; + size_t off_ = 0; + MultipartFormData file_; +}; + +inline std::string to_lower(const char *beg, const char *end) { + std::string out; + auto it = beg; + while (it != end) { + out += static_cast(::tolower(*it)); + it++; + } + return out; +} + +inline std::string make_multipart_data_boundary() { + static const char data[] = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + + // std::random_device might actually be deterministic on some + // platforms, but due to lack of support in the c++ standard library, + // doing better requires either some ugly hacks or breaking portability. + std::random_device seed_gen; + // Request 128 bits of entropy for initialization + std::seed_seq seed_sequence{seed_gen(), seed_gen(), seed_gen(), seed_gen()}; + std::mt19937 engine(seed_sequence); + + std::string result = "--cpp-httplib-multipart-data-"; + + for (auto i = 0; i < 16; i++) { + result += data[engine() % (sizeof(data) - 1)]; + } + + return result; +} + +inline std::pair +get_range_offset_and_length(const Request &req, size_t content_length, + size_t index) { + auto r = req.ranges[index]; + + if (r.first == -1 && r.second == -1) { + return std::make_pair(0, content_length); + } + + auto slen = static_cast(content_length); + + if (r.first == -1) { + r.first = (std::max)(static_cast(0), slen - r.second); + r.second = slen - 1; + } + + if (r.second == -1) { r.second = slen - 1; } + return std::make_pair(r.first, static_cast(r.second - r.first) + 1); +} + +inline std::string make_content_range_header_field(size_t offset, size_t length, + size_t content_length) { + std::string field = "bytes "; + field += std::to_string(offset); + field += "-"; + field += std::to_string(offset + length - 1); + field += "/"; + field += std::to_string(content_length); + return field; +} + +template +bool process_multipart_ranges_data(const Request &req, Response &res, + const std::string &boundary, + const std::string &content_type, + SToken stoken, CToken ctoken, + Content content) { + for (size_t i = 0; i < req.ranges.size(); i++) { + ctoken("--"); + stoken(boundary); + ctoken("\r\n"); + if (!content_type.empty()) { + ctoken("Content-Type: "); + stoken(content_type); + ctoken("\r\n"); + } + + auto offsets = get_range_offset_and_length(req, res.body.size(), i); + auto offset = offsets.first; + auto length = offsets.second; + + ctoken("Content-Range: "); + stoken(make_content_range_header_field(offset, length, res.body.size())); + ctoken("\r\n"); + ctoken("\r\n"); + if (!content(offset, length)) { return false; } + ctoken("\r\n"); + } + + ctoken("--"); + stoken(boundary); + ctoken("--\r\n"); + + return true; +} + +inline bool make_multipart_ranges_data(const Request &req, Response &res, + const std::string &boundary, + const std::string &content_type, + std::string &data) { + return process_multipart_ranges_data( + req, res, boundary, content_type, + [&](const std::string &token) { data += token; }, + [&](const char *token) { data += token; }, + [&](size_t offset, size_t length) { + if (offset < res.body.size()) { + data += res.body.substr(offset, length); + return true; + } + return false; + }); +} + +inline size_t +get_multipart_ranges_data_length(const Request &req, Response &res, + const std::string &boundary, + const std::string &content_type) { + size_t data_length = 0; + + process_multipart_ranges_data( + req, res, boundary, content_type, + [&](const std::string &token) { data_length += token.size(); }, + [&](const char *token) { data_length += strlen(token); }, + [&](size_t /*offset*/, size_t length) { + data_length += length; + return true; + }); + + return data_length; +} + +template +inline bool write_multipart_ranges_data(Stream &strm, const Request &req, + Response &res, + const std::string &boundary, + const std::string &content_type, + const T &is_shutting_down) { + return process_multipart_ranges_data( + req, res, boundary, content_type, + [&](const std::string &token) { strm.write(token); }, + [&](const char *token) { strm.write(token); }, + [&](size_t offset, size_t length) { + return write_content(strm, res.content_provider_, offset, length, + is_shutting_down); + }); +} + +inline std::pair +get_range_offset_and_length(const Request &req, const Response &res, + size_t index) { + auto r = req.ranges[index]; + + if (r.second == -1) { + r.second = static_cast(res.content_length_) - 1; + } + + return std::make_pair(r.first, r.second - r.first + 1); +} + +inline bool expect_content(const Request &req) { + if (req.method == "POST" || req.method == "PUT" || req.method == "PATCH" || + req.method == "PRI" || req.method == "DELETE") { + return true; + } + // TODO: check if Content-Length is set + return false; +} + +inline bool has_crlf(const char *s) { + auto p = s; + while (*p) { + if (*p == '\r' || *p == '\n') { return true; } + p++; + } + return false; +} + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +template +inline std::string message_digest(const std::string &s, Init init, + Update update, Final final, + size_t digest_length) { + using namespace std; + + std::vector md(digest_length, 0); + CTX ctx; + init(&ctx); + update(&ctx, s.data(), s.size()); + final(md.data(), &ctx); + + stringstream ss; + for (auto c : md) { + ss << setfill('0') << setw(2) << hex << (unsigned int)c; + } + return ss.str(); +} + +inline std::string MD5(const std::string &s) { + return message_digest(s, MD5_Init, MD5_Update, MD5_Final, + MD5_DIGEST_LENGTH); +} + +inline std::string SHA_256(const std::string &s) { + return message_digest(s, SHA256_Init, SHA256_Update, SHA256_Final, + SHA256_DIGEST_LENGTH); +} + +inline std::string SHA_512(const std::string &s) { + return message_digest(s, SHA512_Init, SHA512_Update, SHA512_Final, + SHA512_DIGEST_LENGTH); +} +#endif + +#ifdef _WIN32 +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +// NOTE: This code came up with the following stackoverflow post: +// https://stackoverflow.com/questions/9507184/can-openssl-on-windows-use-the-system-certificate-store +inline bool load_system_certs_on_windows(X509_STORE *store) { + auto hStore = CertOpenSystemStoreW((HCRYPTPROV_LEGACY)NULL, L"ROOT"); + + if (!hStore) { return false; } + + PCCERT_CONTEXT pContext = NULL; + while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) != + nullptr) { + auto encoded_cert = + static_cast(pContext->pbCertEncoded); + + auto x509 = d2i_X509(NULL, &encoded_cert, pContext->cbCertEncoded); + if (x509) { + X509_STORE_add_cert(store, x509); + X509_free(x509); + } + } + + CertFreeCertificateContext(pContext); + CertCloseStore(hStore, 0); + + return true; +} +#endif + +class WSInit { +public: + WSInit() { + WSADATA wsaData; + WSAStartup(0x0002, &wsaData); + } + + ~WSInit() { WSACleanup(); } +}; + +static WSInit wsinit_; +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline std::pair make_digest_authentication_header( + const Request &req, const std::map &auth, + size_t cnonce_count, const std::string &cnonce, const std::string &username, + const std::string &password, bool is_proxy = false) { + using namespace std; + + string nc; + { + stringstream ss; + ss << setfill('0') << setw(8) << hex << cnonce_count; + nc = ss.str(); + } + + auto qop = auth.at("qop"); + if (qop.find("auth-int") != std::string::npos) { + qop = "auth-int"; + } else { + qop = "auth"; + } + + std::string algo = "MD5"; + if (auth.find("algorithm") != auth.end()) { algo = auth.at("algorithm"); } + + string response; + { + auto H = algo == "SHA-256" + ? detail::SHA_256 + : algo == "SHA-512" ? detail::SHA_512 : detail::MD5; + + auto A1 = username + ":" + auth.at("realm") + ":" + password; + + auto A2 = req.method + ":" + req.path; + if (qop == "auth-int") { A2 += ":" + H(req.body); } + + response = H(H(A1) + ":" + auth.at("nonce") + ":" + nc + ":" + cnonce + + ":" + qop + ":" + H(A2)); + } + + auto field = "Digest username=\"" + username + "\", realm=\"" + + auth.at("realm") + "\", nonce=\"" + auth.at("nonce") + + "\", uri=\"" + req.path + "\", algorithm=" + algo + + ", qop=" + qop + ", nc=\"" + nc + "\", cnonce=\"" + cnonce + + "\", response=\"" + response + "\""; + + auto key = is_proxy ? "Proxy-Authorization" : "Authorization"; + return std::make_pair(key, field); +} +#endif + +inline bool parse_www_authenticate(const Response &res, + std::map &auth, + bool is_proxy) { + auto auth_key = is_proxy ? "Proxy-Authenticate" : "WWW-Authenticate"; + if (res.has_header(auth_key)) { + static auto re = std::regex(R"~((?:(?:,\s*)?(.+?)=(?:"(.*?)"|([^,]*))))~"); + auto s = res.get_header_value(auth_key); + auto pos = s.find(' '); + if (pos != std::string::npos) { + auto type = s.substr(0, pos); + if (type == "Basic") { + return false; + } else if (type == "Digest") { + s = s.substr(pos + 1); + auto beg = std::sregex_iterator(s.begin(), s.end(), re); + for (auto i = beg; i != std::sregex_iterator(); ++i) { + auto m = *i; + auto key = s.substr(static_cast(m.position(1)), + static_cast(m.length(1))); + auto val = m.length(2) > 0 + ? s.substr(static_cast(m.position(2)), + static_cast(m.length(2))) + : s.substr(static_cast(m.position(3)), + static_cast(m.length(3))); + auth[key] = val; + } + return true; + } + } + } + return false; +} + +// https://stackoverflow.com/questions/440133/how-do-i-create-a-random-alpha-numeric-string-in-c/440240#answer-440240 +inline std::string random_string(size_t length) { + auto randchar = []() -> char { + const char charset[] = "0123456789" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz"; + const size_t max_index = (sizeof(charset) - 1); + return charset[static_cast(std::rand()) % max_index]; + }; + std::string str(length, 0); + std::generate_n(str.begin(), length, randchar); + return str; +} + +class ContentProviderAdapter { +public: + explicit ContentProviderAdapter( + ContentProviderWithoutLength &&content_provider) + : content_provider_(content_provider) {} + + bool operator()(size_t offset, size_t, DataSink &sink) { + return content_provider_(offset, sink); + } + +private: + ContentProviderWithoutLength content_provider_; +}; + +} // namespace detail + +// Header utilities +inline std::pair make_range_header(Ranges ranges) { + std::string field = "bytes="; + auto i = 0; + for (auto r : ranges) { + if (i != 0) { field += ", "; } + if (r.first != -1) { field += std::to_string(r.first); } + field += '-'; + if (r.second != -1) { field += std::to_string(r.second); } + i++; + } + return std::make_pair("Range", std::move(field)); +} + +inline std::pair +make_basic_authentication_header(const std::string &username, + const std::string &password, bool is_proxy) { + auto field = "Basic " + detail::base64_encode(username + ":" + password); + auto key = is_proxy ? "Proxy-Authorization" : "Authorization"; + return std::make_pair(key, std::move(field)); +} + +inline std::pair +make_bearer_token_authentication_header(const std::string &token, + bool is_proxy = false) { + auto field = "Bearer " + token; + auto key = is_proxy ? "Proxy-Authorization" : "Authorization"; + return std::make_pair(key, std::move(field)); +} + +// Request implementation +inline bool Request::has_header(const char *key) const { + return detail::has_header(headers, key); +} + +inline std::string Request::get_header_value(const char *key, size_t id) const { + return detail::get_header_value(headers, key, id, ""); +} + +inline size_t Request::get_header_value_count(const char *key) const { + auto r = headers.equal_range(key); + return static_cast(std::distance(r.first, r.second)); +} + +inline void Request::set_header(const char *key, const char *val) { + if (!detail::has_crlf(key) && !detail::has_crlf(val)) { + headers.emplace(key, val); + } +} + +inline void Request::set_header(const char *key, const std::string &val) { + if (!detail::has_crlf(key) && !detail::has_crlf(val.c_str())) { + headers.emplace(key, val); + } +} + +inline bool Request::has_param(const char *key) const { + return params.find(key) != params.end(); +} + +inline std::string Request::get_param_value(const char *key, size_t id) const { + auto rng = params.equal_range(key); + auto it = rng.first; + std::advance(it, static_cast(id)); + if (it != rng.second) { return it->second; } + return std::string(); +} + +inline size_t Request::get_param_value_count(const char *key) const { + auto r = params.equal_range(key); + return static_cast(std::distance(r.first, r.second)); +} + +inline bool Request::is_multipart_form_data() const { + const auto &content_type = get_header_value("Content-Type"); + return !content_type.find("multipart/form-data"); +} + +inline bool Request::has_file(const char *key) const { + return files.find(key) != files.end(); +} + +inline MultipartFormData Request::get_file_value(const char *key) const { + auto it = files.find(key); + if (it != files.end()) { return it->second; } + return MultipartFormData(); +} + +// Response implementation +inline bool Response::has_header(const char *key) const { + return headers.find(key) != headers.end(); +} + +inline std::string Response::get_header_value(const char *key, + size_t id) const { + return detail::get_header_value(headers, key, id, ""); +} + +inline size_t Response::get_header_value_count(const char *key) const { + auto r = headers.equal_range(key); + return static_cast(std::distance(r.first, r.second)); +} + +inline void Response::set_header(const char *key, const char *val) { + if (!detail::has_crlf(key) && !detail::has_crlf(val)) { + headers.emplace(key, val); + } +} + +inline void Response::set_header(const char *key, const std::string &val) { + if (!detail::has_crlf(key) && !detail::has_crlf(val.c_str())) { + headers.emplace(key, val); + } +} + +inline void Response::set_redirect(const char *url, int stat) { + if (!detail::has_crlf(url)) { + set_header("Location", url); + if (300 <= stat && stat < 400) { + this->status = stat; + } else { + this->status = 302; + } + } +} + +inline void Response::set_redirect(const std::string &url, int stat) { + set_redirect(url.c_str(), stat); +} + +inline void Response::set_content(const char *s, size_t n, + const char *content_type) { + body.assign(s, n); + + auto rng = headers.equal_range("Content-Type"); + headers.erase(rng.first, rng.second); + set_header("Content-Type", content_type); +} + +inline void Response::set_content(const std::string &s, + const char *content_type) { + set_content(s.data(), s.size(), content_type); +} + +inline void Response::set_content_provider( + size_t in_length, const char *content_type, ContentProvider provider, + ContentProviderResourceReleaser resource_releaser) { + assert(in_length > 0); + set_header("Content-Type", content_type); + content_length_ = in_length; + content_provider_ = std::move(provider); + content_provider_resource_releaser_ = resource_releaser; + is_chunked_content_provider_ = false; +} + +inline void Response::set_content_provider( + const char *content_type, ContentProviderWithoutLength provider, + ContentProviderResourceReleaser resource_releaser) { + set_header("Content-Type", content_type); + content_length_ = 0; + content_provider_ = detail::ContentProviderAdapter(std::move(provider)); + content_provider_resource_releaser_ = resource_releaser; + is_chunked_content_provider_ = false; +} + +inline void Response::set_chunked_content_provider( + const char *content_type, ContentProviderWithoutLength provider, + ContentProviderResourceReleaser resource_releaser) { + set_header("Content-Type", content_type); + content_length_ = 0; + content_provider_ = detail::ContentProviderAdapter(std::move(provider)); + content_provider_resource_releaser_ = resource_releaser; + is_chunked_content_provider_ = true; +} + +// Result implementation +inline bool Result::has_request_header(const char *key) const { + return request_headers_.find(key) != request_headers_.end(); +} + +inline std::string Result::get_request_header_value(const char *key, + size_t id) const { + return detail::get_header_value(request_headers_, key, id, ""); +} + +inline size_t Result::get_request_header_value_count(const char *key) const { + auto r = request_headers_.equal_range(key); + return static_cast(std::distance(r.first, r.second)); +} + +// Stream implementation +inline ssize_t Stream::write(const char *ptr) { + return write(ptr, strlen(ptr)); +} + +inline ssize_t Stream::write(const std::string &s) { + return write(s.data(), s.size()); +} + +namespace detail { + +// Socket stream implementation +inline SocketStream::SocketStream(socket_t sock, time_t read_timeout_sec, + time_t read_timeout_usec, + time_t write_timeout_sec, + time_t write_timeout_usec) + : sock_(sock), read_timeout_sec_(read_timeout_sec), + read_timeout_usec_(read_timeout_usec), + write_timeout_sec_(write_timeout_sec), + write_timeout_usec_(write_timeout_usec) {} + +inline SocketStream::~SocketStream() {} + +inline bool SocketStream::is_readable() const { + return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0; +} + +inline bool SocketStream::is_writable() const { + return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0; +} + +inline ssize_t SocketStream::read(char *ptr, size_t size) { + if (!is_readable()) { return -1; } + +#ifdef _WIN32 + if (size > static_cast((std::numeric_limits::max)())) { + return -1; + } + return recv(sock_, ptr, static_cast(size), CPPHTTPLIB_RECV_FLAGS); +#else + return handle_EINTR( + [&]() { return recv(sock_, ptr, size, CPPHTTPLIB_RECV_FLAGS); }); +#endif +} + +inline ssize_t SocketStream::write(const char *ptr, size_t size) { + if (!is_writable()) { return -1; } + +#ifdef _WIN32 + if (size > static_cast((std::numeric_limits::max)())) { + return -1; + } + return send(sock_, ptr, static_cast(size), CPPHTTPLIB_SEND_FLAGS); +#else + return handle_EINTR( + [&]() { return send(sock_, ptr, size, CPPHTTPLIB_SEND_FLAGS); }); +#endif +} + +inline void SocketStream::get_remote_ip_and_port(std::string &ip, + int &port) const { + return detail::get_remote_ip_and_port(sock_, ip, port); +} + +inline socket_t SocketStream::socket() const { return sock_; } + +// Buffer stream implementation +inline bool BufferStream::is_readable() const { return true; } + +inline bool BufferStream::is_writable() const { return true; } + +inline ssize_t BufferStream::read(char *ptr, size_t size) { +#if defined(_MSC_VER) && _MSC_VER <= 1900 + auto len_read = buffer._Copy_s(ptr, size, size, position); +#else + auto len_read = buffer.copy(ptr, size, position); +#endif + position += static_cast(len_read); + return static_cast(len_read); +} + +inline ssize_t BufferStream::write(const char *ptr, size_t size) { + buffer.append(ptr, size); + return static_cast(size); +} + +inline void BufferStream::get_remote_ip_and_port(std::string & /*ip*/, + int & /*port*/) const {} + +inline socket_t BufferStream::socket() const { return 0; } + +inline const std::string &BufferStream::get_buffer() const { return buffer; } + +} // namespace detail + +// HTTP server implementation +inline Server::Server() + : new_task_queue( + [] { return new ThreadPool(CPPHTTPLIB_THREAD_POOL_COUNT); }), + svr_sock_(INVALID_SOCKET), is_running_(false) { +#ifndef _WIN32 + signal(SIGPIPE, SIG_IGN); +#endif +} + +inline Server::~Server() {} + +inline Server &Server::Get(const std::string &pattern, Handler handler) { + get_handlers_.push_back( + std::make_pair(std::regex(pattern), std::move(handler))); + return *this; +} + +inline Server &Server::Post(const std::string &pattern, Handler handler) { + post_handlers_.push_back( + std::make_pair(std::regex(pattern), std::move(handler))); + return *this; +} + +inline Server &Server::Post(const std::string &pattern, + HandlerWithContentReader handler) { + post_handlers_for_content_reader_.push_back( + std::make_pair(std::regex(pattern), std::move(handler))); + return *this; +} + +inline Server &Server::Put(const std::string &pattern, Handler handler) { + put_handlers_.push_back( + std::make_pair(std::regex(pattern), std::move(handler))); + return *this; +} + +inline Server &Server::Put(const std::string &pattern, + HandlerWithContentReader handler) { + put_handlers_for_content_reader_.push_back( + std::make_pair(std::regex(pattern), std::move(handler))); + return *this; +} + +inline Server &Server::Patch(const std::string &pattern, Handler handler) { + patch_handlers_.push_back( + std::make_pair(std::regex(pattern), std::move(handler))); + return *this; +} + +inline Server &Server::Patch(const std::string &pattern, + HandlerWithContentReader handler) { + patch_handlers_for_content_reader_.push_back( + std::make_pair(std::regex(pattern), std::move(handler))); + return *this; +} + +inline Server &Server::Delete(const std::string &pattern, Handler handler) { + delete_handlers_.push_back( + std::make_pair(std::regex(pattern), std::move(handler))); + return *this; +} + +inline Server &Server::Delete(const std::string &pattern, + HandlerWithContentReader handler) { + delete_handlers_for_content_reader_.push_back( + std::make_pair(std::regex(pattern), std::move(handler))); + return *this; +} + +inline Server &Server::Options(const std::string &pattern, Handler handler) { + options_handlers_.push_back( + std::make_pair(std::regex(pattern), std::move(handler))); + return *this; +} + +inline bool Server::set_base_dir(const std::string &dir, + const std::string &mount_point) { + return set_mount_point(mount_point, dir); +} + +inline bool Server::set_mount_point(const std::string &mount_point, + const std::string &dir, Headers headers) { + if (detail::is_dir(dir)) { + std::string mnt = !mount_point.empty() ? mount_point : "/"; + if (!mnt.empty() && mnt[0] == '/') { + base_dirs_.push_back({mnt, dir, std::move(headers)}); + return true; + } + } + return false; +} + +inline bool Server::remove_mount_point(const std::string &mount_point) { + for (auto it = base_dirs_.begin(); it != base_dirs_.end(); ++it) { + if (it->mount_point == mount_point) { + base_dirs_.erase(it); + return true; + } + } + return false; +} + +inline Server & +Server::set_file_extension_and_mimetype_mapping(const char *ext, + const char *mime) { + file_extension_and_mimetype_map_[ext] = mime; + return *this; +} + +inline Server &Server::set_file_request_handler(Handler handler) { + file_request_handler_ = std::move(handler); + return *this; +} + +inline Server &Server::set_error_handler(HandlerWithResponse handler) { + error_handler_ = std::move(handler); + return *this; +} + +inline Server &Server::set_error_handler(Handler handler) { + error_handler_ = [handler](const Request &req, Response &res) { + handler(req, res); + return HandlerResponse::Handled; + }; + return *this; +} + +inline Server &Server::set_exception_handler(ExceptionHandler handler) { + exception_handler_ = std::move(handler); + return *this; +} + +inline Server &Server::set_pre_routing_handler(HandlerWithResponse handler) { + pre_routing_handler_ = std::move(handler); + return *this; +} + +inline Server &Server::set_post_routing_handler(Handler handler) { + post_routing_handler_ = std::move(handler); + return *this; +} + +inline Server &Server::set_logger(Logger logger) { + logger_ = std::move(logger); + return *this; +} + +inline Server & +Server::set_expect_100_continue_handler(Expect100ContinueHandler handler) { + expect_100_continue_handler_ = std::move(handler); + + return *this; +} + +inline Server &Server::set_address_family(int family) { + address_family_ = family; + return *this; +} + +inline Server &Server::set_tcp_nodelay(bool on) { + tcp_nodelay_ = on; + return *this; +} + +inline Server &Server::set_socket_options(SocketOptions socket_options) { + socket_options_ = std::move(socket_options); + return *this; +} + +inline Server &Server::set_default_headers(Headers headers) { + default_headers_ = std::move(headers); + return *this; +} + +inline Server &Server::set_keep_alive_max_count(size_t count) { + keep_alive_max_count_ = count; + return *this; +} + +inline Server &Server::set_keep_alive_timeout(time_t sec) { + keep_alive_timeout_sec_ = sec; + return *this; +} + +inline Server &Server::set_read_timeout(time_t sec, time_t usec) { + read_timeout_sec_ = sec; + read_timeout_usec_ = usec; + return *this; +} + +inline Server &Server::set_write_timeout(time_t sec, time_t usec) { + write_timeout_sec_ = sec; + write_timeout_usec_ = usec; + return *this; +} + +inline Server &Server::set_idle_interval(time_t sec, time_t usec) { + idle_interval_sec_ = sec; + idle_interval_usec_ = usec; + return *this; +} + +inline Server &Server::set_payload_max_length(size_t length) { + payload_max_length_ = length; + return *this; +} + +inline bool Server::bind_to_port(const char *host, int port, int socket_flags) { + if (bind_internal(host, port, socket_flags) < 0) return false; + return true; +} +inline int Server::bind_to_any_port(const char *host, int socket_flags) { + return bind_internal(host, 0, socket_flags); +} + +inline bool Server::listen_after_bind() { return listen_internal(); } + +inline bool Server::listen(const char *host, int port, int socket_flags) { + return bind_to_port(host, port, socket_flags) && listen_internal(); +} + +inline bool Server::is_running() const { return is_running_; } + +inline void Server::stop() { + if (is_running_) { + assert(svr_sock_ != INVALID_SOCKET); + std::atomic sock(svr_sock_.exchange(INVALID_SOCKET)); + detail::shutdown_socket(sock); + detail::close_socket(sock); + } +} + +inline bool Server::parse_request_line(const char *s, Request &req) { + auto len = strlen(s); + if (len < 2 || s[len - 2] != '\r' || s[len - 1] != '\n') { return false; } + len -= 2; + + { + size_t count = 0; + + detail::split(s, s + len, ' ', [&](const char *b, const char *e) { + switch (count) { + case 0: req.method = std::string(b, e); break; + case 1: req.target = std::string(b, e); break; + case 2: req.version = std::string(b, e); break; + default: break; + } + count++; + }); + + if (count != 3) { return false; } + } + + static const std::set methods{ + "GET", "HEAD", "POST", "PUT", "DELETE", + "CONNECT", "OPTIONS", "TRACE", "PATCH", "PRI"}; + + if (methods.find(req.method) == methods.end()) { return false; } + + if (req.version != "HTTP/1.1" && req.version != "HTTP/1.0") { return false; } + + { + size_t count = 0; + + detail::split(req.target.data(), req.target.data() + req.target.size(), '?', + [&](const char *b, const char *e) { + switch (count) { + case 0: + req.path = detail::decode_url(std::string(b, e), false); + break; + case 1: { + if (e - b > 0) { + detail::parse_query_text(std::string(b, e), req.params); + } + break; + } + default: break; + } + count++; + }); + + if (count > 2) { return false; } + } + + return true; +} + +inline bool Server::write_response(Stream &strm, bool close_connection, + const Request &req, Response &res) { + return write_response_core(strm, close_connection, req, res, false); +} + +inline bool Server::write_response_with_content(Stream &strm, + bool close_connection, + const Request &req, + Response &res) { + return write_response_core(strm, close_connection, req, res, true); +} + +inline bool Server::write_response_core(Stream &strm, bool close_connection, + const Request &req, Response &res, + bool need_apply_ranges) { + assert(res.status != -1); + + if (400 <= res.status && error_handler_ && + error_handler_(req, res) == HandlerResponse::Handled) { + need_apply_ranges = true; + } + + std::string content_type; + std::string boundary; + if (need_apply_ranges) { apply_ranges(req, res, content_type, boundary); } + + // Prepare additional headers + if (close_connection || req.get_header_value("Connection") == "close") { + res.set_header("Connection", "close"); + } else { + std::stringstream ss; + ss << "timeout=" << keep_alive_timeout_sec_ + << ", max=" << keep_alive_max_count_; + res.set_header("Keep-Alive", ss.str()); + } + + if (!res.has_header("Content-Type") && + (!res.body.empty() || res.content_length_ > 0 || res.content_provider_)) { + res.set_header("Content-Type", "text/plain"); + } + + if (!res.has_header("Content-Length") && res.body.empty() && + !res.content_length_ && !res.content_provider_) { + res.set_header("Content-Length", "0"); + } + + if (!res.has_header("Accept-Ranges") && req.method == "HEAD") { + res.set_header("Accept-Ranges", "bytes"); + } + + if (post_routing_handler_) { post_routing_handler_(req, res); } + + // Response line and headers + { + detail::BufferStream bstrm; + + if (!bstrm.write_format("HTTP/1.1 %d %s\r\n", res.status, + detail::status_message(res.status))) { + return false; + } + + if (!detail::write_headers(bstrm, res.headers)) { return false; } + + // Flush buffer + auto &data = bstrm.get_buffer(); + strm.write(data.data(), data.size()); + } + + // Body + auto ret = true; + if (req.method != "HEAD") { + if (!res.body.empty()) { + if (!strm.write(res.body)) { ret = false; } + } else if (res.content_provider_) { + if (write_content_with_provider(strm, req, res, boundary, content_type)) { + res.content_provider_success_ = true; + } else { + res.content_provider_success_ = false; + ret = false; + } + } + } + + // Log + if (logger_) { logger_(req, res); } + + return ret; +} + +inline bool +Server::write_content_with_provider(Stream &strm, const Request &req, + Response &res, const std::string &boundary, + const std::string &content_type) { + auto is_shutting_down = [this]() { + return this->svr_sock_ == INVALID_SOCKET; + }; + + if (res.content_length_ > 0) { + if (req.ranges.empty()) { + return detail::write_content(strm, res.content_provider_, 0, + res.content_length_, is_shutting_down); + } else if (req.ranges.size() == 1) { + auto offsets = + detail::get_range_offset_and_length(req, res.content_length_, 0); + auto offset = offsets.first; + auto length = offsets.second; + return detail::write_content(strm, res.content_provider_, offset, length, + is_shutting_down); + } else { + return detail::write_multipart_ranges_data( + strm, req, res, boundary, content_type, is_shutting_down); + } + } else { + if (res.is_chunked_content_provider_) { + auto type = detail::encoding_type(req, res); + + std::unique_ptr compressor; + if (type == detail::EncodingType::Gzip) { +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + compressor = detail::make_unique(); +#endif + } else if (type == detail::EncodingType::Brotli) { +#ifdef CPPHTTPLIB_BROTLI_SUPPORT + compressor = detail::make_unique(); +#endif + } else { + compressor = detail::make_unique(); + } + assert(compressor != nullptr); + + return detail::write_content_chunked(strm, res.content_provider_, + is_shutting_down, *compressor); + } else { + return detail::write_content_without_length(strm, res.content_provider_, + is_shutting_down); + } + } +} + +inline bool Server::read_content(Stream &strm, Request &req, Response &res) { + MultipartFormDataMap::iterator cur; + if (read_content_core( + strm, req, res, + // Regular + [&](const char *buf, size_t n) { + if (req.body.size() + n > req.body.max_size()) { return false; } + req.body.append(buf, n); + return true; + }, + // Multipart + [&](const MultipartFormData &file) { + cur = req.files.emplace(file.name, file); + return true; + }, + [&](const char *buf, size_t n) { + auto &content = cur->second.content; + if (content.size() + n > content.max_size()) { return false; } + content.append(buf, n); + return true; + })) { + const auto &content_type = req.get_header_value("Content-Type"); + if (!content_type.find("application/x-www-form-urlencoded")) { + detail::parse_query_text(req.body, req.params); + } + return true; + } + return false; +} + +inline bool Server::read_content_with_content_receiver( + Stream &strm, Request &req, Response &res, ContentReceiver receiver, + MultipartContentHeader multipart_header, + ContentReceiver multipart_receiver) { + return read_content_core(strm, req, res, std::move(receiver), + std::move(multipart_header), + std::move(multipart_receiver)); +} + +inline bool Server::read_content_core(Stream &strm, Request &req, Response &res, + ContentReceiver receiver, + MultipartContentHeader mulitpart_header, + ContentReceiver multipart_receiver) { + detail::MultipartFormDataParser multipart_form_data_parser; + ContentReceiverWithProgress out; + + if (req.is_multipart_form_data()) { + const auto &content_type = req.get_header_value("Content-Type"); + std::string boundary; + if (!detail::parse_multipart_boundary(content_type, boundary)) { + res.status = 400; + return false; + } + + multipart_form_data_parser.set_boundary(std::move(boundary)); + out = [&](const char *buf, size_t n, uint64_t /*off*/, uint64_t /*len*/) { + /* For debug + size_t pos = 0; + while (pos < n) { + auto read_size = std::min(1, n - pos); + auto ret = multipart_form_data_parser.parse( + buf + pos, read_size, multipart_receiver, mulitpart_header); + if (!ret) { return false; } + pos += read_size; + } + return true; + */ + return multipart_form_data_parser.parse(buf, n, multipart_receiver, + mulitpart_header); + }; + } else { + out = [receiver](const char *buf, size_t n, uint64_t /*off*/, + uint64_t /*len*/) { return receiver(buf, n); }; + } + + if (req.method == "DELETE" && !req.has_header("Content-Length")) { + return true; + } + + if (!detail::read_content(strm, req, payload_max_length_, res.status, nullptr, + out, true)) { + return false; + } + + if (req.is_multipart_form_data()) { + if (!multipart_form_data_parser.is_valid()) { + res.status = 400; + return false; + } + } + + return true; +} + +inline bool Server::handle_file_request(const Request &req, Response &res, + bool head) { + for (const auto &entry : base_dirs_) { + // Prefix match + if (!req.path.compare(0, entry.mount_point.size(), entry.mount_point)) { + std::string sub_path = "/" + req.path.substr(entry.mount_point.size()); + if (detail::is_valid_path(sub_path)) { + auto path = entry.base_dir + sub_path; + if (path.back() == '/') { path += "index.html"; } + + if (detail::is_file(path)) { + detail::read_file(path, res.body); + auto type = + detail::find_content_type(path, file_extension_and_mimetype_map_); + if (type) { res.set_header("Content-Type", type); } + for (const auto &kv : entry.headers) { + res.set_header(kv.first.c_str(), kv.second); + } + res.status = req.has_header("Range") ? 206 : 200; + if (!head && file_request_handler_) { + file_request_handler_(req, res); + } + return true; + } + } + } + } + return false; +} + +inline socket_t +Server::create_server_socket(const char *host, int port, int socket_flags, + SocketOptions socket_options) const { + return detail::create_socket( + host, port, address_family_, socket_flags, tcp_nodelay_, + std::move(socket_options), + [](socket_t sock, struct addrinfo &ai) -> bool { + if (::bind(sock, ai.ai_addr, static_cast(ai.ai_addrlen))) { + return false; + } + if (::listen(sock, 5)) { // Listen through 5 channels + return false; + } + return true; + }); +} + +inline int Server::bind_internal(const char *host, int port, int socket_flags) { + if (!is_valid()) { return -1; } + + svr_sock_ = create_server_socket(host, port, socket_flags, socket_options_); + if (svr_sock_ == INVALID_SOCKET) { return -1; } + + if (port == 0) { + struct sockaddr_storage addr; + socklen_t addr_len = sizeof(addr); + if (getsockname(svr_sock_, reinterpret_cast(&addr), + &addr_len) == -1) { + return -1; + } + if (addr.ss_family == AF_INET) { + return ntohs(reinterpret_cast(&addr)->sin_port); + } else if (addr.ss_family == AF_INET6) { + return ntohs(reinterpret_cast(&addr)->sin6_port); + } else { + return -1; + } + } else { + return port; + } +} + +inline bool Server::listen_internal() { + auto ret = true; + is_running_ = true; + + { + std::unique_ptr task_queue(new_task_queue()); + + while (svr_sock_ != INVALID_SOCKET) { +#ifndef _WIN32 + if (idle_interval_sec_ > 0 || idle_interval_usec_ > 0) { +#endif + auto val = detail::select_read(svr_sock_, idle_interval_sec_, + idle_interval_usec_); + if (val == 0) { // Timeout + task_queue->on_idle(); + continue; + } +#ifndef _WIN32 + } +#endif + socket_t sock = accept(svr_sock_, nullptr, nullptr); + + if (sock == INVALID_SOCKET) { + if (errno == EMFILE) { + // The per-process limit of open file descriptors has been reached. + // Try to accept new connections after a short sleep. + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + continue; + } + if (svr_sock_ != INVALID_SOCKET) { + detail::close_socket(svr_sock_); + ret = false; + } else { + ; // The server socket was closed by user. + } + break; + } + + { + timeval tv; + tv.tv_sec = static_cast(read_timeout_sec_); + tv.tv_usec = static_cast(read_timeout_usec_); + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv, sizeof(tv)); + } + { + timeval tv; + tv.tv_sec = static_cast(write_timeout_sec_); + tv.tv_usec = static_cast(write_timeout_usec_); + setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (char *)&tv, sizeof(tv)); + } + +#if __cplusplus > 201703L + task_queue->enqueue([=, this]() { process_and_close_socket(sock); }); +#else + task_queue->enqueue([=]() { process_and_close_socket(sock); }); +#endif + } + + task_queue->shutdown(); + } + + is_running_ = false; + return ret; +} + +inline bool Server::routing(Request &req, Response &res, Stream &strm) { + if (pre_routing_handler_ && + pre_routing_handler_(req, res) == HandlerResponse::Handled) { + return true; + } + + // File handler + bool is_head_request = req.method == "HEAD"; + if ((req.method == "GET" || is_head_request) && + handle_file_request(req, res, is_head_request)) { + return true; + } + + if (detail::expect_content(req)) { + // Content reader handler + { + ContentReader reader( + [&](ContentReceiver receiver) { + return read_content_with_content_receiver( + strm, req, res, std::move(receiver), nullptr, nullptr); + }, + [&](MultipartContentHeader header, ContentReceiver receiver) { + return read_content_with_content_receiver(strm, req, res, nullptr, + std::move(header), + std::move(receiver)); + }); + + if (req.method == "POST") { + if (dispatch_request_for_content_reader( + req, res, std::move(reader), + post_handlers_for_content_reader_)) { + return true; + } + } else if (req.method == "PUT") { + if (dispatch_request_for_content_reader( + req, res, std::move(reader), + put_handlers_for_content_reader_)) { + return true; + } + } else if (req.method == "PATCH") { + if (dispatch_request_for_content_reader( + req, res, std::move(reader), + patch_handlers_for_content_reader_)) { + return true; + } + } else if (req.method == "DELETE") { + if (dispatch_request_for_content_reader( + req, res, std::move(reader), + delete_handlers_for_content_reader_)) { + return true; + } + } + } + + // Read content into `req.body` + if (!read_content(strm, req, res)) { return false; } + } + + // Regular handler + if (req.method == "GET" || req.method == "HEAD") { + return dispatch_request(req, res, get_handlers_); + } else if (req.method == "POST") { + return dispatch_request(req, res, post_handlers_); + } else if (req.method == "PUT") { + return dispatch_request(req, res, put_handlers_); + } else if (req.method == "DELETE") { + return dispatch_request(req, res, delete_handlers_); + } else if (req.method == "OPTIONS") { + return dispatch_request(req, res, options_handlers_); + } else if (req.method == "PATCH") { + return dispatch_request(req, res, patch_handlers_); + } + + res.status = 400; + return false; +} + +inline bool Server::dispatch_request(Request &req, Response &res, + const Handlers &handlers) { + for (const auto &x : handlers) { + const auto &pattern = x.first; + const auto &handler = x.second; + + if (std::regex_match(req.path, req.matches, pattern)) { + handler(req, res); + return true; + } + } + return false; +} + +inline void Server::apply_ranges(const Request &req, Response &res, + std::string &content_type, + std::string &boundary) { + if (req.ranges.size() > 1) { + boundary = detail::make_multipart_data_boundary(); + + auto it = res.headers.find("Content-Type"); + if (it != res.headers.end()) { + content_type = it->second; + res.headers.erase(it); + } + + res.headers.emplace("Content-Type", + "multipart/byteranges; boundary=" + boundary); + } + + auto type = detail::encoding_type(req, res); + + if (res.body.empty()) { + if (res.content_length_ > 0) { + size_t length = 0; + if (req.ranges.empty()) { + length = res.content_length_; + } else if (req.ranges.size() == 1) { + auto offsets = + detail::get_range_offset_and_length(req, res.content_length_, 0); + auto offset = offsets.first; + length = offsets.second; + auto content_range = detail::make_content_range_header_field( + offset, length, res.content_length_); + res.set_header("Content-Range", content_range); + } else { + length = detail::get_multipart_ranges_data_length(req, res, boundary, + content_type); + } + res.set_header("Content-Length", std::to_string(length)); + } else { + if (res.content_provider_) { + if (res.is_chunked_content_provider_) { + res.set_header("Transfer-Encoding", "chunked"); + if (type == detail::EncodingType::Gzip) { + res.set_header("Content-Encoding", "gzip"); + } else if (type == detail::EncodingType::Brotli) { + res.set_header("Content-Encoding", "br"); + } + } + } + } + } else { + if (req.ranges.empty()) { + ; + } else if (req.ranges.size() == 1) { + auto offsets = + detail::get_range_offset_and_length(req, res.body.size(), 0); + auto offset = offsets.first; + auto length = offsets.second; + auto content_range = detail::make_content_range_header_field( + offset, length, res.body.size()); + res.set_header("Content-Range", content_range); + if (offset < res.body.size()) { + res.body = res.body.substr(offset, length); + } else { + res.body.clear(); + res.status = 416; + } + } else { + std::string data; + if (detail::make_multipart_ranges_data(req, res, boundary, content_type, + data)) { + res.body.swap(data); + } else { + res.body.clear(); + res.status = 416; + } + } + + if (type != detail::EncodingType::None) { + std::unique_ptr compressor; + std::string content_encoding; + + if (type == detail::EncodingType::Gzip) { +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + compressor = detail::make_unique(); + content_encoding = "gzip"; +#endif + } else if (type == detail::EncodingType::Brotli) { +#ifdef CPPHTTPLIB_BROTLI_SUPPORT + compressor = detail::make_unique(); + content_encoding = "br"; +#endif + } + + if (compressor) { + std::string compressed; + if (compressor->compress(res.body.data(), res.body.size(), true, + [&](const char *data, size_t data_len) { + compressed.append(data, data_len); + return true; + })) { + res.body.swap(compressed); + res.set_header("Content-Encoding", content_encoding); + } + } + } + + auto length = std::to_string(res.body.size()); + res.set_header("Content-Length", length); + } +} + +inline bool Server::dispatch_request_for_content_reader( + Request &req, Response &res, ContentReader content_reader, + const HandlersForContentReader &handlers) { + for (const auto &x : handlers) { + const auto &pattern = x.first; + const auto &handler = x.second; + + if (std::regex_match(req.path, req.matches, pattern)) { + handler(req, res, content_reader); + return true; + } + } + return false; +} + +inline bool +Server::process_request(Stream &strm, bool close_connection, + bool &connection_closed, + const std::function &setup_request) { + std::array buf{}; + + detail::stream_line_reader line_reader(strm, buf.data(), buf.size()); + + // Connection has been closed on client + if (!line_reader.getline()) { return false; } + + Request req; + Response res; + + res.version = "HTTP/1.1"; + + for (const auto &header : default_headers_) { + if (res.headers.find(header.first) == res.headers.end()) { + res.headers.insert(header); + } + } + +#ifdef _WIN32 + // TODO: Increase FD_SETSIZE statically (libzmq), dynamically (MySQL). +#else +#ifndef CPPHTTPLIB_USE_POLL + // Socket file descriptor exceeded FD_SETSIZE... + if (strm.socket() >= FD_SETSIZE) { + Headers dummy; + detail::read_headers(strm, dummy); + res.status = 500; + return write_response(strm, close_connection, req, res); + } +#endif +#endif + + // Check if the request URI doesn't exceed the limit + if (line_reader.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) { + Headers dummy; + detail::read_headers(strm, dummy); + res.status = 414; + return write_response(strm, close_connection, req, res); + } + + // Request line and headers + if (!parse_request_line(line_reader.ptr(), req) || + !detail::read_headers(strm, req.headers)) { + res.status = 400; + return write_response(strm, close_connection, req, res); + } + + if (req.get_header_value("Connection") == "close") { + connection_closed = true; + } + + if (req.version == "HTTP/1.0" && + req.get_header_value("Connection") != "Keep-Alive") { + connection_closed = true; + } + + strm.get_remote_ip_and_port(req.remote_addr, req.remote_port); + req.set_header("REMOTE_ADDR", req.remote_addr); + req.set_header("REMOTE_PORT", std::to_string(req.remote_port)); + + if (req.has_header("Range")) { + const auto &range_header_value = req.get_header_value("Range"); + if (!detail::parse_range_header(range_header_value, req.ranges)) { + res.status = 416; + return write_response(strm, close_connection, req, res); + } + } + + if (setup_request) { setup_request(req); } + + if (req.get_header_value("Expect") == "100-continue") { + auto status = 100; + if (expect_100_continue_handler_) { + status = expect_100_continue_handler_(req, res); + } + switch (status) { + case 100: + case 417: + strm.write_format("HTTP/1.1 %d %s\r\n\r\n", status, + detail::status_message(status)); + break; + default: return write_response(strm, close_connection, req, res); + } + } + + // Rounting + bool routed = false; + try { + routed = routing(req, res, strm); + } catch (std::exception &e) { + if (exception_handler_) { + exception_handler_(req, res, e); + routed = true; + } else { + res.status = 500; + res.set_header("EXCEPTION_WHAT", e.what()); + } + } catch (...) { + res.status = 500; + res.set_header("EXCEPTION_WHAT", "UNKNOWN"); + } + + if (routed) { + if (res.status == -1) { res.status = req.ranges.empty() ? 200 : 206; } + return write_response_with_content(strm, close_connection, req, res); + } else { + if (res.status == -1) { res.status = 404; } + return write_response(strm, close_connection, req, res); + } +} + +inline bool Server::is_valid() const { return true; } + +inline bool Server::process_and_close_socket(socket_t sock) { + auto ret = detail::process_server_socket( + sock, keep_alive_max_count_, keep_alive_timeout_sec_, read_timeout_sec_, + read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, + [this](Stream &strm, bool close_connection, bool &connection_closed) { + return process_request(strm, close_connection, connection_closed, + nullptr); + }); + + detail::shutdown_socket(sock); + detail::close_socket(sock); + return ret; +} + +// HTTP client implementation +inline ClientImpl::ClientImpl(const std::string &host) + : ClientImpl(host, 80, std::string(), std::string()) {} + +inline ClientImpl::ClientImpl(const std::string &host, int port) + : ClientImpl(host, port, std::string(), std::string()) {} + +inline ClientImpl::ClientImpl(const std::string &host, int port, + const std::string &client_cert_path, + const std::string &client_key_path) + : host_(host), port_(port), + host_and_port_(adjust_host_string(host) + ":" + std::to_string(port)), + client_cert_path_(client_cert_path), client_key_path_(client_key_path) {} + +inline ClientImpl::~ClientImpl() { + std::lock_guard guard(socket_mutex_); + shutdown_socket(socket_); + close_socket(socket_); +} + +inline bool ClientImpl::is_valid() const { return true; } + +inline void ClientImpl::copy_settings(const ClientImpl &rhs) { + client_cert_path_ = rhs.client_cert_path_; + client_key_path_ = rhs.client_key_path_; + connection_timeout_sec_ = rhs.connection_timeout_sec_; + read_timeout_sec_ = rhs.read_timeout_sec_; + read_timeout_usec_ = rhs.read_timeout_usec_; + write_timeout_sec_ = rhs.write_timeout_sec_; + write_timeout_usec_ = rhs.write_timeout_usec_; + basic_auth_username_ = rhs.basic_auth_username_; + basic_auth_password_ = rhs.basic_auth_password_; + bearer_token_auth_token_ = rhs.bearer_token_auth_token_; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + digest_auth_username_ = rhs.digest_auth_username_; + digest_auth_password_ = rhs.digest_auth_password_; +#endif + keep_alive_ = rhs.keep_alive_; + follow_location_ = rhs.follow_location_; + url_encode_ = rhs.url_encode_; + address_family_ = rhs.address_family_; + tcp_nodelay_ = rhs.tcp_nodelay_; + socket_options_ = rhs.socket_options_; + compress_ = rhs.compress_; + decompress_ = rhs.decompress_; + interface_ = rhs.interface_; + proxy_host_ = rhs.proxy_host_; + proxy_port_ = rhs.proxy_port_; + proxy_basic_auth_username_ = rhs.proxy_basic_auth_username_; + proxy_basic_auth_password_ = rhs.proxy_basic_auth_password_; + proxy_bearer_token_auth_token_ = rhs.proxy_bearer_token_auth_token_; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + proxy_digest_auth_username_ = rhs.proxy_digest_auth_username_; + proxy_digest_auth_password_ = rhs.proxy_digest_auth_password_; +#endif +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + ca_cert_file_path_ = rhs.ca_cert_file_path_; + ca_cert_dir_path_ = rhs.ca_cert_dir_path_; + ca_cert_store_ = rhs.ca_cert_store_; +#endif +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + server_certificate_verification_ = rhs.server_certificate_verification_; +#endif + logger_ = rhs.logger_; +} + +inline socket_t ClientImpl::create_client_socket(Error &error) const { + if (!proxy_host_.empty() && proxy_port_ != -1) { + return detail::create_client_socket( + proxy_host_.c_str(), proxy_port_, address_family_, tcp_nodelay_, + socket_options_, connection_timeout_sec_, connection_timeout_usec_, + read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, + write_timeout_usec_, interface_, error); + } + return detail::create_client_socket( + host_.c_str(), port_, address_family_, tcp_nodelay_, socket_options_, + connection_timeout_sec_, connection_timeout_usec_, read_timeout_sec_, + read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, interface_, + error); +} + +inline bool ClientImpl::create_and_connect_socket(Socket &socket, + Error &error) { + auto sock = create_client_socket(error); + if (sock == INVALID_SOCKET) { return false; } + socket.sock = sock; + return true; +} + +inline void ClientImpl::shutdown_ssl(Socket & /*socket*/, + bool /*shutdown_gracefully*/) { + // If there are any requests in flight from threads other than us, then it's + // a thread-unsafe race because individual ssl* objects are not thread-safe. + assert(socket_requests_in_flight_ == 0 || + socket_requests_are_from_thread_ == std::this_thread::get_id()); +} + +inline void ClientImpl::shutdown_socket(Socket &socket) { + if (socket.sock == INVALID_SOCKET) { return; } + detail::shutdown_socket(socket.sock); +} + +inline void ClientImpl::close_socket(Socket &socket) { + // If there are requests in flight in another thread, usually closing + // the socket will be fine and they will simply receive an error when + // using the closed socket, but it is still a bug since rarely the OS + // may reassign the socket id to be used for a new socket, and then + // suddenly they will be operating on a live socket that is different + // than the one they intended! + assert(socket_requests_in_flight_ == 0 || + socket_requests_are_from_thread_ == std::this_thread::get_id()); + + // It is also a bug if this happens while SSL is still active +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + assert(socket.ssl == nullptr); +#endif + if (socket.sock == INVALID_SOCKET) { return; } + detail::close_socket(socket.sock); + socket.sock = INVALID_SOCKET; +} + +inline bool ClientImpl::read_response_line(Stream &strm, const Request &req, + Response &res) { + std::array buf; + + detail::stream_line_reader line_reader(strm, buf.data(), buf.size()); + + if (!line_reader.getline()) { return false; } + + const static std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r\n"); + + std::cmatch m; + if (!std::regex_match(line_reader.ptr(), m, re)) { + return req.method == "CONNECT"; + } + res.version = std::string(m[1]); + res.status = std::stoi(std::string(m[2])); + res.reason = std::string(m[3]); + + // Ignore '100 Continue' + while (res.status == 100) { + if (!line_reader.getline()) { return false; } // CRLF + if (!line_reader.getline()) { return false; } // next response line + + if (!std::regex_match(line_reader.ptr(), m, re)) { return false; } + res.version = std::string(m[1]); + res.status = std::stoi(std::string(m[2])); + res.reason = std::string(m[3]); + } + + return true; +} + +inline bool ClientImpl::send(Request &req, Response &res, Error &error) { + std::lock_guard request_mutex_guard(request_mutex_); + + { + std::lock_guard guard(socket_mutex_); + // Set this to false immediately - if it ever gets set to true by the end of + // the request, we know another thread instructed us to close the socket. + socket_should_be_closed_when_request_is_done_ = false; + + auto is_alive = false; + if (socket_.is_open()) { + is_alive = detail::select_write(socket_.sock, 0, 0) > 0; + if (!is_alive) { + // Attempt to avoid sigpipe by shutting down nongracefully if it seems + // like the other side has already closed the connection Also, there + // cannot be any requests in flight from other threads since we locked + // request_mutex_, so safe to close everything immediately + const bool shutdown_gracefully = false; + shutdown_ssl(socket_, shutdown_gracefully); + shutdown_socket(socket_); + close_socket(socket_); + } + } + + if (!is_alive) { + if (!create_and_connect_socket(socket_, error)) { return false; } + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + // TODO: refactoring + if (is_ssl()) { + auto &scli = static_cast(*this); + if (!proxy_host_.empty() && proxy_port_ != -1) { + bool success = false; + if (!scli.connect_with_proxy(socket_, res, success, error)) { + return success; + } + } + + if (!scli.initialize_ssl(socket_, error)) { return false; } + } +#endif + } + + // Mark the current socket as being in use so that it cannot be closed by + // anyone else while this request is ongoing, even though we will be + // releasing the mutex. + if (socket_requests_in_flight_ > 1) { + assert(socket_requests_are_from_thread_ == std::this_thread::get_id()); + } + socket_requests_in_flight_ += 1; + socket_requests_are_from_thread_ = std::this_thread::get_id(); + } + + for (const auto &header : default_headers_) { + if (req.headers.find(header.first) == req.headers.end()) { + req.headers.insert(header); + } + } + + auto close_connection = !keep_alive_; + auto ret = process_socket(socket_, [&](Stream &strm) { + return handle_request(strm, req, res, close_connection, error); + }); + + // Briefly lock mutex in order to mark that a request is no longer ongoing + { + std::lock_guard guard(socket_mutex_); + socket_requests_in_flight_ -= 1; + if (socket_requests_in_flight_ <= 0) { + assert(socket_requests_in_flight_ == 0); + socket_requests_are_from_thread_ = std::thread::id(); + } + + if (socket_should_be_closed_when_request_is_done_ || close_connection || + !ret) { + shutdown_ssl(socket_, true); + shutdown_socket(socket_); + close_socket(socket_); + } + } + + if (!ret) { + if (error == Error::Success) { error = Error::Unknown; } + } + + return ret; +} + +inline Result ClientImpl::send(const Request &req) { + auto req2 = req; + return send_(std::move(req2)); +} + +inline Result ClientImpl::send_(Request &&req) { + auto res = detail::make_unique(); + auto error = Error::Success; + auto ret = send(req, *res, error); + return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers)}; +} + +inline bool ClientImpl::handle_request(Stream &strm, Request &req, + Response &res, bool close_connection, + Error &error) { + if (req.path.empty()) { + error = Error::Connection; + return false; + } + + auto req_save = req; + + bool ret; + + if (!is_ssl() && !proxy_host_.empty() && proxy_port_ != -1) { + auto req2 = req; + req2.path = "http://" + host_and_port_ + req.path; + ret = process_request(strm, req2, res, close_connection, error); + req = req2; + req.path = req_save.path; + } else { + ret = process_request(strm, req, res, close_connection, error); + } + + if (!ret) { return false; } + + if (300 < res.status && res.status < 400 && follow_location_) { + req = req_save; + ret = redirect(req, res, error); + } + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + if ((res.status == 401 || res.status == 407) && + req.authorization_count_ < 5) { + auto is_proxy = res.status == 407; + const auto &username = + is_proxy ? proxy_digest_auth_username_ : digest_auth_username_; + const auto &password = + is_proxy ? proxy_digest_auth_password_ : digest_auth_password_; + + if (!username.empty() && !password.empty()) { + std::map auth; + if (detail::parse_www_authenticate(res, auth, is_proxy)) { + Request new_req = req; + new_req.authorization_count_ += 1; + new_req.headers.erase(is_proxy ? "Proxy-Authorization" + : "Authorization"); + new_req.headers.insert(detail::make_digest_authentication_header( + req, auth, new_req.authorization_count_, detail::random_string(10), + username, password, is_proxy)); + + Response new_res; + + ret = send(new_req, new_res, error); + if (ret) { res = new_res; } + } + } + } +#endif + + return ret; +} + +inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) { + if (req.redirect_count_ == 0) { + error = Error::ExceedRedirectCount; + return false; + } + + auto location = detail::decode_url(res.get_header_value("location"), true); + if (location.empty()) { return false; } + + const static std::regex re( + R"((?:(https?):)?(?://(?:\[([\d:]+)\]|([^:/?#]+))(?::(\d+))?)?([^?#]*(?:\?[^#]*)?)(?:#.*)?)"); + + std::smatch m; + if (!std::regex_match(location, m, re)) { return false; } + + auto scheme = is_ssl() ? "https" : "http"; + + auto next_scheme = m[1].str(); + auto next_host = m[2].str(); + if (next_host.empty()) { next_host = m[3].str(); } + auto port_str = m[4].str(); + auto next_path = m[5].str(); + + auto next_port = port_; + if (!port_str.empty()) { + next_port = std::stoi(port_str); + } else if (!next_scheme.empty()) { + next_port = next_scheme == "https" ? 443 : 80; + } + + if (next_scheme.empty()) { next_scheme = scheme; } + if (next_host.empty()) { next_host = host_; } + if (next_path.empty()) { next_path = "/"; } + + if (next_scheme == scheme && next_host == host_ && next_port == port_) { + return detail::redirect(*this, req, res, next_path, location, error); + } else { + if (next_scheme == "https") { +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + SSLClient cli(next_host.c_str(), next_port); + cli.copy_settings(*this); + if (ca_cert_store_) { cli.set_ca_cert_store(ca_cert_store_); } + return detail::redirect(cli, req, res, next_path, location, error); +#else + return false; +#endif + } else { + ClientImpl cli(next_host.c_str(), next_port); + cli.copy_settings(*this); + return detail::redirect(cli, req, res, next_path, location, error); + } + } +} + +inline bool ClientImpl::write_content_with_provider(Stream &strm, + const Request &req, + Error &error) { + auto is_shutting_down = []() { return false; }; + + if (req.is_chunked_content_provider_) { + // TODO: Brotli suport + std::unique_ptr compressor; +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + if (compress_) { + compressor = detail::make_unique(); + } else +#endif + { + compressor = detail::make_unique(); + } + + return detail::write_content_chunked(strm, req.content_provider_, + is_shutting_down, *compressor, error); + } else { + return detail::write_content(strm, req.content_provider_, 0, + req.content_length_, is_shutting_down, error); + } +} // namespace httplib + +inline bool ClientImpl::write_request(Stream &strm, Request &req, + bool close_connection, Error &error) { + // Prepare additional headers + if (close_connection) { + if (!req.has_header("Connection")) { + req.headers.emplace("Connection", "close"); + } + } + + if (!req.has_header("Host")) { + if (is_ssl()) { + if (port_ == 443) { + req.headers.emplace("Host", host_); + } else { + req.headers.emplace("Host", host_and_port_); + } + } else { + if (port_ == 80) { + req.headers.emplace("Host", host_); + } else { + req.headers.emplace("Host", host_and_port_); + } + } + } + + if (!req.has_header("Accept")) { req.headers.emplace("Accept", "*/*"); } + + if (!req.has_header("User-Agent")) { + req.headers.emplace("User-Agent", "cpp-httplib/0.9"); + } + + if (req.body.empty()) { + if (req.content_provider_) { + if (!req.is_chunked_content_provider_) { + if (!req.has_header("Content-Length")) { + auto length = std::to_string(req.content_length_); + req.headers.emplace("Content-Length", length); + } + } + } else { + if (req.method == "POST" || req.method == "PUT" || + req.method == "PATCH") { + req.headers.emplace("Content-Length", "0"); + } + } + } else { + if (!req.has_header("Content-Type")) { + req.headers.emplace("Content-Type", "text/plain"); + } + + if (!req.has_header("Content-Length")) { + auto length = std::to_string(req.body.size()); + req.headers.emplace("Content-Length", length); + } + } + + if (!basic_auth_password_.empty() || !basic_auth_username_.empty()) { + if (!req.has_header("Authorization")) { + req.headers.insert(make_basic_authentication_header( + basic_auth_username_, basic_auth_password_, false)); + } + } + + if (!proxy_basic_auth_username_.empty() && + !proxy_basic_auth_password_.empty()) { + if (!req.has_header("Proxy-Authorization")) { + req.headers.insert(make_basic_authentication_header( + proxy_basic_auth_username_, proxy_basic_auth_password_, true)); + } + } + + if (!bearer_token_auth_token_.empty()) { + if (!req.has_header("Authorization")) { + req.headers.insert(make_bearer_token_authentication_header( + bearer_token_auth_token_, false)); + } + } + + if (!proxy_bearer_token_auth_token_.empty()) { + if (!req.has_header("Proxy-Authorization")) { + req.headers.insert(make_bearer_token_authentication_header( + proxy_bearer_token_auth_token_, true)); + } + } + + // Request line and headers + { + detail::BufferStream bstrm; + + const auto &path = url_encode_ ? detail::encode_url(req.path) : req.path; + bstrm.write_format("%s %s HTTP/1.1\r\n", req.method.c_str(), path.c_str()); + + detail::write_headers(bstrm, req.headers); + + // Flush buffer + auto &data = bstrm.get_buffer(); + if (!detail::write_data(strm, data.data(), data.size())) { + error = Error::Write; + return false; + } + } + + // Body + if (req.body.empty()) { + return write_content_with_provider(strm, req, error); + } + + return detail::write_data(strm, req.body.data(), req.body.size()); +} + +inline std::unique_ptr ClientImpl::send_with_content_provider( + Request &req, + // const char *method, const char *path, const Headers &headers, + const char *body, size_t content_length, ContentProvider content_provider, + ContentProviderWithoutLength content_provider_without_length, + const char *content_type, Error &error) { + + // Request req; + // req.method = method; + // req.headers = headers; + // req.path = path; + + if (content_type) { req.headers.emplace("Content-Type", content_type); } + +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + if (compress_) { req.headers.emplace("Content-Encoding", "gzip"); } +#endif + +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + if (compress_ && !content_provider_without_length) { + // TODO: Brotli support + detail::gzip_compressor compressor; + + if (content_provider) { + auto ok = true; + size_t offset = 0; + DataSink data_sink; + + data_sink.write = [&](const char *data, size_t data_len) -> bool { + if (ok) { + auto last = offset + data_len == content_length; + + auto ret = compressor.compress( + data, data_len, last, [&](const char *data, size_t data_len) { + req.body.append(data, data_len); + return true; + }); + + if (ret) { + offset += data_len; + } else { + ok = false; + } + } + return ok; + }; + + data_sink.is_writable = [&](void) { return ok && true; }; + + while (ok && offset < content_length) { + if (!content_provider(offset, content_length - offset, data_sink)) { + error = Error::Canceled; + return nullptr; + } + } + } else { + if (!compressor.compress(body, content_length, true, + [&](const char *data, size_t data_len) { + req.body.append(data, data_len); + return true; + })) { + error = Error::Compression; + return nullptr; + } + } + } else +#endif + { + if (content_provider) { + req.content_length_ = content_length; + req.content_provider_ = std::move(content_provider); + req.is_chunked_content_provider_ = false; + } else if (content_provider_without_length) { + req.content_length_ = 0; + req.content_provider_ = detail::ContentProviderAdapter( + std::move(content_provider_without_length)); + req.is_chunked_content_provider_ = true; + req.headers.emplace("Transfer-Encoding", "chunked"); + } else { + req.body.assign(body, content_length); + ; + } + } + + auto res = detail::make_unique(); + return send(req, *res, error) ? std::move(res) : nullptr; +} + +inline Result ClientImpl::send_with_content_provider( + const char *method, const char *path, const Headers &headers, + const char *body, size_t content_length, ContentProvider content_provider, + ContentProviderWithoutLength content_provider_without_length, + const char *content_type) { + Request req; + req.method = method; + req.headers = headers; + req.path = path; + + auto error = Error::Success; + + auto res = send_with_content_provider( + req, + // method, path, headers, + body, content_length, std::move(content_provider), + std::move(content_provider_without_length), content_type, error); + + return Result{std::move(res), error, std::move(req.headers)}; +} + +inline std::string +ClientImpl::adjust_host_string(const std::string &host) const { + if (host.find(':') != std::string::npos) { return "[" + host + "]"; } + return host; +} + +inline bool ClientImpl::process_request(Stream &strm, Request &req, + Response &res, bool close_connection, + Error &error) { + // Send request + if (!write_request(strm, req, close_connection, error)) { return false; } + + // Receive response and headers + if (!read_response_line(strm, req, res) || + !detail::read_headers(strm, res.headers)) { + error = Error::Read; + return false; + } + + // Body + if ((res.status != 204) && req.method != "HEAD" && req.method != "CONNECT") { + auto redirect = 300 < res.status && res.status < 400 && follow_location_; + + if (req.response_handler && !redirect) { + if (!req.response_handler(res)) { + error = Error::Canceled; + return false; + } + } + + auto out = + req.content_receiver + ? static_cast( + [&](const char *buf, size_t n, uint64_t off, uint64_t len) { + if (redirect) { return true; } + auto ret = req.content_receiver(buf, n, off, len); + if (!ret) { error = Error::Canceled; } + return ret; + }) + : static_cast( + [&](const char *buf, size_t n, uint64_t /*off*/, + uint64_t /*len*/) { + if (res.body.size() + n > res.body.max_size()) { + return false; + } + res.body.append(buf, n); + return true; + }); + + auto progress = [&](uint64_t current, uint64_t total) { + if (!req.progress || redirect) { return true; } + auto ret = req.progress(current, total); + if (!ret) { error = Error::Canceled; } + return ret; + }; + + int dummy_status; + if (!detail::read_content(strm, res, (std::numeric_limits::max)(), + dummy_status, std::move(progress), std::move(out), + decompress_)) { + if (error != Error::Canceled) { error = Error::Read; } + return false; + } + } + + if (res.get_header_value("Connection") == "close" || + (res.version == "HTTP/1.0" && res.reason != "Connection established")) { + // TODO this requires a not-entirely-obvious chain of calls to be correct + // for this to be safe. Maybe a code refactor (such as moving this out to + // the send function and getting rid of the recursiveness of the mutex) + // could make this more obvious. + + // This is safe to call because process_request is only called by + // handle_request which is only called by send, which locks the request + // mutex during the process. It would be a bug to call it from a different + // thread since it's a thread-safety issue to do these things to the socket + // if another thread is using the socket. + std::lock_guard guard(socket_mutex_); + shutdown_ssl(socket_, true); + shutdown_socket(socket_); + close_socket(socket_); + } + + // Log + if (logger_) { logger_(req, res); } + + return true; +} + +inline bool +ClientImpl::process_socket(const Socket &socket, + std::function callback) { + return detail::process_client_socket( + socket.sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, + write_timeout_usec_, std::move(callback)); +} + +inline bool ClientImpl::is_ssl() const { return false; } + +inline Result ClientImpl::Get(const char *path) { + return Get(path, Headers(), Progress()); +} + +inline Result ClientImpl::Get(const char *path, Progress progress) { + return Get(path, Headers(), std::move(progress)); +} + +inline Result ClientImpl::Get(const char *path, const Headers &headers) { + return Get(path, headers, Progress()); +} + +inline Result ClientImpl::Get(const char *path, const Headers &headers, + Progress progress) { + Request req; + req.method = "GET"; + req.path = path; + req.headers = headers; + req.progress = std::move(progress); + + return send_(std::move(req)); +} + +inline Result ClientImpl::Get(const char *path, + ContentReceiver content_receiver) { + return Get(path, Headers(), nullptr, std::move(content_receiver), nullptr); +} + +inline Result ClientImpl::Get(const char *path, + ContentReceiver content_receiver, + Progress progress) { + return Get(path, Headers(), nullptr, std::move(content_receiver), + std::move(progress)); +} + +inline Result ClientImpl::Get(const char *path, const Headers &headers, + ContentReceiver content_receiver) { + return Get(path, headers, nullptr, std::move(content_receiver), nullptr); +} + +inline Result ClientImpl::Get(const char *path, const Headers &headers, + ContentReceiver content_receiver, + Progress progress) { + return Get(path, headers, nullptr, std::move(content_receiver), + std::move(progress)); +} + +inline Result ClientImpl::Get(const char *path, + ResponseHandler response_handler, + ContentReceiver content_receiver) { + return Get(path, Headers(), std::move(response_handler), + std::move(content_receiver), nullptr); +} + +inline Result ClientImpl::Get(const char *path, const Headers &headers, + ResponseHandler response_handler, + ContentReceiver content_receiver) { + return Get(path, headers, std::move(response_handler), + std::move(content_receiver), nullptr); +} + +inline Result ClientImpl::Get(const char *path, + ResponseHandler response_handler, + ContentReceiver content_receiver, + Progress progress) { + return Get(path, Headers(), std::move(response_handler), + std::move(content_receiver), std::move(progress)); +} + +inline Result ClientImpl::Get(const char *path, const Headers &headers, + ResponseHandler response_handler, + ContentReceiver content_receiver, + Progress progress) { + Request req; + req.method = "GET"; + req.path = path; + req.headers = headers; + req.response_handler = std::move(response_handler); + req.content_receiver = + [content_receiver](const char *data, size_t data_length, + uint64_t /*offset*/, uint64_t /*total_length*/) { + return content_receiver(data, data_length); + }; + req.progress = std::move(progress); + + return send_(std::move(req)); +} + +inline Result ClientImpl::Get(const char *path, const Params ¶ms, + const Headers &headers, Progress progress) { + if (params.empty()) { return Get(path, headers); } + + std::string path_with_query = detail::append_query_params(path, params); + return Get(path_with_query.c_str(), headers, progress); +} + +inline Result ClientImpl::Get(const char *path, const Params ¶ms, + const Headers &headers, + ContentReceiver content_receiver, + Progress progress) { + return Get(path, params, headers, nullptr, content_receiver, progress); +} + +inline Result ClientImpl::Get(const char *path, const Params ¶ms, + const Headers &headers, + ResponseHandler response_handler, + ContentReceiver content_receiver, + Progress progress) { + if (params.empty()) { + return Get(path, headers, response_handler, content_receiver, progress); + } + + std::string path_with_query = detail::append_query_params(path, params); + return Get(path_with_query.c_str(), headers, response_handler, + content_receiver, progress); +} + +inline Result ClientImpl::Head(const char *path) { + return Head(path, Headers()); +} + +inline Result ClientImpl::Head(const char *path, const Headers &headers) { + Request req; + req.method = "HEAD"; + req.headers = headers; + req.path = path; + + return send_(std::move(req)); +} + +inline Result ClientImpl::Post(const char *path) { + return Post(path, std::string(), nullptr); +} + +inline Result ClientImpl::Post(const char *path, const char *body, + size_t content_length, + const char *content_type) { + return Post(path, Headers(), body, content_length, content_type); +} + +inline Result ClientImpl::Post(const char *path, const Headers &headers, + const char *body, size_t content_length, + const char *content_type) { + return send_with_content_provider("POST", path, headers, body, content_length, + nullptr, nullptr, content_type); +} + +inline Result ClientImpl::Post(const char *path, const std::string &body, + const char *content_type) { + return Post(path, Headers(), body, content_type); +} + +inline Result ClientImpl::Post(const char *path, const Headers &headers, + const std::string &body, + const char *content_type) { + return send_with_content_provider("POST", path, headers, body.data(), + body.size(), nullptr, nullptr, + content_type); +} + +inline Result ClientImpl::Post(const char *path, const Params ¶ms) { + return Post(path, Headers(), params); +} + +inline Result ClientImpl::Post(const char *path, size_t content_length, + ContentProvider content_provider, + const char *content_type) { + return Post(path, Headers(), content_length, std::move(content_provider), + content_type); +} + +inline Result ClientImpl::Post(const char *path, + ContentProviderWithoutLength content_provider, + const char *content_type) { + return Post(path, Headers(), std::move(content_provider), content_type); +} + +inline Result ClientImpl::Post(const char *path, const Headers &headers, + size_t content_length, + ContentProvider content_provider, + const char *content_type) { + return send_with_content_provider("POST", path, headers, nullptr, + content_length, std::move(content_provider), + nullptr, content_type); +} + +inline Result ClientImpl::Post(const char *path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const char *content_type) { + return send_with_content_provider("POST", path, headers, nullptr, 0, nullptr, + std::move(content_provider), content_type); +} + +inline Result ClientImpl::Post(const char *path, const Headers &headers, + const Params ¶ms) { + auto query = detail::params_to_query_str(params); + return Post(path, headers, query, "application/x-www-form-urlencoded"); +} + +inline Result ClientImpl::Post(const char *path, + const MultipartFormDataItems &items) { + return Post(path, Headers(), items); +} + +inline Result ClientImpl::Post(const char *path, const Headers &headers, + const MultipartFormDataItems &items) { + return Post(path, headers, items, detail::make_multipart_data_boundary()); +} +inline Result ClientImpl::Post(const char *path, const Headers &headers, + const MultipartFormDataItems &items, + const std::string &boundary) { + for (size_t i = 0; i < boundary.size(); i++) { + char c = boundary[i]; + if (!std::isalnum(c) && c != '-' && c != '_') { + return Result{nullptr, Error::UnsupportedMultipartBoundaryChars}; + } + } + + std::string body; + + for (const auto &item : items) { + body += "--" + boundary + "\r\n"; + body += "Content-Disposition: form-data; name=\"" + item.name + "\""; + if (!item.filename.empty()) { + body += "; filename=\"" + item.filename + "\""; + } + body += "\r\n"; + if (!item.content_type.empty()) { + body += "Content-Type: " + item.content_type + "\r\n"; + } + body += "\r\n"; + body += item.content + "\r\n"; + } + + body += "--" + boundary + "--\r\n"; + + std::string content_type = "multipart/form-data; boundary=" + boundary; + return Post(path, headers, body, content_type.c_str()); +} + +inline Result ClientImpl::Put(const char *path) { + return Put(path, std::string(), nullptr); +} + +inline Result ClientImpl::Put(const char *path, const char *body, + size_t content_length, const char *content_type) { + return Put(path, Headers(), body, content_length, content_type); +} + +inline Result ClientImpl::Put(const char *path, const Headers &headers, + const char *body, size_t content_length, + const char *content_type) { + return send_with_content_provider("PUT", path, headers, body, content_length, + nullptr, nullptr, content_type); +} + +inline Result ClientImpl::Put(const char *path, const std::string &body, + const char *content_type) { + return Put(path, Headers(), body, content_type); +} + +inline Result ClientImpl::Put(const char *path, const Headers &headers, + const std::string &body, + const char *content_type) { + return send_with_content_provider("PUT", path, headers, body.data(), + body.size(), nullptr, nullptr, + content_type); +} + +inline Result ClientImpl::Put(const char *path, size_t content_length, + ContentProvider content_provider, + const char *content_type) { + return Put(path, Headers(), content_length, std::move(content_provider), + content_type); +} + +inline Result ClientImpl::Put(const char *path, + ContentProviderWithoutLength content_provider, + const char *content_type) { + return Put(path, Headers(), std::move(content_provider), content_type); +} + +inline Result ClientImpl::Put(const char *path, const Headers &headers, + size_t content_length, + ContentProvider content_provider, + const char *content_type) { + return send_with_content_provider("PUT", path, headers, nullptr, + content_length, std::move(content_provider), + nullptr, content_type); +} + +inline Result ClientImpl::Put(const char *path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const char *content_type) { + return send_with_content_provider("PUT", path, headers, nullptr, 0, nullptr, + std::move(content_provider), content_type); +} + +inline Result ClientImpl::Put(const char *path, const Params ¶ms) { + return Put(path, Headers(), params); +} + +inline Result ClientImpl::Put(const char *path, const Headers &headers, + const Params ¶ms) { + auto query = detail::params_to_query_str(params); + return Put(path, headers, query, "application/x-www-form-urlencoded"); +} + +inline Result ClientImpl::Patch(const char *path) { + return Patch(path, std::string(), nullptr); +} + +inline Result ClientImpl::Patch(const char *path, const char *body, + size_t content_length, + const char *content_type) { + return Patch(path, Headers(), body, content_length, content_type); +} + +inline Result ClientImpl::Patch(const char *path, const Headers &headers, + const char *body, size_t content_length, + const char *content_type) { + return send_with_content_provider("PATCH", path, headers, body, + content_length, nullptr, nullptr, + content_type); +} + +inline Result ClientImpl::Patch(const char *path, const std::string &body, + const char *content_type) { + return Patch(path, Headers(), body, content_type); +} + +inline Result ClientImpl::Patch(const char *path, const Headers &headers, + const std::string &body, + const char *content_type) { + return send_with_content_provider("PATCH", path, headers, body.data(), + body.size(), nullptr, nullptr, + content_type); +} + +inline Result ClientImpl::Patch(const char *path, size_t content_length, + ContentProvider content_provider, + const char *content_type) { + return Patch(path, Headers(), content_length, std::move(content_provider), + content_type); +} + +inline Result ClientImpl::Patch(const char *path, + ContentProviderWithoutLength content_provider, + const char *content_type) { + return Patch(path, Headers(), std::move(content_provider), content_type); +} + +inline Result ClientImpl::Patch(const char *path, const Headers &headers, + size_t content_length, + ContentProvider content_provider, + const char *content_type) { + return send_with_content_provider("PATCH", path, headers, nullptr, + content_length, std::move(content_provider), + nullptr, content_type); +} + +inline Result ClientImpl::Patch(const char *path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const char *content_type) { + return send_with_content_provider("PATCH", path, headers, nullptr, 0, nullptr, + std::move(content_provider), content_type); +} + +inline Result ClientImpl::Delete(const char *path) { + return Delete(path, Headers(), std::string(), nullptr); +} + +inline Result ClientImpl::Delete(const char *path, const Headers &headers) { + return Delete(path, headers, std::string(), nullptr); +} + +inline Result ClientImpl::Delete(const char *path, const char *body, + size_t content_length, + const char *content_type) { + return Delete(path, Headers(), body, content_length, content_type); +} + +inline Result ClientImpl::Delete(const char *path, const Headers &headers, + const char *body, size_t content_length, + const char *content_type) { + Request req; + req.method = "DELETE"; + req.headers = headers; + req.path = path; + + if (content_type) { req.headers.emplace("Content-Type", content_type); } + req.body.assign(body, content_length); + + return send_(std::move(req)); +} + +inline Result ClientImpl::Delete(const char *path, const std::string &body, + const char *content_type) { + return Delete(path, Headers(), body.data(), body.size(), content_type); +} + +inline Result ClientImpl::Delete(const char *path, const Headers &headers, + const std::string &body, + const char *content_type) { + return Delete(path, headers, body.data(), body.size(), content_type); +} + +inline Result ClientImpl::Options(const char *path) { + return Options(path, Headers()); +} + +inline Result ClientImpl::Options(const char *path, const Headers &headers) { + Request req; + req.method = "OPTIONS"; + req.headers = headers; + req.path = path; + + return send_(std::move(req)); +} + +inline size_t ClientImpl::is_socket_open() const { + std::lock_guard guard(socket_mutex_); + return socket_.is_open(); +} + +inline void ClientImpl::stop() { + std::lock_guard guard(socket_mutex_); + + // If there is anything ongoing right now, the ONLY thread-safe thing we can + // do is to shutdown_socket, so that threads using this socket suddenly + // discover they can't read/write any more and error out. Everything else + // (closing the socket, shutting ssl down) is unsafe because these actions are + // not thread-safe. + if (socket_requests_in_flight_ > 0) { + shutdown_socket(socket_); + + // Aside from that, we set a flag for the socket to be closed when we're + // done. + socket_should_be_closed_when_request_is_done_ = true; + return; + } + + // Otherwise, sitll holding the mutex, we can shut everything down ourselves + shutdown_ssl(socket_, true); + shutdown_socket(socket_); + close_socket(socket_); +} + +inline void ClientImpl::set_connection_timeout(time_t sec, time_t usec) { + connection_timeout_sec_ = sec; + connection_timeout_usec_ = usec; +} + +inline void ClientImpl::set_read_timeout(time_t sec, time_t usec) { + read_timeout_sec_ = sec; + read_timeout_usec_ = usec; +} + +inline void ClientImpl::set_write_timeout(time_t sec, time_t usec) { + write_timeout_sec_ = sec; + write_timeout_usec_ = usec; +} + +inline void ClientImpl::set_basic_auth(const char *username, + const char *password) { + basic_auth_username_ = username; + basic_auth_password_ = password; +} + +inline void ClientImpl::set_bearer_token_auth(const char *token) { + bearer_token_auth_token_ = token; +} + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline void ClientImpl::set_digest_auth(const char *username, + const char *password) { + digest_auth_username_ = username; + digest_auth_password_ = password; +} +#endif + +inline void ClientImpl::set_keep_alive(bool on) { keep_alive_ = on; } + +inline void ClientImpl::set_follow_location(bool on) { follow_location_ = on; } + +inline void ClientImpl::set_url_encode(bool on) { url_encode_ = on; } + +inline void ClientImpl::set_default_headers(Headers headers) { + default_headers_ = std::move(headers); +} + +inline void ClientImpl::set_address_family(int family) { + address_family_ = family; +} + +inline void ClientImpl::set_tcp_nodelay(bool on) { tcp_nodelay_ = on; } + +inline void ClientImpl::set_socket_options(SocketOptions socket_options) { + socket_options_ = std::move(socket_options); +} + +inline void ClientImpl::set_compress(bool on) { compress_ = on; } + +inline void ClientImpl::set_decompress(bool on) { decompress_ = on; } + +inline void ClientImpl::set_interface(const char *intf) { interface_ = intf; } + +inline void ClientImpl::set_proxy(const char *host, int port) { + proxy_host_ = host; + proxy_port_ = port; +} + +inline void ClientImpl::set_proxy_basic_auth(const char *username, + const char *password) { + proxy_basic_auth_username_ = username; + proxy_basic_auth_password_ = password; +} + +inline void ClientImpl::set_proxy_bearer_token_auth(const char *token) { + proxy_bearer_token_auth_token_ = token; +} + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline void ClientImpl::set_proxy_digest_auth(const char *username, + const char *password) { + proxy_digest_auth_username_ = username; + proxy_digest_auth_password_ = password; +} +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline void ClientImpl::set_ca_cert_path(const char *ca_cert_file_path, + const char *ca_cert_dir_path) { + if (ca_cert_file_path) { ca_cert_file_path_ = ca_cert_file_path; } + if (ca_cert_dir_path) { ca_cert_dir_path_ = ca_cert_dir_path; } +} + +inline void ClientImpl::set_ca_cert_store(X509_STORE *ca_cert_store) { + if (ca_cert_store && ca_cert_store != ca_cert_store_) { + ca_cert_store_ = ca_cert_store; + } +} +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline void ClientImpl::enable_server_certificate_verification(bool enabled) { + server_certificate_verification_ = enabled; +} +#endif + +inline void ClientImpl::set_logger(Logger logger) { + logger_ = std::move(logger); +} + +/* + * SSL Implementation + */ +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +namespace detail { + +template +inline SSL *ssl_new(socket_t sock, SSL_CTX *ctx, std::mutex &ctx_mutex, + U SSL_connect_or_accept, V setup) { + SSL *ssl = nullptr; + { + std::lock_guard guard(ctx_mutex); + ssl = SSL_new(ctx); + } + + if (ssl) { + set_nonblocking(sock, true); + auto bio = BIO_new_socket(static_cast(sock), BIO_NOCLOSE); + BIO_set_nbio(bio, 1); + SSL_set_bio(ssl, bio, bio); + + if (!setup(ssl) || SSL_connect_or_accept(ssl) != 1) { + SSL_shutdown(ssl); + { + std::lock_guard guard(ctx_mutex); + SSL_free(ssl); + } + set_nonblocking(sock, false); + return nullptr; + } + BIO_set_nbio(bio, 0); + set_nonblocking(sock, false); + } + + return ssl; +} + +inline void ssl_delete(std::mutex &ctx_mutex, SSL *ssl, + bool shutdown_gracefully) { + // sometimes we may want to skip this to try to avoid SIGPIPE if we know + // the remote has closed the network connection + // Note that it is not always possible to avoid SIGPIPE, this is merely a + // best-efforts. + if (shutdown_gracefully) { SSL_shutdown(ssl); } + + std::lock_guard guard(ctx_mutex); + SSL_free(ssl); +} + +template +bool ssl_connect_or_accept_nonblocking(socket_t sock, SSL *ssl, + U ssl_connect_or_accept, + time_t timeout_sec, + time_t timeout_usec) { + int res = 0; + while ((res = ssl_connect_or_accept(ssl)) != 1) { + auto err = SSL_get_error(ssl, res); + switch (err) { + case SSL_ERROR_WANT_READ: + if (select_read(sock, timeout_sec, timeout_usec) > 0) { continue; } + break; + case SSL_ERROR_WANT_WRITE: + if (select_write(sock, timeout_sec, timeout_usec) > 0) { continue; } + break; + default: break; + } + return false; + } + return true; +} + +template +inline bool +process_server_socket_ssl(SSL *ssl, socket_t sock, size_t keep_alive_max_count, + time_t keep_alive_timeout_sec, + time_t read_timeout_sec, time_t read_timeout_usec, + time_t write_timeout_sec, time_t write_timeout_usec, + T callback) { + return process_server_socket_core( + sock, keep_alive_max_count, keep_alive_timeout_sec, + [&](bool close_connection, bool &connection_closed) { + SSLSocketStream strm(sock, ssl, read_timeout_sec, read_timeout_usec, + write_timeout_sec, write_timeout_usec); + return callback(strm, close_connection, connection_closed); + }); +} + +template +inline bool +process_client_socket_ssl(SSL *ssl, socket_t sock, time_t read_timeout_sec, + time_t read_timeout_usec, time_t write_timeout_sec, + time_t write_timeout_usec, T callback) { + SSLSocketStream strm(sock, ssl, read_timeout_sec, read_timeout_usec, + write_timeout_sec, write_timeout_usec); + return callback(strm); +} + +#if OPENSSL_VERSION_NUMBER < 0x10100000L +static std::shared_ptr> openSSL_locks_; + +class SSLThreadLocks { +public: + SSLThreadLocks() { + openSSL_locks_ = + std::make_shared>(CRYPTO_num_locks()); + CRYPTO_set_locking_callback(locking_callback); + } + + ~SSLThreadLocks() { CRYPTO_set_locking_callback(nullptr); } + +private: + static void locking_callback(int mode, int type, const char * /*file*/, + int /*line*/) { + auto &lk = (*openSSL_locks_)[static_cast(type)]; + if (mode & CRYPTO_LOCK) { + lk.lock(); + } else { + lk.unlock(); + } + } +}; + +#endif + +class SSLInit { +public: + SSLInit() { +#if OPENSSL_VERSION_NUMBER < 0x1010001fL + SSL_load_error_strings(); + SSL_library_init(); +#else + OPENSSL_init_ssl( + OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL); +#endif + } + + ~SSLInit() { +#if OPENSSL_VERSION_NUMBER < 0x1010001fL + ERR_free_strings(); +#endif + } + +private: +#if OPENSSL_VERSION_NUMBER < 0x10100000L + SSLThreadLocks thread_init_; +#endif +}; + +// SSL socket stream implementation +inline SSLSocketStream::SSLSocketStream(socket_t sock, SSL *ssl, + time_t read_timeout_sec, + time_t read_timeout_usec, + time_t write_timeout_sec, + time_t write_timeout_usec) + : sock_(sock), ssl_(ssl), read_timeout_sec_(read_timeout_sec), + read_timeout_usec_(read_timeout_usec), + write_timeout_sec_(write_timeout_sec), + write_timeout_usec_(write_timeout_usec) { + SSL_clear_mode(ssl, SSL_MODE_AUTO_RETRY); +} + +inline SSLSocketStream::~SSLSocketStream() {} + +inline bool SSLSocketStream::is_readable() const { + return detail::select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0; +} + +inline bool SSLSocketStream::is_writable() const { + return detail::select_write(sock_, write_timeout_sec_, write_timeout_usec_) > + 0; +} + +inline ssize_t SSLSocketStream::read(char *ptr, size_t size) { + if (SSL_pending(ssl_) > 0) { + return SSL_read(ssl_, ptr, static_cast(size)); + } else if (is_readable()) { + auto ret = SSL_read(ssl_, ptr, static_cast(size)); + if (ret < 0) { + auto err = SSL_get_error(ssl_, ret); + int n = 1000; +#ifdef _WIN32 + while (--n >= 0 && + (err == SSL_ERROR_WANT_READ || + err == SSL_ERROR_SYSCALL && WSAGetLastError() == WSAETIMEDOUT)) { +#else + while (--n >= 0 && err == SSL_ERROR_WANT_READ) { +#endif + if (SSL_pending(ssl_) > 0) { + return SSL_read(ssl_, ptr, static_cast(size)); + } else if (is_readable()) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + ret = SSL_read(ssl_, ptr, static_cast(size)); + if (ret >= 0) { return ret; } + err = SSL_get_error(ssl_, ret); + } else { + return -1; + } + } + } + return ret; + } + return -1; +} + +inline ssize_t SSLSocketStream::write(const char *ptr, size_t size) { + if (is_writable()) { return SSL_write(ssl_, ptr, static_cast(size)); } + return -1; +} + +inline void SSLSocketStream::get_remote_ip_and_port(std::string &ip, + int &port) const { + detail::get_remote_ip_and_port(sock_, ip, port); +} + +inline socket_t SSLSocketStream::socket() const { return sock_; } + +static SSLInit sslinit_; + +} // namespace detail + +// SSL HTTP server implementation +inline SSLServer::SSLServer(const char *cert_path, const char *private_key_path, + const char *client_ca_cert_file_path, + const char *client_ca_cert_dir_path) { + ctx_ = SSL_CTX_new(TLS_method()); + + if (ctx_) { + SSL_CTX_set_options(ctx_, + SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | + SSL_OP_NO_COMPRESSION | + SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); + + // auto ecdh = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1); + // SSL_CTX_set_tmp_ecdh(ctx_, ecdh); + // EC_KEY_free(ecdh); + + if (SSL_CTX_use_certificate_chain_file(ctx_, cert_path) != 1 || + SSL_CTX_use_PrivateKey_file(ctx_, private_key_path, SSL_FILETYPE_PEM) != + 1) { + SSL_CTX_free(ctx_); + ctx_ = nullptr; + } else if (client_ca_cert_file_path || client_ca_cert_dir_path) { + // if (client_ca_cert_file_path) { + // auto list = SSL_load_client_CA_file(client_ca_cert_file_path); + // SSL_CTX_set_client_CA_list(ctx_, list); + // } + + SSL_CTX_load_verify_locations(ctx_, client_ca_cert_file_path, + client_ca_cert_dir_path); + + SSL_CTX_set_verify( + ctx_, + SSL_VERIFY_PEER | + SSL_VERIFY_FAIL_IF_NO_PEER_CERT, // SSL_VERIFY_CLIENT_ONCE, + nullptr); + } + } +} + +inline SSLServer::SSLServer(X509 *cert, EVP_PKEY *private_key, + X509_STORE *client_ca_cert_store) { + ctx_ = SSL_CTX_new(SSLv23_server_method()); + + if (ctx_) { + SSL_CTX_set_options(ctx_, + SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | + SSL_OP_NO_COMPRESSION | + SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); + + if (SSL_CTX_use_certificate(ctx_, cert) != 1 || + SSL_CTX_use_PrivateKey(ctx_, private_key) != 1) { + SSL_CTX_free(ctx_); + ctx_ = nullptr; + } else if (client_ca_cert_store) { + + SSL_CTX_set_cert_store(ctx_, client_ca_cert_store); + + SSL_CTX_set_verify( + ctx_, + SSL_VERIFY_PEER | + SSL_VERIFY_FAIL_IF_NO_PEER_CERT, // SSL_VERIFY_CLIENT_ONCE, + nullptr); + } + } +} + +inline SSLServer::~SSLServer() { + if (ctx_) { SSL_CTX_free(ctx_); } +} + +inline bool SSLServer::is_valid() const { return ctx_; } + +inline bool SSLServer::process_and_close_socket(socket_t sock) { + auto ssl = detail::ssl_new( + sock, ctx_, ctx_mutex_, + [&](SSL *ssl) { + return detail::ssl_connect_or_accept_nonblocking( + sock, ssl, SSL_accept, read_timeout_sec_, read_timeout_usec_); + }, + [](SSL * /*ssl*/) { return true; }); + + bool ret = false; + if (ssl) { + ret = detail::process_server_socket_ssl( + ssl, sock, keep_alive_max_count_, keep_alive_timeout_sec_, + read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, + write_timeout_usec_, + [this, ssl](Stream &strm, bool close_connection, + bool &connection_closed) { + return process_request(strm, close_connection, connection_closed, + [&](Request &req) { req.ssl = ssl; }); + }); + + // Shutdown gracefully if the result seemed successful, non-gracefully if + // the connection appeared to be closed. + const bool shutdown_gracefully = ret; + detail::ssl_delete(ctx_mutex_, ssl, shutdown_gracefully); + } + + detail::shutdown_socket(sock); + detail::close_socket(sock); + return ret; +} + +// SSL HTTP client implementation +inline SSLClient::SSLClient(const std::string &host) + : SSLClient(host, 443, std::string(), std::string()) {} + +inline SSLClient::SSLClient(const std::string &host, int port) + : SSLClient(host, port, std::string(), std::string()) {} + +inline SSLClient::SSLClient(const std::string &host, int port, + const std::string &client_cert_path, + const std::string &client_key_path) + : ClientImpl(host, port, client_cert_path, client_key_path) { + ctx_ = SSL_CTX_new(SSLv23_client_method()); + + detail::split(&host_[0], &host_[host_.size()], '.', + [&](const char *b, const char *e) { + host_components_.emplace_back(std::string(b, e)); + }); + if (!client_cert_path.empty() && !client_key_path.empty()) { + if (SSL_CTX_use_certificate_file(ctx_, client_cert_path.c_str(), + SSL_FILETYPE_PEM) != 1 || + SSL_CTX_use_PrivateKey_file(ctx_, client_key_path.c_str(), + SSL_FILETYPE_PEM) != 1) { + SSL_CTX_free(ctx_); + ctx_ = nullptr; + } + } +} + +inline SSLClient::SSLClient(const std::string &host, int port, + X509 *client_cert, EVP_PKEY *client_key) + : ClientImpl(host, port) { + ctx_ = SSL_CTX_new(SSLv23_client_method()); + + detail::split(&host_[0], &host_[host_.size()], '.', + [&](const char *b, const char *e) { + host_components_.emplace_back(std::string(b, e)); + }); + if (client_cert != nullptr && client_key != nullptr) { + if (SSL_CTX_use_certificate(ctx_, client_cert) != 1 || + SSL_CTX_use_PrivateKey(ctx_, client_key) != 1) { + SSL_CTX_free(ctx_); + ctx_ = nullptr; + } + } +} + +inline SSLClient::~SSLClient() { + if (ctx_) { SSL_CTX_free(ctx_); } + // Make sure to shut down SSL since shutdown_ssl will resolve to the + // base function rather than the derived function once we get to the + // base class destructor, and won't free the SSL (causing a leak). + shutdown_ssl_impl(socket_, true); +} + +inline bool SSLClient::is_valid() const { return ctx_; } + +inline void SSLClient::set_ca_cert_store(X509_STORE *ca_cert_store) { + if (ca_cert_store) { + if (ctx_) { + if (SSL_CTX_get_cert_store(ctx_) != ca_cert_store) { + // Free memory allocated for old cert and use new store `ca_cert_store` + SSL_CTX_set_cert_store(ctx_, ca_cert_store); + } + } else { + X509_STORE_free(ca_cert_store); + } + } +} + +inline long SSLClient::get_openssl_verify_result() const { + return verify_result_; +} + +inline SSL_CTX *SSLClient::ssl_context() const { return ctx_; } + +inline bool SSLClient::create_and_connect_socket(Socket &socket, Error &error) { + return is_valid() && ClientImpl::create_and_connect_socket(socket, error); +} + +// Assumes that socket_mutex_ is locked and that there are no requests in flight +inline bool SSLClient::connect_with_proxy(Socket &socket, Response &res, + bool &success, Error &error) { + success = true; + Response res2; + if (!detail::process_client_socket( + socket.sock, read_timeout_sec_, read_timeout_usec_, + write_timeout_sec_, write_timeout_usec_, [&](Stream &strm) { + Request req2; + req2.method = "CONNECT"; + req2.path = host_and_port_; + return process_request(strm, req2, res2, false, error); + })) { + // Thread-safe to close everything because we are assuming there are no + // requests in flight + shutdown_ssl(socket, true); + shutdown_socket(socket); + close_socket(socket); + success = false; + return false; + } + + if (res2.status == 407) { + if (!proxy_digest_auth_username_.empty() && + !proxy_digest_auth_password_.empty()) { + std::map auth; + if (detail::parse_www_authenticate(res2, auth, true)) { + Response res3; + if (!detail::process_client_socket( + socket.sock, read_timeout_sec_, read_timeout_usec_, + write_timeout_sec_, write_timeout_usec_, [&](Stream &strm) { + Request req3; + req3.method = "CONNECT"; + req3.path = host_and_port_; + req3.headers.insert(detail::make_digest_authentication_header( + req3, auth, 1, detail::random_string(10), + proxy_digest_auth_username_, proxy_digest_auth_password_, + true)); + return process_request(strm, req3, res3, false, error); + })) { + // Thread-safe to close everything because we are assuming there are + // no requests in flight + shutdown_ssl(socket, true); + shutdown_socket(socket); + close_socket(socket); + success = false; + return false; + } + } + } else { + res = res2; + return false; + } + } + + return true; +} + +inline bool SSLClient::load_certs() { + bool ret = true; + + std::call_once(initialize_cert_, [&]() { + std::lock_guard guard(ctx_mutex_); + if (!ca_cert_file_path_.empty()) { + if (!SSL_CTX_load_verify_locations(ctx_, ca_cert_file_path_.c_str(), + nullptr)) { + ret = false; + } + } else if (!ca_cert_dir_path_.empty()) { + if (!SSL_CTX_load_verify_locations(ctx_, nullptr, + ca_cert_dir_path_.c_str())) { + ret = false; + } + } else { +#ifdef _WIN32 + detail::load_system_certs_on_windows(SSL_CTX_get_cert_store(ctx_)); +#else + SSL_CTX_set_default_verify_paths(ctx_); +#endif + } + }); + + return ret; +} + +inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) { + auto ssl = detail::ssl_new( + socket.sock, ctx_, ctx_mutex_, + [&](SSL *ssl) { + if (server_certificate_verification_) { + if (!load_certs()) { + error = Error::SSLLoadingCerts; + return false; + } + SSL_set_verify(ssl, SSL_VERIFY_NONE, nullptr); + } + + if (!detail::ssl_connect_or_accept_nonblocking( + socket.sock, ssl, SSL_connect, connection_timeout_sec_, + connection_timeout_usec_)) { + error = Error::SSLConnection; + return false; + } + + if (server_certificate_verification_) { + verify_result_ = SSL_get_verify_result(ssl); + + if (verify_result_ != X509_V_OK) { + error = Error::SSLServerVerification; + return false; + } + + auto server_cert = SSL_get_peer_certificate(ssl); + + if (server_cert == nullptr) { + error = Error::SSLServerVerification; + return false; + } + + if (!verify_host(server_cert)) { + X509_free(server_cert); + error = Error::SSLServerVerification; + return false; + } + X509_free(server_cert); + } + + return true; + }, + [&](SSL *ssl) { + SSL_set_tlsext_host_name(ssl, host_.c_str()); + return true; + }); + + if (ssl) { + socket.ssl = ssl; + return true; + } + + shutdown_socket(socket); + close_socket(socket); + return false; +} + +inline void SSLClient::shutdown_ssl(Socket &socket, bool shutdown_gracefully) { + shutdown_ssl_impl(socket, shutdown_gracefully); +} + +inline void SSLClient::shutdown_ssl_impl(Socket &socket, + bool shutdown_gracefully) { + if (socket.sock == INVALID_SOCKET) { + assert(socket.ssl == nullptr); + return; + } + if (socket.ssl) { + detail::ssl_delete(ctx_mutex_, socket.ssl, shutdown_gracefully); + socket.ssl = nullptr; + } + assert(socket.ssl == nullptr); +} + +inline bool +SSLClient::process_socket(const Socket &socket, + std::function callback) { + assert(socket.ssl); + return detail::process_client_socket_ssl( + socket.ssl, socket.sock, read_timeout_sec_, read_timeout_usec_, + write_timeout_sec_, write_timeout_usec_, std::move(callback)); +} + +inline bool SSLClient::is_ssl() const { return true; } + +inline bool SSLClient::verify_host(X509 *server_cert) const { + /* Quote from RFC2818 section 3.1 "Server Identity" + + If a subjectAltName extension of type dNSName is present, that MUST + be used as the identity. Otherwise, the (most specific) Common Name + field in the Subject field of the certificate MUST be used. Although + the use of the Common Name is existing practice, it is deprecated and + Certification Authorities are encouraged to use the dNSName instead. + + Matching is performed using the matching rules specified by + [RFC2459]. If more than one identity of a given type is present in + the certificate (e.g., more than one dNSName name, a match in any one + of the set is considered acceptable.) Names may contain the wildcard + character * which is considered to match any single domain name + component or component fragment. E.g., *.a.com matches foo.a.com but + not bar.foo.a.com. f*.com matches foo.com but not bar.com. + + In some cases, the URI is specified as an IP address rather than a + hostname. In this case, the iPAddress subjectAltName must be present + in the certificate and must exactly match the IP in the URI. + + */ + return verify_host_with_subject_alt_name(server_cert) || + verify_host_with_common_name(server_cert); +} + +inline bool +SSLClient::verify_host_with_subject_alt_name(X509 *server_cert) const { + auto ret = false; + + auto type = GEN_DNS; + + struct in6_addr addr6; + struct in_addr addr; + size_t addr_len = 0; + +#ifndef __MINGW32__ + if (inet_pton(AF_INET6, host_.c_str(), &addr6)) { + type = GEN_IPADD; + addr_len = sizeof(struct in6_addr); + } else if (inet_pton(AF_INET, host_.c_str(), &addr)) { + type = GEN_IPADD; + addr_len = sizeof(struct in_addr); + } +#endif + + auto alt_names = static_cast( + X509_get_ext_d2i(server_cert, NID_subject_alt_name, nullptr, nullptr)); + + if (alt_names) { + auto dsn_matched = false; + auto ip_mached = false; + + auto count = sk_GENERAL_NAME_num(alt_names); + + for (decltype(count) i = 0; i < count && !dsn_matched; i++) { + auto val = sk_GENERAL_NAME_value(alt_names, i); + if (val->type == type) { + auto name = (const char *)ASN1_STRING_get0_data(val->d.ia5); + auto name_len = (size_t)ASN1_STRING_length(val->d.ia5); + + switch (type) { + case GEN_DNS: dsn_matched = check_host_name(name, name_len); break; + + case GEN_IPADD: + if (!memcmp(&addr6, name, addr_len) || + !memcmp(&addr, name, addr_len)) { + ip_mached = true; + } + break; + } + } + } + + if (dsn_matched || ip_mached) { ret = true; } + } + + GENERAL_NAMES_free((STACK_OF(GENERAL_NAME) *)alt_names); + return ret; +} + +inline bool SSLClient::verify_host_with_common_name(X509 *server_cert) const { + const auto subject_name = X509_get_subject_name(server_cert); + + if (subject_name != nullptr) { + char name[BUFSIZ]; + auto name_len = X509_NAME_get_text_by_NID(subject_name, NID_commonName, + name, sizeof(name)); + + if (name_len != -1) { + return check_host_name(name, static_cast(name_len)); + } + } + + return false; +} + +inline bool SSLClient::check_host_name(const char *pattern, + size_t pattern_len) const { + if (host_.size() == pattern_len && host_ == pattern) { return true; } + + // Wildcard match + // https://bugs.launchpad.net/ubuntu/+source/firefox-3.0/+bug/376484 + std::vector pattern_components; + detail::split(&pattern[0], &pattern[pattern_len], '.', + [&](const char *b, const char *e) { + pattern_components.emplace_back(std::string(b, e)); + }); + + if (host_components_.size() != pattern_components.size()) { return false; } + + auto itr = pattern_components.begin(); + for (const auto &h : host_components_) { + auto &p = *itr; + if (p != h && p != "*") { + auto partial_match = (p.size() > 0 && p[p.size() - 1] == '*' && + !p.compare(0, p.size() - 1, h)); + if (!partial_match) { return false; } + } + ++itr; + } + + return true; +} +#endif + +// Universal client implementation +inline Client::Client(const std::string &scheme_host_port) + : Client(scheme_host_port, std::string(), std::string()) {} + +inline Client::Client(const std::string &scheme_host_port, + const std::string &client_cert_path, + const std::string &client_key_path) { + const static std::regex re( + R"((?:([a-z]+):\/\/)?(?:\[([\d:]+)\]|([^:/?#]+))(?::(\d+))?)"); + + std::smatch m; + if (std::regex_match(scheme_host_port, m, re)) { + auto scheme = m[1].str(); + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + if (!scheme.empty() && (scheme != "http" && scheme != "https")) { +#else + if (!scheme.empty() && scheme != "http") { +#endif + std::string msg = "'" + scheme + "' scheme is not supported."; + throw std::invalid_argument(msg); + return; + } + + auto is_ssl = scheme == "https"; + + auto host = m[2].str(); + if (host.empty()) { host = m[3].str(); } + + auto port_str = m[4].str(); + auto port = !port_str.empty() ? std::stoi(port_str) : (is_ssl ? 443 : 80); + + if (is_ssl) { +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + cli_ = detail::make_unique(host.c_str(), port, + client_cert_path, client_key_path); + is_ssl_ = is_ssl; +#endif + } else { + cli_ = detail::make_unique(host.c_str(), port, + client_cert_path, client_key_path); + } + } else { + cli_ = detail::make_unique(scheme_host_port, 80, + client_cert_path, client_key_path); + } +} + +inline Client::Client(const std::string &host, int port) + : cli_(detail::make_unique(host, port)) {} + +inline Client::Client(const std::string &host, int port, + const std::string &client_cert_path, + const std::string &client_key_path) + : cli_(detail::make_unique(host, port, client_cert_path, + client_key_path)) {} + +inline Client::~Client() {} + +inline bool Client::is_valid() const { + return cli_ != nullptr && cli_->is_valid(); +} + +inline Result Client::Get(const char *path) { return cli_->Get(path); } +inline Result Client::Get(const char *path, const Headers &headers) { + return cli_->Get(path, headers); +} +inline Result Client::Get(const char *path, Progress progress) { + return cli_->Get(path, std::move(progress)); +} +inline Result Client::Get(const char *path, const Headers &headers, + Progress progress) { + return cli_->Get(path, headers, std::move(progress)); +} +inline Result Client::Get(const char *path, ContentReceiver content_receiver) { + return cli_->Get(path, std::move(content_receiver)); +} +inline Result Client::Get(const char *path, const Headers &headers, + ContentReceiver content_receiver) { + return cli_->Get(path, headers, std::move(content_receiver)); +} +inline Result Client::Get(const char *path, ContentReceiver content_receiver, + Progress progress) { + return cli_->Get(path, std::move(content_receiver), std::move(progress)); +} +inline Result Client::Get(const char *path, const Headers &headers, + ContentReceiver content_receiver, Progress progress) { + return cli_->Get(path, headers, std::move(content_receiver), + std::move(progress)); +} +inline Result Client::Get(const char *path, ResponseHandler response_handler, + ContentReceiver content_receiver) { + return cli_->Get(path, std::move(response_handler), + std::move(content_receiver)); +} +inline Result Client::Get(const char *path, const Headers &headers, + ResponseHandler response_handler, + ContentReceiver content_receiver) { + return cli_->Get(path, headers, std::move(response_handler), + std::move(content_receiver)); +} +inline Result Client::Get(const char *path, ResponseHandler response_handler, + ContentReceiver content_receiver, Progress progress) { + return cli_->Get(path, std::move(response_handler), + std::move(content_receiver), std::move(progress)); +} +inline Result Client::Get(const char *path, const Headers &headers, + ResponseHandler response_handler, + ContentReceiver content_receiver, Progress progress) { + return cli_->Get(path, headers, std::move(response_handler), + std::move(content_receiver), std::move(progress)); +} +inline Result Client::Get(const char *path, const Params ¶ms, + const Headers &headers, Progress progress) { + return cli_->Get(path, params, headers, progress); +} +inline Result Client::Get(const char *path, const Params ¶ms, + const Headers &headers, + ContentReceiver content_receiver, Progress progress) { + return cli_->Get(path, params, headers, content_receiver, progress); +} +inline Result Client::Get(const char *path, const Params ¶ms, + const Headers &headers, + ResponseHandler response_handler, + ContentReceiver content_receiver, Progress progress) { + return cli_->Get(path, params, headers, response_handler, content_receiver, + progress); +} + +inline Result Client::Head(const char *path) { return cli_->Head(path); } +inline Result Client::Head(const char *path, const Headers &headers) { + return cli_->Head(path, headers); +} + +inline Result Client::Post(const char *path) { return cli_->Post(path); } +inline Result Client::Post(const char *path, const char *body, + size_t content_length, const char *content_type) { + return cli_->Post(path, body, content_length, content_type); +} +inline Result Client::Post(const char *path, const Headers &headers, + const char *body, size_t content_length, + const char *content_type) { + return cli_->Post(path, headers, body, content_length, content_type); +} +inline Result Client::Post(const char *path, const std::string &body, + const char *content_type) { + return cli_->Post(path, body, content_type); +} +inline Result Client::Post(const char *path, const Headers &headers, + const std::string &body, const char *content_type) { + return cli_->Post(path, headers, body, content_type); +} +inline Result Client::Post(const char *path, size_t content_length, + ContentProvider content_provider, + const char *content_type) { + return cli_->Post(path, content_length, std::move(content_provider), + content_type); +} +inline Result Client::Post(const char *path, + ContentProviderWithoutLength content_provider, + const char *content_type) { + return cli_->Post(path, std::move(content_provider), content_type); +} +inline Result Client::Post(const char *path, const Headers &headers, + size_t content_length, + ContentProvider content_provider, + const char *content_type) { + return cli_->Post(path, headers, content_length, std::move(content_provider), + content_type); +} +inline Result Client::Post(const char *path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const char *content_type) { + return cli_->Post(path, headers, std::move(content_provider), content_type); +} +inline Result Client::Post(const char *path, const Params ¶ms) { + return cli_->Post(path, params); +} +inline Result Client::Post(const char *path, const Headers &headers, + const Params ¶ms) { + return cli_->Post(path, headers, params); +} +inline Result Client::Post(const char *path, + const MultipartFormDataItems &items) { + return cli_->Post(path, items); +} +inline Result Client::Post(const char *path, const Headers &headers, + const MultipartFormDataItems &items) { + return cli_->Post(path, headers, items); +} +inline Result Client::Post(const char *path, const Headers &headers, + const MultipartFormDataItems &items, + const std::string &boundary) { + return cli_->Post(path, headers, items, boundary); +} +inline Result Client::Put(const char *path) { return cli_->Put(path); } +inline Result Client::Put(const char *path, const char *body, + size_t content_length, const char *content_type) { + return cli_->Put(path, body, content_length, content_type); +} +inline Result Client::Put(const char *path, const Headers &headers, + const char *body, size_t content_length, + const char *content_type) { + return cli_->Put(path, headers, body, content_length, content_type); +} +inline Result Client::Put(const char *path, const std::string &body, + const char *content_type) { + return cli_->Put(path, body, content_type); +} +inline Result Client::Put(const char *path, const Headers &headers, + const std::string &body, const char *content_type) { + return cli_->Put(path, headers, body, content_type); +} +inline Result Client::Put(const char *path, size_t content_length, + ContentProvider content_provider, + const char *content_type) { + return cli_->Put(path, content_length, std::move(content_provider), + content_type); +} +inline Result Client::Put(const char *path, + ContentProviderWithoutLength content_provider, + const char *content_type) { + return cli_->Put(path, std::move(content_provider), content_type); +} +inline Result Client::Put(const char *path, const Headers &headers, + size_t content_length, + ContentProvider content_provider, + const char *content_type) { + return cli_->Put(path, headers, content_length, std::move(content_provider), + content_type); +} +inline Result Client::Put(const char *path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const char *content_type) { + return cli_->Put(path, headers, std::move(content_provider), content_type); +} +inline Result Client::Put(const char *path, const Params ¶ms) { + return cli_->Put(path, params); +} +inline Result Client::Put(const char *path, const Headers &headers, + const Params ¶ms) { + return cli_->Put(path, headers, params); +} +inline Result Client::Patch(const char *path) { return cli_->Patch(path); } +inline Result Client::Patch(const char *path, const char *body, + size_t content_length, const char *content_type) { + return cli_->Patch(path, body, content_length, content_type); +} +inline Result Client::Patch(const char *path, const Headers &headers, + const char *body, size_t content_length, + const char *content_type) { + return cli_->Patch(path, headers, body, content_length, content_type); +} +inline Result Client::Patch(const char *path, const std::string &body, + const char *content_type) { + return cli_->Patch(path, body, content_type); +} +inline Result Client::Patch(const char *path, const Headers &headers, + const std::string &body, const char *content_type) { + return cli_->Patch(path, headers, body, content_type); +} +inline Result Client::Patch(const char *path, size_t content_length, + ContentProvider content_provider, + const char *content_type) { + return cli_->Patch(path, content_length, std::move(content_provider), + content_type); +} +inline Result Client::Patch(const char *path, + ContentProviderWithoutLength content_provider, + const char *content_type) { + return cli_->Patch(path, std::move(content_provider), content_type); +} +inline Result Client::Patch(const char *path, const Headers &headers, + size_t content_length, + ContentProvider content_provider, + const char *content_type) { + return cli_->Patch(path, headers, content_length, std::move(content_provider), + content_type); +} +inline Result Client::Patch(const char *path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const char *content_type) { + return cli_->Patch(path, headers, std::move(content_provider), content_type); +} +inline Result Client::Delete(const char *path) { return cli_->Delete(path); } +inline Result Client::Delete(const char *path, const Headers &headers) { + return cli_->Delete(path, headers); +} +inline Result Client::Delete(const char *path, const char *body, + size_t content_length, const char *content_type) { + return cli_->Delete(path, body, content_length, content_type); +} +inline Result Client::Delete(const char *path, const Headers &headers, + const char *body, size_t content_length, + const char *content_type) { + return cli_->Delete(path, headers, body, content_length, content_type); +} +inline Result Client::Delete(const char *path, const std::string &body, + const char *content_type) { + return cli_->Delete(path, body, content_type); +} +inline Result Client::Delete(const char *path, const Headers &headers, + const std::string &body, + const char *content_type) { + return cli_->Delete(path, headers, body, content_type); +} +inline Result Client::Options(const char *path) { return cli_->Options(path); } +inline Result Client::Options(const char *path, const Headers &headers) { + return cli_->Options(path, headers); +} + +inline bool Client::send(Request &req, Response &res, Error &error) { + return cli_->send(req, res, error); +} + +inline Result Client::send(const Request &req) { return cli_->send(req); } + +inline size_t Client::is_socket_open() const { return cli_->is_socket_open(); } + +inline void Client::stop() { cli_->stop(); } + +inline void Client::set_default_headers(Headers headers) { + cli_->set_default_headers(std::move(headers)); +} + +inline void Client::set_address_family(int family) { + cli_->set_address_family(family); +} + +inline void Client::set_tcp_nodelay(bool on) { cli_->set_tcp_nodelay(on); } + +inline void Client::set_socket_options(SocketOptions socket_options) { + cli_->set_socket_options(std::move(socket_options)); +} + +inline void Client::set_connection_timeout(time_t sec, time_t usec) { + cli_->set_connection_timeout(sec, usec); +} + +inline void Client::set_read_timeout(time_t sec, time_t usec) { + cli_->set_read_timeout(sec, usec); +} + +inline void Client::set_write_timeout(time_t sec, time_t usec) { + cli_->set_write_timeout(sec, usec); +} + +inline void Client::set_basic_auth(const char *username, const char *password) { + cli_->set_basic_auth(username, password); +} +inline void Client::set_bearer_token_auth(const char *token) { + cli_->set_bearer_token_auth(token); +} +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline void Client::set_digest_auth(const char *username, + const char *password) { + cli_->set_digest_auth(username, password); +} +#endif + +inline void Client::set_keep_alive(bool on) { cli_->set_keep_alive(on); } +inline void Client::set_follow_location(bool on) { + cli_->set_follow_location(on); +} + +inline void Client::set_url_encode(bool on) { cli_->set_url_encode(on); } + +inline void Client::set_compress(bool on) { cli_->set_compress(on); } + +inline void Client::set_decompress(bool on) { cli_->set_decompress(on); } + +inline void Client::set_interface(const char *intf) { + cli_->set_interface(intf); +} + +inline void Client::set_proxy(const char *host, int port) { + cli_->set_proxy(host, port); +} +inline void Client::set_proxy_basic_auth(const char *username, + const char *password) { + cli_->set_proxy_basic_auth(username, password); +} +inline void Client::set_proxy_bearer_token_auth(const char *token) { + cli_->set_proxy_bearer_token_auth(token); +} +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline void Client::set_proxy_digest_auth(const char *username, + const char *password) { + cli_->set_proxy_digest_auth(username, password); +} +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline void Client::enable_server_certificate_verification(bool enabled) { + cli_->enable_server_certificate_verification(enabled); +} +#endif + +inline void Client::set_logger(Logger logger) { cli_->set_logger(logger); } + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline void Client::set_ca_cert_path(const char *ca_cert_file_path, + const char *ca_cert_dir_path) { + cli_->set_ca_cert_path(ca_cert_file_path, ca_cert_dir_path); +} + +inline void Client::set_ca_cert_store(X509_STORE *ca_cert_store) { + if (is_ssl_) { + static_cast(*cli_).set_ca_cert_store(ca_cert_store); + } else { + cli_->set_ca_cert_store(ca_cert_store); + } +} + +inline long Client::get_openssl_verify_result() const { + if (is_ssl_) { + return static_cast(*cli_).get_openssl_verify_result(); + } + return -1; // NOTE: -1 doesn't match any of X509_V_ERR_??? +} + +inline SSL_CTX *Client::ssl_context() const { + if (is_ssl_) { return static_cast(*cli_).ssl_context(); } + return nullptr; +} +#endif + +// ---------------------------------------------------------------------------- + +} // namespace httplib + +#endif // CPPHTTPLIB_HTTPLIB_H diff --git a/vendor/nlohmann/json.hpp b/vendor/nlohmann/json.hpp index b80386f..cc822a5 100644 --- a/vendor/nlohmann/json.hpp +++ b/vendor/nlohmann/json.hpp @@ -1,12 +1,12 @@ /* __ _____ _____ _____ __| | __| | | | JSON for Modern C++ -| | |__ | | | | | | version 3.2.0 +| | |__ | | | | | | version 3.8.0 |_____|_____|_____|_|___| https://github.com/nlohmann/json Licensed under the MIT License . SPDX-License-Identifier: MIT -Copyright (c) 2013-2018 Niels Lohmann . +Copyright (c) 2013-2019 Niels Lohmann . Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -27,7651 +27,12058 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -#ifndef NLOHMANN_JSON_HPP -#define NLOHMANN_JSON_HPP +#ifndef INCLUDE_NLOHMANN_JSON_HPP_ +#define INCLUDE_NLOHMANN_JSON_HPP_ #define NLOHMANN_JSON_VERSION_MAJOR 3 -#define NLOHMANN_JSON_VERSION_MINOR 2 +#define NLOHMANN_JSON_VERSION_MINOR 8 #define NLOHMANN_JSON_VERSION_PATCH 0 #include // all_of, find, for_each #include // assert -#include // and, not, or #include // nullptr_t, ptrdiff_t, size_t #include // hash, less #include // initializer_list #include // istream, ostream -#include // iterator_traits, random_access_iterator_tag +#include // random_access_iterator_tag +#include // unique_ptr #include // accumulate #include // string, stoi, to_string #include // declval, forward, move, pair, swap +#include // vector -// #include -#ifndef NLOHMANN_JSON_FWD_HPP -#define NLOHMANN_JSON_FWD_HPP +// #include -#include // int64_t, uint64_t + +#include + +// #include + + +#include // transform +#include // array +#include // forward_list +#include // inserter, front_inserter, end #include // map -#include // allocator #include // string -#include // vector +#include // tuple, make_tuple +#include // is_arithmetic, is_same, is_enum, underlying_type, is_convertible +#include // unordered_map +#include // pair, declval +#include // valarray -/*! -@brief namespace for Niels Lohmann -@see https://github.com/nlohmann -@since version 1.0.0 -*/ -namespace nlohmann -{ -/*! -@brief default JSONSerializer template argument +// #include -This serializer ignores the template arguments and uses ADL -([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl)) -for serialization. -*/ -template -struct adl_serializer; -template class ObjectType = - std::map, - template class ArrayType = std::vector, - class StringType = std::string, class BooleanType = bool, - class NumberIntegerType = std::int64_t, - class NumberUnsignedType = std::uint64_t, - class NumberFloatType = double, - template class AllocatorType = std::allocator, - template class JSONSerializer = - adl_serializer> -class basic_json; +// Header is removed in C++20. +// See for more information. -/*! -@brief JSON Pointer +#if __cplusplus <= 201703L + #include // and, not, or +#endif -A JSON pointer defines a string syntax for identifying a specific value -within a JSON document. It can be used with functions `at` and -`operator[]`. Furthermore, JSON pointers are the base for JSON patches. +// #include -@sa [RFC 6901](https://tools.ietf.org/html/rfc6901) -@since version 2.0.0 -*/ -template -class json_pointer; +#include // exception +#include // runtime_error +#include // to_string -/*! -@brief default JSON class +// #include -This type is the default specialization of the @ref basic_json class which -uses the standard template types. -@since version 1.0.0 -*/ -using json = basic_json<>; -} +#include // size_t -#endif +namespace nlohmann +{ +namespace detail +{ +/// struct to capture the start position of the current token +struct position_t +{ + /// the total number of characters read + std::size_t chars_read_total = 0; + /// the number of characters read in the current line + std::size_t chars_read_current_line = 0; + /// the number of lines read + std::size_t lines_read = 0; + + /// conversion to size_t to preserve SAX interface + constexpr operator size_t() const + { + return chars_read_total; + } +}; + +} // namespace detail +} // namespace nlohmann // #include -// This file contains all internal macro definitions -// You MUST include macro_unscope.hpp at the end of json.hpp to undef all of them +#include // pair +// #include +/* Hedley - https://nemequ.github.io/hedley + * Created by Evan Nemerson + * + * To the extent possible under law, the author(s) have dedicated all + * copyright and related and neighboring rights to this software to + * the public domain worldwide. This software is distributed without + * any warranty. + * + * For details, see . + * SPDX-License-Identifier: CC0-1.0 + */ + +#if !defined(JSON_HEDLEY_VERSION) || (JSON_HEDLEY_VERSION < 13) +#if defined(JSON_HEDLEY_VERSION) + #undef JSON_HEDLEY_VERSION +#endif +#define JSON_HEDLEY_VERSION 13 -// exclude unsupported compilers -#if !defined(JSON_SKIP_UNSUPPORTED_COMPILER_CHECK) - #if defined(__clang__) - #if (__clang_major__ * 10000 + __clang_minor__ * 100 + __clang_patchlevel__) < 30400 - #error "unsupported Clang version - see https://github.com/nlohmann/json#supported-compilers" - #endif - #elif defined(__GNUC__) && !(defined(__ICC) || defined(__INTEL_COMPILER)) - #if (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) < 40900 - #error "unsupported GCC version - see https://github.com/nlohmann/json#supported-compilers" - #endif - #endif +#if defined(JSON_HEDLEY_STRINGIFY_EX) + #undef JSON_HEDLEY_STRINGIFY_EX #endif +#define JSON_HEDLEY_STRINGIFY_EX(x) #x -// disable float-equal warnings on GCC/clang -#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) - #pragma GCC diagnostic push - #pragma GCC diagnostic ignored "-Wfloat-equal" +#if defined(JSON_HEDLEY_STRINGIFY) + #undef JSON_HEDLEY_STRINGIFY #endif +#define JSON_HEDLEY_STRINGIFY(x) JSON_HEDLEY_STRINGIFY_EX(x) -// disable documentation warnings on clang -#if defined(__clang__) - #pragma GCC diagnostic push - #pragma GCC diagnostic ignored "-Wdocumentation" +#if defined(JSON_HEDLEY_CONCAT_EX) + #undef JSON_HEDLEY_CONCAT_EX #endif +#define JSON_HEDLEY_CONCAT_EX(a,b) a##b -// allow for portable deprecation warnings -#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) - #define JSON_DEPRECATED __attribute__((deprecated)) -#elif defined(_MSC_VER) - #define JSON_DEPRECATED __declspec(deprecated) -#else - #define JSON_DEPRECATED +#if defined(JSON_HEDLEY_CONCAT) + #undef JSON_HEDLEY_CONCAT #endif +#define JSON_HEDLEY_CONCAT(a,b) JSON_HEDLEY_CONCAT_EX(a,b) -// allow to disable exceptions -#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) - #define JSON_THROW(exception) throw exception - #define JSON_TRY try - #define JSON_CATCH(exception) catch(exception) - #define JSON_INTERNAL_CATCH(exception) catch(exception) -#else - #define JSON_THROW(exception) std::abort() - #define JSON_TRY if(true) - #define JSON_CATCH(exception) if(false) - #define JSON_INTERNAL_CATCH(exception) if(false) +#if defined(JSON_HEDLEY_CONCAT3_EX) + #undef JSON_HEDLEY_CONCAT3_EX #endif +#define JSON_HEDLEY_CONCAT3_EX(a,b,c) a##b##c -// override exception macros -#if defined(JSON_THROW_USER) - #undef JSON_THROW - #define JSON_THROW JSON_THROW_USER +#if defined(JSON_HEDLEY_CONCAT3) + #undef JSON_HEDLEY_CONCAT3 #endif -#if defined(JSON_TRY_USER) - #undef JSON_TRY - #define JSON_TRY JSON_TRY_USER +#define JSON_HEDLEY_CONCAT3(a,b,c) JSON_HEDLEY_CONCAT3_EX(a,b,c) + +#if defined(JSON_HEDLEY_VERSION_ENCODE) + #undef JSON_HEDLEY_VERSION_ENCODE #endif -#if defined(JSON_CATCH_USER) - #undef JSON_CATCH - #define JSON_CATCH JSON_CATCH_USER - #define JSON_INTERNAL_CATCH JSON_CATCH_USER +#define JSON_HEDLEY_VERSION_ENCODE(major,minor,revision) (((major) * 1000000) + ((minor) * 1000) + (revision)) + +#if defined(JSON_HEDLEY_VERSION_DECODE_MAJOR) + #undef JSON_HEDLEY_VERSION_DECODE_MAJOR #endif -#if defined(JSON_INTERNAL_CATCH_USER) - #undef JSON_INTERNAL_CATCH - #define JSON_INTERNAL_CATCH JSON_INTERNAL_CATCH_USER +#define JSON_HEDLEY_VERSION_DECODE_MAJOR(version) ((version) / 1000000) + +#if defined(JSON_HEDLEY_VERSION_DECODE_MINOR) + #undef JSON_HEDLEY_VERSION_DECODE_MINOR #endif +#define JSON_HEDLEY_VERSION_DECODE_MINOR(version) (((version) % 1000000) / 1000) -// manual branch prediction -#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) - #define JSON_LIKELY(x) __builtin_expect(!!(x), 1) - #define JSON_UNLIKELY(x) __builtin_expect(!!(x), 0) -#else - #define JSON_LIKELY(x) x - #define JSON_UNLIKELY(x) x +#if defined(JSON_HEDLEY_VERSION_DECODE_REVISION) + #undef JSON_HEDLEY_VERSION_DECODE_REVISION #endif +#define JSON_HEDLEY_VERSION_DECODE_REVISION(version) ((version) % 1000) -// C++ language standard detection -#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1) // fix for issue #464 - #define JSON_HAS_CPP_17 - #define JSON_HAS_CPP_14 -#elif (defined(__cplusplus) && __cplusplus >= 201402L) || (defined(_HAS_CXX14) && _HAS_CXX14 == 1) - #define JSON_HAS_CPP_14 +#if defined(JSON_HEDLEY_GNUC_VERSION) + #undef JSON_HEDLEY_GNUC_VERSION +#endif +#if defined(__GNUC__) && defined(__GNUC_PATCHLEVEL__) + #define JSON_HEDLEY_GNUC_VERSION JSON_HEDLEY_VERSION_ENCODE(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) +#elif defined(__GNUC__) + #define JSON_HEDLEY_GNUC_VERSION JSON_HEDLEY_VERSION_ENCODE(__GNUC__, __GNUC_MINOR__, 0) #endif -// Ugly macros to avoid uglier copy-paste when specializing basic_json. They -// may be removed in the future once the class is split. +#if defined(JSON_HEDLEY_GNUC_VERSION_CHECK) + #undef JSON_HEDLEY_GNUC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_GNUC_VERSION) + #define JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_GNUC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) (0) +#endif -#define NLOHMANN_BASIC_JSON_TPL_DECLARATION \ - template class ObjectType, \ - template class ArrayType, \ - class StringType, class BooleanType, class NumberIntegerType, \ - class NumberUnsignedType, class NumberFloatType, \ - template class AllocatorType, \ - template class JSONSerializer> +#if defined(JSON_HEDLEY_MSVC_VERSION) + #undef JSON_HEDLEY_MSVC_VERSION +#endif +#if defined(_MSC_FULL_VER) && (_MSC_FULL_VER >= 140000000) + #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_FULL_VER / 10000000, (_MSC_FULL_VER % 10000000) / 100000, (_MSC_FULL_VER % 100000) / 100) +#elif defined(_MSC_FULL_VER) + #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_FULL_VER / 1000000, (_MSC_FULL_VER % 1000000) / 10000, (_MSC_FULL_VER % 10000) / 10) +#elif defined(_MSC_VER) + #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_VER / 100, _MSC_VER % 100, 0) +#endif -#define NLOHMANN_BASIC_JSON_TPL \ - basic_json +#if defined(JSON_HEDLEY_MSVC_VERSION_CHECK) + #undef JSON_HEDLEY_MSVC_VERSION_CHECK +#endif +#if !defined(_MSC_VER) + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (0) +#elif defined(_MSC_VER) && (_MSC_VER >= 1400) + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_FULL_VER >= ((major * 10000000) + (minor * 100000) + (patch))) +#elif defined(_MSC_VER) && (_MSC_VER >= 1200) + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_FULL_VER >= ((major * 1000000) + (minor * 10000) + (patch))) +#else + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_VER >= ((major * 100) + (minor))) +#endif -/*! -@brief Helper to determine whether there's a key_type for T. +#if defined(JSON_HEDLEY_INTEL_VERSION) + #undef JSON_HEDLEY_INTEL_VERSION +#endif +#if defined(__INTEL_COMPILER) && defined(__INTEL_COMPILER_UPDATE) + #define JSON_HEDLEY_INTEL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER / 100, __INTEL_COMPILER % 100, __INTEL_COMPILER_UPDATE) +#elif defined(__INTEL_COMPILER) + #define JSON_HEDLEY_INTEL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER / 100, __INTEL_COMPILER % 100, 0) +#endif -This helper is used to tell associative containers apart from other containers -such as sequence containers. For instance, `std::map` passes the test as it -contains a `mapped_type`, whereas `std::vector` fails the test. +#if defined(JSON_HEDLEY_INTEL_VERSION_CHECK) + #undef JSON_HEDLEY_INTEL_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_INTEL_VERSION) + #define JSON_HEDLEY_INTEL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_INTEL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_INTEL_VERSION_CHECK(major,minor,patch) (0) +#endif -@sa http://stackoverflow.com/a/7728728/266378 -@since version 1.0.0, overworked in version 2.0.6 -*/ -#define NLOHMANN_JSON_HAS_HELPER(type) \ - template struct has_##type { \ - private: \ - template \ - static int detect(U &&); \ - static void detect(...); \ - public: \ - static constexpr bool value = \ - std::is_integral()))>::value; \ - } +#if defined(JSON_HEDLEY_PGI_VERSION) + #undef JSON_HEDLEY_PGI_VERSION +#endif +#if defined(__PGI) && defined(__PGIC__) && defined(__PGIC_MINOR__) && defined(__PGIC_PATCHLEVEL__) + #define JSON_HEDLEY_PGI_VERSION JSON_HEDLEY_VERSION_ENCODE(__PGIC__, __PGIC_MINOR__, __PGIC_PATCHLEVEL__) +#endif -// #include +#if defined(JSON_HEDLEY_PGI_VERSION_CHECK) + #undef JSON_HEDLEY_PGI_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_PGI_VERSION) + #define JSON_HEDLEY_PGI_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_PGI_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_PGI_VERSION_CHECK(major,minor,patch) (0) +#endif +#if defined(JSON_HEDLEY_SUNPRO_VERSION) + #undef JSON_HEDLEY_SUNPRO_VERSION +#endif +#if defined(__SUNPRO_C) && (__SUNPRO_C > 0x1000) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((((__SUNPRO_C >> 16) & 0xf) * 10) + ((__SUNPRO_C >> 12) & 0xf), (((__SUNPRO_C >> 8) & 0xf) * 10) + ((__SUNPRO_C >> 4) & 0xf), (__SUNPRO_C & 0xf) * 10) +#elif defined(__SUNPRO_C) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((__SUNPRO_C >> 8) & 0xf, (__SUNPRO_C >> 4) & 0xf, (__SUNPRO_C) & 0xf) +#elif defined(__SUNPRO_CC) && (__SUNPRO_CC > 0x1000) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((((__SUNPRO_CC >> 16) & 0xf) * 10) + ((__SUNPRO_CC >> 12) & 0xf), (((__SUNPRO_CC >> 8) & 0xf) * 10) + ((__SUNPRO_CC >> 4) & 0xf), (__SUNPRO_CC & 0xf) * 10) +#elif defined(__SUNPRO_CC) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((__SUNPRO_CC >> 8) & 0xf, (__SUNPRO_CC >> 4) & 0xf, (__SUNPRO_CC) & 0xf) +#endif -#include // not -#include // size_t -#include // conditional, enable_if, false_type, integral_constant, is_constructible, is_integral, is_same, remove_cv, remove_reference, true_type +#if defined(JSON_HEDLEY_SUNPRO_VERSION_CHECK) + #undef JSON_HEDLEY_SUNPRO_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_SUNPRO_VERSION) + #define JSON_HEDLEY_SUNPRO_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_SUNPRO_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_SUNPRO_VERSION_CHECK(major,minor,patch) (0) +#endif -namespace nlohmann -{ -namespace detail -{ -// alias templates to reduce boilerplate -template -using enable_if_t = typename std::enable_if::type; +#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION) + #undef JSON_HEDLEY_EMSCRIPTEN_VERSION +#endif +#if defined(__EMSCRIPTEN__) + #define JSON_HEDLEY_EMSCRIPTEN_VERSION JSON_HEDLEY_VERSION_ENCODE(__EMSCRIPTEN_major__, __EMSCRIPTEN_minor__, __EMSCRIPTEN_tiny__) +#endif -template -using uncvref_t = typename std::remove_cv::type>::type; +#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK) + #undef JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION) + #define JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_EMSCRIPTEN_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK(major,minor,patch) (0) +#endif -// implementation of C++14 index_sequence and affiliates -// source: https://stackoverflow.com/a/32223343 -template -struct index_sequence -{ - using type = index_sequence; - using value_type = std::size_t; - static constexpr std::size_t size() noexcept - { - return sizeof...(Ints); - } -}; +#if defined(JSON_HEDLEY_ARM_VERSION) + #undef JSON_HEDLEY_ARM_VERSION +#endif +#if defined(__CC_ARM) && defined(__ARMCOMPILER_VERSION) + #define JSON_HEDLEY_ARM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ARMCOMPILER_VERSION / 1000000, (__ARMCOMPILER_VERSION % 1000000) / 10000, (__ARMCOMPILER_VERSION % 10000) / 100) +#elif defined(__CC_ARM) && defined(__ARMCC_VERSION) + #define JSON_HEDLEY_ARM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ARMCC_VERSION / 1000000, (__ARMCC_VERSION % 1000000) / 10000, (__ARMCC_VERSION % 10000) / 100) +#endif -template -struct merge_and_renumber; +#if defined(JSON_HEDLEY_ARM_VERSION_CHECK) + #undef JSON_HEDLEY_ARM_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_ARM_VERSION) + #define JSON_HEDLEY_ARM_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_ARM_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_ARM_VERSION_CHECK(major,minor,patch) (0) +#endif -template -struct merge_and_renumber, index_sequence> - : index_sequence < I1..., (sizeof...(I1) + I2)... > {}; +#if defined(JSON_HEDLEY_IBM_VERSION) + #undef JSON_HEDLEY_IBM_VERSION +#endif +#if defined(__ibmxl__) + #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ibmxl_version__, __ibmxl_release__, __ibmxl_modification__) +#elif defined(__xlC__) && defined(__xlC_ver__) + #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__xlC__ >> 8, __xlC__ & 0xff, (__xlC_ver__ >> 8) & 0xff) +#elif defined(__xlC__) + #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__xlC__ >> 8, __xlC__ & 0xff, 0) +#endif -template -struct make_index_sequence - : merge_and_renumber < typename make_index_sequence < N / 2 >::type, - typename make_index_sequence < N - N / 2 >::type > {}; +#if defined(JSON_HEDLEY_IBM_VERSION_CHECK) + #undef JSON_HEDLEY_IBM_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_IBM_VERSION) + #define JSON_HEDLEY_IBM_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_IBM_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_IBM_VERSION_CHECK(major,minor,patch) (0) +#endif -template<> struct make_index_sequence<0> : index_sequence<> {}; -template<> struct make_index_sequence<1> : index_sequence<0> {}; +#if defined(JSON_HEDLEY_TI_VERSION) + #undef JSON_HEDLEY_TI_VERSION +#endif +#if \ + defined(__TI_COMPILER_VERSION__) && \ + ( \ + defined(__TMS470__) || defined(__TI_ARM__) || \ + defined(__MSP430__) || \ + defined(__TMS320C2000__) \ + ) +#if (__TI_COMPILER_VERSION__ >= 16000000) + #define JSON_HEDLEY_TI_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif +#endif -template -using index_sequence_for = make_index_sequence; +#if defined(JSON_HEDLEY_TI_VERSION_CHECK) + #undef JSON_HEDLEY_TI_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_VERSION) + #define JSON_HEDLEY_TI_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_VERSION_CHECK(major,minor,patch) (0) +#endif -/* -Implementation of two C++17 constructs: conjunction, negation. This is needed -to avoid evaluating all the traits in a condition +#if defined(JSON_HEDLEY_TI_CL2000_VERSION) + #undef JSON_HEDLEY_TI_CL2000_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__TMS320C2000__) + #define JSON_HEDLEY_TI_CL2000_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif -For example: not std::is_same::value and has_value_type::value -will not compile when T = void (on MSVC at least). Whereas -conjunction>, has_value_type>::value will -stop evaluating if negation<...>::value == false +#if defined(JSON_HEDLEY_TI_CL2000_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL2000_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL2000_VERSION) + #define JSON_HEDLEY_TI_CL2000_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL2000_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL2000_VERSION_CHECK(major,minor,patch) (0) +#endif -Please note that those constructs must be used with caution, since symbols can -become very long quickly (which can slow down compilation and cause MSVC -internal compiler errors). Only use it when you have to (see example ahead). -*/ -template struct conjunction : std::true_type {}; -template struct conjunction : B1 {}; -template -struct conjunction : std::conditional, B1>::type {}; +#if defined(JSON_HEDLEY_TI_CL430_VERSION) + #undef JSON_HEDLEY_TI_CL430_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__MSP430__) + #define JSON_HEDLEY_TI_CL430_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif -template struct negation : std::integral_constant {}; +#if defined(JSON_HEDLEY_TI_CL430_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL430_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL430_VERSION) + #define JSON_HEDLEY_TI_CL430_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL430_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL430_VERSION_CHECK(major,minor,patch) (0) +#endif -// dispatch utility (taken from ranges-v3) -template struct priority_tag : priority_tag < N - 1 > {}; -template<> struct priority_tag<0> {}; - -// taken from ranges-v3 -template -struct static_const -{ - static constexpr T value{}; -}; +#if defined(JSON_HEDLEY_TI_ARMCL_VERSION) + #undef JSON_HEDLEY_TI_ARMCL_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && (defined(__TMS470__) || defined(__TI_ARM__)) + #define JSON_HEDLEY_TI_ARMCL_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif -template -constexpr T static_const::value; -} -} +#if defined(JSON_HEDLEY_TI_ARMCL_VERSION_CHECK) + #undef JSON_HEDLEY_TI_ARMCL_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_ARMCL_VERSION) + #define JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_ARMCL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(major,minor,patch) (0) +#endif -// #include +#if defined(JSON_HEDLEY_TI_CL6X_VERSION) + #undef JSON_HEDLEY_TI_CL6X_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__TMS320C6X__) + #define JSON_HEDLEY_TI_CL6X_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif +#if defined(JSON_HEDLEY_TI_CL6X_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL6X_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL6X_VERSION) + #define JSON_HEDLEY_TI_CL6X_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL6X_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL6X_VERSION_CHECK(major,minor,patch) (0) +#endif -#include // not -#include // numeric_limits -#include // false_type, is_constructible, is_integral, is_same, true_type -#include // declval +#if defined(JSON_HEDLEY_TI_CL7X_VERSION) + #undef JSON_HEDLEY_TI_CL7X_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__C7000__) + #define JSON_HEDLEY_TI_CL7X_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif -// #include +#if defined(JSON_HEDLEY_TI_CL7X_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL7X_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL7X_VERSION) + #define JSON_HEDLEY_TI_CL7X_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL7X_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL7X_VERSION_CHECK(major,minor,patch) (0) +#endif -// #include +#if defined(JSON_HEDLEY_TI_CLPRU_VERSION) + #undef JSON_HEDLEY_TI_CLPRU_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__PRU__) + #define JSON_HEDLEY_TI_CLPRU_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif -// #include +#if defined(JSON_HEDLEY_TI_CLPRU_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CLPRU_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CLPRU_VERSION) + #define JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CLPRU_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(major,minor,patch) (0) +#endif +#if defined(JSON_HEDLEY_CRAY_VERSION) + #undef JSON_HEDLEY_CRAY_VERSION +#endif +#if defined(_CRAYC) + #if defined(_RELEASE_PATCHLEVEL) + #define JSON_HEDLEY_CRAY_VERSION JSON_HEDLEY_VERSION_ENCODE(_RELEASE_MAJOR, _RELEASE_MINOR, _RELEASE_PATCHLEVEL) + #else + #define JSON_HEDLEY_CRAY_VERSION JSON_HEDLEY_VERSION_ENCODE(_RELEASE_MAJOR, _RELEASE_MINOR, 0) + #endif +#endif -namespace nlohmann -{ -/*! -@brief detail namespace with internal helper functions +#if defined(JSON_HEDLEY_CRAY_VERSION_CHECK) + #undef JSON_HEDLEY_CRAY_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_CRAY_VERSION) + #define JSON_HEDLEY_CRAY_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_CRAY_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_CRAY_VERSION_CHECK(major,minor,patch) (0) +#endif -This namespace collects functions that should not be exposed, -implementations of some @ref basic_json methods, and meta-programming helpers. +#if defined(JSON_HEDLEY_IAR_VERSION) + #undef JSON_HEDLEY_IAR_VERSION +#endif +#if defined(__IAR_SYSTEMS_ICC__) + #if __VER__ > 1000 + #define JSON_HEDLEY_IAR_VERSION JSON_HEDLEY_VERSION_ENCODE((__VER__ / 1000000), ((__VER__ / 1000) % 1000), (__VER__ % 1000)) + #else + #define JSON_HEDLEY_IAR_VERSION JSON_HEDLEY_VERSION_ENCODE(VER / 100, __VER__ % 100, 0) + #endif +#endif -@since version 2.1.0 -*/ -namespace detail -{ -///////////// -// helpers // -///////////// +#if defined(JSON_HEDLEY_IAR_VERSION_CHECK) + #undef JSON_HEDLEY_IAR_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_IAR_VERSION) + #define JSON_HEDLEY_IAR_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_IAR_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_IAR_VERSION_CHECK(major,minor,patch) (0) +#endif -template struct is_basic_json : std::false_type {}; +#if defined(JSON_HEDLEY_TINYC_VERSION) + #undef JSON_HEDLEY_TINYC_VERSION +#endif +#if defined(__TINYC__) + #define JSON_HEDLEY_TINYC_VERSION JSON_HEDLEY_VERSION_ENCODE(__TINYC__ / 1000, (__TINYC__ / 100) % 10, __TINYC__ % 100) +#endif -NLOHMANN_BASIC_JSON_TPL_DECLARATION -struct is_basic_json : std::true_type {}; +#if defined(JSON_HEDLEY_TINYC_VERSION_CHECK) + #undef JSON_HEDLEY_TINYC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TINYC_VERSION) + #define JSON_HEDLEY_TINYC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TINYC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TINYC_VERSION_CHECK(major,minor,patch) (0) +#endif -//////////////////////// -// has_/is_ functions // -//////////////////////// +#if defined(JSON_HEDLEY_DMC_VERSION) + #undef JSON_HEDLEY_DMC_VERSION +#endif +#if defined(__DMC__) + #define JSON_HEDLEY_DMC_VERSION JSON_HEDLEY_VERSION_ENCODE(__DMC__ >> 8, (__DMC__ >> 4) & 0xf, __DMC__ & 0xf) +#endif -// source: https://stackoverflow.com/a/37193089/4116453 +#if defined(JSON_HEDLEY_DMC_VERSION_CHECK) + #undef JSON_HEDLEY_DMC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_DMC_VERSION) + #define JSON_HEDLEY_DMC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_DMC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_DMC_VERSION_CHECK(major,minor,patch) (0) +#endif -template -struct is_complete_type : std::false_type {}; +#if defined(JSON_HEDLEY_COMPCERT_VERSION) + #undef JSON_HEDLEY_COMPCERT_VERSION +#endif +#if defined(__COMPCERT_VERSION__) + #define JSON_HEDLEY_COMPCERT_VERSION JSON_HEDLEY_VERSION_ENCODE(__COMPCERT_VERSION__ / 10000, (__COMPCERT_VERSION__ / 100) % 100, __COMPCERT_VERSION__ % 100) +#endif -template -struct is_complete_type : std::true_type {}; +#if defined(JSON_HEDLEY_COMPCERT_VERSION_CHECK) + #undef JSON_HEDLEY_COMPCERT_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_COMPCERT_VERSION) + #define JSON_HEDLEY_COMPCERT_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_COMPCERT_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_COMPCERT_VERSION_CHECK(major,minor,patch) (0) +#endif -NLOHMANN_JSON_HAS_HELPER(mapped_type); -NLOHMANN_JSON_HAS_HELPER(key_type); -NLOHMANN_JSON_HAS_HELPER(value_type); -NLOHMANN_JSON_HAS_HELPER(iterator); +#if defined(JSON_HEDLEY_PELLES_VERSION) + #undef JSON_HEDLEY_PELLES_VERSION +#endif +#if defined(__POCC__) + #define JSON_HEDLEY_PELLES_VERSION JSON_HEDLEY_VERSION_ENCODE(__POCC__ / 100, __POCC__ % 100, 0) +#endif -template -struct is_compatible_object_type_impl : std::false_type {}; +#if defined(JSON_HEDLEY_PELLES_VERSION_CHECK) + #undef JSON_HEDLEY_PELLES_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_PELLES_VERSION) + #define JSON_HEDLEY_PELLES_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_PELLES_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_PELLES_VERSION_CHECK(major,minor,patch) (0) +#endif -template -struct is_compatible_object_type_impl -{ - static constexpr auto value = - std::is_constructible::value and - std::is_constructible::value; -}; +#if defined(JSON_HEDLEY_GCC_VERSION) + #undef JSON_HEDLEY_GCC_VERSION +#endif +#if \ + defined(JSON_HEDLEY_GNUC_VERSION) && \ + !defined(__clang__) && \ + !defined(JSON_HEDLEY_INTEL_VERSION) && \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_ARM_VERSION) && \ + !defined(JSON_HEDLEY_TI_VERSION) && \ + !defined(JSON_HEDLEY_TI_ARMCL_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL430_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL2000_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL6X_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL7X_VERSION) && \ + !defined(JSON_HEDLEY_TI_CLPRU_VERSION) && \ + !defined(__COMPCERT__) + #define JSON_HEDLEY_GCC_VERSION JSON_HEDLEY_GNUC_VERSION +#endif -template -struct is_compatible_string_type_impl : std::false_type {}; +#if defined(JSON_HEDLEY_GCC_VERSION_CHECK) + #undef JSON_HEDLEY_GCC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_GCC_VERSION) + #define JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_GCC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) (0) +#endif -template -struct is_compatible_string_type_impl -{ - static constexpr auto value = - std::is_same::value and - std::is_constructible::value; -}; +#if defined(JSON_HEDLEY_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_HAS_ATTRIBUTE +#endif +#if defined(__has_attribute) + #define JSON_HEDLEY_HAS_ATTRIBUTE(attribute) __has_attribute(attribute) +#else + #define JSON_HEDLEY_HAS_ATTRIBUTE(attribute) (0) +#endif -template -struct is_compatible_object_type -{ - static auto constexpr value = is_compatible_object_type_impl < - conjunction>, - has_mapped_type, - has_key_type>::value, - typename BasicJsonType::object_t, CompatibleObjectType >::value; -}; +#if defined(JSON_HEDLEY_GNUC_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_GNUC_HAS_ATTRIBUTE +#endif +#if defined(__has_attribute) + #define JSON_HEDLEY_GNUC_HAS_ATTRIBUTE(attribute,major,minor,patch) __has_attribute(attribute) +#else + #define JSON_HEDLEY_GNUC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif -template -struct is_compatible_string_type -{ - static auto constexpr value = is_compatible_string_type_impl < - conjunction>, - has_value_type>::value, - typename BasicJsonType::string_t, CompatibleStringType >::value; -}; +#if defined(JSON_HEDLEY_GCC_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_GCC_HAS_ATTRIBUTE +#endif +#if defined(__has_attribute) + #define JSON_HEDLEY_GCC_HAS_ATTRIBUTE(attribute,major,minor,patch) __has_attribute(attribute) +#else + #define JSON_HEDLEY_GCC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif -template -struct is_basic_json_nested_type -{ - static auto constexpr value = std::is_same::value or - std::is_same::value or - std::is_same::value or - std::is_same::value; -}; +#if defined(JSON_HEDLEY_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_HAS_CPP_ATTRIBUTE +#endif +#if \ + defined(__has_cpp_attribute) && \ + defined(__cplusplus) && \ + (!defined(JSON_HEDLEY_SUNPRO_VERSION) || JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0)) + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) __has_cpp_attribute(attribute) +#else + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) (0) +#endif -template -struct is_compatible_array_type -{ - static auto constexpr value = - conjunction>, - negation>, - negation>, - negation>, - has_value_type, - has_iterator>::value; -}; +#if defined(JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS) + #undef JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS +#endif +#if !defined(__cplusplus) || !defined(__has_cpp_attribute) + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) (0) +#elif \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_IAR_VERSION) && \ + (!defined(JSON_HEDLEY_SUNPRO_VERSION) || JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0)) && \ + (!defined(JSON_HEDLEY_MSVC_VERSION) || JSON_HEDLEY_MSVC_VERSION_CHECK(19,20,0)) + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) JSON_HEDLEY_HAS_CPP_ATTRIBUTE(ns::attribute) +#else + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) (0) +#endif -template -struct is_compatible_integer_type_impl : std::false_type {}; +#if defined(JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE +#endif +#if defined(__has_cpp_attribute) && defined(__cplusplus) + #define JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) __has_cpp_attribute(attribute) +#else + #define JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif -template -struct is_compatible_integer_type_impl -{ - // is there an assert somewhere on overflows? - using RealLimits = std::numeric_limits; - using CompatibleLimits = std::numeric_limits; +#if defined(JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE +#endif +#if defined(__has_cpp_attribute) && defined(__cplusplus) + #define JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) __has_cpp_attribute(attribute) +#else + #define JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif - static constexpr auto value = - std::is_constructible::value and - CompatibleLimits::is_integer and - RealLimits::is_signed == CompatibleLimits::is_signed; -}; +#if defined(JSON_HEDLEY_HAS_BUILTIN) + #undef JSON_HEDLEY_HAS_BUILTIN +#endif +#if defined(__has_builtin) + #define JSON_HEDLEY_HAS_BUILTIN(builtin) __has_builtin(builtin) +#else + #define JSON_HEDLEY_HAS_BUILTIN(builtin) (0) +#endif -template -struct is_compatible_integer_type -{ - static constexpr auto value = - is_compatible_integer_type_impl < - std::is_integral::value and - not std::is_same::value, - RealIntegerType, CompatibleNumberIntegerType > ::value; -}; +#if defined(JSON_HEDLEY_GNUC_HAS_BUILTIN) + #undef JSON_HEDLEY_GNUC_HAS_BUILTIN +#endif +#if defined(__has_builtin) + #define JSON_HEDLEY_GNUC_HAS_BUILTIN(builtin,major,minor,patch) __has_builtin(builtin) +#else + #define JSON_HEDLEY_GNUC_HAS_BUILTIN(builtin,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif -// trait checking if JSONSerializer::from_json(json const&, udt&) exists -template -struct has_from_json -{ - private: - // also check the return type of from_json - template::from_json( - std::declval(), std::declval()))>::value>> - static int detect(U&&); - static void detect(...); +#if defined(JSON_HEDLEY_GCC_HAS_BUILTIN) + #undef JSON_HEDLEY_GCC_HAS_BUILTIN +#endif +#if defined(__has_builtin) + #define JSON_HEDLEY_GCC_HAS_BUILTIN(builtin,major,minor,patch) __has_builtin(builtin) +#else + #define JSON_HEDLEY_GCC_HAS_BUILTIN(builtin,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif - public: - static constexpr bool value = std::is_integral>()))>::value; -}; +#if defined(JSON_HEDLEY_HAS_FEATURE) + #undef JSON_HEDLEY_HAS_FEATURE +#endif +#if defined(__has_feature) + #define JSON_HEDLEY_HAS_FEATURE(feature) __has_feature(feature) +#else + #define JSON_HEDLEY_HAS_FEATURE(feature) (0) +#endif -// This trait checks if JSONSerializer::from_json(json const&) exists -// this overload is used for non-default-constructible user-defined-types -template -struct has_non_default_from_json -{ - private: - template < - typename U, - typename = enable_if_t::from_json(std::declval()))>::value >> - static int detect(U&&); - static void detect(...); +#if defined(JSON_HEDLEY_GNUC_HAS_FEATURE) + #undef JSON_HEDLEY_GNUC_HAS_FEATURE +#endif +#if defined(__has_feature) + #define JSON_HEDLEY_GNUC_HAS_FEATURE(feature,major,minor,patch) __has_feature(feature) +#else + #define JSON_HEDLEY_GNUC_HAS_FEATURE(feature,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif - public: - static constexpr bool value = std::is_integral>()))>::value; -}; +#if defined(JSON_HEDLEY_GCC_HAS_FEATURE) + #undef JSON_HEDLEY_GCC_HAS_FEATURE +#endif +#if defined(__has_feature) + #define JSON_HEDLEY_GCC_HAS_FEATURE(feature,major,minor,patch) __has_feature(feature) +#else + #define JSON_HEDLEY_GCC_HAS_FEATURE(feature,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif -// This trait checks if BasicJsonType::json_serializer::to_json exists -template -struct has_to_json -{ - private: - template::to_json( - std::declval(), std::declval()))> - static int detect(U&&); - static void detect(...); +#if defined(JSON_HEDLEY_HAS_EXTENSION) + #undef JSON_HEDLEY_HAS_EXTENSION +#endif +#if defined(__has_extension) + #define JSON_HEDLEY_HAS_EXTENSION(extension) __has_extension(extension) +#else + #define JSON_HEDLEY_HAS_EXTENSION(extension) (0) +#endif - public: - static constexpr bool value = std::is_integral>()))>::value; -}; +#if defined(JSON_HEDLEY_GNUC_HAS_EXTENSION) + #undef JSON_HEDLEY_GNUC_HAS_EXTENSION +#endif +#if defined(__has_extension) + #define JSON_HEDLEY_GNUC_HAS_EXTENSION(extension,major,minor,patch) __has_extension(extension) +#else + #define JSON_HEDLEY_GNUC_HAS_EXTENSION(extension,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif -template -struct is_compatible_complete_type -{ - static constexpr bool value = - not std::is_base_of::value and - not is_basic_json::value and - not is_basic_json_nested_type::value and - has_to_json::value; -}; +#if defined(JSON_HEDLEY_GCC_HAS_EXTENSION) + #undef JSON_HEDLEY_GCC_HAS_EXTENSION +#endif +#if defined(__has_extension) + #define JSON_HEDLEY_GCC_HAS_EXTENSION(extension,major,minor,patch) __has_extension(extension) +#else + #define JSON_HEDLEY_GCC_HAS_EXTENSION(extension,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif -template -struct is_compatible_type - : conjunction, - is_compatible_complete_type> -{ -}; -} -} +#if defined(JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE) + #undef JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE +#endif +#if defined(__has_declspec_attribute) + #define JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE(attribute) __has_declspec_attribute(attribute) +#else + #define JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE(attribute) (0) +#endif -// #include +#if defined(JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE) + #undef JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE +#endif +#if defined(__has_declspec_attribute) + #define JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) __has_declspec_attribute(attribute) +#else + #define JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif +#if defined(JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE) + #undef JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE +#endif +#if defined(__has_declspec_attribute) + #define JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) __has_declspec_attribute(attribute) +#else + #define JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif -#include // exception -#include // runtime_error -#include // to_string +#if defined(JSON_HEDLEY_HAS_WARNING) + #undef JSON_HEDLEY_HAS_WARNING +#endif +#if defined(__has_warning) + #define JSON_HEDLEY_HAS_WARNING(warning) __has_warning(warning) +#else + #define JSON_HEDLEY_HAS_WARNING(warning) (0) +#endif -namespace nlohmann -{ -namespace detail -{ -//////////////// -// exceptions // -//////////////// +#if defined(JSON_HEDLEY_GNUC_HAS_WARNING) + #undef JSON_HEDLEY_GNUC_HAS_WARNING +#endif +#if defined(__has_warning) + #define JSON_HEDLEY_GNUC_HAS_WARNING(warning,major,minor,patch) __has_warning(warning) +#else + #define JSON_HEDLEY_GNUC_HAS_WARNING(warning,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif -/*! -@brief general exception of the @ref basic_json class +#if defined(JSON_HEDLEY_GCC_HAS_WARNING) + #undef JSON_HEDLEY_GCC_HAS_WARNING +#endif +#if defined(__has_warning) + #define JSON_HEDLEY_GCC_HAS_WARNING(warning,major,minor,patch) __has_warning(warning) +#else + #define JSON_HEDLEY_GCC_HAS_WARNING(warning,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif -This class is an extension of `std::exception` objects with a member @a id for -exception ids. It is used as the base class for all exceptions thrown by the -@ref basic_json class. This class can hence be used as "wildcard" to catch -exceptions. +/* JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_ is for + HEDLEY INTERNAL USE ONLY. API subject to change without notice. */ +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_ +#endif +#if defined(__cplusplus) +# if JSON_HEDLEY_HAS_WARNING("-Wc++98-compat") +# if JSON_HEDLEY_HAS_WARNING("-Wc++17-extensions") +# define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(xpr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wc++98-compat\"") \ + _Pragma("clang diagnostic ignored \"-Wc++17-extensions\"") \ + xpr \ + JSON_HEDLEY_DIAGNOSTIC_POP +# else +# define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(xpr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wc++98-compat\"") \ + xpr \ + JSON_HEDLEY_DIAGNOSTIC_POP +# endif +# endif +#endif +#if !defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(x) x +#endif -Subclasses: -- @ref parse_error for exceptions indicating a parse error -- @ref invalid_iterator for exceptions indicating errors with iterators -- @ref type_error for exceptions indicating executing a member function with - a wrong type -- @ref out_of_range for exceptions indicating access out of the defined range -- @ref other_error for exceptions indicating other library errors +#if defined(JSON_HEDLEY_CONST_CAST) + #undef JSON_HEDLEY_CONST_CAST +#endif +#if defined(__cplusplus) +# define JSON_HEDLEY_CONST_CAST(T, expr) (const_cast(expr)) +#elif \ + JSON_HEDLEY_HAS_WARNING("-Wcast-qual") || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,6,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) +# define JSON_HEDLEY_CONST_CAST(T, expr) (__extension__ ({ \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL \ + ((T) (expr)); \ + JSON_HEDLEY_DIAGNOSTIC_POP \ + })) +#else +# define JSON_HEDLEY_CONST_CAST(T, expr) ((T) (expr)) +#endif -@internal -@note To have nothrow-copy-constructible exceptions, we internally use - `std::runtime_error` which can cope with arbitrary-length error messages. - Intermediate strings are built with static functions and then passed to - the actual constructor. -@endinternal +#if defined(JSON_HEDLEY_REINTERPRET_CAST) + #undef JSON_HEDLEY_REINTERPRET_CAST +#endif +#if defined(__cplusplus) + #define JSON_HEDLEY_REINTERPRET_CAST(T, expr) (reinterpret_cast(expr)) +#else + #define JSON_HEDLEY_REINTERPRET_CAST(T, expr) ((T) (expr)) +#endif -@liveexample{The following code shows how arbitrary library exceptions can be -caught.,exception} +#if defined(JSON_HEDLEY_STATIC_CAST) + #undef JSON_HEDLEY_STATIC_CAST +#endif +#if defined(__cplusplus) + #define JSON_HEDLEY_STATIC_CAST(T, expr) (static_cast(expr)) +#else + #define JSON_HEDLEY_STATIC_CAST(T, expr) ((T) (expr)) +#endif -@since version 3.0.0 -*/ -class exception : public std::exception -{ - public: - /// returns the explanatory string - const char* what() const noexcept override - { - return m.what(); - } +#if defined(JSON_HEDLEY_CPP_CAST) + #undef JSON_HEDLEY_CPP_CAST +#endif +#if defined(__cplusplus) +# if JSON_HEDLEY_HAS_WARNING("-Wold-style-cast") +# define JSON_HEDLEY_CPP_CAST(T, expr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wold-style-cast\"") \ + ((T) (expr)) \ + JSON_HEDLEY_DIAGNOSTIC_POP +# elif JSON_HEDLEY_IAR_VERSION_CHECK(8,3,0) +# define JSON_HEDLEY_CPP_CAST(T, expr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("diag_suppress=Pe137") \ + JSON_HEDLEY_DIAGNOSTIC_POP \ +# else +# define JSON_HEDLEY_CPP_CAST(T, expr) ((T) (expr)) +# endif +#else +# define JSON_HEDLEY_CPP_CAST(T, expr) (expr) +#endif - /// the id of the exception - const int id; +#if \ + (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) || \ + defined(__clang__) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(18,4,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,7,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(2,0,1) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,0,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(5,0,0) || \ + JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,17) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(8,0,0) || \ + (JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) && defined(__C99_PRAGMA_OPERATOR)) + #define JSON_HEDLEY_PRAGMA(value) _Pragma(#value) +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) + #define JSON_HEDLEY_PRAGMA(value) __pragma(value) +#else + #define JSON_HEDLEY_PRAGMA(value) +#endif - protected: - exception(int id_, const char* what_arg) : id(id_), m(what_arg) {} +#if defined(JSON_HEDLEY_DIAGNOSTIC_PUSH) + #undef JSON_HEDLEY_DIAGNOSTIC_PUSH +#endif +#if defined(JSON_HEDLEY_DIAGNOSTIC_POP) + #undef JSON_HEDLEY_DIAGNOSTIC_POP +#endif +#if defined(__clang__) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("clang diagnostic push") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("clang diagnostic pop") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("warning(push)") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("warning(pop)") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,6,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("GCC diagnostic push") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("GCC diagnostic pop") +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH __pragma(warning(push)) + #define JSON_HEDLEY_DIAGNOSTIC_POP __pragma(warning(pop)) +#elif JSON_HEDLEY_ARM_VERSION_CHECK(5,6,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("push") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("pop") +#elif \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,4,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("diag_push") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("diag_pop") +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(2,90,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("warning(push)") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("warning(pop)") +#else + #define JSON_HEDLEY_DIAGNOSTIC_PUSH + #define JSON_HEDLEY_DIAGNOSTIC_POP +#endif - static std::string name(const std::string& ename, int id_) - { - return "[json.exception." + ename + "." + std::to_string(id_) + "] "; - } +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wdeprecated-declarations") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\"") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("warning(disable:1478 1786)") +#elif JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1215,1444") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,3,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED __pragma(warning(disable:4996)) +#elif \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1291,1718") +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,13,0) && !defined(__cplusplus) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("error_messages(off,E_DEPRECATED_ATT,E_DEPRECATED_ATT_MESS)") +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,13,0) && defined(__cplusplus) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("error_messages(off,symdeprecated,symdeprecated2)") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress=Pe1444,Pe1215") +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(2,90,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("warn(disable:2241)") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED +#endif - private: - /// an exception object as storage for error messages - std::runtime_error m; -}; +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunknown-pragmas") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("clang diagnostic ignored \"-Wunknown-pragmas\"") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("warning(disable:161)") +#elif JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 1675") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,3,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("GCC diagnostic ignored \"-Wunknown-pragmas\"") +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS __pragma(warning(disable:4068)) +#elif \ + JSON_HEDLEY_TI_VERSION_CHECK(16,9,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,0,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,3,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 163") +#elif JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 163") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress=Pe161") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS +#endif -/*! -@brief exception indicating a parse error +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunknown-attributes") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("clang diagnostic ignored \"-Wunknown-attributes\"") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,6,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(17,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("warning(disable:1292)") +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(19,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES __pragma(warning(disable:5030)) +#elif JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1097") +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,14,0) && defined(__cplusplus) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("error_messages(off,attrskipunsup)") +#elif \ + JSON_HEDLEY_TI_VERSION_CHECK(18,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,3,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1173") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress=Pe1097") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES +#endif -This exception is thrown by the library when a parse error occurs. Parse errors -can occur during the deserialization of JSON text, CBOR, MessagePack, as well -as when using JSON Patch. +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wcast-qual") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL _Pragma("clang diagnostic ignored \"-Wcast-qual\"") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL _Pragma("warning(disable:2203 2331)") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(3,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL _Pragma("GCC diagnostic ignored \"-Wcast-qual\"") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL +#endif -Member @a byte holds the byte index of the last read character in the input -file. +#if defined(JSON_HEDLEY_DEPRECATED) + #undef JSON_HEDLEY_DEPRECATED +#endif +#if defined(JSON_HEDLEY_DEPRECATED_FOR) + #undef JSON_HEDLEY_DEPRECATED_FOR +#endif +#if JSON_HEDLEY_MSVC_VERSION_CHECK(14,0,0) + #define JSON_HEDLEY_DEPRECATED(since) __declspec(deprecated("Since " # since)) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __declspec(deprecated("Since " #since "; use " #replacement)) +#elif defined(__cplusplus) && (__cplusplus >= 201402L) + #define JSON_HEDLEY_DEPRECATED(since) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[deprecated("Since " #since)]]) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[deprecated("Since " #since "; use " #replacement)]]) +#elif \ + JSON_HEDLEY_HAS_EXTENSION(attribute_deprecated_with_message) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,5,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,6,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,13,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(18,1,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(18,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,3,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,3,0) + #define JSON_HEDLEY_DEPRECATED(since) __attribute__((__deprecated__("Since " #since))) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __attribute__((__deprecated__("Since " #since "; use " #replacement))) +#elif \ + JSON_HEDLEY_HAS_ATTRIBUTE(deprecated) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) + #define JSON_HEDLEY_DEPRECATED(since) __attribute__((__deprecated__)) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __attribute__((__deprecated__)) +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ + JSON_HEDLEY_PELLES_VERSION_CHECK(6,50,0) + #define JSON_HEDLEY_DEPRECATED(since) __declspec(deprecated) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __declspec(deprecated) +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DEPRECATED(since) _Pragma("deprecated") + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) _Pragma("deprecated") +#else + #define JSON_HEDLEY_DEPRECATED(since) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) +#endif -Exceptions have ids 1xx. +#if defined(JSON_HEDLEY_UNAVAILABLE) + #undef JSON_HEDLEY_UNAVAILABLE +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(warning) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,3,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_UNAVAILABLE(available_since) __attribute__((__warning__("Not available until " #available_since))) +#else + #define JSON_HEDLEY_UNAVAILABLE(available_since) +#endif -name / id | example message | description ------------------------------- | --------------- | ------------------------- -json.exception.parse_error.101 | parse error at 2: unexpected end of input; expected string literal | This error indicates a syntax error while deserializing a JSON text. The error message describes that an unexpected token (character) was encountered, and the member @a byte indicates the error position. -json.exception.parse_error.102 | parse error at 14: missing or wrong low surrogate | JSON uses the `\uxxxx` format to describe Unicode characters. Code points above above 0xFFFF are split into two `\uxxxx` entries ("surrogate pairs"). This error indicates that the surrogate pair is incomplete or contains an invalid code point. -json.exception.parse_error.103 | parse error: code points above 0x10FFFF are invalid | Unicode supports code points up to 0x10FFFF. Code points above 0x10FFFF are invalid. -json.exception.parse_error.104 | parse error: JSON patch must be an array of objects | [RFC 6902](https://tools.ietf.org/html/rfc6902) requires a JSON Patch document to be a JSON document that represents an array of objects. -json.exception.parse_error.105 | parse error: operation must have string member 'op' | An operation of a JSON Patch document must contain exactly one "op" member, whose value indicates the operation to perform. Its value must be one of "add", "remove", "replace", "move", "copy", or "test"; other values are errors. -json.exception.parse_error.106 | parse error: array index '01' must not begin with '0' | An array index in a JSON Pointer ([RFC 6901](https://tools.ietf.org/html/rfc6901)) may be `0` or any number without a leading `0`. -json.exception.parse_error.107 | parse error: JSON pointer must be empty or begin with '/' - was: 'foo' | A JSON Pointer must be a Unicode string containing a sequence of zero or more reference tokens, each prefixed by a `/` character. -json.exception.parse_error.108 | parse error: escape character '~' must be followed with '0' or '1' | In a JSON Pointer, only `~0` and `~1` are valid escape sequences. -json.exception.parse_error.109 | parse error: array index 'one' is not a number | A JSON Pointer array index must be a number. -json.exception.parse_error.110 | parse error at 1: cannot read 2 bytes from vector | When parsing CBOR or MessagePack, the byte vector ends before the complete value has been read. -json.exception.parse_error.112 | parse error at 1: error reading CBOR; last byte: 0xF8 | Not all types of CBOR or MessagePack are supported. This exception occurs if an unsupported byte was read. -json.exception.parse_error.113 | parse error at 2: expected a CBOR string; last byte: 0x98 | While parsing a map key, a value that is not a string has been read. +#if defined(JSON_HEDLEY_WARN_UNUSED_RESULT) + #undef JSON_HEDLEY_WARN_UNUSED_RESULT +#endif +#if defined(JSON_HEDLEY_WARN_UNUSED_RESULT_MSG) + #undef JSON_HEDLEY_WARN_UNUSED_RESULT_MSG +#endif +#if (JSON_HEDLEY_HAS_CPP_ATTRIBUTE(nodiscard) >= 201907L) + #define JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard]]) + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard(msg)]]) +#elif JSON_HEDLEY_HAS_CPP_ATTRIBUTE(nodiscard) + #define JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard]]) + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard]]) +#elif \ + JSON_HEDLEY_HAS_ATTRIBUTE(warn_unused_result) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0) && defined(__cplusplus)) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) + #define JSON_HEDLEY_WARN_UNUSED_RESULT __attribute__((__warn_unused_result__)) + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) __attribute__((__warn_unused_result__)) +#elif defined(_Check_return_) /* SAL */ + #define JSON_HEDLEY_WARN_UNUSED_RESULT _Check_return_ + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) _Check_return_ +#else + #define JSON_HEDLEY_WARN_UNUSED_RESULT + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) +#endif -@note For an input with n bytes, 1 is the index of the first character and n+1 - is the index of the terminating null byte or the end of file. This also - holds true when reading a byte vector (CBOR or MessagePack). +#if defined(JSON_HEDLEY_SENTINEL) + #undef JSON_HEDLEY_SENTINEL +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(sentinel) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,4,0) + #define JSON_HEDLEY_SENTINEL(position) __attribute__((__sentinel__(position))) +#else + #define JSON_HEDLEY_SENTINEL(position) +#endif -@liveexample{The following code shows how a `parse_error` exception can be -caught.,parse_error} +#if defined(JSON_HEDLEY_NO_RETURN) + #undef JSON_HEDLEY_NO_RETURN +#endif +#if JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_NO_RETURN __noreturn +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_NO_RETURN __attribute__((__noreturn__)) +#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L + #define JSON_HEDLEY_NO_RETURN _Noreturn +#elif defined(__cplusplus) && (__cplusplus >= 201103L) + #define JSON_HEDLEY_NO_RETURN JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[noreturn]]) +#elif \ + JSON_HEDLEY_HAS_ATTRIBUTE(noreturn) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,2,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) + #define JSON_HEDLEY_NO_RETURN __attribute__((__noreturn__)) +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) + #define JSON_HEDLEY_NO_RETURN _Pragma("does_not_return") +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) + #define JSON_HEDLEY_NO_RETURN __declspec(noreturn) +#elif JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,0,0) && defined(__cplusplus) + #define JSON_HEDLEY_NO_RETURN _Pragma("FUNC_NEVER_RETURNS;") +#elif JSON_HEDLEY_COMPCERT_VERSION_CHECK(3,2,0) + #define JSON_HEDLEY_NO_RETURN __attribute((noreturn)) +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(9,0,0) + #define JSON_HEDLEY_NO_RETURN __declspec(noreturn) +#else + #define JSON_HEDLEY_NO_RETURN +#endif -@sa @ref exception for the base class of the library exceptions -@sa @ref invalid_iterator for exceptions indicating errors with iterators -@sa @ref type_error for exceptions indicating executing a member function with - a wrong type -@sa @ref out_of_range for exceptions indicating access out of the defined range -@sa @ref other_error for exceptions indicating other library errors +#if defined(JSON_HEDLEY_NO_ESCAPE) + #undef JSON_HEDLEY_NO_ESCAPE +#endif +#if JSON_HEDLEY_HAS_ATTRIBUTE(noescape) + #define JSON_HEDLEY_NO_ESCAPE __attribute__((__noescape__)) +#else + #define JSON_HEDLEY_NO_ESCAPE +#endif -@since version 3.0.0 -*/ -class parse_error : public exception -{ - public: - /*! - @brief create a parse error exception - @param[in] id_ the id of the exception - @param[in] byte_ the byte index where the error occurred (or 0 if the - position cannot be determined) - @param[in] what_arg the explanatory string - @return parse_error object - */ - static parse_error create(int id_, std::size_t byte_, const std::string& what_arg) - { - std::string w = exception::name("parse_error", id_) + "parse error" + - (byte_ != 0 ? (" at " + std::to_string(byte_)) : "") + - ": " + what_arg; - return parse_error(id_, byte_, w.c_str()); - } +#if defined(JSON_HEDLEY_UNREACHABLE) + #undef JSON_HEDLEY_UNREACHABLE +#endif +#if defined(JSON_HEDLEY_UNREACHABLE_RETURN) + #undef JSON_HEDLEY_UNREACHABLE_RETURN +#endif +#if defined(JSON_HEDLEY_ASSUME) + #undef JSON_HEDLEY_ASSUME +#endif +#if \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_ASSUME(expr) __assume(expr) +#elif JSON_HEDLEY_HAS_BUILTIN(__builtin_assume) + #define JSON_HEDLEY_ASSUME(expr) __builtin_assume(expr) +#elif \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(4,0,0) + #if defined(__cplusplus) + #define JSON_HEDLEY_ASSUME(expr) std::_nassert(expr) + #else + #define JSON_HEDLEY_ASSUME(expr) _nassert(expr) + #endif +#endif +#if \ + (JSON_HEDLEY_HAS_BUILTIN(__builtin_unreachable) && (!defined(JSON_HEDLEY_ARM_VERSION))) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,5,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(18,10,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(13,1,5) + #define JSON_HEDLEY_UNREACHABLE() __builtin_unreachable() +#elif defined(JSON_HEDLEY_ASSUME) + #define JSON_HEDLEY_UNREACHABLE() JSON_HEDLEY_ASSUME(0) +#endif +#if !defined(JSON_HEDLEY_ASSUME) + #if defined(JSON_HEDLEY_UNREACHABLE) + #define JSON_HEDLEY_ASSUME(expr) JSON_HEDLEY_STATIC_CAST(void, ((expr) ? 1 : (JSON_HEDLEY_UNREACHABLE(), 1))) + #else + #define JSON_HEDLEY_ASSUME(expr) JSON_HEDLEY_STATIC_CAST(void, expr) + #endif +#endif +#if defined(JSON_HEDLEY_UNREACHABLE) + #if \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(4,0,0) + #define JSON_HEDLEY_UNREACHABLE_RETURN(value) return (JSON_HEDLEY_STATIC_CAST(void, JSON_HEDLEY_ASSUME(0)), (value)) + #else + #define JSON_HEDLEY_UNREACHABLE_RETURN(value) JSON_HEDLEY_UNREACHABLE() + #endif +#else + #define JSON_HEDLEY_UNREACHABLE_RETURN(value) return (value) +#endif +#if !defined(JSON_HEDLEY_UNREACHABLE) + #define JSON_HEDLEY_UNREACHABLE() JSON_HEDLEY_ASSUME(0) +#endif - /*! - @brief byte index of the parse error +JSON_HEDLEY_DIAGNOSTIC_PUSH +#if JSON_HEDLEY_HAS_WARNING("-Wpedantic") + #pragma clang diagnostic ignored "-Wpedantic" +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wc++98-compat-pedantic") && defined(__cplusplus) + #pragma clang diagnostic ignored "-Wc++98-compat-pedantic" +#endif +#if JSON_HEDLEY_GCC_HAS_WARNING("-Wvariadic-macros",4,0,0) + #if defined(__clang__) + #pragma clang diagnostic ignored "-Wvariadic-macros" + #elif defined(JSON_HEDLEY_GCC_VERSION) + #pragma GCC diagnostic ignored "-Wvariadic-macros" + #endif +#endif +#if defined(JSON_HEDLEY_NON_NULL) + #undef JSON_HEDLEY_NON_NULL +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(nonnull) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,3,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) + #define JSON_HEDLEY_NON_NULL(...) __attribute__((__nonnull__(__VA_ARGS__))) +#else + #define JSON_HEDLEY_NON_NULL(...) +#endif +JSON_HEDLEY_DIAGNOSTIC_POP - The byte index of the last read character in the input file. +#if defined(JSON_HEDLEY_PRINTF_FORMAT) + #undef JSON_HEDLEY_PRINTF_FORMAT +#endif +#if defined(__MINGW32__) && JSON_HEDLEY_GCC_HAS_ATTRIBUTE(format,4,4,0) && !defined(__USE_MINGW_ANSI_STDIO) + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __attribute__((__format__(ms_printf, string_idx, first_to_check))) +#elif defined(__MINGW32__) && JSON_HEDLEY_GCC_HAS_ATTRIBUTE(format,4,4,0) && defined(__USE_MINGW_ANSI_STDIO) + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __attribute__((__format__(gnu_printf, string_idx, first_to_check))) +#elif \ + JSON_HEDLEY_HAS_ATTRIBUTE(format) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,6,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __attribute__((__format__(__printf__, string_idx, first_to_check))) +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(6,0,0) + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __declspec(vaformat(printf,string_idx,first_to_check)) +#else + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) +#endif - @note For an input with n bytes, 1 is the index of the first character and - n+1 is the index of the terminating null byte or the end of file. - This also holds true when reading a byte vector (CBOR or MessagePack). - */ - const std::size_t byte; +#if defined(JSON_HEDLEY_CONSTEXPR) + #undef JSON_HEDLEY_CONSTEXPR +#endif +#if defined(__cplusplus) + #if __cplusplus >= 201103L + #define JSON_HEDLEY_CONSTEXPR JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(constexpr) + #endif +#endif +#if !defined(JSON_HEDLEY_CONSTEXPR) + #define JSON_HEDLEY_CONSTEXPR +#endif - private: - parse_error(int id_, std::size_t byte_, const char* what_arg) - : exception(id_, what_arg), byte(byte_) {} -}; +#if defined(JSON_HEDLEY_PREDICT) + #undef JSON_HEDLEY_PREDICT +#endif +#if defined(JSON_HEDLEY_LIKELY) + #undef JSON_HEDLEY_LIKELY +#endif +#if defined(JSON_HEDLEY_UNLIKELY) + #undef JSON_HEDLEY_UNLIKELY +#endif +#if defined(JSON_HEDLEY_UNPREDICTABLE) + #undef JSON_HEDLEY_UNPREDICTABLE +#endif +#if JSON_HEDLEY_HAS_BUILTIN(__builtin_unpredictable) + #define JSON_HEDLEY_UNPREDICTABLE(expr) __builtin_unpredictable((expr)) +#endif +#if \ + JSON_HEDLEY_HAS_BUILTIN(__builtin_expect_with_probability) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(9,0,0) +# define JSON_HEDLEY_PREDICT(expr, value, probability) __builtin_expect_with_probability( (expr), (value), (probability)) +# define JSON_HEDLEY_PREDICT_TRUE(expr, probability) __builtin_expect_with_probability(!!(expr), 1 , (probability)) +# define JSON_HEDLEY_PREDICT_FALSE(expr, probability) __builtin_expect_with_probability(!!(expr), 0 , (probability)) +# define JSON_HEDLEY_LIKELY(expr) __builtin_expect (!!(expr), 1 ) +# define JSON_HEDLEY_UNLIKELY(expr) __builtin_expect (!!(expr), 0 ) +#elif \ + JSON_HEDLEY_HAS_BUILTIN(__builtin_expect) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0) && defined(__cplusplus)) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,7,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,1,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,27) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(8,1,0) +# define JSON_HEDLEY_PREDICT(expr, expected, probability) \ + (((probability) >= 0.9) ? __builtin_expect((expr), (expected)) : (JSON_HEDLEY_STATIC_CAST(void, expected), (expr))) +# define JSON_HEDLEY_PREDICT_TRUE(expr, probability) \ + (__extension__ ({ \ + double hedley_probability_ = (probability); \ + ((hedley_probability_ >= 0.9) ? __builtin_expect(!!(expr), 1) : ((hedley_probability_ <= 0.1) ? __builtin_expect(!!(expr), 0) : !!(expr))); \ + })) +# define JSON_HEDLEY_PREDICT_FALSE(expr, probability) \ + (__extension__ ({ \ + double hedley_probability_ = (probability); \ + ((hedley_probability_ >= 0.9) ? __builtin_expect(!!(expr), 0) : ((hedley_probability_ <= 0.1) ? __builtin_expect(!!(expr), 1) : !!(expr))); \ + })) +# define JSON_HEDLEY_LIKELY(expr) __builtin_expect(!!(expr), 1) +# define JSON_HEDLEY_UNLIKELY(expr) __builtin_expect(!!(expr), 0) +#else +# define JSON_HEDLEY_PREDICT(expr, expected, probability) (JSON_HEDLEY_STATIC_CAST(void, expected), (expr)) +# define JSON_HEDLEY_PREDICT_TRUE(expr, probability) (!!(expr)) +# define JSON_HEDLEY_PREDICT_FALSE(expr, probability) (!!(expr)) +# define JSON_HEDLEY_LIKELY(expr) (!!(expr)) +# define JSON_HEDLEY_UNLIKELY(expr) (!!(expr)) +#endif +#if !defined(JSON_HEDLEY_UNPREDICTABLE) + #define JSON_HEDLEY_UNPREDICTABLE(expr) JSON_HEDLEY_PREDICT(expr, 1, 0.5) +#endif -/*! -@brief exception indicating errors with iterators +#if defined(JSON_HEDLEY_MALLOC) + #undef JSON_HEDLEY_MALLOC +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(malloc) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(12,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) + #define JSON_HEDLEY_MALLOC __attribute__((__malloc__)) +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) + #define JSON_HEDLEY_MALLOC _Pragma("returns_new_memory") +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(14, 0, 0) + #define JSON_HEDLEY_MALLOC __declspec(restrict) +#else + #define JSON_HEDLEY_MALLOC +#endif -This exception is thrown if iterators passed to a library function do not match -the expected semantics. +#if defined(JSON_HEDLEY_PURE) + #undef JSON_HEDLEY_PURE +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(pure) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(2,96,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) +# define JSON_HEDLEY_PURE __attribute__((__pure__)) +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) +# define JSON_HEDLEY_PURE _Pragma("does_not_write_global_data") +#elif defined(__cplusplus) && \ + ( \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(2,0,1) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(4,0,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) \ + ) +# define JSON_HEDLEY_PURE _Pragma("FUNC_IS_PURE;") +#else +# define JSON_HEDLEY_PURE +#endif -Exceptions have ids 2xx. +#if defined(JSON_HEDLEY_CONST) + #undef JSON_HEDLEY_CONST +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(const) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(2,5,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) + #define JSON_HEDLEY_CONST __attribute__((__const__)) +#elif \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) + #define JSON_HEDLEY_CONST _Pragma("no_side_effect") +#else + #define JSON_HEDLEY_CONST JSON_HEDLEY_PURE +#endif -name / id | example message | description ------------------------------------ | --------------- | ------------------------- -json.exception.invalid_iterator.201 | iterators are not compatible | The iterators passed to constructor @ref basic_json(InputIT first, InputIT last) are not compatible, meaning they do not belong to the same container. Therefore, the range (@a first, @a last) is invalid. -json.exception.invalid_iterator.202 | iterator does not fit current value | In an erase or insert function, the passed iterator @a pos does not belong to the JSON value for which the function was called. It hence does not define a valid position for the deletion/insertion. -json.exception.invalid_iterator.203 | iterators do not fit current value | Either iterator passed to function @ref erase(IteratorType first, IteratorType last) does not belong to the JSON value from which values shall be erased. It hence does not define a valid range to delete values from. -json.exception.invalid_iterator.204 | iterators out of range | When an iterator range for a primitive type (number, boolean, or string) is passed to a constructor or an erase function, this range has to be exactly (@ref begin(), @ref end()), because this is the only way the single stored value is expressed. All other ranges are invalid. -json.exception.invalid_iterator.205 | iterator out of range | When an iterator for a primitive type (number, boolean, or string) is passed to an erase function, the iterator has to be the @ref begin() iterator, because it is the only way to address the stored value. All other iterators are invalid. -json.exception.invalid_iterator.206 | cannot construct with iterators from null | The iterators passed to constructor @ref basic_json(InputIT first, InputIT last) belong to a JSON null value and hence to not define a valid range. -json.exception.invalid_iterator.207 | cannot use key() for non-object iterators | The key() member function can only be used on iterators belonging to a JSON object, because other types do not have a concept of a key. -json.exception.invalid_iterator.208 | cannot use operator[] for object iterators | The operator[] to specify a concrete offset cannot be used on iterators belonging to a JSON object, because JSON objects are unordered. -json.exception.invalid_iterator.209 | cannot use offsets with object iterators | The offset operators (+, -, +=, -=) cannot be used on iterators belonging to a JSON object, because JSON objects are unordered. -json.exception.invalid_iterator.210 | iterators do not fit | The iterator range passed to the insert function are not compatible, meaning they do not belong to the same container. Therefore, the range (@a first, @a last) is invalid. -json.exception.invalid_iterator.211 | passed iterators may not belong to container | The iterator range passed to the insert function must not be a subrange of the container to insert to. -json.exception.invalid_iterator.212 | cannot compare iterators of different containers | When two iterators are compared, they must belong to the same container. -json.exception.invalid_iterator.213 | cannot compare order of object iterators | The order of object iterators cannot be compared, because JSON objects are unordered. -json.exception.invalid_iterator.214 | cannot get value | Cannot get value for iterator: Either the iterator belongs to a null value or it is an iterator to a primitive type (number, boolean, or string), but the iterator is different to @ref begin(). +#if defined(JSON_HEDLEY_RESTRICT) + #undef JSON_HEDLEY_RESTRICT +#endif +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) && !defined(__cplusplus) + #define JSON_HEDLEY_RESTRICT restrict +#elif \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_MSVC_VERSION_CHECK(14,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,4) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,14,0) && defined(__cplusplus)) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) || \ + defined(__clang__) + #define JSON_HEDLEY_RESTRICT __restrict +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,3,0) && !defined(__cplusplus) + #define JSON_HEDLEY_RESTRICT _Restrict +#else + #define JSON_HEDLEY_RESTRICT +#endif -@liveexample{The following code shows how an `invalid_iterator` exception can be -caught.,invalid_iterator} +#if defined(JSON_HEDLEY_INLINE) + #undef JSON_HEDLEY_INLINE +#endif +#if \ + (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) || \ + (defined(__cplusplus) && (__cplusplus >= 199711L)) + #define JSON_HEDLEY_INLINE inline +#elif \ + defined(JSON_HEDLEY_GCC_VERSION) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(6,2,0) + #define JSON_HEDLEY_INLINE __inline__ +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(12,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,1,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,0,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) + #define JSON_HEDLEY_INLINE __inline +#else + #define JSON_HEDLEY_INLINE +#endif -@sa @ref exception for the base class of the library exceptions -@sa @ref parse_error for exceptions indicating a parse error -@sa @ref type_error for exceptions indicating executing a member function with - a wrong type -@sa @ref out_of_range for exceptions indicating access out of the defined range -@sa @ref other_error for exceptions indicating other library errors +#if defined(JSON_HEDLEY_ALWAYS_INLINE) + #undef JSON_HEDLEY_ALWAYS_INLINE +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(always_inline) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) +# define JSON_HEDLEY_ALWAYS_INLINE __attribute__((__always_inline__)) JSON_HEDLEY_INLINE +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(12,0,0) +# define JSON_HEDLEY_ALWAYS_INLINE __forceinline +#elif defined(__cplusplus) && \ + ( \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,1,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) \ + ) +# define JSON_HEDLEY_ALWAYS_INLINE _Pragma("FUNC_ALWAYS_INLINE;") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) +# define JSON_HEDLEY_ALWAYS_INLINE _Pragma("inline=forced") +#else +# define JSON_HEDLEY_ALWAYS_INLINE JSON_HEDLEY_INLINE +#endif -@since version 3.0.0 -*/ -class invalid_iterator : public exception -{ - public: - static invalid_iterator create(int id_, const std::string& what_arg) - { - std::string w = exception::name("invalid_iterator", id_) + what_arg; - return invalid_iterator(id_, w.c_str()); - } +#if defined(JSON_HEDLEY_NEVER_INLINE) + #undef JSON_HEDLEY_NEVER_INLINE +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(noinline) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) + #define JSON_HEDLEY_NEVER_INLINE __attribute__((__noinline__)) +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) + #define JSON_HEDLEY_NEVER_INLINE __declspec(noinline) +#elif JSON_HEDLEY_PGI_VERSION_CHECK(10,2,0) + #define JSON_HEDLEY_NEVER_INLINE _Pragma("noinline") +#elif JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,0,0) && defined(__cplusplus) + #define JSON_HEDLEY_NEVER_INLINE _Pragma("FUNC_CANNOT_INLINE;") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_NEVER_INLINE _Pragma("inline=never") +#elif JSON_HEDLEY_COMPCERT_VERSION_CHECK(3,2,0) + #define JSON_HEDLEY_NEVER_INLINE __attribute((noinline)) +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(9,0,0) + #define JSON_HEDLEY_NEVER_INLINE __declspec(noinline) +#else + #define JSON_HEDLEY_NEVER_INLINE +#endif - private: - invalid_iterator(int id_, const char* what_arg) - : exception(id_, what_arg) {} -}; +#if defined(JSON_HEDLEY_PRIVATE) + #undef JSON_HEDLEY_PRIVATE +#endif +#if defined(JSON_HEDLEY_PUBLIC) + #undef JSON_HEDLEY_PUBLIC +#endif +#if defined(JSON_HEDLEY_IMPORT) + #undef JSON_HEDLEY_IMPORT +#endif +#if defined(_WIN32) || defined(__CYGWIN__) +# define JSON_HEDLEY_PRIVATE +# define JSON_HEDLEY_PUBLIC __declspec(dllexport) +# define JSON_HEDLEY_IMPORT __declspec(dllimport) +#else +# if \ + JSON_HEDLEY_HAS_ATTRIBUTE(visibility) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,3,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(13,1,0) || \ + ( \ + defined(__TI_EABI__) && \ + ( \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) \ + ) \ + ) +# define JSON_HEDLEY_PRIVATE __attribute__((__visibility__("hidden"))) +# define JSON_HEDLEY_PUBLIC __attribute__((__visibility__("default"))) +# else +# define JSON_HEDLEY_PRIVATE +# define JSON_HEDLEY_PUBLIC +# endif +# define JSON_HEDLEY_IMPORT extern +#endif -/*! -@brief exception indicating executing a member function with a wrong type +#if defined(JSON_HEDLEY_NO_THROW) + #undef JSON_HEDLEY_NO_THROW +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(nothrow) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,3,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_NO_THROW __attribute__((__nothrow__)) +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) + #define JSON_HEDLEY_NO_THROW __declspec(nothrow) +#else + #define JSON_HEDLEY_NO_THROW +#endif -This exception is thrown in case of a type error; that is, a library function is -executed on a JSON value whose type does not match the expected semantics. +#if defined(JSON_HEDLEY_FALL_THROUGH) + #undef JSON_HEDLEY_FALL_THROUGH +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(fallthrough) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(7,0,0) + #define JSON_HEDLEY_FALL_THROUGH __attribute__((__fallthrough__)) +#elif JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(clang,fallthrough) + #define JSON_HEDLEY_FALL_THROUGH JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[clang::fallthrough]]) +#elif JSON_HEDLEY_HAS_CPP_ATTRIBUTE(fallthrough) + #define JSON_HEDLEY_FALL_THROUGH JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[fallthrough]]) +#elif defined(__fallthrough) /* SAL */ + #define JSON_HEDLEY_FALL_THROUGH __fallthrough +#else + #define JSON_HEDLEY_FALL_THROUGH +#endif -Exceptions have ids 3xx. +#if defined(JSON_HEDLEY_RETURNS_NON_NULL) + #undef JSON_HEDLEY_RETURNS_NON_NULL +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(returns_nonnull) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,9,0) + #define JSON_HEDLEY_RETURNS_NON_NULL __attribute__((__returns_nonnull__)) +#elif defined(_Ret_notnull_) /* SAL */ + #define JSON_HEDLEY_RETURNS_NON_NULL _Ret_notnull_ +#else + #define JSON_HEDLEY_RETURNS_NON_NULL +#endif -name / id | example message | description ------------------------------ | --------------- | ------------------------- -json.exception.type_error.301 | cannot create object from initializer list | To create an object from an initializer list, the initializer list must consist only of a list of pairs whose first element is a string. When this constraint is violated, an array is created instead. -json.exception.type_error.302 | type must be object, but is array | During implicit or explicit value conversion, the JSON type must be compatible to the target type. For instance, a JSON string can only be converted into string types, but not into numbers or boolean types. -json.exception.type_error.303 | incompatible ReferenceType for get_ref, actual type is object | To retrieve a reference to a value stored in a @ref basic_json object with @ref get_ref, the type of the reference must match the value type. For instance, for a JSON array, the @a ReferenceType must be @ref array_t&. -json.exception.type_error.304 | cannot use at() with string | The @ref at() member functions can only be executed for certain JSON types. -json.exception.type_error.305 | cannot use operator[] with string | The @ref operator[] member functions can only be executed for certain JSON types. -json.exception.type_error.306 | cannot use value() with string | The @ref value() member functions can only be executed for certain JSON types. -json.exception.type_error.307 | cannot use erase() with string | The @ref erase() member functions can only be executed for certain JSON types. -json.exception.type_error.308 | cannot use push_back() with string | The @ref push_back() and @ref operator+= member functions can only be executed for certain JSON types. -json.exception.type_error.309 | cannot use insert() with | The @ref insert() member functions can only be executed for certain JSON types. -json.exception.type_error.310 | cannot use swap() with number | The @ref swap() member functions can only be executed for certain JSON types. -json.exception.type_error.311 | cannot use emplace_back() with string | The @ref emplace_back() member function can only be executed for certain JSON types. -json.exception.type_error.312 | cannot use update() with string | The @ref update() member functions can only be executed for certain JSON types. -json.exception.type_error.313 | invalid value to unflatten | The @ref unflatten function converts an object whose keys are JSON Pointers back into an arbitrary nested JSON value. The JSON Pointers must not overlap, because then the resulting value would not be well defined. -json.exception.type_error.314 | only objects can be unflattened | The @ref unflatten function only works for an object whose keys are JSON Pointers. -json.exception.type_error.315 | values in object must be primitive | The @ref unflatten function only works for an object whose keys are JSON Pointers and whose values are primitive. -json.exception.type_error.316 | invalid UTF-8 byte at index 10: 0x7E | The @ref dump function only works with UTF-8 encoded strings; that is, if you assign a `std::string` to a JSON value, make sure it is UTF-8 encoded. | +#if defined(JSON_HEDLEY_ARRAY_PARAM) + #undef JSON_HEDLEY_ARRAY_PARAM +#endif +#if \ + defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) && \ + !defined(__STDC_NO_VLA__) && \ + !defined(__cplusplus) && \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_TINYC_VERSION) + #define JSON_HEDLEY_ARRAY_PARAM(name) (name) +#else + #define JSON_HEDLEY_ARRAY_PARAM(name) +#endif -@liveexample{The following code shows how a `type_error` exception can be -caught.,type_error} +#if defined(JSON_HEDLEY_IS_CONSTANT) + #undef JSON_HEDLEY_IS_CONSTANT +#endif +#if defined(JSON_HEDLEY_REQUIRE_CONSTEXPR) + #undef JSON_HEDLEY_REQUIRE_CONSTEXPR +#endif +/* JSON_HEDLEY_IS_CONSTEXPR_ is for + HEDLEY INTERNAL USE ONLY. API subject to change without notice. */ +#if defined(JSON_HEDLEY_IS_CONSTEXPR_) + #undef JSON_HEDLEY_IS_CONSTEXPR_ +#endif +#if \ + JSON_HEDLEY_HAS_BUILTIN(__builtin_constant_p) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,19) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(13,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,1,0) || \ + (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) && !defined(__cplusplus)) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(8,1,0) + #define JSON_HEDLEY_IS_CONSTANT(expr) __builtin_constant_p(expr) +#endif +#if !defined(__cplusplus) +# if \ + JSON_HEDLEY_HAS_BUILTIN(__builtin_types_compatible_p) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(13,1,0) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,4,0) || \ + JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,24) +#if defined(__INTPTR_TYPE__) + #define JSON_HEDLEY_IS_CONSTEXPR_(expr) __builtin_types_compatible_p(__typeof__((1 ? (void*) ((__INTPTR_TYPE__) ((expr) * 0)) : (int*) 0)), int*) +#else + #include + #define JSON_HEDLEY_IS_CONSTEXPR_(expr) __builtin_types_compatible_p(__typeof__((1 ? (void*) ((intptr_t) ((expr) * 0)) : (int*) 0)), int*) +#endif +# elif \ + ( \ + defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) && \ + !defined(JSON_HEDLEY_SUNPRO_VERSION) && \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_IAR_VERSION)) || \ + JSON_HEDLEY_HAS_EXTENSION(c_generic_selections) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,9,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(17,0,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(12,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,3,0) +#if defined(__INTPTR_TYPE__) + #define JSON_HEDLEY_IS_CONSTEXPR_(expr) _Generic((1 ? (void*) ((__INTPTR_TYPE__) ((expr) * 0)) : (int*) 0), int*: 1, void*: 0) +#else + #include + #define JSON_HEDLEY_IS_CONSTEXPR_(expr) _Generic((1 ? (void*) ((intptr_t) * 0) : (int*) 0), int*: 1, void*: 0) +#endif +# elif \ + defined(JSON_HEDLEY_GCC_VERSION) || \ + defined(JSON_HEDLEY_INTEL_VERSION) || \ + defined(JSON_HEDLEY_TINYC_VERSION) || \ + defined(JSON_HEDLEY_TI_ARMCL_VERSION) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(18,12,0) || \ + defined(JSON_HEDLEY_TI_CL2000_VERSION) || \ + defined(JSON_HEDLEY_TI_CL6X_VERSION) || \ + defined(JSON_HEDLEY_TI_CL7X_VERSION) || \ + defined(JSON_HEDLEY_TI_CLPRU_VERSION) || \ + defined(__clang__) +# define JSON_HEDLEY_IS_CONSTEXPR_(expr) ( \ + sizeof(void) != \ + sizeof(*( \ + 1 ? \ + ((void*) ((expr) * 0L) ) : \ +((struct { char v[sizeof(void) * 2]; } *) 1) \ + ) \ + ) \ + ) +# endif +#endif +#if defined(JSON_HEDLEY_IS_CONSTEXPR_) + #if !defined(JSON_HEDLEY_IS_CONSTANT) + #define JSON_HEDLEY_IS_CONSTANT(expr) JSON_HEDLEY_IS_CONSTEXPR_(expr) + #endif + #define JSON_HEDLEY_REQUIRE_CONSTEXPR(expr) (JSON_HEDLEY_IS_CONSTEXPR_(expr) ? (expr) : (-1)) +#else + #if !defined(JSON_HEDLEY_IS_CONSTANT) + #define JSON_HEDLEY_IS_CONSTANT(expr) (0) + #endif + #define JSON_HEDLEY_REQUIRE_CONSTEXPR(expr) (expr) +#endif -@sa @ref exception for the base class of the library exceptions -@sa @ref parse_error for exceptions indicating a parse error -@sa @ref invalid_iterator for exceptions indicating errors with iterators -@sa @ref out_of_range for exceptions indicating access out of the defined range -@sa @ref other_error for exceptions indicating other library errors +#if defined(JSON_HEDLEY_BEGIN_C_DECLS) + #undef JSON_HEDLEY_BEGIN_C_DECLS +#endif +#if defined(JSON_HEDLEY_END_C_DECLS) + #undef JSON_HEDLEY_END_C_DECLS +#endif +#if defined(JSON_HEDLEY_C_DECL) + #undef JSON_HEDLEY_C_DECL +#endif +#if defined(__cplusplus) + #define JSON_HEDLEY_BEGIN_C_DECLS extern "C" { + #define JSON_HEDLEY_END_C_DECLS } + #define JSON_HEDLEY_C_DECL extern "C" +#else + #define JSON_HEDLEY_BEGIN_C_DECLS + #define JSON_HEDLEY_END_C_DECLS + #define JSON_HEDLEY_C_DECL +#endif -@since version 3.0.0 -*/ -class type_error : public exception -{ - public: - static type_error create(int id_, const std::string& what_arg) - { - std::string w = exception::name("type_error", id_) + what_arg; - return type_error(id_, w.c_str()); - } +#if defined(JSON_HEDLEY_STATIC_ASSERT) + #undef JSON_HEDLEY_STATIC_ASSERT +#endif +#if \ + !defined(__cplusplus) && ( \ + (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L)) || \ + JSON_HEDLEY_HAS_FEATURE(c_static_assert) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(6,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + defined(_Static_assert) \ + ) +# define JSON_HEDLEY_STATIC_ASSERT(expr, message) _Static_assert(expr, message) +#elif \ + (defined(__cplusplus) && (__cplusplus >= 201103L)) || \ + JSON_HEDLEY_MSVC_VERSION_CHECK(16,0,0) +# define JSON_HEDLEY_STATIC_ASSERT(expr, message) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(static_assert(expr, message)) +#else +# define JSON_HEDLEY_STATIC_ASSERT(expr, message) +#endif - private: - type_error(int id_, const char* what_arg) : exception(id_, what_arg) {} -}; +#if defined(JSON_HEDLEY_NULL) + #undef JSON_HEDLEY_NULL +#endif +#if defined(__cplusplus) + #if __cplusplus >= 201103L + #define JSON_HEDLEY_NULL JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(nullptr) + #elif defined(NULL) + #define JSON_HEDLEY_NULL NULL + #else + #define JSON_HEDLEY_NULL JSON_HEDLEY_STATIC_CAST(void*, 0) + #endif +#elif defined(NULL) + #define JSON_HEDLEY_NULL NULL +#else + #define JSON_HEDLEY_NULL ((void*) 0) +#endif -/*! -@brief exception indicating access out of the defined range +#if defined(JSON_HEDLEY_MESSAGE) + #undef JSON_HEDLEY_MESSAGE +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunknown-pragmas") +# define JSON_HEDLEY_MESSAGE(msg) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS \ + JSON_HEDLEY_PRAGMA(message msg) \ + JSON_HEDLEY_DIAGNOSTIC_POP +#elif \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) +# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(message msg) +#elif JSON_HEDLEY_CRAY_VERSION_CHECK(5,0,0) +# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(_CRI message msg) +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) +# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(message(msg)) +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(2,0,0) +# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(message(msg)) +#else +# define JSON_HEDLEY_MESSAGE(msg) +#endif -This exception is thrown in case a library function is called on an input -parameter that exceeds the expected range, for instance in case of array -indices or nonexisting object keys. +#if defined(JSON_HEDLEY_WARNING) + #undef JSON_HEDLEY_WARNING +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunknown-pragmas") +# define JSON_HEDLEY_WARNING(msg) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS \ + JSON_HEDLEY_PRAGMA(clang warning msg) \ + JSON_HEDLEY_DIAGNOSTIC_POP +#elif \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,8,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(18,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) +# define JSON_HEDLEY_WARNING(msg) JSON_HEDLEY_PRAGMA(GCC warning msg) +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) +# define JSON_HEDLEY_WARNING(msg) JSON_HEDLEY_PRAGMA(message(msg)) +#else +# define JSON_HEDLEY_WARNING(msg) JSON_HEDLEY_MESSAGE(msg) +#endif -Exceptions have ids 4xx. +#if defined(JSON_HEDLEY_REQUIRE) + #undef JSON_HEDLEY_REQUIRE +#endif +#if defined(JSON_HEDLEY_REQUIRE_MSG) + #undef JSON_HEDLEY_REQUIRE_MSG +#endif +#if JSON_HEDLEY_HAS_ATTRIBUTE(diagnose_if) +# if JSON_HEDLEY_HAS_WARNING("-Wgcc-compat") +# define JSON_HEDLEY_REQUIRE(expr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wgcc-compat\"") \ + __attribute__((diagnose_if(!(expr), #expr, "error"))) \ + JSON_HEDLEY_DIAGNOSTIC_POP +# define JSON_HEDLEY_REQUIRE_MSG(expr,msg) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wgcc-compat\"") \ + __attribute__((diagnose_if(!(expr), msg, "error"))) \ + JSON_HEDLEY_DIAGNOSTIC_POP +# else +# define JSON_HEDLEY_REQUIRE(expr) __attribute__((diagnose_if(!(expr), #expr, "error"))) +# define JSON_HEDLEY_REQUIRE_MSG(expr,msg) __attribute__((diagnose_if(!(expr), msg, "error"))) +# endif +#else +# define JSON_HEDLEY_REQUIRE(expr) +# define JSON_HEDLEY_REQUIRE_MSG(expr,msg) +#endif -name / id | example message | description -------------------------------- | --------------- | ------------------------- -json.exception.out_of_range.401 | array index 3 is out of range | The provided array index @a i is larger than @a size-1. -json.exception.out_of_range.402 | array index '-' (3) is out of range | The special array index `-` in a JSON Pointer never describes a valid element of the array, but the index past the end. That is, it can only be used to add elements at this position, but not to read it. -json.exception.out_of_range.403 | key 'foo' not found | The provided key was not found in the JSON object. -json.exception.out_of_range.404 | unresolved reference token 'foo' | A reference token in a JSON Pointer could not be resolved. -json.exception.out_of_range.405 | JSON pointer has no parent | The JSON Patch operations 'remove' and 'add' can not be applied to the root element of the JSON value. -json.exception.out_of_range.406 | number overflow parsing '10E1000' | A parsed number could not be stored as without changing it to NaN or INF. -json.exception.out_of_range.407 | number overflow serializing '9223372036854775808' | UBJSON only supports integers numbers up to 9223372036854775807. | -json.exception.out_of_range.408 | excessive array size: 8658170730974374167 | The size (following `#`) of an UBJSON array or object exceeds the maximal capacity. | +#if defined(JSON_HEDLEY_FLAGS) + #undef JSON_HEDLEY_FLAGS +#endif +#if JSON_HEDLEY_HAS_ATTRIBUTE(flag_enum) + #define JSON_HEDLEY_FLAGS __attribute__((__flag_enum__)) +#endif -@liveexample{The following code shows how an `out_of_range` exception can be -caught.,out_of_range} +#if defined(JSON_HEDLEY_FLAGS_CAST) + #undef JSON_HEDLEY_FLAGS_CAST +#endif +#if JSON_HEDLEY_INTEL_VERSION_CHECK(19,0,0) +# define JSON_HEDLEY_FLAGS_CAST(T, expr) (__extension__ ({ \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("warning(disable:188)") \ + ((T) (expr)); \ + JSON_HEDLEY_DIAGNOSTIC_POP \ + })) +#else +# define JSON_HEDLEY_FLAGS_CAST(T, expr) JSON_HEDLEY_STATIC_CAST(T, expr) +#endif -@sa @ref exception for the base class of the library exceptions -@sa @ref parse_error for exceptions indicating a parse error -@sa @ref invalid_iterator for exceptions indicating errors with iterators -@sa @ref type_error for exceptions indicating executing a member function with - a wrong type -@sa @ref other_error for exceptions indicating other library errors +#if defined(JSON_HEDLEY_EMPTY_BASES) + #undef JSON_HEDLEY_EMPTY_BASES +#endif +#if JSON_HEDLEY_MSVC_VERSION_CHECK(19,0,23918) && !JSON_HEDLEY_MSVC_VERSION_CHECK(20,0,0) + #define JSON_HEDLEY_EMPTY_BASES __declspec(empty_bases) +#else + #define JSON_HEDLEY_EMPTY_BASES +#endif -@since version 3.0.0 -*/ -class out_of_range : public exception -{ - public: - static out_of_range create(int id_, const std::string& what_arg) - { - std::string w = exception::name("out_of_range", id_) + what_arg; - return out_of_range(id_, w.c_str()); - } +/* Remaining macros are deprecated. */ - private: - out_of_range(int id_, const char* what_arg) : exception(id_, what_arg) {} -}; +#if defined(JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK) + #undef JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK +#endif +#if defined(__clang__) + #define JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK(major,minor,patch) (0) +#else + #define JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK(major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif -/*! -@brief exception indicating other library errors +#if defined(JSON_HEDLEY_CLANG_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_CLANG_HAS_ATTRIBUTE +#endif +#define JSON_HEDLEY_CLANG_HAS_ATTRIBUTE(attribute) JSON_HEDLEY_HAS_ATTRIBUTE(attribute) -This exception is thrown in case of errors that cannot be classified with the -other exception types. +#if defined(JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE +#endif +#define JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE(attribute) JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) -Exceptions have ids 5xx. +#if defined(JSON_HEDLEY_CLANG_HAS_BUILTIN) + #undef JSON_HEDLEY_CLANG_HAS_BUILTIN +#endif +#define JSON_HEDLEY_CLANG_HAS_BUILTIN(builtin) JSON_HEDLEY_HAS_BUILTIN(builtin) -name / id | example message | description ------------------------------- | --------------- | ------------------------- -json.exception.other_error.501 | unsuccessful: {"op":"test","path":"/baz", "value":"bar"} | A JSON Patch operation 'test' failed. The unsuccessful operation is also printed. +#if defined(JSON_HEDLEY_CLANG_HAS_FEATURE) + #undef JSON_HEDLEY_CLANG_HAS_FEATURE +#endif +#define JSON_HEDLEY_CLANG_HAS_FEATURE(feature) JSON_HEDLEY_HAS_FEATURE(feature) -@sa @ref exception for the base class of the library exceptions -@sa @ref parse_error for exceptions indicating a parse error -@sa @ref invalid_iterator for exceptions indicating errors with iterators -@sa @ref type_error for exceptions indicating executing a member function with - a wrong type -@sa @ref out_of_range for exceptions indicating access out of the defined range +#if defined(JSON_HEDLEY_CLANG_HAS_EXTENSION) + #undef JSON_HEDLEY_CLANG_HAS_EXTENSION +#endif +#define JSON_HEDLEY_CLANG_HAS_EXTENSION(extension) JSON_HEDLEY_HAS_EXTENSION(extension) -@liveexample{The following code shows how an `other_error` exception can be -caught.,other_error} +#if defined(JSON_HEDLEY_CLANG_HAS_DECLSPEC_DECLSPEC_ATTRIBUTE) + #undef JSON_HEDLEY_CLANG_HAS_DECLSPEC_DECLSPEC_ATTRIBUTE +#endif +#define JSON_HEDLEY_CLANG_HAS_DECLSPEC_ATTRIBUTE(attribute) JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE(attribute) -@since version 3.0.0 +#if defined(JSON_HEDLEY_CLANG_HAS_WARNING) + #undef JSON_HEDLEY_CLANG_HAS_WARNING +#endif +#define JSON_HEDLEY_CLANG_HAS_WARNING(warning) JSON_HEDLEY_HAS_WARNING(warning) + +#endif /* !defined(JSON_HEDLEY_VERSION) || (JSON_HEDLEY_VERSION < X) */ + + +// This file contains all internal macro definitions +// You MUST include macro_unscope.hpp at the end of json.hpp to undef all of them + +// exclude unsupported compilers +#if !defined(JSON_SKIP_UNSUPPORTED_COMPILER_CHECK) + #if defined(__clang__) + #if (__clang_major__ * 10000 + __clang_minor__ * 100 + __clang_patchlevel__) < 30400 + #error "unsupported Clang version - see https://github.com/nlohmann/json#supported-compilers" + #endif + #elif defined(__GNUC__) && !(defined(__ICC) || defined(__INTEL_COMPILER)) + #if (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) < 40800 + #error "unsupported GCC version - see https://github.com/nlohmann/json#supported-compilers" + #endif + #endif +#endif + +// C++ language standard detection +#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1) // fix for issue #464 + #define JSON_HAS_CPP_17 + #define JSON_HAS_CPP_14 +#elif (defined(__cplusplus) && __cplusplus >= 201402L) || (defined(_HAS_CXX14) && _HAS_CXX14 == 1) + #define JSON_HAS_CPP_14 +#endif + +// disable float-equal warnings on GCC/clang +#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wfloat-equal" +#endif + +// disable documentation warnings on clang +#if defined(__clang__) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wdocumentation" +#endif + +// allow to disable exceptions +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) + #define JSON_THROW(exception) throw exception + #define JSON_TRY try + #define JSON_CATCH(exception) catch(exception) + #define JSON_INTERNAL_CATCH(exception) catch(exception) +#else + #include + #define JSON_THROW(exception) std::abort() + #define JSON_TRY if(true) + #define JSON_CATCH(exception) if(false) + #define JSON_INTERNAL_CATCH(exception) if(false) +#endif + +// override exception macros +#if defined(JSON_THROW_USER) + #undef JSON_THROW + #define JSON_THROW JSON_THROW_USER +#endif +#if defined(JSON_TRY_USER) + #undef JSON_TRY + #define JSON_TRY JSON_TRY_USER +#endif +#if defined(JSON_CATCH_USER) + #undef JSON_CATCH + #define JSON_CATCH JSON_CATCH_USER + #undef JSON_INTERNAL_CATCH + #define JSON_INTERNAL_CATCH JSON_CATCH_USER +#endif +#if defined(JSON_INTERNAL_CATCH_USER) + #undef JSON_INTERNAL_CATCH + #define JSON_INTERNAL_CATCH JSON_INTERNAL_CATCH_USER +#endif + +/*! +@brief macro to briefly define a mapping between an enum and JSON +@def NLOHMANN_JSON_SERIALIZE_ENUM +@since version 3.4.0 */ -class other_error : public exception -{ - public: - static other_error create(int id_, const std::string& what_arg) - { - std::string w = exception::name("other_error", id_) + what_arg; - return other_error(id_, w.c_str()); +#define NLOHMANN_JSON_SERIALIZE_ENUM(ENUM_TYPE, ...) \ + template \ + inline void to_json(BasicJsonType& j, const ENUM_TYPE& e) \ + { \ + static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ + static const std::pair m[] = __VA_ARGS__; \ + auto it = std::find_if(std::begin(m), std::end(m), \ + [e](const std::pair& ej_pair) -> bool \ + { \ + return ej_pair.first == e; \ + }); \ + j = ((it != std::end(m)) ? it : std::begin(m))->second; \ + } \ + template \ + inline void from_json(const BasicJsonType& j, ENUM_TYPE& e) \ + { \ + static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ + static const std::pair m[] = __VA_ARGS__; \ + auto it = std::find_if(std::begin(m), std::end(m), \ + [&j](const std::pair& ej_pair) -> bool \ + { \ + return ej_pair.second == j; \ + }); \ + e = ((it != std::end(m)) ? it : std::begin(m))->first; \ } - private: - other_error(int id_, const char* what_arg) : exception(id_, what_arg) {} -}; -} -} +// Ugly macros to avoid uglier copy-paste when specializing basic_json. They +// may be removed in the future once the class is split. -// #include +#define NLOHMANN_BASIC_JSON_TPL_DECLARATION \ + template class ObjectType, \ + template class ArrayType, \ + class StringType, class BooleanType, class NumberIntegerType, \ + class NumberUnsignedType, class NumberFloatType, \ + template class AllocatorType, \ + template class JSONSerializer, \ + class BinaryType> +#define NLOHMANN_BASIC_JSON_TPL \ + basic_json -#include // array -#include // and -#include // size_t -#include // uint8_t namespace nlohmann { namespace detail { -/////////////////////////// -// JSON type enumeration // -/////////////////////////// +//////////////// +// exceptions // +//////////////// /*! -@brief the JSON type enumeration - -This enumeration collects the different JSON types. It is internally used to -distinguish the stored values, and the functions @ref basic_json::is_null(), -@ref basic_json::is_object(), @ref basic_json::is_array(), -@ref basic_json::is_string(), @ref basic_json::is_boolean(), -@ref basic_json::is_number() (with @ref basic_json::is_number_integer(), -@ref basic_json::is_number_unsigned(), and @ref basic_json::is_number_float()), -@ref basic_json::is_discarded(), @ref basic_json::is_primitive(), and -@ref basic_json::is_structured() rely on it. +@brief general exception of the @ref basic_json class -@note There are three enumeration entries (number_integer, number_unsigned, and -number_float), because the library distinguishes these three types for numbers: -@ref basic_json::number_unsigned_t is used for unsigned integers, -@ref basic_json::number_integer_t is used for signed integers, and -@ref basic_json::number_float_t is used for floating-point numbers or to -approximate integers which do not fit in the limits of their respective type. +This class is an extension of `std::exception` objects with a member @a id for +exception ids. It is used as the base class for all exceptions thrown by the +@ref basic_json class. This class can hence be used as "wildcard" to catch +exceptions. -@sa @ref basic_json::basic_json(const value_t value_type) -- create a JSON -value with the default value for a given type - -@since version 1.0.0 -*/ -enum class value_t : std::uint8_t -{ - null, ///< null value - object, ///< object (unordered set of name/value pairs) - array, ///< array (ordered collection of values) - string, ///< string value - boolean, ///< boolean value - number_integer, ///< number value (signed integer) - number_unsigned, ///< number value (unsigned integer) - number_float, ///< number value (floating-point) - discarded ///< discarded by the the parser callback function -}; +Subclasses: +- @ref parse_error for exceptions indicating a parse error +- @ref invalid_iterator for exceptions indicating errors with iterators +- @ref type_error for exceptions indicating executing a member function with + a wrong type +- @ref out_of_range for exceptions indicating access out of the defined range +- @ref other_error for exceptions indicating other library errors -/*! -@brief comparison operator for JSON types +@internal +@note To have nothrow-copy-constructible exceptions, we internally use + `std::runtime_error` which can cope with arbitrary-length error messages. + Intermediate strings are built with static functions and then passed to + the actual constructor. +@endinternal -Returns an ordering that is similar to Python: -- order: null < boolean < number < object < array < string -- furthermore, each type is not smaller than itself -- discarded values are not comparable +@liveexample{The following code shows how arbitrary library exceptions can be +caught.,exception} -@since version 1.0.0 +@since version 3.0.0 */ -inline bool operator<(const value_t lhs, const value_t rhs) noexcept +class exception : public std::exception { - static constexpr std::array order = {{ - 0 /* null */, 3 /* object */, 4 /* array */, 5 /* string */, - 1 /* boolean */, 2 /* integer */, 2 /* unsigned */, 2 /* float */ - } - }; - - const auto l_index = static_cast(lhs); - const auto r_index = static_cast(rhs); - return l_index < order.size() and r_index < order.size() and order[l_index] < order[r_index]; -} -} -} + public: + /// returns the explanatory string + JSON_HEDLEY_RETURNS_NON_NULL + const char* what() const noexcept override + { + return m.what(); + } -// #include + /// the id of the exception + const int id; + protected: + JSON_HEDLEY_NON_NULL(3) + exception(int id_, const char* what_arg) : id(id_), m(what_arg) {} -#include // transform -#include // array -#include // and, not -#include // forward_list -#include // inserter, front_inserter, end -#include // map -#include // string -#include // tuple, make_tuple -#include // is_arithmetic, is_same, is_enum, underlying_type, is_convertible -#include // unordered_map -#include // pair, declval -#include // valarray + static std::string name(const std::string& ename, int id_) + { + return "[json.exception." + ename + "." + std::to_string(id_) + "] "; + } -// #include + private: + /// an exception object as storage for error messages + std::runtime_error m; +}; -// #include +/*! +@brief exception indicating a parse error -// #include +This exception is thrown by the library when a parse error occurs. Parse errors +can occur during the deserialization of JSON text, CBOR, MessagePack, as well +as when using JSON Patch. -// #include +Member @a byte holds the byte index of the last read character in the input +file. -// #include +Exceptions have ids 1xx. +name / id | example message | description +------------------------------ | --------------- | ------------------------- +json.exception.parse_error.101 | parse error at 2: unexpected end of input; expected string literal | This error indicates a syntax error while deserializing a JSON text. The error message describes that an unexpected token (character) was encountered, and the member @a byte indicates the error position. +json.exception.parse_error.102 | parse error at 14: missing or wrong low surrogate | JSON uses the `\uxxxx` format to describe Unicode characters. Code points above above 0xFFFF are split into two `\uxxxx` entries ("surrogate pairs"). This error indicates that the surrogate pair is incomplete or contains an invalid code point. +json.exception.parse_error.103 | parse error: code points above 0x10FFFF are invalid | Unicode supports code points up to 0x10FFFF. Code points above 0x10FFFF are invalid. +json.exception.parse_error.104 | parse error: JSON patch must be an array of objects | [RFC 6902](https://tools.ietf.org/html/rfc6902) requires a JSON Patch document to be a JSON document that represents an array of objects. +json.exception.parse_error.105 | parse error: operation must have string member 'op' | An operation of a JSON Patch document must contain exactly one "op" member, whose value indicates the operation to perform. Its value must be one of "add", "remove", "replace", "move", "copy", or "test"; other values are errors. +json.exception.parse_error.106 | parse error: array index '01' must not begin with '0' | An array index in a JSON Pointer ([RFC 6901](https://tools.ietf.org/html/rfc6901)) may be `0` or any number without a leading `0`. +json.exception.parse_error.107 | parse error: JSON pointer must be empty or begin with '/' - was: 'foo' | A JSON Pointer must be a Unicode string containing a sequence of zero or more reference tokens, each prefixed by a `/` character. +json.exception.parse_error.108 | parse error: escape character '~' must be followed with '0' or '1' | In a JSON Pointer, only `~0` and `~1` are valid escape sequences. +json.exception.parse_error.109 | parse error: array index 'one' is not a number | A JSON Pointer array index must be a number. +json.exception.parse_error.110 | parse error at 1: cannot read 2 bytes from vector | When parsing CBOR or MessagePack, the byte vector ends before the complete value has been read. +json.exception.parse_error.112 | parse error at 1: error reading CBOR; last byte: 0xF8 | Not all types of CBOR or MessagePack are supported. This exception occurs if an unsupported byte was read. +json.exception.parse_error.113 | parse error at 2: expected a CBOR string; last byte: 0x98 | While parsing a map key, a value that is not a string has been read. +json.exception.parse_error.114 | parse error: Unsupported BSON record type 0x0F | The parsing of the corresponding BSON record type is not implemented (yet). -namespace nlohmann -{ -namespace detail -{ -template -void from_json(const BasicJsonType& j, typename std::nullptr_t& n) -{ - if (JSON_UNLIKELY(not j.is_null())) - { - JSON_THROW(type_error::create(302, "type must be null, but is " + std::string(j.type_name()))); - } - n = nullptr; -} +@note For an input with n bytes, 1 is the index of the first character and n+1 + is the index of the terminating null byte or the end of file. This also + holds true when reading a byte vector (CBOR or MessagePack). -// overloads for basic_json template parameters -template::value and - not std::is_same::value, - int> = 0> -void get_arithmetic_value(const BasicJsonType& j, ArithmeticType& val) -{ - switch (static_cast(j)) - { - case value_t::number_unsigned: - { - val = static_cast(*j.template get_ptr()); - break; - } - case value_t::number_integer: - { - val = static_cast(*j.template get_ptr()); - break; - } - case value_t::number_float: - { - val = static_cast(*j.template get_ptr()); - break; - } +@liveexample{The following code shows how a `parse_error` exception can be +caught.,parse_error} - default: - JSON_THROW(type_error::create(302, "type must be number, but is " + std::string(j.type_name()))); - } -} +@sa - @ref exception for the base class of the library exceptions +@sa - @ref invalid_iterator for exceptions indicating errors with iterators +@sa - @ref type_error for exceptions indicating executing a member function with + a wrong type +@sa - @ref out_of_range for exceptions indicating access out of the defined range +@sa - @ref other_error for exceptions indicating other library errors -template -void from_json(const BasicJsonType& j, typename BasicJsonType::boolean_t& b) +@since version 3.0.0 +*/ +class parse_error : public exception { - if (JSON_UNLIKELY(not j.is_boolean())) + public: + /*! + @brief create a parse error exception + @param[in] id_ the id of the exception + @param[in] pos the position where the error occurred (or with + chars_read_total=0 if the position cannot be + determined) + @param[in] what_arg the explanatory string + @return parse_error object + */ + static parse_error create(int id_, const position_t& pos, const std::string& what_arg) { - JSON_THROW(type_error::create(302, "type must be boolean, but is " + std::string(j.type_name()))); + std::string w = exception::name("parse_error", id_) + "parse error" + + position_string(pos) + ": " + what_arg; + return parse_error(id_, pos.chars_read_total, w.c_str()); } - b = *j.template get_ptr(); -} -template -void from_json(const BasicJsonType& j, typename BasicJsonType::string_t& s) -{ - if (JSON_UNLIKELY(not j.is_string())) + static parse_error create(int id_, std::size_t byte_, const std::string& what_arg) { - JSON_THROW(type_error::create(302, "type must be string, but is " + std::string(j.type_name()))); + std::string w = exception::name("parse_error", id_) + "parse error" + + (byte_ != 0 ? (" at byte " + std::to_string(byte_)) : "") + + ": " + what_arg; + return parse_error(id_, byte_, w.c_str()); } - s = *j.template get_ptr(); -} -template < - typename BasicJsonType, typename CompatibleStringType, - enable_if_t < - is_compatible_string_type::value and - not std::is_same::value, - int > = 0 > -void from_json(const BasicJsonType& j, CompatibleStringType& s) -{ - if (JSON_UNLIKELY(not j.is_string())) + /*! + @brief byte index of the parse error + + The byte index of the last read character in the input file. + + @note For an input with n bytes, 1 is the index of the first character and + n+1 is the index of the terminating null byte or the end of file. + This also holds true when reading a byte vector (CBOR or MessagePack). + */ + const std::size_t byte; + + private: + parse_error(int id_, std::size_t byte_, const char* what_arg) + : exception(id_, what_arg), byte(byte_) {} + + static std::string position_string(const position_t& pos) { - JSON_THROW(type_error::create(302, "type must be string, but is " + std::string(j.type_name()))); + return " at line " + std::to_string(pos.lines_read + 1) + + ", column " + std::to_string(pos.chars_read_current_line); } +}; - s = *j.template get_ptr(); -} +/*! +@brief exception indicating errors with iterators -template -void from_json(const BasicJsonType& j, typename BasicJsonType::number_float_t& val) -{ - get_arithmetic_value(j, val); -} +This exception is thrown if iterators passed to a library function do not match +the expected semantics. -template -void from_json(const BasicJsonType& j, typename BasicJsonType::number_unsigned_t& val) -{ - get_arithmetic_value(j, val); -} +Exceptions have ids 2xx. -template -void from_json(const BasicJsonType& j, typename BasicJsonType::number_integer_t& val) -{ - get_arithmetic_value(j, val); -} +name / id | example message | description +----------------------------------- | --------------- | ------------------------- +json.exception.invalid_iterator.201 | iterators are not compatible | The iterators passed to constructor @ref basic_json(InputIT first, InputIT last) are not compatible, meaning they do not belong to the same container. Therefore, the range (@a first, @a last) is invalid. +json.exception.invalid_iterator.202 | iterator does not fit current value | In an erase or insert function, the passed iterator @a pos does not belong to the JSON value for which the function was called. It hence does not define a valid position for the deletion/insertion. +json.exception.invalid_iterator.203 | iterators do not fit current value | Either iterator passed to function @ref erase(IteratorType first, IteratorType last) does not belong to the JSON value from which values shall be erased. It hence does not define a valid range to delete values from. +json.exception.invalid_iterator.204 | iterators out of range | When an iterator range for a primitive type (number, boolean, or string) is passed to a constructor or an erase function, this range has to be exactly (@ref begin(), @ref end()), because this is the only way the single stored value is expressed. All other ranges are invalid. +json.exception.invalid_iterator.205 | iterator out of range | When an iterator for a primitive type (number, boolean, or string) is passed to an erase function, the iterator has to be the @ref begin() iterator, because it is the only way to address the stored value. All other iterators are invalid. +json.exception.invalid_iterator.206 | cannot construct with iterators from null | The iterators passed to constructor @ref basic_json(InputIT first, InputIT last) belong to a JSON null value and hence to not define a valid range. +json.exception.invalid_iterator.207 | cannot use key() for non-object iterators | The key() member function can only be used on iterators belonging to a JSON object, because other types do not have a concept of a key. +json.exception.invalid_iterator.208 | cannot use operator[] for object iterators | The operator[] to specify a concrete offset cannot be used on iterators belonging to a JSON object, because JSON objects are unordered. +json.exception.invalid_iterator.209 | cannot use offsets with object iterators | The offset operators (+, -, +=, -=) cannot be used on iterators belonging to a JSON object, because JSON objects are unordered. +json.exception.invalid_iterator.210 | iterators do not fit | The iterator range passed to the insert function are not compatible, meaning they do not belong to the same container. Therefore, the range (@a first, @a last) is invalid. +json.exception.invalid_iterator.211 | passed iterators may not belong to container | The iterator range passed to the insert function must not be a subrange of the container to insert to. +json.exception.invalid_iterator.212 | cannot compare iterators of different containers | When two iterators are compared, they must belong to the same container. +json.exception.invalid_iterator.213 | cannot compare order of object iterators | The order of object iterators cannot be compared, because JSON objects are unordered. +json.exception.invalid_iterator.214 | cannot get value | Cannot get value for iterator: Either the iterator belongs to a null value or it is an iterator to a primitive type (number, boolean, or string), but the iterator is different to @ref begin(). -template::value, int> = 0> -void from_json(const BasicJsonType& j, EnumType& e) -{ - typename std::underlying_type::type val; - get_arithmetic_value(j, val); - e = static_cast(val); -} +@liveexample{The following code shows how an `invalid_iterator` exception can be +caught.,invalid_iterator} -template -void from_json(const BasicJsonType& j, typename BasicJsonType::array_t& arr) +@sa - @ref exception for the base class of the library exceptions +@sa - @ref parse_error for exceptions indicating a parse error +@sa - @ref type_error for exceptions indicating executing a member function with + a wrong type +@sa - @ref out_of_range for exceptions indicating access out of the defined range +@sa - @ref other_error for exceptions indicating other library errors + +@since version 3.0.0 +*/ +class invalid_iterator : public exception { - if (JSON_UNLIKELY(not j.is_array())) + public: + static invalid_iterator create(int id_, const std::string& what_arg) { - JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name()))); + std::string w = exception::name("invalid_iterator", id_) + what_arg; + return invalid_iterator(id_, w.c_str()); } - arr = *j.template get_ptr(); -} -// forward_list doesn't have an insert method -template::value, int> = 0> -void from_json(const BasicJsonType& j, std::forward_list& l) -{ - if (JSON_UNLIKELY(not j.is_array())) - { - JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name()))); - } - std::transform(j.rbegin(), j.rend(), - std::front_inserter(l), [](const BasicJsonType & i) - { - return i.template get(); - }); -} + private: + JSON_HEDLEY_NON_NULL(3) + invalid_iterator(int id_, const char* what_arg) + : exception(id_, what_arg) {} +}; -// valarray doesn't have an insert method -template::value, int> = 0> -void from_json(const BasicJsonType& j, std::valarray& l) -{ - if (JSON_UNLIKELY(not j.is_array())) - { - JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name()))); - } - l.resize(j.size()); - std::copy(j.m_value.array->begin(), j.m_value.array->end(), std::begin(l)); -} +/*! +@brief exception indicating executing a member function with a wrong type -template -void from_json_array_impl(const BasicJsonType& j, CompatibleArrayType& arr, priority_tag<0> /*unused*/) -{ - using std::end; +This exception is thrown in case of a type error; that is, a library function is +executed on a JSON value whose type does not match the expected semantics. - std::transform(j.begin(), j.end(), - std::inserter(arr, end(arr)), [](const BasicJsonType & i) - { - // get() returns *this, this won't call a from_json - // method when value_type is BasicJsonType - return i.template get(); - }); -} +Exceptions have ids 3xx. -template -auto from_json_array_impl(const BasicJsonType& j, CompatibleArrayType& arr, priority_tag<1> /*unused*/) --> decltype( - arr.reserve(std::declval()), - void()) -{ - using std::end; +name / id | example message | description +----------------------------- | --------------- | ------------------------- +json.exception.type_error.301 | cannot create object from initializer list | To create an object from an initializer list, the initializer list must consist only of a list of pairs whose first element is a string. When this constraint is violated, an array is created instead. +json.exception.type_error.302 | type must be object, but is array | During implicit or explicit value conversion, the JSON type must be compatible to the target type. For instance, a JSON string can only be converted into string types, but not into numbers or boolean types. +json.exception.type_error.303 | incompatible ReferenceType for get_ref, actual type is object | To retrieve a reference to a value stored in a @ref basic_json object with @ref get_ref, the type of the reference must match the value type. For instance, for a JSON array, the @a ReferenceType must be @ref array_t &. +json.exception.type_error.304 | cannot use at() with string | The @ref at() member functions can only be executed for certain JSON types. +json.exception.type_error.305 | cannot use operator[] with string | The @ref operator[] member functions can only be executed for certain JSON types. +json.exception.type_error.306 | cannot use value() with string | The @ref value() member functions can only be executed for certain JSON types. +json.exception.type_error.307 | cannot use erase() with string | The @ref erase() member functions can only be executed for certain JSON types. +json.exception.type_error.308 | cannot use push_back() with string | The @ref push_back() and @ref operator+= member functions can only be executed for certain JSON types. +json.exception.type_error.309 | cannot use insert() with | The @ref insert() member functions can only be executed for certain JSON types. +json.exception.type_error.310 | cannot use swap() with number | The @ref swap() member functions can only be executed for certain JSON types. +json.exception.type_error.311 | cannot use emplace_back() with string | The @ref emplace_back() member function can only be executed for certain JSON types. +json.exception.type_error.312 | cannot use update() with string | The @ref update() member functions can only be executed for certain JSON types. +json.exception.type_error.313 | invalid value to unflatten | The @ref unflatten function converts an object whose keys are JSON Pointers back into an arbitrary nested JSON value. The JSON Pointers must not overlap, because then the resulting value would not be well defined. +json.exception.type_error.314 | only objects can be unflattened | The @ref unflatten function only works for an object whose keys are JSON Pointers. +json.exception.type_error.315 | values in object must be primitive | The @ref unflatten function only works for an object whose keys are JSON Pointers and whose values are primitive. +json.exception.type_error.316 | invalid UTF-8 byte at index 10: 0x7E | The @ref dump function only works with UTF-8 encoded strings; that is, if you assign a `std::string` to a JSON value, make sure it is UTF-8 encoded. | +json.exception.type_error.317 | JSON value cannot be serialized to requested format | The dynamic type of the object cannot be represented in the requested serialization format (e.g. a raw `true` or `null` JSON object cannot be serialized to BSON) | - arr.reserve(j.size()); - std::transform(j.begin(), j.end(), - std::inserter(arr, end(arr)), [](const BasicJsonType & i) - { - // get() returns *this, this won't call a from_json - // method when value_type is BasicJsonType - return i.template get(); - }); -} +@liveexample{The following code shows how a `type_error` exception can be +caught.,type_error} -template -void from_json_array_impl(const BasicJsonType& j, std::array& arr, priority_tag<2> /*unused*/) -{ - for (std::size_t i = 0; i < N; ++i) - { - arr[i] = j.at(i).template get(); - } -} +@sa - @ref exception for the base class of the library exceptions +@sa - @ref parse_error for exceptions indicating a parse error +@sa - @ref invalid_iterator for exceptions indicating errors with iterators +@sa - @ref out_of_range for exceptions indicating access out of the defined range +@sa - @ref other_error for exceptions indicating other library errors -template < - typename BasicJsonType, typename CompatibleArrayType, - enable_if_t < - is_compatible_array_type::value and - not std::is_same::value and - std::is_constructible < - BasicJsonType, typename CompatibleArrayType::value_type >::value, - int > = 0 > -void from_json(const BasicJsonType& j, CompatibleArrayType& arr) +@since version 3.0.0 +*/ +class type_error : public exception { - if (JSON_UNLIKELY(not j.is_array())) + public: + static type_error create(int id_, const std::string& what_arg) { - JSON_THROW(type_error::create(302, "type must be array, but is " + - std::string(j.type_name()))); + std::string w = exception::name("type_error", id_) + what_arg; + return type_error(id_, w.c_str()); } - from_json_array_impl(j, arr, priority_tag<2> {}); -} + private: + JSON_HEDLEY_NON_NULL(3) + type_error(int id_, const char* what_arg) : exception(id_, what_arg) {} +}; -template::value, int> = 0> -void from_json(const BasicJsonType& j, CompatibleObjectType& obj) +/*! +@brief exception indicating access out of the defined range + +This exception is thrown in case a library function is called on an input +parameter that exceeds the expected range, for instance in case of array +indices or nonexisting object keys. + +Exceptions have ids 4xx. + +name / id | example message | description +------------------------------- | --------------- | ------------------------- +json.exception.out_of_range.401 | array index 3 is out of range | The provided array index @a i is larger than @a size-1. +json.exception.out_of_range.402 | array index '-' (3) is out of range | The special array index `-` in a JSON Pointer never describes a valid element of the array, but the index past the end. That is, it can only be used to add elements at this position, but not to read it. +json.exception.out_of_range.403 | key 'foo' not found | The provided key was not found in the JSON object. +json.exception.out_of_range.404 | unresolved reference token 'foo' | A reference token in a JSON Pointer could not be resolved. +json.exception.out_of_range.405 | JSON pointer has no parent | The JSON Patch operations 'remove' and 'add' can not be applied to the root element of the JSON value. +json.exception.out_of_range.406 | number overflow parsing '10E1000' | A parsed number could not be stored as without changing it to NaN or INF. +json.exception.out_of_range.407 | number overflow serializing '9223372036854775808' | UBJSON and BSON only support integer numbers up to 9223372036854775807. | +json.exception.out_of_range.408 | excessive array size: 8658170730974374167 | The size (following `#`) of an UBJSON array or object exceeds the maximal capacity. | +json.exception.out_of_range.409 | BSON key cannot contain code point U+0000 (at byte 2) | Key identifiers to be serialized to BSON cannot contain code point U+0000, since the key is stored as zero-terminated c-string | + +@liveexample{The following code shows how an `out_of_range` exception can be +caught.,out_of_range} + +@sa - @ref exception for the base class of the library exceptions +@sa - @ref parse_error for exceptions indicating a parse error +@sa - @ref invalid_iterator for exceptions indicating errors with iterators +@sa - @ref type_error for exceptions indicating executing a member function with + a wrong type +@sa - @ref other_error for exceptions indicating other library errors + +@since version 3.0.0 +*/ +class out_of_range : public exception { - if (JSON_UNLIKELY(not j.is_object())) + public: + static out_of_range create(int id_, const std::string& what_arg) { - JSON_THROW(type_error::create(302, "type must be object, but is " + std::string(j.type_name()))); + std::string w = exception::name("out_of_range", id_) + what_arg; + return out_of_range(id_, w.c_str()); } - auto inner_object = j.template get_ptr(); - using value_type = typename CompatibleObjectType::value_type; - std::transform( - inner_object->begin(), inner_object->end(), - std::inserter(obj, obj.begin()), - [](typename BasicJsonType::object_t::value_type const & p) - { - return value_type(p.first, p.second.template get()); - }); -} + private: + JSON_HEDLEY_NON_NULL(3) + out_of_range(int id_, const char* what_arg) : exception(id_, what_arg) {} +}; -// overload for arithmetic types, not chosen for basic_json template arguments -// (BooleanType, etc..); note: Is it really necessary to provide explicit -// overloads for boolean_t etc. in case of a custom BooleanType which is not -// an arithmetic type? -template::value and - not std::is_same::value and - not std::is_same::value and - not std::is_same::value and - not std::is_same::value, - int> = 0> -void from_json(const BasicJsonType& j, ArithmeticType& val) -{ - switch (static_cast(j)) - { - case value_t::number_unsigned: - { - val = static_cast(*j.template get_ptr()); - break; - } - case value_t::number_integer: - { - val = static_cast(*j.template get_ptr()); - break; - } - case value_t::number_float: - { - val = static_cast(*j.template get_ptr()); - break; - } - case value_t::boolean: - { - val = static_cast(*j.template get_ptr()); - break; - } +/*! +@brief exception indicating other library errors - default: - JSON_THROW(type_error::create(302, "type must be number, but is " + std::string(j.type_name()))); - } -} +This exception is thrown in case of errors that cannot be classified with the +other exception types. -template -void from_json(const BasicJsonType& j, std::pair& p) -{ - p = {j.at(0).template get(), j.at(1).template get()}; -} +Exceptions have ids 5xx. -template -void from_json_tuple_impl(const BasicJsonType& j, Tuple& t, index_sequence) -{ - t = std::make_tuple(j.at(Idx).template get::type>()...); -} +name / id | example message | description +------------------------------ | --------------- | ------------------------- +json.exception.other_error.501 | unsuccessful: {"op":"test","path":"/baz", "value":"bar"} | A JSON Patch operation 'test' failed. The unsuccessful operation is also printed. -template -void from_json(const BasicJsonType& j, std::tuple& t) -{ - from_json_tuple_impl(j, t, index_sequence_for {}); -} +@sa - @ref exception for the base class of the library exceptions +@sa - @ref parse_error for exceptions indicating a parse error +@sa - @ref invalid_iterator for exceptions indicating errors with iterators +@sa - @ref type_error for exceptions indicating executing a member function with + a wrong type +@sa - @ref out_of_range for exceptions indicating access out of the defined range -template ::value>> -void from_json(const BasicJsonType& j, std::map& m) -{ - if (JSON_UNLIKELY(not j.is_array())) - { - JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name()))); - } - for (const auto& p : j) - { - if (JSON_UNLIKELY(not p.is_array())) - { - JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(p.type_name()))); - } - m.emplace(p.at(0).template get(), p.at(1).template get()); - } -} +@liveexample{The following code shows how an `other_error` exception can be +caught.,other_error} -template ::value>> -void from_json(const BasicJsonType& j, std::unordered_map& m) +@since version 3.0.0 +*/ +class other_error : public exception { - if (JSON_UNLIKELY(not j.is_array())) - { - JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name()))); - } - for (const auto& p : j) + public: + static other_error create(int id_, const std::string& what_arg) { - if (JSON_UNLIKELY(not p.is_array())) - { - JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(p.type_name()))); - } - m.emplace(p.at(0).template get(), p.at(1).template get()); + std::string w = exception::name("other_error", id_) + what_arg; + return other_error(id_, w.c_str()); } -} -struct from_json_fn -{ private: - template - auto call(const BasicJsonType& j, T& val, priority_tag<1> /*unused*/) const - noexcept(noexcept(from_json(j, val))) - -> decltype(from_json(j, val), void()) - { - return from_json(j, val); - } + JSON_HEDLEY_NON_NULL(3) + other_error(int id_, const char* what_arg) : exception(id_, what_arg) {} +}; +} // namespace detail +} // namespace nlohmann - template - void call(const BasicJsonType& /*unused*/, T& /*unused*/, priority_tag<0> /*unused*/) const noexcept - { - static_assert(sizeof(BasicJsonType) == 0, - "could not find from_json() method in T's namespace"); -#ifdef _MSC_VER - // MSVC does not show a stacktrace for the above assert - using decayed = uncvref_t; - static_assert(sizeof(typename decayed::force_msvc_stacktrace) == 0, - "forcing MSVC stacktrace to show which T we're talking about."); -#endif - } - - public: - template - void operator()(const BasicJsonType& j, T& val) const - noexcept(noexcept(std::declval().call(j, val, priority_tag<1> {}))) - { - return call(j, val, priority_tag<1> {}); - } -}; -} - -/// namespace to hold default `from_json` function -/// to see why this is required: -/// http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4381.html -namespace -{ -constexpr const auto& from_json = detail::static_const::value; -} -} - -// #include - - -#include // or, and, not -#include // begin, end -#include // tuple, get -#include // is_same, is_constructible, is_floating_point, is_enum, underlying_type -#include // move, forward, declval, pair -#include // valarray -#include // vector +// #include // #include -// #include - -// #include - -// #include - #include // size_t -#include // string, to_string -#include // input_iterator_tag +#include // conditional, enable_if, false_type, integral_constant, is_constructible, is_integral, is_same, remove_cv, remove_reference, true_type -// #include +// #include namespace nlohmann { namespace detail { -/// proxy class for the items() function -template class iteration_proxy +// alias templates to reduce boilerplate +template +using enable_if_t = typename std::enable_if::type; + +template +using uncvref_t = typename std::remove_cv::type>::type; + +// implementation of C++14 index_sequence and affiliates +// source: https://stackoverflow.com/a/32223343 +template +struct index_sequence { - private: - /// helper class for iteration - class iteration_proxy_internal - { - public: - using difference_type = std::ptrdiff_t; - using value_type = iteration_proxy_internal; - using pointer = iteration_proxy_internal*; - using reference = iteration_proxy_internal&; - using iterator_category = std::input_iterator_tag; - - private: - /// the iterator - IteratorType anchor; - /// an index for arrays (used to create key names) - std::size_t array_index = 0; - /// last stringified array index - mutable std::size_t array_index_last = 0; - /// a string representation of the array index - mutable std::string array_index_str = "0"; - /// an empty string (to return a reference for primitive values) - const std::string empty_str = ""; - - public: - explicit iteration_proxy_internal(IteratorType it) noexcept : anchor(it) {} - - iteration_proxy_internal(const iteration_proxy_internal&) = default; - iteration_proxy_internal& operator=(const iteration_proxy_internal&) = default; - - /// dereference operator (needed for range-based for) - iteration_proxy_internal& operator*() - { - return *this; - } + using type = index_sequence; + using value_type = std::size_t; + static constexpr std::size_t size() noexcept + { + return sizeof...(Ints); + } +}; - /// increment operator (needed for range-based for) - iteration_proxy_internal& operator++() - { - ++anchor; - ++array_index; +template +struct merge_and_renumber; - return *this; - } +template +struct merge_and_renumber, index_sequence> + : index_sequence < I1..., (sizeof...(I1) + I2)... > {}; - /// equality operator (needed for InputIterator) - bool operator==(const iteration_proxy_internal& o) const noexcept - { - return anchor == o.anchor; - } +template +struct make_index_sequence + : merge_and_renumber < typename make_index_sequence < N / 2 >::type, + typename make_index_sequence < N - N / 2 >::type > {}; - /// inequality operator (needed for range-based for) - bool operator!=(const iteration_proxy_internal& o) const noexcept - { - return anchor != o.anchor; - } +template<> struct make_index_sequence<0> : index_sequence<> {}; +template<> struct make_index_sequence<1> : index_sequence<0> {}; - /// return key of the iterator - const std::string& key() const - { - assert(anchor.m_object != nullptr); +template +using index_sequence_for = make_index_sequence; - switch (anchor.m_object->type()) - { - // use integer array index as key - case value_t::array: - { - if (array_index != array_index_last) - { - array_index_str = std::to_string(array_index); - array_index_last = array_index; - } - return array_index_str; - } +// dispatch utility (taken from ranges-v3) +template struct priority_tag : priority_tag < N - 1 > {}; +template<> struct priority_tag<0> {}; - // use key from the object - case value_t::object: - return anchor.key(); +// taken from ranges-v3 +template +struct static_const +{ + static constexpr T value{}; +}; - // use an empty key for all primitive types - default: - return empty_str; - } - } +template +constexpr T static_const::value; +} // namespace detail +} // namespace nlohmann - /// return value of the iterator - typename IteratorType::reference value() const - { - return anchor.value(); - } - }; +// #include - /// the container to iterate - typename IteratorType::reference container; - public: - /// construct iteration proxy from a container - explicit iteration_proxy(typename IteratorType::reference cont) noexcept - : container(cont) {} +#include // numeric_limits +#include // false_type, is_constructible, is_integral, is_same, true_type +#include // declval - /// return iterator begin (needed for range-based for) - iteration_proxy_internal begin() noexcept - { - return iteration_proxy_internal(container.begin()); - } +// #include + +// #include - /// return iterator end (needed for range-based for) - iteration_proxy_internal end() noexcept - { - return iteration_proxy_internal(container.end()); - } -}; -} -} + +#include // random_access_iterator_tag + +// #include namespace nlohmann { namespace detail { -////////////////// -// constructors // -////////////////// - -template struct external_constructor; - -template<> -struct external_constructor +template struct make_void { - template - static void construct(BasicJsonType& j, typename BasicJsonType::boolean_t b) noexcept - { - j.m_type = value_t::boolean; - j.m_value = b; - j.assert_invariant(); - } + using type = void; }; +template using void_t = typename make_void::type; +} // namespace detail +} // namespace nlohmann -template<> -struct external_constructor -{ - template - static void construct(BasicJsonType& j, const typename BasicJsonType::string_t& s) - { - j.m_type = value_t::string; - j.m_value = s; - j.assert_invariant(); - } +// #include - template - static void construct(BasicJsonType& j, typename BasicJsonType::string_t&& s) - { - j.m_type = value_t::string; - j.m_value = std::move(s); - j.assert_invariant(); - } - template::value, - int> = 0> - static void construct(BasicJsonType& j, const CompatibleStringType& str) - { - j.m_type = value_t::string; - j.m_value.string = j.template create(str); - j.assert_invariant(); - } +namespace nlohmann +{ +namespace detail +{ +template +struct iterator_types {}; + +template +struct iterator_types < + It, + void_t> +{ + using difference_type = typename It::difference_type; + using value_type = typename It::value_type; + using pointer = typename It::pointer; + using reference = typename It::reference; + using iterator_category = typename It::iterator_category; }; -template<> -struct external_constructor +// This is required as some compilers implement std::iterator_traits in a way that +// doesn't work with SFINAE. See https://github.com/nlohmann/json/issues/1341. +template +struct iterator_traits { - template - static void construct(BasicJsonType& j, typename BasicJsonType::number_float_t val) noexcept - { - j.m_type = value_t::number_float; - j.m_value = val; - j.assert_invariant(); - } }; -template<> -struct external_constructor +template +struct iterator_traits < T, enable_if_t < !std::is_pointer::value >> + : iterator_types { - template - static void construct(BasicJsonType& j, typename BasicJsonType::number_unsigned_t val) noexcept - { - j.m_type = value_t::number_unsigned; - j.m_value = val; - j.assert_invariant(); - } }; -template<> -struct external_constructor +template +struct iterator_traits::value>> { - template - static void construct(BasicJsonType& j, typename BasicJsonType::number_integer_t val) noexcept - { - j.m_type = value_t::number_integer; - j.m_value = val; - j.assert_invariant(); - } + using iterator_category = std::random_access_iterator_tag; + using value_type = T; + using difference_type = ptrdiff_t; + using pointer = T*; + using reference = T&; }; +} // namespace detail +} // namespace nlohmann -template<> -struct external_constructor -{ - template - static void construct(BasicJsonType& j, const typename BasicJsonType::array_t& arr) - { - j.m_type = value_t::array; - j.m_value = arr; - j.assert_invariant(); - } +// #include - template - static void construct(BasicJsonType& j, typename BasicJsonType::array_t&& arr) - { - j.m_type = value_t::array; - j.m_value = std::move(arr); - j.assert_invariant(); - } +// #include - template::value, - int> = 0> - static void construct(BasicJsonType& j, const CompatibleArrayType& arr) - { - using std::begin; - using std::end; - j.m_type = value_t::array; - j.m_value.array = j.template create(begin(arr), end(arr)); - j.assert_invariant(); - } +// #include - template - static void construct(BasicJsonType& j, const std::vector& arr) - { - j.m_type = value_t::array; - j.m_value = value_t::array; - j.m_value.array->reserve(arr.size()); - for (const bool x : arr) - { - j.m_value.array->push_back(x); - } - j.assert_invariant(); - } - template::value, int> = 0> - static void construct(BasicJsonType& j, const std::valarray& arr) - { - j.m_type = value_t::array; - j.m_value = value_t::array; - j.m_value.array->resize(arr.size()); - std::copy(std::begin(arr), std::end(arr), j.m_value.array->begin()); - j.assert_invariant(); - } +#include + +// #include + + +// https://en.cppreference.com/w/cpp/experimental/is_detected +namespace nlohmann +{ +namespace detail +{ +struct nonesuch +{ + nonesuch() = delete; + ~nonesuch() = delete; + nonesuch(nonesuch const&) = delete; + nonesuch(nonesuch const&&) = delete; + void operator=(nonesuch const&) = delete; + void operator=(nonesuch&&) = delete; }; -template<> -struct external_constructor +template class Op, + class... Args> +struct detector { - template - static void construct(BasicJsonType& j, const typename BasicJsonType::object_t& obj) - { - j.m_type = value_t::object; - j.m_value = obj; - j.assert_invariant(); - } + using value_t = std::false_type; + using type = Default; +}; - template - static void construct(BasicJsonType& j, typename BasicJsonType::object_t&& obj) - { - j.m_type = value_t::object; - j.m_value = std::move(obj); - j.assert_invariant(); - } +template class Op, class... Args> +struct detector>, Op, Args...> +{ + using value_t = std::true_type; + using type = Op; +}; - template::value, int> = 0> - static void construct(BasicJsonType& j, const CompatibleObjectType& obj) - { - using std::begin; - using std::end; +template