Repository: NVIDIA/NVFlare

Issue:
[BUG] nvflare config does not allow @ sign in directory 
**Describe the bug**

If I run `nvflare config` and point it to the unzipped location of my console, I get this error: 

```
nvflare config -d ~/.nvflare/myproject/xxxlead@gmail.com --job_templates_dir ~/NVFlare/job_templates/

Unable to handle command: config due to: Expected '}', found '@'  (at char 90), (line:3, col:59)
```

If I download the console from the dashboard it will be stored as `xxxlead@gmail.com.zip` so the unzipped folder will likely always have an @ sign in the folder name.  

This should also work with an + sign in the email `xxx+lead@gmail.com` as `+` works as a suffix that allows people to create multiple identities but work with the same email address.

Discussion:
@chesterxgchen: @dirkpetersen  thanks for reporting.  there is no special characters treatment right now. We are using Pyhocon parser to parse. We have to see what we need to do in order to support special characters. @chesterxgchen: This is turn out to be an simple fix, we just need to quote the string for the pyhocon parser.  I fix it in this PR: 
https://github.com/NVIDIA/NVFlare/pull/2607



Solution patch:
diff --git a/nvflare/tool/job/job_cli.py b/nvflare/tool/job/job_cli.py
index ceab31fe..b05f6865 100644
--- a/nvflare/tool/job/job_cli.py
+++ b/nvflare/tool/job/job_cli.py
@@ -134,7 +134,7 @@ def create_job(cmd_args):
         job_folder = cmd_args.job_folder
         prepare_job_folder(cmd_args)
         app_custom_dirs = prepare_app_dirs(job_folder, app_names)
-        prepare_app_scripts(app_custom_dirs, cmd_args)
+        prepare_app_scripts(job_folder, app_custom_dirs, cmd_args)
         config_dirs = get_config_dirs(job_folder, app_names)
 
         fmt, real_config_path = ConfigFactory.search_config_format(CONFIG_FED_CLIENT_CONF, config_dirs)
@@ -688,10 +688,22 @@ def prepare_job_folder(cmd_args):
             )
 
 
-def prepare_app_scripts(app_custom_dirs, cmd_args):
+def is_subdir(path, directory):
+    # Normalize the paths to avoid issues with different OS formats
+    path = os.path.realpath(path)
+    directory = os.path.realpath(directory)
+    # Check if the directory is a prefix of the path
+    return os.path.commonpath([path, directory]) == directory
+
+
+def prepare_app_scripts(job_folder, app_custom_dirs, cmd_args):
+    script_dir = cmd_args.script_dir
+
     for app_custom_dir in app_custom_dirs:
-        if cmd_args.script_dir and len(cmd_args.script_dir.strip()) > 0:
-            if os.path.exists(cmd_args.script_dir):
+        if script_dir and len(script_dir.strip()) > 0:
+            if os.path.exists(script_dir):
+                if script_dir == job_folder or is_subdir(job_folder, script_dir):
+                    raise ValueError("job_folder must not be the same or sub directory of script_dir")
                 copy_tree(cmd_args.script_dir, app_custom_dir)
                 remove_pycache_files(app_custom_dir)
             else:
diff --git a/nvflare/utils/cli_utils.py b/nvflare/utils/cli_utils.py
index f580c657..3c3c2c41 100644
--- a/nvflare/utils/cli_utils.py
+++ b/nvflare/utils/cli_utils.py
@@ -92,7 +92,7 @@ def create_startup_kit_config(nvflare_config: ConfigTree, startup_kit_dir: Optio
         startup_kit_dir = get_startup_kit_dir(startup_kit_dir)
         conf_str = f"""
             startup_kit {{
-                path = {startup_kit_dir}
+                path = "{startup_kit_dir}"
             }}
         """
         conf: ConfigTree = CF.parse_string(conf_str)


Test patch:
diff --git a/tests/unit_test/tool/job/job_cli_test.py b/tests/unit_test/tool/job/job_cli_test.py
index 33e57dc5..ce3d0eb9 100644
--- a/tests/unit_test/tool/job/job_cli_test.py
+++ b/tests/unit_test/tool/job/job_cli_test.py
@@ -14,6 +14,7 @@
 
 import pytest
 
+from nvflare.tool.job import job_cli
 from nvflare.tool.job.job_cli import convert_args_list_to_dict
 
 
@@ -22,3 +23,11 @@ class TestJobCLI:
     def test_convert_args_list_to_dict(self, inputs, result):
         r = convert_args_list_to_dict(inputs)
         assert r == result
+
+    @pytest.mark.parametrize(
+        "directory, path, expected",
+        [("/home/user/project", "/home/user/project/subdir", True), (".", ".", True), ("./code", ".", False)],
+    )
+    def test_is_sub_dir(self, path, directory, expected):
+        print(f"{input=}, {directory=}, {expected=}")
+        assert expected == job_cli.is_subdir(path, directory)