Fix exported player data folder validation

This commit is contained in:
Alexander Sellite 2026-07-30 22:17:00 -04:00
parent c0860fac03
commit b3880c9e2b
8 changed files with 219 additions and 18 deletions

View file

@ -9,7 +9,7 @@ custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter="builds/*,playtest/*,scripts/*"
export_path="builds/v0.3.0-prealpha.2/windows-x86_64/NETfishing.exe"
export_path="builds/v0.3.0-prealpha.3/windows-x86_64/NETfishing.exe"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
@ -35,11 +35,11 @@ application/modify_resources=true
application/icon=""
application/console_wrapper_icon=""
application/icon_interpolation=4
application/file_version="0.3.0.2"
application/product_version="0.3.0.2"
application/file_version="0.3.0.3"
application/product_version="0.3.0.3"
application/company_name=""
application/product_name="NETfishing"
application/file_description="NETfishing v0.3.0-prealpha.2"
application/file_description="NETfishing v0.3.0-prealpha.3"
application/copyright=""
application/trademarks=""
application/export_angle=0
@ -62,7 +62,7 @@ custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter="builds/*,playtest/*,scripts/*"
export_path="builds/v0.3.0-prealpha.2/linux-x86_64/NETfishing.x86_64"
export_path="builds/v0.3.0-prealpha.3/linux-x86_64/NETfishing.x86_64"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""

View file

@ -14,6 +14,9 @@ const README_FILENAME := "README.txt"
const ENVIRONMENT_VARIABLE := "NETFISHING_DATA_DIR"
const APPLICATION_ID := "netfishing"
const APP_DATA_PORTABLE_PATH := "user://portable-data"
const PATH_ALLOWED: StringName = &"allowed"
const PATH_SOURCE_PROJECT: StringName = &"source_project"
const PATH_INSTALLATION: StringName = &"installation"
enum Mode {
UNRESOLVED,
@ -226,15 +229,77 @@ func _activate_existing(path: String, expected_id: String, permit_creation: bool
func _validate_candidate(path: String, create: bool) -> bool:
if path.is_empty() or not path.is_absolute_path():
var normalized: String = _normalize(path)
if normalized.is_empty() or not normalized.is_absolute_path():
return _fail("Choose an absolute filesystem folder.")
var project: String = ProjectSettings.globalize_path("res://").trim_suffix("/")
if path == project or path.begins_with(project + "/"):
var classification: StringName = classify_candidate_path(
normalized,
ProjectSettings.globalize_path("res://"),
OS.get_executable_path().get_base_dir(),
)
if classification == PATH_SOURCE_PROJECT:
return _fail("The project folder cannot be used as the player data folder.")
if not DirAccess.dir_exists_absolute(path):
if not create or DirAccess.make_dir_recursive_absolute(path) != OK:
if classification == PATH_INSTALLATION:
return _fail(
"The application installation folder cannot be used as the player data folder."
)
if not DirAccess.dir_exists_absolute(normalized):
if not create or DirAccess.make_dir_recursive_absolute(normalized) != OK:
return _fail("The selected data folder is unavailable.")
return _test_writable(path)
return _test_writable(normalized)
static func classify_candidate_path(
candidate_path: String,
project_reference: String,
installation_reference: String,
case_insensitive: bool = OS.get_name() == "Windows",
) -> StringName:
var candidate: String = _normalize_comparison_path(
candidate_path, case_insensitive
)
if candidate.is_empty() or not candidate.is_absolute_path():
return PATH_ALLOWED
var project: String = _normalize_reference_path(
project_reference, case_insensitive
)
if not project.is_empty() and _is_same_or_child_path(candidate, project):
return PATH_SOURCE_PROJECT
var installation: String = _normalize_reference_path(
installation_reference, case_insensitive
)
if (
not installation.is_empty()
and _is_same_or_child_path(candidate, installation)
):
return PATH_INSTALLATION
return PATH_ALLOWED
static func _normalize_reference_path(path: String, case_insensitive: bool) -> String:
var normalized: String = _normalize_comparison_path(path, case_insensitive)
if normalized.is_empty() or not normalized.is_absolute_path():
return ""
if normalized == "/" or _is_windows_drive_root(normalized):
return ""
return normalized
static func _normalize_comparison_path(path: String, case_insensitive: bool) -> String:
var normalized: String = path.strip_edges().replace("\\", "/").simplify_path()
while normalized.length() > 1 and normalized.ends_with("/"):
if _is_windows_drive_root(normalized):
break
normalized = normalized.left(-1)
return normalized.to_lower() if case_insensitive else normalized
static func _is_windows_drive_root(path: String) -> bool:
return path.length() == 3 and path[1] == ":" and path[2] == "/"
static func _is_same_or_child_path(candidate: String, reference: String) -> bool:
return candidate == reference or candidate.begins_with(reference + "/")
func _test_writable(path: String) -> bool:
@ -340,7 +405,12 @@ func _directory_is_empty(path: String) -> bool:
func _normalize(path: String) -> String:
return path.simplify_path().trim_suffix("/")
var normalized: String = path.replace("\\", "/").simplify_path()
while normalized.length() > 1 and normalized.ends_with("/"):
if _is_windows_drive_root(normalized):
break
normalized = normalized.left(-1)
return normalized
func _read_json(path: String, maximum := 1024 * 1024) -> Dictionary:

View file

@ -1,5 +1,5 @@
NETfishing
v0.3.0-prealpha.2
v0.3.0-prealpha.3
Pre-Alpha 0.3
Thank you for trying this early private playtest.

View file

@ -11,7 +11,7 @@ config_version=5
[application]
config/name="NETFISHING"
config/version="0.3.0-prealpha.2"
config/version="0.3.0-prealpha.3"
run/main_scene="res://main/main.tscn"
config/features=PackedStringArray("4.7", "GL Compatibility")
config/icon="res://icon.svg"

View file

@ -4,12 +4,12 @@ set -euo pipefail
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
readonly PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
readonly BUILD_ROOT="${PROJECT_ROOT}/builds/v0.3.0-prealpha.2"
readonly BUILD_ROOT="${PROJECT_ROOT}/builds/v0.3.0-prealpha.3"
readonly WINDOWS_DIR="${BUILD_ROOT}/windows-x86_64"
readonly LINUX_DIR="${BUILD_ROOT}/linux-x86_64"
readonly README_SOURCE="${PROJECT_ROOT}/playtest/README-PLAYTEST.txt"
readonly WINDOWS_ZIP="${BUILD_ROOT}/NETfishing-v0.3.0-prealpha.2-windows-x86_64.zip"
readonly LINUX_ZIP="${BUILD_ROOT}/NETfishing-v0.3.0-prealpha.2-linux-x86_64.zip"
readonly WINDOWS_ZIP="${BUILD_ROOT}/NETfishing-v0.3.0-prealpha.3-windows-x86_64.zip"
readonly LINUX_ZIP="${BUILD_ROOT}/NETfishing-v0.3.0-prealpha.3-linux-x86_64.zip"
readonly GODOT_BIN="${GODOT_BIN:-godot}"
if [[ ! -f "${PROJECT_ROOT}/project.godot" ]]; then

View file

@ -0,0 +1,130 @@
extends SceneTree
const PROJECT_PATH := "/tmp/netfishing"
const INSTALL_PATH := "/opt/NETfishing"
func _initialize() -> void:
var root_to_create: String = OS.get_environment("NETFISHING_TEST_CREATE_ROOT")
if not root_to_create.is_empty():
_create_validation_root(root_to_create)
return
var failures: PackedStringArray = []
_expect(
"external temporary directory",
"/tmp/NETfishing-test-data",
PlayerDataRoot.PATH_ALLOWED,
failures,
)
_expect(
"external trailing slash",
"/tmp/NETfishing-test-data/",
PlayerDataRoot.PATH_ALLOWED,
failures,
)
_expect(
"similar project-name sibling",
"/tmp/netfishing-data",
PlayerDataRoot.PATH_ALLOWED,
failures,
)
_expect(
"documents directory",
"/home/player/Documents/NETfishing",
PlayerDataRoot.PATH_ALLOWED,
failures,
)
_expect("source project", PROJECT_PATH, PlayerDataRoot.PATH_SOURCE_PROJECT, failures)
_expect(
"source project child",
PROJECT_PATH.path_join("player-data"),
PlayerDataRoot.PATH_SOURCE_PROJECT,
failures,
)
_expect(
"installation directory",
INSTALL_PATH,
PlayerDataRoot.PATH_INSTALLATION,
failures,
)
_expect(
"installation child",
INSTALL_PATH.path_join("portable-data"),
PlayerDataRoot.PATH_INSTALLATION,
failures,
)
_expect_with_references(
"empty project reference",
"/tmp/NETfishing-test-data",
"",
INSTALL_PATH,
PlayerDataRoot.PATH_ALLOWED,
failures,
)
_expect_with_references(
"root project reference",
"/tmp/NETfishing-test-data",
"/",
INSTALL_PATH,
PlayerDataRoot.PATH_ALLOWED,
failures,
)
_expect_with_references(
"Windows component and case normalization",
"C:\\Games\\NETfishing Data",
"C:\\Games\\NETfishing",
"C:\\Program Files\\NETfishing",
PlayerDataRoot.PATH_ALLOWED,
failures,
true,
)
if failures.is_empty():
print("PlayerDataRoot path validation: PASS")
quit(0)
return
for failure: String in failures:
push_error(failure)
quit(1)
func _create_validation_root(path: String) -> void:
var data_root := PlayerDataRoot.new()
get_root().add_child(data_root)
if data_root.select_new_root(path):
print("PlayerDataRoot validation root created: ", data_root.root_path)
quit(0)
return
push_error(data_root.error_message)
quit(1)
func _expect(
label: String,
candidate: String,
expected: StringName,
failures: PackedStringArray,
) -> void:
_expect_with_references(
label,
candidate,
PROJECT_PATH,
INSTALL_PATH,
expected,
failures,
)
func _expect_with_references(
label: String,
candidate: String,
project: String,
installation: String,
expected: StringName,
failures: PackedStringArray,
case_insensitive: bool = false,
) -> void:
var actual: StringName = PlayerDataRoot.classify_candidate_path(
candidate, project, installation, case_insensitive
)
if actual != expected:
failures.append("%s: expected %s, got %s" % [label, expected, actual])

View file

@ -0,0 +1 @@
uid://e8glhlm205au

View file

@ -206,7 +206,7 @@ unique_name_in_owner = true
layout_mode = 2
theme_override_colors/font_color = Color(0.682, 0.733, 0.761, 1)
theme_override_font_sizes/font_size = 22
text = "v0.3.0-prealpha.2"
text = "v0.3.0-prealpha.3"
horizontal_alignment = 1
[node name="Spacer" type="Control" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent"]