Repository: sopel-irc/sopel

Issue:
config: FilenameAttribute doesn't handle quotes well
### Description
If the value of a `FilenameAttribute` set in the config file is quoted, it silently creates a mess.

### Reproduction steps
1. Set `core.logdir = "/path/to/logs"`
2. Run Sopel
3. Sopel quietly creates a silly directory tree inside `$HOMEDIR`:
```
$ tree ~/.sopel
/home/dgw/.sopel
├── "
│   └── path
│       └── to
│           └── logs"
│               ├── default.error.log
│               ├── default.exceptions.log
│               ├── default.raw.log
│               └── default.sopel.log
...
```

### Expected behavior
Even though quotes aren't required in Sopel's config file, they should be sensibly handled.

### Environment
- Sopel `.version`: current `master`, aca10553da2a996aaaa4051741fae6fb7e6c4940
- Sopel installed via: `-e .` @ above commit
- Python version: 3.8, 3.9
- Operating system: Ubuntu 20.04 (WSL)

### Notes
This is a leftover todo from #1959, where I updated the docs but didn't make any code changes related to this.


Discussion:
Can we just switch to TOML >.>
Definitely not for 8.0.
As written in the doc:

> Quoting a value unnecessarily can lead to unexpected behavior such as an absolute pathname being interpreted as relative to Sopel’s home directory.
Yes @Exirel, that's what I added in #1959. But in the interest of user-friendliness (Sopel is supposed to be "simple", remember) I think we should add sensible behavior like stripping quotes (and logging a warning), unless there are reasonable use cases where someone might intentionally want a folder named `"`.

Resolving patch:
diff --git a/sopel/config/types.py b/sopel/config/types.py
--- a/sopel/config/types.py
+++ b/sopel/config/types.py
@@ -697,6 +697,11 @@ def _parse(self, value, settings, section):
         if not value:
             return self.parse(value)
 
+        if value.startswith('"') and value.endswith('"'):
+            value = value.strip('"')
+        elif value.startswith("'") and value.endswith("'"):
+            value = value.strip("'")
+
         result = os.path.expanduser(value)
         if not os.path.isabs(result):
             if not self.relative:


Test patch:
diff --git a/test/test_config.py b/test/test_config.py
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -227,6 +227,67 @@ def test_fileattribute_given_file_when_dir(fakeconfig):
         fakeconfig.fake.ad_fileattr = testfile
 
 
+def test_fileattribute_relative_with_quotes(fakeconfig):
+    testfile = os.path.join(fakeconfig.core.homedir, 'test.tmp')
+
+    # double quotes
+    fakeconfig.fake.rf_fileattr = '"test.tmp"'
+    assert fakeconfig.fake.rf_fileattr == testfile
+
+    # single quotes
+    fakeconfig.fake.rf_fileattr = "'test.tmp'"
+    assert fakeconfig.fake.rf_fileattr == testfile
+
+
+def test_fileattribute_relative_with_nested_double_quotes(fakeconfig):
+    testfile = os.path.join(fakeconfig.core.homedir, '"test.tmp"')
+
+    # double quotes nested inside single quotes
+    fakeconfig.fake.rf_fileattr = "'\"test.tmp\"'"
+    assert fakeconfig.fake.rf_fileattr == testfile
+
+
+def test_fileattribute_relative_with_nested_single_quotes(fakeconfig):
+    testfile = os.path.join(fakeconfig.core.homedir, "'test.tmp'")
+
+    # single quotes nested inside double quotes
+    fakeconfig.fake.rf_fileattr = '"\'test.tmp\'"'
+    assert fakeconfig.fake.rf_fileattr == testfile
+
+
+def test_fileattribute_relative_with_inside_quotes(fakeconfig):
+    testfile = os.path.join(fakeconfig.core.homedir, 'test".tmp')
+    fakeconfig.fake.rf_fileattr = 'test".tmp'
+
+    assert fakeconfig.fake.rf_fileattr == testfile, (
+        'Quote inside the filename must be kept'
+    )
+
+
+def test_fileattribute_absolute_with_quotes(fakeconfig):
+    testfile = os.path.join(fakeconfig.core.homedir, 'test.tmp')
+
+    # double quotes
+    fakeconfig.fake.af_fileattr = '"%s"' % testfile
+    assert fakeconfig.fake.af_fileattr == testfile
+
+    # single quotes
+    fakeconfig.fake.af_fileattr = "'%s'" % testfile
+    assert fakeconfig.fake.af_fileattr == testfile
+
+
+def test_fileattribute_absolute_with_inside_quotes(fakeconfig):
+    testfile = os.path.join(fakeconfig.core.homedir, 'test".tmp')
+
+    # double quotes
+    fakeconfig.fake.af_fileattr = '"%s"' % testfile
+    assert fakeconfig.fake.af_fileattr == testfile
+
+    # single quotes
+    fakeconfig.fake.af_fileattr = "'%s'" % testfile
+    assert fakeconfig.fake.af_fileattr == testfile
+
+
 def test_configparser_env_priority_over_file(monkeypatch, fakeconfig):
     monkeypatch.setenv('SOPEL_CORE_OWNER', 'not_dgw')
     assert fakeconfig.core.owner == 'not_dgw'