Code to implement HTTP requests done. Also, rewrote str_to_int64, str_to_double

This commit is contained in:
2022-04-15 17:24:07 -04:00
parent 453809b65c
commit b6d603034e
17 changed files with 3705 additions and 99 deletions

View File

@@ -77,6 +77,7 @@ CORE_OBJ_FILES=\
obj/idalloc.o\
obj/globaldb.o\
obj/sched.o\
obj/http.o\
obj/table.o\
obj/gui.o\
obj/luasnap.o\

View File

@@ -297,25 +297,25 @@ bool AnimStep::from_string(const eng::string &config) {
if (key == "action") {
action_ = val;
} else if (key == "id") {
int64_t id = util::strtoint(val, -1);
int64_t id = util::str_to_int64(val, -1);
if (id < 0) return false;
id_ = id;
} else if (key == "plane") {
set_plane(val);
} else if (key == "x") {
double v = util::strtodouble(val);
double v = util::str_to_double(val);
if (std::isnan(v)) return false;
set_x(v);
} else if (key == "y") {
double v = util::strtodouble(val);
double v = util::str_to_double(val);
if (std::isnan(v)) return false;
set_y(v);
} else if (key == "z") {
double v = util::strtodouble(val);
double v = util::str_to_double(val);
if (std::isnan(v)) return false;
set_z(v);
} else if (key == "facing") {
double v = util::strtodouble(val);
double v = util::str_to_double(val);
if (std::isnan(v)) return false;
set_facing(v);
} else if (key == "graphic") {

File diff suppressed because it is too large Load Diff

View File

@@ -39,8 +39,8 @@ bool Gui::has_action(const eng::string &action) const {
return false;
}
eng::string Gui::get_action(int index) {
if ((index < 0) || (index >= int(elts_.size()))) {
eng::string Gui::get_action(int64_t index) {
if ((index < 0) || (index >= int64_t(elts_.size()))) {
return "";
}
return elts_[index].action();

View File

@@ -40,7 +40,7 @@ public:
void clear(int64_t p) { place_ = p; elts_.clear(); }
bool has_action(const eng::string &action) const;
void menu_item(const eng::string &action, const eng::string &label);
eng::string get_action(int index);
eng::string get_action(int64_t index);
eng::string menu_debug_string() const;
// Put a pointer to a gui into the lua registry.

View File

@@ -1,26 +1,414 @@
#include "http.hpp"
#include "wrap-sstream.hpp"
#include "wrap-string.hpp"
#include "luastack.hpp"
#include <cstdint>
static eng::string url_encode(const eng::string &value) {
eng::ostringstream escaped;
escaped.fill('0');
escaped << hex;
static void url_encode(const eng::string &value, StreamBuffer *sb) {
const char *hexdigits = "0123456789ABCDEF";
for (int i = 0; i < int(value.size()); i++) {
char c = value[i];
// Keep alphanumeric and other accepted characters intact
// Any other characters are percent-encoded
if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
escaped << c;
if (util::ascii_isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~' || (c == '/')) {
sb->write_char(c);
} else if (c == ' ') {
sb->write_char('+');
} else {
escaped << std::uppercase;
escaped << '%' << std::setw(2) << int((unsigned char) c);
escaped << std::nouppercase;
sb->write_char('%');
sb->write_char(hexdigits[c>>4]);
sb->write_char(hexdigits[c&15]);
}
}
}
return escaped.str();
class ErrorStringStream : public eng::ostringstream {
private:
eng::string *target_;
public:
ErrorStringStream(eng::string *target) : target_(target) {}
~ErrorStringStream() {
if (target_->empty()) {
(*target_) = str();
}
}
};
HttpRequest::HttpRequest() {
verify_certificate_ = true;
port_ = 0;
}
void HttpRequest::set_verify_certificate(bool flag) {
verify_certificate_ = flag;
}
eng::string HttpRequest::target() const {
assert(check().empty());
eng::ostringstream oss;
oss << (verify_certificate_ ? "cert" : "nocert");
oss << ':' << host_ << ':' << port_;
return oss.str();
}
void HttpRequest::set_method(const eng::string &s) {
eng::string method = util::ascii_toupper(s);
if ((method != "GET") && (method != "HEAD")) {
ErrorStringStream error(&error_);
error << "HTTPS method not implemented: " << method;
error << ". Currently, only HEAD and GET are implemented.";
return;
}
if ((!method_.empty()) && (method_ != method)) {
ErrorStringStream error(&error_);
error << "HTTPS method specified twice: " << method_ << " and " << method;
return;
}
method_ = method;
}
void HttpRequest::set_host(const eng::string &s) {
eng::string host = util::ascii_tolower(s);
if (host.empty()) {
ErrorStringStream error(&error_);
error << "HTTPS hostname cannot be empty string.";
return;
}
// This is not quite strict, but it's close. I believe
// the DNS lookup will fail for invalid hostnames anyway.
for (char c : host) {
if ((c != '-') && (c != '.') && (!util::ascii_isalnum(c))) {
ErrorStringStream error(&error_);
error << "HTTPS hostnames can only contain letters, digits, and hyphen: " << host;
return;
}
}
if (!host_.empty()) {
ErrorStringStream error(&error_);
error << "HTTPS hostname specified twice: " << host_ << " and " << host;
return;
}
host_ = host;
}
void HttpRequest::set_port(int port) {
if ((port < 1) || (port > 65535)) {
ErrorStringStream error(&error_);
error << "HTTP port must be between 1 and 65535: " << port;
return;
}
if (port_ != 0) {
ErrorStringStream error(&error_);
error << "HTTPS port specified twice: " << port_ << " and " << port;
return;
}
port_ = port;
}
void HttpRequest::set_url(const eng::string &url) {
if (util::has_prefix(url, "https://")) {
ErrorStringStream error(&error_);
error << "set_url(full_url) not implemented yet.";
return;
} else if (util::has_prefix(url, "/")) {
if (!path_.empty()) {
ErrorStringStream error(&error_);
error << "HTTP path specified twice: " << path_ << " and " << url;
return;
}
path_ = url;
} else {
ErrorStringStream error(&error_);
error << "HTTP url must start with https://, or with /";
return;
}
}
void HttpRequest::set_param(const eng::string &key, const eng::string &val) {
if (params_.find(key) != params_.end()) {
ErrorStringStream error(&error_);
error << "HTTP url parameter specified twice: " << key;
return;
}
params_[key] = val;
}
void HttpRequest::set_verify_certificate(LuaStack &LS, LuaSlot val) {
if (!LS.isboolean(val)) {
ErrorStringStream error(&error_);
error << "HTTP verify_certificate must be a boolean";
return;
}
set_verify_certificate(LS.ckboolean(val));
}
void HttpRequest::set_method(LuaStack &LS, LuaSlot val) {
if (!LS.isstring(val)) {
ErrorStringStream error(&error_);
error << "HTTP method must be a string";
return;
}
set_method(LS.ckstring(val));
}
void HttpRequest::set_host(LuaStack &LS, LuaSlot val) {
if (!LS.isstring(val)) {
ErrorStringStream error(&error_);
error << "HTTP host must be a string";
return;
}
set_host(LS.ckstring(val));
}
void HttpRequest::set_port(LuaStack &LS, LuaSlot val) {
if (!LS.isint(val)) {
ErrorStringStream error(&error_);
error << "HTTP port must be an int";
return;
}
set_port(LS.ckint(val));
}
void HttpRequest::set_url(LuaStack &LS, LuaSlot val) {
if (!LS.isstring(val)) {
ErrorStringStream error(&error_);
error << "HTTP url must be a string";
return;
}
set_url(LS.ckstring(val));
}
void HttpRequest::set_param(LuaStack &LS, LuaSlot key, LuaSlot val) {
if (!LS.isstring(key)) {
ErrorStringStream error(&error_);
error << "HTTP url parameter key must be a string";
return;
}
if (!LS.isstring(val)) {
ErrorStringStream error(&error_);
error << "HTTP url parameter val must be a string";
return;
}
set_param(LS.ckstring(key), LS.ckstring(val));
}
void HttpRequest::set_params(LuaStack &LS0, LuaSlot tab) {
if (!LS0.istable(tab)) {
ErrorStringStream error(&error_);
error << "HTTP params must be a table";
return;
}
LuaVar key, val;
LuaStack LS(LS0.state(), key, val);
LS.set(key, LuaNil);
while (LS.next(tab, key, val)) {
set_param(LS, key, val);
}
}
void HttpRequest::set_defaults() {
if (method_.empty()) {
method_ = "GET";
}
if (port_ == 0) {
port_ = 443;
}
}
void HttpRequest::set_config(LuaStack &LS0, LuaSlot tab) {
LuaVar key, val;
LuaStack LS(LS0.state(), key, val);
LS.set(key, LuaNil);
while (LS.next(tab, key, val)) {
eng::string kstr;
if (LS.isstring(key)) kstr = LS.ckstring(key);
if (kstr == "method") {
set_method(LS, val);
} else if (kstr == "host") {
set_host(LS, val);
} else if (kstr == "port") {
set_port(LS, val);
} else if (kstr == "url") {
set_url(LS, val);
} else if (kstr == "params") {
set_params(LS, val);
} else if (kstr == "verifycertificate") {
set_verify_certificate(LS, val);
} else if (kstr == "") {
ErrorStringStream error(&error_);
error << "HTTP config parameter names must be strings.";
} else {
ErrorStringStream error(&error_);
error << "HTTP unrecognized config parameter: " << kstr;
}
}
}
eng::string HttpRequest::check() const {
if (!error_.empty()) {
return error_;
}
if (method_.empty()) {
return "HTTP method has not been set";
}
if (host_.empty()) {
return "HTTP host has not been set";
}
if (port_ == 0) {
return "HTTP port has not been set";
}
if (path_.empty()) {
return "HTTP url has not been set";
}
return "";
}
void HttpRequest::send_internal(StreamBuffer *sb, bool debug_string) const {
// If there's an error in the request, handle it. In debug string mode,
// we just put the error into the output. In production mode, we assert
// fail.
eng::string error = check();
if (debug_string) {
if (!error.empty()) {
sb->write_bytes(error);
return;
}
} else {
assert(error.empty());
}
// Choose a linebreak.
eng::string linebreak = (debug_string) ? "\n" : "\r\n";
// Send the command.
sb->write_bytes(method_);
sb->write_char(' ');
url_encode(path_, sb);
bool first_param = true;
for (const auto &pair : params_) {
sb->write_char(first_param ? '?' : '&');
url_encode(pair.first, sb);
sb->write_char('=');
url_encode(pair.second, sb);
first_param = false;
}
sb->write_bytes(" HTTP/1.1");
sb->write_bytes(linebreak);
// Send the host header.
sb->write_bytes("Host: ");
sb->write_bytes(host_);
sb->write_char(':');
sb->ostream() << port_;
sb->write_bytes(linebreak);
// The empty accept-encoding header notifies the
// server that we don't support gzip, deflate, or
// other content compression.
sb->write_bytes("Accept-encoding:");
sb->write_bytes(linebreak);
// Add a user-agent header. Not sure why.
sb->write_bytes("User-agent: Mozilla 5.0 (luprex)");
sb->write_bytes(linebreak);
// Send the extra linebreak.
if (!debug_string) {
sb->write_bytes(linebreak);
}
}
eng::string HttpRequest::DebugString() {
StreamBuffer sb;
send_internal(&sb, true);
return eng::string(sb.view());
}
HttpResponse::HttpResponse() {
response_code_ = 0;
response_length_ = 0;
mime_type_ = "application/empty";
}
void HttpResponse::fail(int response_code, const eng::string &error) {
response_code_ = response_code;
error_ = error;
response_length_ = 0;
mime_type_ = "application/empty";
content_ = "";
}
static std::string_view readline(std::string_view &v) {
std::string_view result = util::sv_split_one(v, '\n');
return util::sv_rtrim(result, '\r');
}
void HttpResponse::parse(const StreamBuffer *sb) {
// We're not going to modify the StreamBuffer at all.
// Instead, we work entirely on a view.
std::string_view view = sb->view();
// Special case this.
if (view.empty()) {
fail(500, "HTTP server response completely empty");
return;
}
// Parse the status line.
std::string_view status = readline(view);
if (status.empty()) {
fail(500, "HTTP status-line not present in response");
return;
}
//std::string_view status_code = util::sv_split_one(status, ' ');
}
LuaDefine(http_request, "reqtab",
"|Given an HTTP request in the form of a table, returns the same "
"|request as a string, to assist with debugging."
"|"
"|The table can contain:"
"|"
"| method (ie, GET, HEAD, POST, etc)"
"| host (ie, 'google.com')"
"| port (default: 443)"
"| url (ie, '/index.html')"
"| params (a table of url parameters)"
"| verifycertificate (default: true)"
"|"
"|The url can start with 'https://', or with '/'. If it starts"
"|with 'https://', then the URL includes the host and port, which"
"|then must not be specified separately."
"|"
"|Note that plain HTTP is not supported - we only allow HTTPS."
"|However, you can talk to a server that has a dummy certificate"
"|by specifying verifycertificate=false."
"|"
"|This module will automatically url encode everything for you."
"|Therefore, you shouldn't url encode anything, otherwise,"
"|you'll end up double-encoding."
"|"
"|You cannot include url parameters as part of the url. If you try,"
"|then your ?, &, and = characters will get url encoded, which will"
"|cause them to not function. To use url parameters, you must"
"|use the separate params table."
"|") {
LuaArg tab;
LuaRet str;
LuaStack LS(L, tab, str);
HttpRequest req;
req.set_config(LS, tab);
req.set_defaults();
eng::string error = req.check();
if (!error.empty()) {
luaL_error(L, "%s", error.c_str());
return 0;
}
LS.set(str, req.DebugString());
return LS.result();
}

View File

@@ -16,26 +16,143 @@
#include "eng-malloc.hpp"
#include "wrap-string.hpp"
#include "wrap-vector.hpp"
#include "wrap-map.hpp"
#include "luastack.hpp"
#include "streambuffer.hpp"
class HttpRequest : public eng::nevernew {
public:
enum Method { INVALID, GET, POST, HEAD };
private:
// If the request contains an error, the error
// message is stored here.
eng::string error_;
// If true, verify the server's certificate.
// True is the default.
bool verify_certificate_;
// Method: GET, HEAD, POST, etc.
eng::string method_;
// The hostname. This is used both for DNS lookup,
// and to create an HTTP Host header.
eng::string host_;
// Port number.
int port_;
// The path is always UTF-8. This field should not be urlencoded.
// Instead, urlencoding is done automatically when the request
// is sent. Should not include protocol, host, port, or parameters.
eng::string path_;
// If params is nonempty, then we will add URL parameters
// to the URL. The contents of the params field should not be
// urlencoded, the urlencoding is done automatically when the
// request is sent.
eng::map<eng::string, eng::string> params_;
private:
void send_internal(StreamBuffer *target, bool debug_string) const;
public:
eng::string host_;
int port_;
Method method_;
eng::string encoded_url_;
eng::string content_type_;
// Construct an empty HTTP request.
// All of the fields have empty values.
HttpRequest();
// Get fields.
const eng::string &error() const { return error_; }
bool verify_certificate() const { return verify_certificate_; }
const eng::string &method() const { return method_; }
const eng::string &host() const { return host_; }
int port() const { return port_; }
const eng::string &path() const { return path_; }
// Get the network target, eg, "cert:host:port"
eng::string target() const;
// Populate an HTTP request a piece at a time.
// If you pass an invalid value, or if the field is
// already set, the routine will generate an error message
// and store it in the error field. In that case, the set
// will not happen. If there's already an error in the error
// field, it will not be overwritten.
void set_verify_certificate(bool flag);
void set_method(const eng::string &method);
void set_host(const eng::string &host);
void set_port(int port);
void set_url(const eng::string &url);
void set_param(const eng::string &key, const eng::string &value);
// Same as above, but using Lua values.
void set_verify_certificate(LuaStack &LS, LuaSlot val);
void set_method(LuaStack &LS, LuaSlot val);
void set_host(LuaStack &LS, LuaSlot val);
void set_port(LuaStack &LS, LuaSlot val);
void set_url(LuaStack &LS, LuaSlot val);
void set_param(LuaStack &LS, LuaSlot key, LuaSlot val);
void set_params(LuaStack &LS, LuaSlot tab);
// Set default values for any fields that should have
// defaults. This must be done after setting regular
// values.
void set_defaults();
// Verify that the request is error free and that
// defaults have been set.
eng::string check() const;
// Populate an HTTP request from a Lua table.
void set_config(LuaStack &LS0, LuaSlot tab);
// Put the request into the stream, assuming HTTP/1.1
void send(StreamBuffer *target) const { send_internal(target, false); }
// Get the request as a debug string.
eng::string DebugString();
};
class HttpReply : public eng::nevernew {
public:
int error_code_;
eng::string error_message_;
class HttpResponse {
private:
// The HTTP response code.
int response_code_;
// If the HTTP response contains an error, the
// error message is stored here. If the HTTP response
// is a success such as "200 OK" or "201 Created", this
// is the empty string, not "OK" or "Created".
eng::string error_;
// The length in bytes of the entire response.
// May be zero, which means that the response
// was so garbled that we couldn't determine the length.
bool response_length_;
// MIME type of the content.
eng::string mime_type_;
// The content as string.
eng::string content_;
private:
public:
// Construct a blank response.
HttpResponse();
// Store an error message. This is used when the client detects an error,
// such as a DNS lookup fail, a connection failed, an SSL negotiation
// failed, or the like. Clears the content, leaving only the error
// and response code.
void fail(int response_code, const eng::string &error);
// Parse the HTTP response. Note that the response is not
// removed from the StreamBuffer, which is always unmodified.
// If you want to remove the response from the StreamBuffer, see
// response_length.
void parse(const StreamBuffer *sb);
// Convert the HTTP response to a lua table.
void store(LuaStack &LS, LuaSlot tab);
};
#endif // HTTP_HPP

View File

@@ -125,13 +125,13 @@ public:
void do_menu_command(const StringVec &cmd) {
world_to_asynchronous();
int64_t place = util::strtoint(cmd[1], actor_id_);
int64_t place = util::str_to_int64(cmd[1], actor_id_);
world_->update_gui(actor_id_, place, &gui_);
stdostream() << gui_.menu_debug_string();
}
void do_choose_command(const StringVec &cmd) {
eng::string action = gui_.get_action(util::strtoint(cmd[1], -1));
eng::string action = gui_.get_action(util::str_to_int64(cmd[1]));
if (action == "") {
stdostream() << "Invalid menu item #" << std::endl;
return;

View File

@@ -53,7 +53,7 @@ void LuaConsole::simplify(const StringVec &words) {
words_ = words;
if (words.size() == 0) {
return;
} else if (util::validinteger(words[0])) {
} else if (util::valid_int64(words[0])) {
if (words.size() == 1) {
words_.clear();
words_.push_back("choose");
@@ -62,7 +62,7 @@ void LuaConsole::simplify(const StringVec &words) {
synerr("/choose command takes no arguments");
}
} else if (words[0] == "choose") {
if ((words.size() == 2)&&(util::validinteger(words[1]))) {
if ((words.size() == 2)&&(util::valid_int64(words[1]))) {
// OK
} else {
synerr("/choose [menu-line-number]");
@@ -74,7 +74,7 @@ void LuaConsole::simplify(const StringVec &words) {
} else if (words[0] == "menu") {
if (words.size() == 1) {
words_.push_back("-");
} else if ((words.size() == 2)&&(util::validinteger(words[1]))) {
} else if ((words.size() == 2)&&(util::valid_int64(words[1]))) {
// OK
} else {
synerr("/menu [optional-tangible-id]");

View File

@@ -52,6 +52,22 @@ lua_State *LuaStack::newstate (lua_Alloc allocf) {
return L;
}
bool LuaStack::isinteger(LuaSlot s) const {
if (lua_type(L_, s) == LUA_TNUMBER) {
lua_Number result = lua_tonumber(L_, s);
if (lua_Integer(result) == result) return true;
}
return false;
}
bool LuaStack::isint(LuaSlot s) const {
if (lua_type(L_, s) == LUA_TNUMBER) {
lua_Number result = lua_tonumber(L_, s);
if (int(result) == result) return true;
}
return false;
}
bool LuaStack::ckboolean(LuaSlot s) const {
luaL_checktype(L_, s, LUA_TBOOLEAN);
return lua_toboolean(L_, s) ? true:false;
@@ -59,12 +75,24 @@ bool LuaStack::ckboolean(LuaSlot s) const {
lua_Integer LuaStack::ckinteger(LuaSlot s) const {
luaL_checktype(L_, s, LUA_TNUMBER);
return lua_tointeger(L_, s);
lua_Number result = lua_tonumber(L_, s);
lua_Integer iresult(result);
if (iresult != result) {
luaL_error(L_, "not a valid integer");
return 0;
}
return iresult;
}
int LuaStack::ckint(LuaSlot s) const {
luaL_checktype(L_, s, LUA_TNUMBER);
return (int)lua_tointeger(L_, s);
lua_Number result = lua_tonumber(L_, s);
int iresult(result);
if (iresult != result) {
luaL_error(L_, "not a valid int");
return 0;
}
return iresult;
}
lua_Number LuaStack::cknumber(LuaSlot s) const {

View File

@@ -348,6 +348,8 @@ public:
bool istable(LuaSlot s) const { return lua_type(L_, s) == LUA_TTABLE; }
bool isstring(LuaSlot s) const { return lua_type(L_, s) == LUA_TSTRING; }
bool isnumber(LuaSlot s) const { return lua_type(L_, s) == LUA_TNUMBER; }
bool isinteger(LuaSlot s) const;
bool isint(LuaSlot s) const;
bool isthread(LuaSlot s) const { return lua_type(L_, s) == LUA_TTHREAD; }
bool isfunction(LuaSlot s) const { return lua_type(L_, s) == LUA_TFUNCTION; }
bool isboolean(LuaSlot s) const { return lua_type(L_, s) == LUA_TBOOLEAN; }

View File

@@ -263,6 +263,12 @@ void StreamBuffer::write_uint64(uint64_t vv) {
write_cursor_ += 8;
}
void StreamBuffer::write_char(char c) {
make_space(1);
write_cursor_[0] = c;
write_cursor_ += 1;
}
void StreamBuffer::write_float(float f) {
make_space(4);
memcpy(write_cursor_, &f, 4);
@@ -307,6 +313,13 @@ int64_t StreamBuffer::read_int64() {
return v;
}
char StreamBuffer::read_char() {
check_available(1);
char c = read_cursor_[0];
read_cursor_ += 1;
return c;
}
float StreamBuffer::read_float() {
check_available(4);
float f;

View File

@@ -310,6 +310,7 @@ public:
void write_uint16(uint64_t v);
void write_uint32(uint64_t v);
void write_uint64(uint64_t v);
void write_char(char c);
void write_float(float f);
void write_double(double d);
@@ -325,6 +326,7 @@ public:
uint16_t read_uint16() { return read_int16(); }
uint32_t read_uint32() { return read_int32(); }
uint64_t read_uint64() { return read_int64(); }
char read_char();
float read_float();
double read_double();

View File

@@ -47,13 +47,13 @@ private:
}
void do_menu_command(const StringVec &cmd) {
int64_t place = util::strtoint(cmd[1], actor_id_);
int64_t place = util::str_to_int64(cmd[1], actor_id_);
world_->update_gui(actor_id_, place, &gui_);
stdostream() << gui_.menu_debug_string();
}
void do_choose_command(const StringVec &cmd) {
eng::string action = gui_.get_action(util::strtoint(cmd[1], -1));
eng::string action = gui_.get_action(util::str_to_int64(cmd[1]));
if (action == "") {
stdostream() << "Invalid menu item #" << std::endl;
return;

View File

@@ -1,5 +1,6 @@
#include "wrap-string.hpp"
#include "wrap-vector.hpp"
#include "fast-float.hpp"
#include <algorithm>
@@ -11,6 +12,8 @@
#include <cassert>
#include <cstdlib>
#include <cmath>
#include <charconv>
#ifdef WIN32
#endif
@@ -22,12 +25,24 @@
namespace util {
static bool ascii_isalpha(char c) {
return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z'));
eng::string ascii_tolower(const eng::string &s) {
eng::string mod = s;
for (int i = 0; i < int(mod.size()); i++) {
if (ascii_isupper(mod[i])) {
mod[i] += 'a' - 'A';
}
}
return mod;
}
static bool ascii_isdigit(char c) {
return ((c >= '0') && (c <= '9'));
eng::string ascii_toupper(const eng::string &s) {
eng::string mod = s;
for (int i = 0; i < int(mod.size()); i++) {
if (ascii_islower(mod[i])) {
mod[i] += 'A' - 'a';
}
}
return mod;
}
bool is_identifier(const eng::string &str) {
@@ -271,33 +286,40 @@ bool has_suffix(const eng::string &s, const eng::string &suffix) {
}
}
bool validinteger(const eng::string &value) {
char *endptr;
if (value.size() == 0) return false;
strtoll(value.c_str(), &endptr, 10);
return (endptr == value.c_str() + value.size());
bool valid_int64(std::string_view value) {
int64_t result;
const char *last = value.data() + value.size();
auto r = std::from_chars(value.data(), last, result, 10);
if (r.ec != std::errc()) return false;
if (r.ptr != last) return false;
return true;
}
int64_t strtoint(const eng::string &value, int64_t errval) {
char *endptr;
if (value.size() == 0) return errval;
int64_t result = strtoll(value.c_str(), &endptr, 10);
if (endptr == value.c_str() + value.size()) {
int64_t str_to_int64(std::string_view value, int64_t errval) {
int64_t result;
const char *last = value.data() + value.size();
auto r = std::from_chars(value.data(), last, result, 10);
if (r.ec != std::errc()) return errval;
if (r.ptr != last) return errval;
return result;
} else {
return errval;
}
}
double strtodouble(const eng::string &value) {
char *endptr;
if (value.size() == 0) return std::nan("");
double result = strtod(value.c_str(), &endptr);
if (endptr == value.c_str() + value.size()) {
return result;
} else {
return std::nan("");
bool valid_double(std::string_view value) {
double result;
const char *last = value.data() + value.size();
auto r = fast_float::from_chars(value.data(), last, result);
if (r.ec != std::errc()) return false;
if (r.ptr != last) return false;
return true;
}
double str_to_double(std::string_view value, double errval) {
double result;
const char *last = value.data() + value.size();
auto r = fast_float::from_chars(value.data(), last, result);
if (r.ec != std::errc()) return errval;
if (r.ptr != last) return errval;
return result;
}
std::string_view sv_ltrim(std::string_view v) {
@@ -330,6 +352,30 @@ std::string_view sv_trim(std::string_view v) {
return std::string_view(b, e-b);
}
std::string_view sv_ltrim(std::string_view v, char c) {
while ((!v.empty()) && (v.front() == c)) {
v.remove_prefix(1);
}
return v;
}
std::string_view sv_rtrim(std::string_view v, char c) {
while ((!v.empty()) && (v.back() == c)) {
v.remove_suffix(1);
}
return v;
}
std::string_view sv_trim(std::string_view v, char c) {
while ((!v.empty()) && (v.front() == c)) {
v.remove_prefix(1);
}
while ((!v.empty()) && (v.back() == c)) {
v.remove_suffix(1);
}
return v;
}
eng::string ltrim(std::string_view v) {
return eng::string(sv_ltrim(v));
}
@@ -342,24 +388,34 @@ eng::string trim(std::string_view v) {
return eng::string(sv_trim(v));
}
std::string_view sv_split_one(std::string_view &v, char sep) {
size_t pos = v.find(sep);
// If there's no separator in the buffer, return a null view.
if (pos == std::string_view::npos) {
return std::string_view();
}
// Split into stuff before the separator, and stuff after.
std::string_view result = v.substr(0, pos);
v = v.substr(pos + 1);
return result;
}
std::string_view sv_read_line(std::string_view &source) {
size_t pos = source.find('\n');
std::string_view result;
if (pos == std::string_view::npos) {
result = source;
source = "";
source = std::string_view();
} else {
result = source.substr(0, pos);
source = source.substr(pos + 1);
}
int fsize = result.size();
if ((fsize >= 1) && (result[fsize - 1] == '\r')) {
result.remove_suffix(1);
}
result = sv_rtrim(result, '\r');
return result;
}
double distance_squared(double x1, double y1, double x2, double y2) {
double dx = x1 - x2;
double dy = y1 - y2;
@@ -456,18 +512,14 @@ LuaDefine(unittests_util, "", "some unit tests") {
LuaAssert(L, util::toupper("fooBar") == "FOOBAR");
LuaAssert(L, util::tolower("fooBar") == "foobar");
// test validinteger, strtoint, strtodouble
LuaAssert(L, util::validinteger("123") == true);
LuaAssert(L, util::validinteger("123.4") == false);
LuaAssert(L, util::validinteger("12ab") == false);
LuaAssert(L, util::validinteger("") == false);
LuaAssert(L, util::strtoint("123", -5) == 123);
LuaAssert(L, util::strtoint("123.4", -5) == -5);
LuaAssert(L, util::strtoint("12ab", -5) == -5);
LuaAssert(L, util::strtoint("", -5) == -5);
LuaAssert(L, util::strtodouble("123.5") == 123.5);
LuaAssert(L, std::isnan(util::strtodouble("12ab")));
LuaAssert(L, std::isnan(util::strtodouble("")));
// test str_to_int64, str_to_double
LuaAssert(L, util::str_to_int64("123") == 123);
LuaAssert(L, util::str_to_int64("123.4") == INT64_MIN);
LuaAssert(L, util::str_to_int64("12ab") == INT64_MIN);
LuaAssert(L, util::str_to_int64("") == INT64_MIN);
LuaAssert(L, util::str_to_double("123.5") == 123.5);
LuaAssert(L, std::isnan(util::str_to_double("12ab")));
LuaAssert(L, std::isnan(util::str_to_double("")));
// Test trim, ltrim, rtrim
LuaAssert(L, util::ltrim(" foo ") == "foo ");

View File

@@ -38,6 +38,19 @@ using LuaSourceVec = eng::vector<StringPair>;
using LuaSourcePtr = std::unique_ptr<LuaSourceVec>;
using HashValue = std::pair<uint64_t, uint64_t>;
using IdVector = eng::vector<int64_t>;
using OptionalInt64 = std::optional<int64_t>;
// This value is sometimes used to represent 'bad integer'
const int64_t BADINT64 = std::numeric_limits<int64_t>::min();
// Test character class ignoring 'current locale'.
inline bool ascii_isupper(char c) { return (c >= 'A') && (c <= 'Z'); }
inline bool ascii_islower(char c) { return (c >= 'a') && (c <= 'z'); }
inline bool ascii_isdigit(char c) { return (c >= '0') && (c <= '9'); }
inline bool ascii_isalpha(char c) { return ascii_isupper(c) || ascii_islower(c); }
inline bool ascii_isalnum(char c) { return ascii_isalpha(c) || ascii_isdigit(c); }
eng::string ascii_tolower(const eng::string &c);
eng::string ascii_toupper(const eng::string &c);
// Return seconds elapsed, for profiling purposes.
double profiling_clock();
@@ -99,28 +112,39 @@ eng::string toupper(eng::string input);
bool has_prefix(const eng::string &s, const eng::string &prefix);
bool has_suffix(const eng::string &s, const eng::string &suffix);
// Return true if the string can be parsed as an integer.
bool validinteger(const eng::string &value);
// Check if numbers can be parsed as int64/double
bool valid_int64(std::string_view v);
bool valid_double(std::string_view v);
// String to integer. Returns errval if the number is not parseable.
int64_t strtoint(const eng::string &value, int64_t errval);
// String to double. Returns NAN if the number is not parseable.
double strtodouble(const eng::string &value);
// Parse numbers as int64/double. Returns errval on failure.
int64_t str_to_int64(std::string_view v, int64_t errval = INT64_MIN);
double str_to_double(std::string_view v, double errval = std::numeric_limits<double>::quiet_NaN());
// Trim a string_view
std::string_view sv_ltrim(std::string_view v);
std::string_view sv_rtrim(std::string_view v);
std::string_view sv_trim(std::string_view v);
std::string_view sv_ltrim(std::string_view v, char c);
std::string_view sv_rtrim(std::string_view v, char c);
std::string_view sv_trim(std::string_view v, char c);
// sv_is_null is different from checking for empty.
inline bool sv_is_null(const std::string_view &v) { return v.data() == nullptr; }
// Split a string view into stuff before and after separator.
// If the separator doesn't occur, returns a null string view
// and doesn't modify the source.
std::string_view sv_split_one(std::string_view &source, char sep);
// Read a line from a string_view.
std::string_view sv_read_line(std::string_view &source);
// Trim strings: left end, right end, both ends.
eng::string ltrim(std::string_view s);
eng::string rtrim(std::string_view s);
eng::string trim(std::string_view s);
// Read a line from a string_view
std::string_view sv_read_line(std::string_view &source);
// Calculate distance between two points
double distance_squared(double x1, double y1, double x2, double y2);

View File

@@ -434,8 +434,8 @@ void World::invoke_flush_prints(int64_t actor_id, int64_t place_id, const eng::s
if (actor_id != place_id) {
return;
}
int line = util::strtoint(action, -1);
if (line < 0) {
int64_t line = util::str_to_int64(action, -1);
if ((line < 0)||(line > INT_MAX)) {
return;
}
Tangible *tactor = tangible_get(actor_id);