Repository: bids-standard/pybids (language: python)

Issue:
Bug/Ambiguous parsing when extension but no suffix present
Found a bit of buggy parsing when fuzzing inputs on another project.

For the following dataset:
```
sub-0
	sub-0.ext
```

The sole file gets parsed with the following entities:


```py
{'extension': '.ext', 'subject': '0', 'suffix': '0'}
```

Clearly `0` should not be attached both to `subject` and `suffix`.

My inclination is to add a negative lookbehind to the `suffix` regex preventing alphanums preceded by a `-` from being matched: `(?<!-)[._]*([a-zA-Z0-9]*?)\.[^\/\\]$`. This should generally prevent it from snatching the values from other entities.

However, I'm not sure if bids *requires* files with extensions to also have suffixes? The regexes in the config file seem to suggest this is so (the `suffix` and `extension` regexes are identical, with only the capture group location moved).

But even if so, pybids needs to do *something* with the above file, and ignoring it as invalid seems harsh.

Also not sure how this situation will change if official bids spec files are used.


Discussion / hints:
BIDS does require all files to have suffixes, with the exception of `dataset_description.json` and (arguably) `participants.{tsv,json}`.
Fair enough, but the example above still needs to be handled somehow, and the status-quo seems unacceptable. Either some error handling kicks in, or paths without suffixes should be parsed as accepted. I'll note that, at least for derivatives, pybids is already quite forgiving of unexpected paths, only parsing what it knows. In this case, pybids is parsing *too much*.
That's fair. I think the fix is probably just changing the `[._]*` to `(?:_|^)` so that the either the suffix follows a `_` or has nothing preceding it.

```python
r'(?:_|^)([A-Za-z0-9]+)\.[^/\\]+$'
```

Fix patch:
diff --git a/bids/layout/config/bids.json b/bids/layout/config/bids.json
index dada4b1cf..bad844ad3 100644
--- a/bids/layout/config/bids.json
+++ b/bids/layout/config/bids.json
@@ -95,7 +95,7 @@
         },
         {
             "name": "suffix",
-            "pattern": "[._]*([a-zA-Z0-9]*?)\\.[^/\\\\]+$"
+            "pattern": "(?:^|[_/\\\\])([a-zA-Z0-9]+)\\.[^/\\\\]+$"
         },
         {
             "name": "scans",
@@ -111,7 +111,7 @@
         },
         {
             "name": "extension",
-            "pattern": "[._]*[a-zA-Z0-9]*?(\\.[^/\\\\]+)$"
+            "pattern": "[^./\\\\](\\.[^/\\\\]+)$"
         }
     ],
     "default_path_patterns": [


Test patch:
diff --git a/bids/layout/tests/test_utils.py b/bids/layout/tests/test_utils.py
index 1023fb769..26f1511be 100644
--- a/bids/layout/tests/test_utils.py
+++ b/bids/layout/tests/test_utils.py
@@ -48,6 +48,19 @@ def test_parse_file_entities(mock_config):
     assert target == parse_file_entities(filename, entities=entities)
 
 
+@pytest.mark.parametrize(
+    "filename, target",
+    [
+        ('/path/to/sub-01.ext', {'subject': '01', 'extension': '.ext'}),
+        ('/path/to/stub.ext', {'suffix': 'stub', 'extension': '.ext'}),
+        ('/path/to/.dotfile', {}),
+        ('/path/to/stub', {}),
+    ]
+)
+def test_parse_degenerate_files(mock_config, filename, target):
+    assert parse_file_entities(filename, config='bids') == target
+
+
 def test_add_config_paths():
     bids_dir = os.path.dirname(bids.__file__)
     bids_json = os.path.join(bids_dir, 'layout', 'config', 'bids.json')