Router: Uses Tree and Use named Parameters
This commit is contained in:
parent
dee4f9ec68
commit
2bd34c2bb1
8 changed files with 139 additions and 122 deletions
|
@ -29,6 +29,6 @@ public:
|
||||||
ParameterValue &Header(const std::string &key) { return Headers[key]; }
|
ParameterValue &Header(const std::string &key) { return Headers[key]; }
|
||||||
std::unordered_map<std::string, ParameterValue> Parameters;
|
std::unordered_map<std::string, ParameterValue> Parameters;
|
||||||
std::unordered_map<std::string, ParameterValue> Headers;
|
std::unordered_map<std::string, ParameterValue> Headers;
|
||||||
std::vector<std::string> URLParameters;
|
std::unordered_map<std::string, std::string> URLParameters;
|
||||||
};
|
};
|
||||||
} // namespace VWeb
|
} // namespace VWeb
|
|
@ -3,20 +3,38 @@
|
||||||
#include "Route.h"
|
#include "Route.h"
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <vector>
|
#include <memory>
|
||||||
|
|
||||||
namespace VWeb {
|
namespace VWeb {
|
||||||
typedef std::function<bool(Request &, Response &)> RouteFunction;
|
typedef std::function<bool(Request &, Response &)> RouteFunction;
|
||||||
typedef std::function<std::shared_ptr<Route>()> RouteInstaniateFunction;
|
typedef std::function<std::shared_ptr<Route>()> RouteInstaniateFunction;
|
||||||
|
|
||||||
|
struct RouteTree {
|
||||||
|
void Add(const std::string &path, uint32_t allowedMethods,
|
||||||
|
RouteInstaniateFunction instaniate);
|
||||||
|
Ref<Route> Find(const std::string &path, Request &request);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
struct Node {
|
||||||
|
explicit Node(const uint64_t id) : ID(id) {}
|
||||||
|
std::unordered_map<std::string, std::unique_ptr<Node>> Children{};
|
||||||
|
uint64_t ID{0};
|
||||||
|
};
|
||||||
|
Node Root{0};
|
||||||
|
uint64_t m_NodeID = 1;
|
||||||
|
struct RouteInstance {
|
||||||
|
uint32_t AllowedMethods = HttpMethod::OPTIONS | HttpMethod::HEAD;
|
||||||
|
RouteInstaniateFunction Instaniate;
|
||||||
|
};
|
||||||
|
std::unordered_map<uint64_t, RouteInstance> m_Routes;
|
||||||
|
};
|
||||||
|
|
||||||
class Router {
|
class Router {
|
||||||
public:
|
public:
|
||||||
Router();
|
Router();
|
||||||
void DeleteRoute(const std::string &name);
|
|
||||||
|
|
||||||
Ref<Response> HandleRoute(Ref<Request> &request);
|
Ref<Response> HandleRoute(Ref<Request> &request);
|
||||||
Ref<Route> FindRoute(Ref<Request> &request);
|
Ref<Route> FindRoute(Ref<Request> &request);
|
||||||
static void AddToArgs(Ref<Request> &request, std::vector<std::string> &items);
|
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
void Register(const std::string &endpoint, HttpMethod allowedMethod) {
|
void Register(const std::string &endpoint, HttpMethod allowedMethod) {
|
||||||
|
@ -28,23 +46,18 @@ public:
|
||||||
Register(const std::string &endpoint,
|
Register(const std::string &endpoint,
|
||||||
uint32_t allowedMethods = static_cast<uint32_t>(HttpMethod::ALL)) {
|
uint32_t allowedMethods = static_cast<uint32_t>(HttpMethod::ALL)) {
|
||||||
static_assert(std::is_base_of_v<Route, T>, "must be a Route");
|
static_assert(std::is_base_of_v<Route, T>, "must be a Route");
|
||||||
allowedMethods |= HttpMethod::HEAD | HttpMethod::OPTIONS;
|
m_Tree.Add(endpoint,
|
||||||
m_Routes[endpoint] = {.AllowedMethods = allowedMethods,
|
allowedMethods | HttpMethod::HEAD | HttpMethod::OPTIONS,
|
||||||
.Instaniate = [] { return std::make_shared<T>(); }};
|
[] { return std::make_shared<T>(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
void Get(const std::string &path, RouteFunction);
|
void Get(const std::string &path, const RouteFunction &);
|
||||||
void Post(const std::string &path, RouteFunction);
|
void Post(const std::string &path, const RouteFunction &);
|
||||||
void Put(const std::string &path, RouteFunction);
|
void Put(const std::string &path, const RouteFunction &);
|
||||||
void Patch(const std::string &path, RouteFunction);
|
void Patch(const std::string &path, const RouteFunction &);
|
||||||
void Delete(const std::string &path, RouteFunction);
|
void Delete(const std::string &path, const RouteFunction &);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct RouteInstance {
|
RouteTree m_Tree{};
|
||||||
uint32_t AllowedMethods = HttpMethod::OPTIONS | HttpMethod::HEAD;
|
|
||||||
RouteInstaniateFunction Instaniate;
|
|
||||||
};
|
|
||||||
std::unordered_map<std::string, RouteInstance> m_Routes;
|
|
||||||
std::unordered_map<std::string, RouteFunction> m_FunctionRoutes;
|
|
||||||
};
|
};
|
||||||
} // namespace VWeb
|
} // namespace VWeb
|
|
@ -21,7 +21,6 @@ public:
|
||||||
void Stop() { m_IsExit = true; }
|
void Stop() { m_IsExit = true; }
|
||||||
Ref<Router> &GetRouter() { return m_Router; }
|
Ref<Router> &GetRouter() { return m_Router; }
|
||||||
Ref<ServerConfig> &GetServerConfig() { return m_ServerConfig; }
|
Ref<ServerConfig> &GetServerConfig() { return m_ServerConfig; }
|
||||||
void RemoveRoute(const std::string &path) const;
|
|
||||||
|
|
||||||
Ref<MiddleWareHandler> &Middleware();
|
Ref<MiddleWareHandler> &Middleware();
|
||||||
|
|
||||||
|
|
|
@ -49,10 +49,9 @@ void ParseParameterString(Request &req, const std::string &toParse) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string GetPostBody(const std::string& originalBody)
|
std::string GetPostBody(const std::string &originalBody) {
|
||||||
{
|
|
||||||
auto body = String::Split(originalBody, "\r\n\r\n", 1);
|
auto body = String::Split(originalBody, "\r\n\r\n", 1);
|
||||||
if (body.size() > 1 && ! body[body.size() - 1].empty())
|
if (body.size() > 1 && !body[body.size() - 1].empty())
|
||||||
return String::TrimCopy(String::UrlDecode(body[body.size() - 1]));
|
return String::TrimCopy(String::UrlDecode(body[body.size() - 1]));
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
@ -62,7 +61,7 @@ void ParseParameters(Request &request, RequestHandler &requestHandler) {
|
||||||
size_t hasURLParameters = uri.find('?');
|
size_t hasURLParameters = uri.find('?');
|
||||||
if (hasURLParameters != std::string::npos) {
|
if (hasURLParameters != std::string::npos) {
|
||||||
ParseParameterString(request, uri.substr(hasURLParameters + 1));
|
ParseParameterString(request, uri.substr(hasURLParameters + 1));
|
||||||
request.URI = uri.substr (0, hasURLParameters);
|
request.URI = uri.substr(0, hasURLParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.Method == HttpMethod::HEAD || request.Method == HttpMethod::GET ||
|
if (request.Method == HttpMethod::HEAD || request.Method == HttpMethod::GET ||
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#include "Includes/VWeb.h"
|
#include "Includes/VWeb.h"
|
||||||
#include "StringUtils.h"
|
#include "StringUtils.h"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
@ -30,14 +31,59 @@ public:
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Router::Router() { Register<ErrorRoute>("@"); }
|
void RouteTree::Add(const std::string &path, uint32_t allowedMethods,
|
||||||
|
RouteInstaniateFunction instaniate) {
|
||||||
void Router::DeleteRoute(const std::string &name) {
|
auto segments = String::Split(path, "/");
|
||||||
if (m_Routes.contains(name)) {
|
auto node = &Root;
|
||||||
m_Routes.erase(name);
|
for (const auto &segment : segments) {
|
||||||
|
if (segment.empty())
|
||||||
|
continue;
|
||||||
|
if (!node->Children.contains(segment)) {
|
||||||
|
node->Children[segment] = std::make_unique<Node>(m_NodeID++);
|
||||||
}
|
}
|
||||||
|
node = node->Children.at(segment).get();
|
||||||
|
}
|
||||||
|
m_Routes[node->ID] = {.AllowedMethods = allowedMethods,
|
||||||
|
.Instaniate = std::move(instaniate)};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ref<Route> RouteTree::Find(const std::string &path, Request &request) {
|
||||||
|
auto segments = String::Split(path, "/");
|
||||||
|
auto node = &Root;
|
||||||
|
for (const auto &segment : segments) {
|
||||||
|
if (segment.empty())
|
||||||
|
continue;
|
||||||
|
if (auto it = node->Children.find(segment); it != node->Children.end()) {
|
||||||
|
node = it->second.get();
|
||||||
|
} else {
|
||||||
|
// Arguments...
|
||||||
|
bool foundParameter = false;
|
||||||
|
for (auto &[key, child] : node->Children) {
|
||||||
|
if (key[0] == ':') {
|
||||||
|
node = child.get();
|
||||||
|
foundParameter = true;
|
||||||
|
request.URLParameters[key.substr(1)] = segment;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!foundParameter) {
|
||||||
|
request.URLParameters = {};
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m_Routes.contains(node->ID)) {
|
||||||
|
const auto &instance = m_Routes[node->ID];
|
||||||
|
auto ref = instance.Instaniate();
|
||||||
|
ref->SetAllowedMethods(instance.AllowedMethods);
|
||||||
|
return ref;
|
||||||
|
}
|
||||||
|
request.URLParameters = {};
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Router::Router() { Register<ErrorRoute>("@"); }
|
||||||
|
|
||||||
static void HandleOptions(Ref<Response> &response, uint32_t allowedMethods) {
|
static void HandleOptions(Ref<Response> &response, uint32_t allowedMethods) {
|
||||||
std::stringstream str{};
|
std::stringstream str{};
|
||||||
bool isFirst = true;
|
bool isFirst = true;
|
||||||
|
@ -60,21 +106,18 @@ Ref<Response> Router::HandleRoute(Ref<Request> &request) {
|
||||||
response->Method = request->Method;
|
response->Method = request->Method;
|
||||||
|
|
||||||
if (!route) {
|
if (!route) {
|
||||||
// Lets check if we can run it through functions routes..
|
route = m_Tree.Find(s_HttpMethodToString[request->Method] + request->URI,
|
||||||
const auto it = m_FunctionRoutes.find(
|
*request);
|
||||||
s_HttpMethodToString[request->Method] + request->URI);
|
if (!route) {
|
||||||
if (it != m_FunctionRoutes.end()) {
|
response->SetStatus(HttpStatusCode::NotFound);
|
||||||
it->second(*request, *response);
|
m_Tree.Find("@", *request)->Execute(*request, *response);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
response->SetStatus(HttpStatusCode::NotFound);
|
|
||||||
m_Routes["@"].Instaniate()->Execute(*request, *response);
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!route->IsAllowed(*request)) {
|
if (!route->IsAllowed(*request)) {
|
||||||
response->SetStatus(HttpStatusCode::Forbidden);
|
response->SetStatus(HttpStatusCode::Forbidden);
|
||||||
m_Routes["@"].Instaniate()->Execute(*request, *response);
|
m_Tree.Find("@", *request)->Execute(*request, *response);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,77 +128,56 @@ Ref<Response> Router::HandleRoute(Ref<Request> &request) {
|
||||||
|
|
||||||
if (!route->Execute(*request, *response)) {
|
if (!route->Execute(*request, *response)) {
|
||||||
std::string rKey = "@" + std::to_string(to_underlying(response->Status));
|
std::string rKey = "@" + std::to_string(to_underlying(response->Status));
|
||||||
m_Routes.contains(rKey)
|
auto r = m_Tree.Find(rKey, *request);
|
||||||
? m_Routes[rKey].Instaniate()->Execute(*request, *response)
|
if (r) {
|
||||||
: m_Routes["@"].Instaniate()->Execute(*request, *response);
|
r->Execute(*request, *response);
|
||||||
|
} else {
|
||||||
|
m_Tree.Find("@", *request)->Execute(*request, *response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Ref<Route> Instaniate(const RouteInstaniateFunction &func,
|
|
||||||
uint32_t allowedMethods) {
|
|
||||||
auto ref = func();
|
|
||||||
ref->SetAllowedMethods(allowedMethods);
|
|
||||||
return ref;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ref<Route> Router::FindRoute(Ref<Request> &request) {
|
Ref<Route> Router::FindRoute(Ref<Request> &request) {
|
||||||
const auto &url = request->URI;
|
const auto &url = request->URI;
|
||||||
|
|
||||||
if (url.starts_with("@"))
|
if (url.starts_with("@"))
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
return m_Tree.Find(url, *request);
|
||||||
{
|
|
||||||
if (const auto it = m_Routes.find(url);
|
|
||||||
it != m_Routes.end() && it->second.AllowedMethods & request->Method) {
|
|
||||||
return Instaniate(it->second.Instaniate, it->second.AllowedMethods);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto split = String::Split(url, "/");
|
|
||||||
if (split.size() > 1) {
|
|
||||||
AddToArgs(request, split);
|
|
||||||
while (split.size() > 1) {
|
|
||||||
std::string nUrl = String::Join(split, "/");
|
|
||||||
if (auto it = m_Routes.find(url);
|
|
||||||
it != m_Routes.end() && it->second.AllowedMethods & request->Method) {
|
|
||||||
return Instaniate(it->second.Instaniate, it->second.AllowedMethods);
|
|
||||||
}
|
|
||||||
AddToArgs(request, split);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
{
|
|
||||||
if (const auto it = m_Routes.find("/");
|
|
||||||
it != m_Routes.end() && it->second.AllowedMethods & request->Method) {
|
|
||||||
return Instaniate(it->second.Instaniate, it->second.AllowedMethods);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Router::AddToArgs(Ref<Request> &request, std::vector<std::string> &items) {
|
struct InlineRoute : Route {
|
||||||
request->URLParameters.push_back(items[items.size() - 1]);
|
explicit InlineRoute(RouteFunction function) : Func(std::move(function)) {}
|
||||||
items.pop_back();
|
RouteFunction Func;
|
||||||
}
|
bool Execute(Request &request, Response &response) override {
|
||||||
|
Func(request, response);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool IsAllowed(Request &request) override { return true; }
|
||||||
|
};
|
||||||
|
|
||||||
void Router::Get(const std::string &path, RouteFunction func) {
|
void Router::Get(const std::string &path, const RouteFunction &func) {
|
||||||
m_FunctionRoutes[s_HttpMethodToString[HttpMethod::GET] + path] =
|
m_Tree.Add(s_HttpMethodToString[HttpMethod::GET] + path,
|
||||||
std::move(func);
|
(uint32_t)HttpMethod::GET,
|
||||||
|
[func] { return std::make_shared<InlineRoute>(func); });
|
||||||
}
|
}
|
||||||
void Router::Post(const std::string &path, RouteFunction func) {
|
void Router::Post(const std::string &path, const RouteFunction &func) {
|
||||||
m_FunctionRoutes[s_HttpMethodToString[HttpMethod::POST] + path] =
|
m_Tree.Add(s_HttpMethodToString[HttpMethod::POST] + path,
|
||||||
std::move(func);
|
(uint32_t)HttpMethod::POST,
|
||||||
|
[func] { return std::make_shared<InlineRoute>(func); });
|
||||||
}
|
}
|
||||||
void Router::Put(const std::string &path, RouteFunction func) {
|
void Router::Put(const std::string &path, const RouteFunction &func) {
|
||||||
m_FunctionRoutes[s_HttpMethodToString[HttpMethod::PUT] + path] =
|
m_Tree.Add(s_HttpMethodToString[HttpMethod::PUT] + path,
|
||||||
std::move(func);
|
(uint32_t)HttpMethod::PUT,
|
||||||
|
[func] { return std::make_shared<InlineRoute>(func); });
|
||||||
}
|
}
|
||||||
void Router::Patch(const std::string &path, RouteFunction func) {
|
void Router::Patch(const std::string &path, const RouteFunction &func) {
|
||||||
m_FunctionRoutes[s_HttpMethodToString[HttpMethod::PATCH] + path] =
|
m_Tree.Add(s_HttpMethodToString[HttpMethod::PATCH] + path,
|
||||||
std::move(func);
|
(uint32_t)HttpMethod::PATCH,
|
||||||
|
[func] { return std::make_shared<InlineRoute>(func); });
|
||||||
}
|
}
|
||||||
void Router::Delete(const std::string &path, RouteFunction func) {
|
void Router::Delete(const std::string &path, const RouteFunction &func) {
|
||||||
m_FunctionRoutes[s_HttpMethodToString[HttpMethod::DELETE] + path] =
|
m_Tree.Add(s_HttpMethodToString[HttpMethod::DELETE] + path,
|
||||||
std::move(func);
|
(uint32_t)HttpMethod::DELETE,
|
||||||
|
[func] { return std::make_shared<InlineRoute>(func); });
|
||||||
}
|
}
|
||||||
} // namespace VWeb
|
} // namespace VWeb
|
||||||
|
|
|
@ -29,9 +29,6 @@ void Server::Start() {
|
||||||
fprintf(stdout, "[VWeb] Running Server On: 0.0.0.0:%d\n",
|
fprintf(stdout, "[VWeb] Running Server On: 0.0.0.0:%d\n",
|
||||||
m_ServerConfig->Port);
|
m_ServerConfig->Port);
|
||||||
}
|
}
|
||||||
void Server::RemoveRoute(const std::string &path) const {
|
|
||||||
m_Router->DeleteRoute(path);
|
|
||||||
}
|
|
||||||
void Server::Execute() {
|
void Server::Execute() {
|
||||||
constexpr size_t MAX_EVENTS = 5000;
|
constexpr size_t MAX_EVENTS = 5000;
|
||||||
struct epoll_event events[MAX_EVENTS];
|
struct epoll_event events[MAX_EVENTS];
|
||||||
|
|
|
@ -1,22 +1,8 @@
|
||||||
cmake_minimum_required(VERSION 3.17)
|
cmake_minimum_required(VERSION 3.17)
|
||||||
project(VWeb_Example)
|
project(VWeb_Example)
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
|
||||||
set(THREADS_PREFER_PTHREAD_FLAG ON)
|
|
||||||
find_package(Threads REQUIRED)
|
find_package(Threads REQUIRED)
|
||||||
find_package(VWeb 1.0 REQUIRED)
|
find_package(VWeb 1.0 REQUIRED)
|
||||||
add_executable(VWeb_Example main.cpp)
|
add_executable(VWeb_Example main.cpp)
|
||||||
|
|
||||||
include_directories(${CMAKE_SOURCE_DIR}/..)
|
include_directories(${CMAKE_SOURCE_DIR}/..)
|
||||||
|
target_link_libraries(VWeb_Example Threads::Threads VWeb)
|
||||||
set(mode Release)
|
|
||||||
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
|
|
||||||
set(mode Debug)
|
|
||||||
endif ()
|
|
||||||
set(vweb_lib ${CMAKE_SOURCE_DIR}/../dist/libVWeb.${mode}.a)
|
|
||||||
|
|
||||||
SET_SOURCE_FILES_PROPERTIES(
|
|
||||||
main.cpp
|
|
||||||
PROPERTIES OBJECT_DEPENDS ${vweb_lib}
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(VWeb_Example Threads::Threads ${vweb_lib})
|
|
|
@ -2,34 +2,35 @@
|
||||||
|
|
||||||
class MyCompleteController : public VWeb::Route {
|
class MyCompleteController : public VWeb::Route {
|
||||||
public:
|
public:
|
||||||
bool Get(const VWeb::Request&, VWeb::Response& response) {
|
bool Get(VWeb::Request &req, VWeb::Response &response) override {
|
||||||
response << "MyCompleteController: GET";
|
response << "MyCompleteController: GET:: \r\n\r\nParameters:\r\n\r\n";
|
||||||
|
for (auto &[key, value] : req.URLParameters) {
|
||||||
|
response << key << ": " << value << "\r\n";
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
bool Post(const VWeb::Request&, VWeb::Response& response) {
|
bool Post(VWeb::Request &, VWeb::Response &response) override {
|
||||||
response << "MyCompleteController: POST";
|
response << "MyCompleteController: POST";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IsAllowed(const VWeb::Request& request) {
|
|
||||||
return request.HasHeader("Auth");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
bool Ping(const VWeb::Request&, VWeb::Response& response) {
|
bool Ping(const VWeb::Request &, VWeb::Response &response) {
|
||||||
response << "Pong";
|
response << "Pong";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
int main() {
|
int main() {
|
||||||
using namespace VWeb;
|
using namespace VWeb;
|
||||||
VWeb::Server server;
|
VWeb::Server server;
|
||||||
auto& router = server.GetRouter();
|
auto &router = server.GetRouter();
|
||||||
// For debugging and profiling more than 1 thread can be hard.
|
// For debugging and profiling more than 1 thread can be hard.
|
||||||
server.GetServerConfig()->WorkerThreads = 1;
|
server.GetServerConfig()->WorkerThreads = 1;
|
||||||
router->Get("/test", [](Request&, Response& response) {
|
router->Get("/test", [](Request &, Response &response) {
|
||||||
response << "NICE";
|
response << "NICE";
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
router->Get("/ping", &Ping);
|
router->Get("/ping", &Ping);
|
||||||
|
router->Register<MyCompleteController>("/auth/:id/",
|
||||||
|
HttpMethod::GET | HttpMethod::POST);
|
||||||
server.Start();
|
server.Start();
|
||||||
server.Join();
|
server.Join();
|
||||||
return 0;
|
return 0;
|
||||||
|
|
Loading…
Reference in a new issue