Repository: beeware/briefcase

Issue:
Giving a formal name with a character such as ', /, \, causes briefcase new to crash
### Describe the bug

When you execute `briefcase new`  and set the name to / you get an IndexError: string index out of range.  

This is also true for some other characters such as /, \ , ", ', and ,
It appears to effect all of that class of characters due to the way the class name is created.

The problem appears to be in config.py make_class_name:
    
    # Normalize to NFKC form, then remove any character that isn't
    # in the allowed categories, or is the underscore character;
    # Capitalize the resulting word.
    class_name = "".join(
        ch
        for ch in unicodedata.normalize("NFKC", formal_name)
        if unicodedata.category(ch) in xid_continue or ch in {"_"}
    )

This causes the name to be stripped of all characters, making the string length zero.

This causes an attempt to make a class name to fail

    # If the first character isn't in the 'start' character set,
    # and it isn't already an underscore, prepend an underscore.
    if unicodedata.category(class_name[0]) not in xid_start and class_name[0] != "_":
        class_name = f"_{class_name}"

    return class_name

### Steps to reproduce

1. Run `briefcase new`
2. When prompted Formal Name [Hello World]: enter \


### Expected behavior

Validation should force you to create a legal name, or additionally prompt you to create proper class names.

### Screenshots

_No response_

### Environment

- Operating System: Linux
- Python version: 3.11
- Software versions:
  - Briefcase: 0.3.19.dev26+g96734be6.d20240520
  - Toga:
  - ...


### Logs

_No response_

### Additional context

_No response_

Discussion:

Patch:
diff --git a/changes/1800.misc.rst b/changes/1800.misc.rst
deleted file mode 100644
index 8edd7a8d..00000000
--- a/changes/1800.misc.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Updated setuptools-scm from 8.0.4 to 8.1.0.
diff --git a/changes/1810.bugfix.rst b/changes/1810.bugfix.rst
new file mode 100644
index 00000000..24150ad2
--- /dev/null
+++ b/changes/1810.bugfix.rst
@@ -0,0 +1,1 @@
+The formal name of an app is now validated.
diff --git a/changes/1820.bugfix.rst b/changes/1820.bugfix.rst
deleted file mode 100644
index a2f4a2e7..00000000
--- a/changes/1820.bugfix.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-macOS apps now generate ``info.plist`` entries for camera, photo library and microphone permissions.
diff --git a/docs/reference/platforms/macOS/xcode.rst b/docs/reference/platforms/macOS/xcode.rst
index 6836acc1..d00b8be0 100644
--- a/docs/reference/platforms/macOS/xcode.rst
+++ b/docs/reference/platforms/macOS/xcode.rst
@@ -117,10 +117,8 @@ Permissions
 Briefcase cross platform permissions map to a combination of ``info`` and ``entitlement``
 keys:
 
-* ``microphone``: an ``info`` entry for ``NSMicrophoneUsageDescription``; and
-  an ``entitlement`` of ``com.apple.security.device.audio-input``
-* ``camera``: an ``info`` entry for ``NSCameraUsageDescription``; and
-  an ``entitlement`` of ``com.apple.security.device.camera``
+* ``microphone``: an ``entitlement`` of ``com.apple.security.device.audio-input``
+* ``camera``: an ``entitlement`` of ``com.apple.security.device.camera``
 * ``coarse_location``: an ``info`` entry for ``NSLocationUsageDescription``
   (ignored if ``background_location`` or ``fine_location`` is defined); plus an
   entitlement of ``com.apple.security.personal-information.location``
@@ -129,9 +127,7 @@ keys:
   ``com.apple.security.personal-information.location``
 * ``background_location``: an ``info`` entry for ``NSLocationUsageDescription``;
   plus an ``entitlement`` of ``com.apple.security.personal-information.location``
-* ``photo_library``: an ``info`` entry for ``NSPhotoLibraryUsageDescription``;
-  plus an ``entitlement`` of
-  ``com.apple.security.personal-information.photos-library``
+* ``photo_library``: an ``entitlement`` of ``com.apple.security.personal-information.photos-library``
 
 Platform quirks
 ===============
diff --git a/pyproject.toml b/pyproject.toml
index 13b1038e..e1f4f264 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -103,7 +103,7 @@ dev = [
     "pre-commit == 3.7.1 ; python_version >= '3.9'",
     "pytest == 8.2.1",
     "pytest-xdist == 3.6.1",
-    "setuptools_scm == 8.1.0",
+    "setuptools_scm == 8.0.4",
     "tox == 4.15.0",
 ]
 docs = [
diff --git a/src/briefcase/commands/new.py b/src/briefcase/commands/new.py
index ef52b040..1a8e6dff 100644
--- a/src/briefcase/commands/new.py
+++ b/src/briefcase/commands/new.py
@@ -164,6 +164,26 @@ class NewCommand(BaseCommand):
             # use a dummy app name as the suggestion.
             return "myapp"
 
+    def validate_formal_name(self, candidate):
+        """Determine if the formal name is valid.
+
+        A formal name is valid if it contains at least one identifier character.
+
+        :param candidate: The candidate name
+        :returns: True the formal name is valid.
+        :raises: ValueError if the name is not a valid formal name.
+        """
+        if not make_class_name(candidate):  # Check whether a class name may be derived
+            raise ValueError(
+                self.input.textwrap(
+                    f"{candidate!r} is not a valid formal name.\n"
+                    "\n"
+                    "Formal names must include at least one valid Python identifier character."
+                )
+            )
+
+        return True
+
     def _validate_existing_app_name(self, candidate):
         """Perform internal validation preventing the use of pre-existing app names.
 
@@ -459,6 +479,7 @@ class NewCommand(BaseCommand):
             ),
             variable="formal name",
             default="Hello World",
+            validator=self.validate_formal_name,
             override_value=project_overrides.pop("formal_name", None),
         )
 
diff --git a/src/briefcase/config.py b/src/briefcase/config.py
index 34d50800..bf158cfc 100644
--- a/src/briefcase/config.py
+++ b/src/briefcase/config.py
@@ -73,7 +73,11 @@ def make_class_name(formal_name):
 
     # If the first character isn't in the 'start' character set,
     # and it isn't already an underscore, prepend an underscore.
-    if unicodedata.category(class_name[0]) not in xid_start and class_name[0] != "_":
+    if (
+        class_name
+        and unicodedata.category(class_name[0]) not in xid_start
+        and class_name[0] != "_"
+    ):
         class_name = f"_{class_name}"
 
     return class_name
diff --git a/src/briefcase/platforms/macOS/__init__.py b/src/briefcase/platforms/macOS/__init__.py
index e1ad5a87..415ebd3e 100644
--- a/src/briefcase/platforms/macOS/__init__.py
+++ b/src/briefcase/platforms/macOS/__init__.py
@@ -168,10 +168,8 @@ in the macOS configuration section of your pyproject.toml.
 
         if cross_platform["camera"]:
             entitlements["com.apple.security.device.camera"] = True
-            info["NSCameraUsageDescription"] = cross_platform["camera"]
         if cross_platform["microphone"]:
             entitlements["com.apple.security.device.microphone"] = True
-            info["NSMicrophoneUsageDescription"] = cross_platform["microphone"]
 
         if cross_platform["background_location"]:
             info["NSLocationUsageDescription"] = cross_platform["background_location"]
@@ -184,7 +182,6 @@ in the macOS configuration section of your pyproject.toml.
             entitlements["com.apple.security.personal-information.location"] = True
 
         if cross_platform["photo_library"]:
-            info["NSPhotoLibraryUsageDescription"] = cross_platform["photo_library"]
             entitlements["com.apple.security.personal-information.photo_library"] = True
 
         # Override any info and entitlement definitions with the platform specific definitions


Test patch:
diff --git a/tests/commands/new/test_validate_formal_name.py b/tests/commands/new/test_validate_formal_name.py
new file mode 100644
index 00000000..3daa39a5
--- /dev/null
+++ b/tests/commands/new/test_validate_formal_name.py
@@ -0,0 +1,48 @@
+import pytest
+
+
+@pytest.mark.parametrize(
+    "name",
+    [
+        # Various forms of capitalization and alphanumeric
+        "Hello World",
+        "helloworld",
+        "helloWorld",
+        "hello42world",
+        "42helloworld",
+        # Names that include punctuation
+        "hello_world",
+        "hello-world",
+        "_helloworld",
+        "/helloworld",
+        "Hello / World!",
+        # Internationalized names that can be unicode-simplified
+        "Hallo Vögel",
+        "Bonjour Garçon",
+        # Internationalized names that cannot be unicode-simplified
+        "你好 世界!",
+    ],
+)
+def test_valid_formal_name(new_command, name):
+    """Test that valid formal names are accepted."""
+    assert new_command.validate_formal_name(name)
+
+
+@pytest.mark.parametrize(
+    "name",
+    [
+        "",  # Empty
+        " ",  # Just a space
+        "\t",  # Other whitespace characters
+        "/",  # Just a slash
+        "'",
+        "\\",
+        "/'\\",  # Multiple invalid characters
+    ],
+)
+def test_invalid_formal_name(new_command, name, tmp_path):
+    """Test that invalid app names are rejected."""
+    (tmp_path / "existing").mkdir()
+
+    with pytest.raises(ValueError):
+        new_command.validate_formal_name(name)
diff --git a/tests/platforms/macOS/app/test_create.py b/tests/platforms/macOS/app/test_create.py
index 91ab045e..8fed8470 100644
--- a/tests/platforms/macOS/app/test_create.py
+++ b/tests/platforms/macOS/app/test_create.py
@@ -73,9 +73,7 @@ def create_command(tmp_path, first_app_templated):
             {},
             {},
             {
-                "info": {
-                    "NSCameraUsageDescription": "I need to see you",
-                },
+                "info": {},
                 "entitlements": {
                     "com.apple.security.cs.allow-unsigned-executable-memory": True,
                     "com.apple.security.cs.disable-library-validation": True,
@@ -91,9 +89,7 @@ def create_command(tmp_path, first_app_templated):
             {},
             {},
             {
-                "info": {
-                    "NSMicrophoneUsageDescription": "I need to hear you",
-                },
+                "info": {},
                 "entitlements": {
                     "com.apple.security.cs.allow-unsigned-executable-memory": True,
                     "com.apple.security.cs.disable-library-validation": True,
@@ -240,9 +236,7 @@ def create_command(tmp_path, first_app_templated):
             {},
             {},
             {
-                "info": {
-                    "NSPhotoLibraryUsageDescription": "I need to see your library",
-                },
+                "info": {},
                 "entitlements": {
                     "com.apple.security.cs.allow-unsigned-executable-memory": True,
                     "com.apple.security.cs.disable-library-validation": True,