Repository: godotengine/godot (language: cpp)

Issue:
Validate `Custom User Dir Name`, disallow absolute `user://` and `res://` paths
### Tested versions

4.4.beta4 but was already present in the previous versions 

### System information

Apple Macbook M3 Pro - MacOS Sequoia 15.3.1

### Issue description

When using Forward+ renderer on macosx error log is filled with
```cpp
E 0:00:00:099   _save_to_cache: Condition "f.is_null()" is true.
  <C++ Source>  servers/rendering/renderer_rd/shader_rd.cpp:512 @ _save_to_cache()
```

**EDIT:** with this settings (`user://`)
![Image](https://github.com/user-attachments/assets/f7f5fe5d-0113-489d-9b02-23882d3ccec9)


![Image](https://github.com/user-attachments/assets/ef3c862d-d66e-43e1-9e4e-e6a8f3351764)

The errors disappear with renderer "Compatibility"

I tried deleting the shader_cache directory inside project and in global library user data, they do fill again both

I checked all shaders of my project, no error or warning in shader editor

I don't know how to debut the path that is accessed in the editor code:  

![Image](https://github.com/user-attachments/assets/69a6ba71-ce3d-41cc-a0c5-1a6fe84b7c5e)

### Steps to reproduce

I don't know if this is linked to my project or a common issue on the development platform

### Minimal reproduction project (MRP)

I can't provide a minimal project


Discussion / hints:
> The errors disappear with renderer "Compatibility"

--> https://github.com/godotengine/godot/issues/87079

What are your project settings for `Shader Cache`? 

Oups, I forgot to mention that the error **appears** when I **enable** shader cache
Can you also reproduce this with version `4.4-rc2`? 
https://godotengine.org/download/archive/

On `MacMini M1 - 15.3.1`, I couldn't reproduce it (simple 2d project).

Please try to create a `Minimal reproduction project (MRP)`, make a copy and step by step first hide and then remove the nodes. _(also remove unnecessary assets, e.g. sound files)_
Then remove the hidden `.godot` folder, not necessary, zip the project and upload it here. 

Note: press `Command + Shift + "."` to show hidden files in Finder.




I tried with lates rc3 version, error is still displayed. 
I should be linked to my project because if I open other projects, error is not present with shader cache enabled
I tried to create a minimal project, but I'va been working on this one for 3 years, it's very complicated to remove elements little by little.
I removed everything from the project, it did not change anything
BUT, when removing these settings I added for exported macos project, error is gone:

![Image](https://github.com/user-attachments/assets/f7f5fe5d-0113-489d-9b02-23882d3ccec9)
I was able to reproduce it too. _(Metal and Vulkan)_

![Image](https://github.com/user-attachments/assets/74d71670-36be-4615-9dd3-a1d3103e0aca)



```gdscript
func _ready() -> void:
	print("_ready")
	print("get_user_data_dir: ", OS.get_user_data_dir())
```


**output:**
```
_ready
get_user_data_dir: /Users/alex/Library/Application Support/user-//test/

```
@oxeron try without `user://`

![Image](https://github.com/user-attachments/assets/c251ae43-2c86-4c63-9269-4ee00fa0a02d)

```gdscript
print("get_user_data_dir: ", OS.get_user_data_dir())
```

**output**
`/Users/alex/Library/Application Support/test`

It works without user:// but I have two questions:
- why is this settings used on my dev platform, I thought it was only for exported version using macos flag
- will my exported macos version still work on player's hardware ?

What else have you configured so that `.macos` is output there?

![Image](https://github.com/user-attachments/assets/23ba1b55-994a-4f34-869f-ccfbc254008f)

> BUT, when removing these settings I added for exported macos project, error is gone:
> I thought it was only for exported version using macos flag

And is it in the "Project Settings" dialog? Because I didn't find it in the "Export (MacOS)" dialog.
_I'm not yet familiar with MacOS export._
This is my complete Project settings for Application/Config :
![Image](https://github.com/user-attachments/assets/fe501944-bf2e-4d59-bae1-f2c7b4111963)
These settings aren't valid. "Custom User Dir Name" is what defines what the `user://` folder should be. So it can't use `user://` to define itself. Likewise it can't use `res://`, this path is the pck file and read only.

We should validate those fields better.
@oxeron Did you create the configurations yourself? 

`Version Pre` and everything with `.web / .linux / .windows / .macos` at the end or do you use addons for that? I don't have these configurations. That's why I think we can only validate the official configurations, and not the configurations inserted by addons.


> It works without user:// but I have two questions:
> 
> * why is this settings used on my dev platform, I thought it was only for exported version using macos flag
> * will my exported macos version still work on player's hardware ?



![Image](https://github.com/user-attachments/assets/8b85c877-a7d0-4e7f-9f33-acbf8b934096)


Maybe tag `good first issue`.

I couldn't find any validations in the "Project Settings" dialog. I think everything will only be validated when the project is started.

-------------

`EditorNode::get_singleton()->ensure_main_scene`
Search for these lines as an example. This function validates the configuration `application/run/main_scene` whether the path is valid.





> Maybe tag `good first issue`.
> 
> I couldn't find any validations in the "Project Settings" dialog. I think everything will only be validated when the project is started.
> 
> `EditorNode::get_singleton()->ensure_main_scene` Search for these lines as an example. This function validates the configuration `application/run/main_scene` whether the path is valid.

@Alex2782  I would like to take a look this issue, before that I want to confirm a bit more since I am new to this repo 
So now the goal is:
validating the **user_data_dir** path in **ensure_main_scene** method ? 


Use the function `ensure_main_scene` only as an example how and where the project settings are checked.
My suggestion would be a new validation function. For example `validate_custom_directory`.


1. if [use_custom_user_dir](https://docs.godotengine.org/en/stable/classes/class_projectsettings.html#class-projectsettings-property-application-config-use-custom-user-dir) == true, then check [custom_user_dir_name](https://docs.godotengine.org/en/stable/classes/class_projectsettings.html#class-projectsettings-property-application-config-custom-user-dir-name)
2. if `custom_user_dir_name.left(7) == "user://"` or `custom_user_dir_name.left(6) == "res://"`, then display an error message that this is not allowed.

But whether this solution is acceptable will probably be decided by others, who will certainly give you further assistance.


![Image](https://github.com/user-attachments/assets/aca648fc-5087-4724-80c3-488eb89b841a)
I can't reproduce the bug on Windows, although this is likely due to DirAccess platform differences. When I put `user://` into the custom dir name, it gets sanitized into `user-//`, which creates a valid path.

EDIT:
Actually, the folder exists and I can navigate to it, but I still get the shader cache error. Looks like shader cache is doing something differently.
https://github.com/godotengine/godot/issues/103367#issuecomment-2692953054
`.web / .linux / .windows / .macos`


@KoBeWi Do you know if these settings are officially supported by the engine?
After a quick search I couldn't find anything in the sources.

------------------

`application/config/custom_user_dir_name`

When users enter `user://` there, they probably expect the path to be absolute. But that doesn't work (on MacOS).

```gdscript
	ProjectSettings.set_setting("application/config/use_custom_user_dir", true)
	
	ProjectSettings.set_setting("application/config/custom_user_dir_name", "test")
	print("1) get_user_data_dir: ", OS.get_user_data_dir())

	ProjectSettings.set_setting("application/config/custom_user_dir_name", "test/test2")
	print("2) get_user_data_dir: ", OS.get_user_data_dir())

	ProjectSettings.set_setting("application/config/custom_user_dir_name", "user://test")
	print("3) get_user_data_dir: ", OS.get_user_data_dir())
```

**output:**
```
1) get_user_data_dir: /Users/alex/Library/Application Support/test
2) get_user_data_dir: /Users/alex/Library/Application Support/test/test2
3) get_user_data_dir: /Users/alex/Library/Application Support/user-//test
```

------------------

`//` is an invalid path to write

https://github.com/godotengine/godot/blob/4c311cbee68c0b66ff8ebb8b0defdd9979dd2a41/servers/rendering/renderer_rd/shader_rd.cpp#L508-L512

![Image](https://github.com/user-attachments/assets/bdcc5766-18bb-4a8e-8fe8-88a09071872f)



> Do you know if these settings are officially supported by the engine?
After a quick search I couldn't find anything in the sources.

They are overrides. You can create them using this list:

![Image](https://github.com/user-attachments/assets/5bc0ea75-ed27-4ddf-afe8-372eb5561392)

The engine will pick a value based on active feature tags, so you can customize different values based on platform, whether running a debug build etc.

Fix patch:
diff --git a/core/os/os.cpp b/core/os/os.cpp
index 385d1f44801b..e2b6fb72fb08 100644
--- a/core/os/os.cpp
+++ b/core/os/os.cpp
@@ -254,7 +254,7 @@ String OS::get_safe_dir_name(const String &p_dir_name, bool p_allow_paths) const
 	if (p_allow_paths) {
 		// Dir separators are allowed, but disallow ".." to avoid going up the filesystem
 		invalid_chars.push_back("..");
-		safe_dir_name = safe_dir_name.replace("\\", "/").strip_edges();
+		safe_dir_name = safe_dir_name.replace("\\", "/").replace("//", "/").strip_edges();
 	} else {
 		invalid_chars.push_back("/");
 		invalid_chars.push_back("\\");
diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp
index 6e2789f559a0..ba16a6c1fb54 100644
--- a/editor/editor_node.cpp
+++ b/editor/editor_node.cpp
@@ -5702,6 +5702,30 @@ bool EditorNode::ensure_main_scene(bool p_from_native) {
 		return false;
 	}
 
+	if (!EditorNode::validate_custom_directory()) {
+		current_menu_option = -1;
+		return false;
+	}
+
+	return true;
+}
+
+bool EditorNode::validate_custom_directory() {
+	bool use_custom_dir = GLOBAL_GET("application/config/use_custom_user_dir");
+
+	if (use_custom_dir) {
+		String data_dir = OS::get_singleton()->get_user_data_dir();
+		Ref<DirAccess> dir = DirAccess::create(DirAccess::ACCESS_USERDATA);
+		if (dir->change_dir(data_dir) != OK) {
+			dir->make_dir_recursive(data_dir);
+			if (dir->change_dir(data_dir) != OK) {
+				open_project_settings->set_text(vformat(TTR("User data dir '%s' is not valid. Change to a valid one?"), data_dir));
+				open_project_settings->popup_centered();
+				return false;
+			}
+		}
+	}
+
 	return true;
 }
 
@@ -8262,6 +8286,11 @@ EditorNode::EditorNode() {
 	select_current_scene_button = pick_main_scene->add_button(TTR("Select Current"), true, "select_current");
 	pick_main_scene->connect("custom_action", callable_mp(this, &EditorNode::_pick_main_scene_custom_action));
 
+	open_project_settings = memnew(ConfirmationDialog);
+	gui_base->add_child(open_project_settings);
+	open_project_settings->set_ok_button_text(TTRC("Open Project Settings"));
+	open_project_settings->connect(SceneStringName(confirmed), callable_mp(this, &EditorNode::_menu_option).bind(PROJECT_OPEN_SETTINGS));
+
 	for (int i = 0; i < _init_callbacks.size(); i++) {
 		_init_callbacks[i]();
 	}
diff --git a/editor/editor_node.h b/editor/editor_node.h
index 4c9a51eef8c9..fd9d01f4d9b8 100644
--- a/editor/editor_node.h
+++ b/editor/editor_node.h
@@ -356,6 +356,7 @@ class EditorNode : public Node {
 	ConfirmationDialog *save_confirmation = nullptr;
 	ConfirmationDialog *import_confirmation = nullptr;
 	ConfirmationDialog *pick_main_scene = nullptr;
+	ConfirmationDialog *open_project_settings = nullptr;
 	Button *select_current_scene_button = nullptr;
 	AcceptDialog *accept = nullptr;
 	AcceptDialog *save_accept = nullptr;
@@ -962,6 +963,7 @@ class EditorNode : public Node {
 	Vector<Ref<EditorResourceConversionPlugin>> find_resource_conversion_plugin_for_type_name(const String &p_type);
 
 	bool ensure_main_scene(bool p_from_native);
+	bool validate_custom_directory();
 };
 
 class EditorPluginList : public Object {


Test patch: