From d3ca269c79aec18a8c6d938b13af5896ed978136 Mon Sep 17 00:00:00 2001
From: Lars Mueller <appgurulars@gmx.de>
Date: Tue, 13 Aug 2024 02:50:36 +0200
Subject: [PATCH] Add `minetest.is_valid_player_name` utility

---
 doc/lua_api.md                  |  8 +++++---
 src/main.cpp                    |  9 +++------
 src/player.cpp                  |  4 ++++
 src/player.h                    |  3 +++
 src/script/lua_api/l_object.cpp |  8 ++------
 src/script/lua_api/l_util.cpp   | 12 ++++++++++++
 src/script/lua_api/l_util.h     |  3 +++
 7 files changed, 32 insertions(+), 15 deletions(-)

diff --git a/doc/lua_api.md b/doc/lua_api.md
index 5713e8696..389ea73f2 100644
--- a/doc/lua_api.md
+++ b/doc/lua_api.md
@@ -7097,6 +7097,8 @@ Misc.
 * `minetest.is_player(obj)`: boolean, whether `obj` is a player
 * `minetest.player_exists(name)`: boolean, whether player exists
   (regardless of online status)
+* `minetest.is_valid_player_name(name)`: boolean, whether the given name
+  could be used as a player name (regardless of whether said player exists).
 * `minetest.hud_replace_builtin(name, hud_definition)`
     * Replaces definition of a builtin hud element
     * `name`: `"breath"`, `"health"` or `"minimap"`
@@ -7989,9 +7991,9 @@ child will follow movement and rotation of that bone.
 * `set_observers(observers)`: sets observers (players this object is sent to)
     * If `observers` is `nil`, the object's observers are "unmanaged":
       The object is sent to all players as governed by server settings. This is the default.
-    * `observers` is a "set" of player names: `{[player name] = true, [other player name] = true, ...}`
-        * A set is a table where the keys are the elements of the set (in this case, player names)
-          and the values are all `true`.
+    * `observers` is a "set" of player names: `{name1 = true, name2 = true, ...}`
+        * A set is a table where the keys are the elements of the set
+          (in this case, *valid* player names) and the values are all `true`.
     * Attachments: The *effective observers* of an object are made up of
       all players who can observe the object *and* are also effective observers
       of its parent object (if there is one).
diff --git a/src/main.cpp b/src/main.cpp
index 7474ae84c..9f358bb66 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1095,13 +1095,9 @@ static bool run_dedicated_server(const GameParams &game_params, const Settings &
 
 	if (cmd_args.exists("terminal")) {
 #if USE_CURSES
-		bool name_ok = true;
 		std::string admin_nick = g_settings->get("name");
 
-		name_ok = name_ok && !admin_nick.empty();
-		name_ok = name_ok && string_allowed(admin_nick, PLAYERNAME_ALLOWED_CHARS);
-
-		if (!name_ok) {
+		if (!is_valid_player_name(admin_nick)) {
 			if (admin_nick.empty()) {
 				errorstream << "No name given for admin. "
 					<< "Please check your minetest.conf that it "
@@ -1110,7 +1106,8 @@ static bool run_dedicated_server(const GameParams &game_params, const Settings &
 			} else {
 				errorstream << "Name for admin '"
 					<< admin_nick << "' is not valid. "
-					<< "Please check that it only contains allowed characters. "
+					<< "Please check that it only contains allowed characters "
+					<< "and that it is at most 20 characters long. "
 					<< "Valid characters are: " << PLAYERNAME_ALLOWED_CHARS_USER_EXPL
 					<< std::endl;
 			}
diff --git a/src/player.cpp b/src/player.cpp
index e55de5937..fd902aa83 100644
--- a/src/player.cpp
+++ b/src/player.cpp
@@ -30,6 +30,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "porting.h"  // strlcpy
 
 
+bool is_valid_player_name(std::string_view name) {
+	return !name.empty() && name.size() <= PLAYERNAME_SIZE && string_allowed(name, PLAYERNAME_ALLOWED_CHARS);
+}
+
 Player::Player(const std::string &name, IItemDefManager *idef):
 	inventory(idef)
 {
diff --git a/src/player.h b/src/player.h
index 9991dd774..dd2be4986 100644
--- a/src/player.h
+++ b/src/player.h
@@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "constants.h"
 #include "network/networkprotocol.h"
 #include "util/basic_macros.h"
+#include "util/string.h"
 #include <list>
 #include <mutex>
 #include <functional>
@@ -35,6 +36,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #define PLAYERNAME_ALLOWED_CHARS "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
 #define PLAYERNAME_ALLOWED_CHARS_USER_EXPL "'a' to 'z', 'A' to 'Z', '0' to '9', '-', '_'"
 
+bool is_valid_player_name(std::string_view name);
+
 struct PlayerFovSpec
 {
 	f32 fov;
diff --git a/src/script/lua_api/l_object.cpp b/src/script/lua_api/l_object.cpp
index 89974fb0e..00c825ddc 100644
--- a/src/script/lua_api/l_object.cpp
+++ b/src/script/lua_api/l_object.cpp
@@ -859,12 +859,8 @@ int ObjectRef::l_set_observers(lua_State *L)
 	lua_pushnil(L);
 	while (lua_next(L, 2) != 0) {
 		std::string name = readParam<std::string>(L, -2);
-		if (name.empty())
-			throw LuaError("Observer name is empty");
-		if (name.size() > PLAYERNAME_SIZE)
-			throw LuaError("Observer name is too long");
-		if (!string_allowed(name, PLAYERNAME_ALLOWED_CHARS))
-			throw LuaError("Observer name contains invalid characters");
+		if (!is_valid_player_name(name))
+			throw LuaError("Observer name is not a valid player name");
 		if (!lua_toboolean(L, -1)) // falsy value?
 			throw LuaError("Values in the `observers` table need to be true");
 		observer_names.insert(std::move(name));
diff --git a/src/script/lua_api/l_util.cpp b/src/script/lua_api/l_util.cpp
index 883b9480c..fac3e54d1 100644
--- a/src/script/lua_api/l_util.cpp
+++ b/src/script/lua_api/l_util.cpp
@@ -44,6 +44,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "util/sha1.h"
 #include "my_sha256.h"
 #include "util/png.h"
+#include "player.h"
 #include <cstdio>
 
 // only available in zstd 1.3.5+
@@ -674,6 +675,16 @@ int ModApiUtil::l_urlencode(lua_State *L)
 	return 1;
 }
 
+// is_valid_player_name(name)
+int ModApiUtil::l_is_valid_player_name(lua_State *L)
+{
+	NO_MAP_LOCK_REQUIRED;
+
+	auto s = readParam<std::string_view>(L, 1);
+	lua_pushboolean(L, is_valid_player_name(s));
+	return 1;
+}
+
 void ModApiUtil::Initialize(lua_State *L, int top)
 {
 	API_FCT(log);
@@ -722,6 +733,7 @@ void ModApiUtil::Initialize(lua_State *L, int top)
 	API_FCT(set_last_run_mod);
 
 	API_FCT(urlencode);
+	API_FCT(is_valid_player_name);
 
 	LuaSettings::create(L, g_settings, g_settings_path);
 	lua_setfield(L, top, "settings");
diff --git a/src/script/lua_api/l_util.h b/src/script/lua_api/l_util.h
index 056e09090..e0daf3e79 100644
--- a/src/script/lua_api/l_util.h
+++ b/src/script/lua_api/l_util.h
@@ -134,6 +134,9 @@ private:
 	// urlencode(value)
 	static int l_urlencode(lua_State *L);
 
+	// is_valid_player_name(name)
+	static int l_is_valid_player_name(lua_State *L);
+
 public:
 	static void Initialize(lua_State *L, int top);
 	static void InitializeAsync(lua_State *L, int top);