Repository: sphinx-doc/sphinx

Issue:
copyright accepts different values when SOURCE_DATE_EPOCH is/is not in the environment
### Describe the bug

Docs say the copyright comes with a form of: '2008, Author Name'.
https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-copyright

However, some projects do not define the author and up until Sphinx 7.1.1 this was an input that successfully generated the pages: copyright = "2012-2023"
For environments where SOURCE_DATE_EPOCH is not set, which is possibly the most often encountered use case, this still works. The value of the copyright key is taken in the exact form as it's defined in conf.py.

If SOURCE_DATE_EPOCH is set the value read from conf.py is processed via the logic introduced here: https://github.com/sphinx-doc/sphinx/commit/8452300d54dce2da751941d9547dd54dc03e69bf

Producing:
```
Extension error (sphinx.config):
Handler <function correct_copyright_year at 0x77f66be49080> for event 'config-inited' threw an exception (exception: string index out of range)
```
This behavior is inconsistent.

### How to Reproduce

change tests/roots/test-copyright-multiline/conf.py to:
```
copyright = (
    '2006',
    '2006-2009',
    '2006-2009, Alice',
    '2010-2013, Bob',
    '2014-2017, Charlie',
    '2018-2021, David',
    '2022-2025, Eve',
)
html_theme = 'basic'
```
Run tox tests.
Test without SOURCE_DATE_EPOCH set will pass while the ones with it will fail.

### Environment Information

```text
Sphinx 7.2.6
```


### Sphinx extensions

_No response_

### Additional context

Very similar issue https://github.com/sphinx-doc/sphinx/issues/11627
Project using the '2012-2023' format https://github.com/Flask-Middleware/flask-security https://github.com/Flask-Middleware/flask-security/blob/5.3.3/docs/conf.py#L53


Discussion:
This bug originates from this line of code:

https://github.com/sphinx-doc/sphinx/blob/fa290049515c38e68edda7e8c17be69b8793bb84/sphinx/config.py#L613

We make a direct array index access to `copyright_line[9]`, that in the example provided (`2012-2023`) is outside-of-range.

Personally I don't love automatically rewriting copyright entries; but we shouldn't break existing documentation builds, so I have a fix coming up for this.



Patch:
diff --git a/sphinx/config.py b/sphinx/config.py
index 5675cfba370..22b98a83414 100644
--- a/sphinx/config.py
+++ b/sphinx/config.py
@@ -610,7 +610,7 @@ def _substitute_copyright_year(copyright_line: str, replace_year: str) -> str:
     if copyright_line[4] != '-':
         return copyright_line
 
-    if copyright_line[5:9].isdigit() and copyright_line[9] in ' ,':
+    if copyright_line[5:9].isdigit() and copyright_line[9:10] in {'', ' ', ','}:
         return copyright_line[:5] + replace_year + copyright_line[9:]
 
     return copyright_line


Test patch:
diff --git a/tests/test_config/test_config.py b/tests/test_config/test_config.py
index 317a50bf356..ee305274ecf 100644
--- a/tests/test_config/test_config.py
+++ b/tests/test_config/test_config.py
@@ -8,7 +8,13 @@
 
 import sphinx
 from sphinx.builders.gettext import _gettext_compact_validator
-from sphinx.config import ENUM, Config, _Opt, check_confval_types
+from sphinx.config import (
+    ENUM,
+    Config,
+    _Opt,
+    check_confval_types,
+    correct_copyright_year,
+)
 from sphinx.deprecation import RemovedInSphinx90Warning
 from sphinx.errors import ConfigError, ExtensionError, VersionRequirementError
 
@@ -556,6 +562,24 @@ def test_multi_line_copyright(source_date_year, app, monkeypatch):
         ) in content
 
 
+@pytest.mark.parametrize(('conf_copyright', 'expected_copyright'), [
+    ('1970', '{current_year}'),
+    # https://github.com/sphinx-doc/sphinx/issues/11913
+    ('1970-1990', '1970-{current_year}'),
+    ('1970-1990 Alice', '1970-{current_year} Alice'),
+])
+def test_correct_copyright_year(conf_copyright, expected_copyright, source_date_year):
+    config = Config({}, {'copyright': conf_copyright})
+    correct_copyright_year(_app=None, config=config)
+    actual_copyright = config['copyright']
+
+    if source_date_year is None:
+        expected_copyright = conf_copyright
+    else:
+        expected_copyright = expected_copyright.format(current_year=source_date_year)
+    assert actual_copyright == expected_copyright
+
+
 def test_gettext_compact_command_line_true():
     config = Config({}, {'gettext_compact': '1'})
     config.add('gettext_compact', True, '', {bool, str})