Repository: pydata/xarray

Issue:
'/' in variable name causes to_netcdf() to crash
### What happened?

If variable name includes a '/' then to_netcdf crashes

### What did you expect to happen?

_No response_

### Minimal Complete Verifiable Example

```Python
import pandas as pd
df = pd.read_csv('slashCSV.csv')
index = ['temperature']
df = df.set_index(index)
xarr = df.to_xarray()
xarr.to_netcdf("new.nc")


- slashCSV.csv contents:
PASS/FAIL,temperature
PASS, 10
FAIL, 20
```


### MVCE confirmation

- [X] Minimal example — the example is as focused as reasonably possible to demonstrate the underlying issue in xarray.
- [X] Complete example — the example is self-contained, including all data and the text of any traceback.
- [X] Verifiable example — the example copy & pastes into an IPython prompt or [Binder notebook](https://mybinder.org/v2/gh/pydata/xarray/main?urlpath=lab/tree/doc/examples/blank_template.ipynb), returning the result.
- [X] New issue — a search of GitHub Issues suggests this is not a duplicate.

### Relevant log output

```Python
File "/opt/homebrew/lib/python3.11/site-packages/xarray/backends/netCDF4_.py", line 56, in __init__
    array = self.get_array()
            ^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/xarray/backends/netCDF4_.py", line 83, in get_array
    variable = ds.variables[self.variable_name]
               ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
KeyError: 'PASS/FAIL'
```


### Anything else we need to know?

_No response_

### Environment

<details>

INSTALLED VERSIONS
------------------
commit: None
python: 3.11.2 (main, Mar 24 2023, 00:16:47) [Clang 14.0.0 (clang-1400.0.29.202)]
python-bits: 64
OS: Darwin
OS-release: 22.5.0
machine: arm64
processor: arm
byteorder: little
LC_ALL: None
LANG: en_US.UTF-8
LOCALE: ('en_US', 'UTF-8')
libhdf5: 1.12.2
libnetcdf: 4.9.1

xarray: 2023.2.0
pandas: 1.5.3
numpy: 1.24.2
scipy: 1.10.1
netCDF4: 1.6.3
pydap: None
h5netcdf: 1.1.0
h5py: 3.8.0
Nio: None
zarr: 2.14.2
cftime: 1.6.2
nc_time_axis: None
PseudoNetCDF: None
rasterio: 1.3.6
cfgrib: 0.9.10.3
iris: None
bottleneck: None
dask: None
distributed: None
matplotlib: None
cartopy: None
seaborn: None
numbagg: None
fsspec: 2023.3.0
cupy: None
pint: None
sparse: None
flox: None
numpy_groupies: None
setuptools: 65.6.3
pip: 23.1.1
conda: None
pytest: None
mypy: None
IPython: None
sphinx: None
</details>


Discussion:

Patch:
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index be7f8ade..87559ee9 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -34,6 +34,9 @@ Deprecations
 Bug fixes
 ~~~~~~~~~
 
+- Ensure no forward slashes in variable and dimension names for HDF5-based engines.
+  (:issue:`7943`, :pull:`7953`) By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_.
+
 
 Documentation
 ~~~~~~~~~~~~~
diff --git a/xarray/backends/h5netcdf_.py b/xarray/backends/h5netcdf_.py
index 7389f6a2..697ebb8a 100644
--- a/xarray/backends/h5netcdf_.py
+++ b/xarray/backends/h5netcdf_.py
@@ -20,6 +20,7 @@ from xarray.backends.locks import HDF5_LOCK, combine_locks, ensure_lock, get_wri
 from xarray.backends.netCDF4_ import (
     BaseNetCDF4Array,
     _encode_nc4_variable,
+    _ensure_no_forward_slash_in_name,
     _extract_nc4_variable_encoding,
     _get_datatype,
     _nc4_require_group,
@@ -256,6 +257,7 @@ class H5NetCDFStore(WritableCFDataStore):
             }
 
     def set_dimension(self, name, length, is_unlimited=False):
+        _ensure_no_forward_slash_in_name(name)
         if is_unlimited:
             self.ds.dimensions[name] = None
             self.ds.resize_dimension(name, length)
@@ -273,6 +275,7 @@ class H5NetCDFStore(WritableCFDataStore):
     ):
         import h5py
 
+        _ensure_no_forward_slash_in_name(name)
         attrs = variable.attrs.copy()
         dtype = _get_datatype(variable, raise_on_invalid_encoding=check_encoding)
 
diff --git a/xarray/backends/netCDF4_.py b/xarray/backends/netCDF4_.py
index 8a5d48c8..b5c3413e 100644
--- a/xarray/backends/netCDF4_.py
+++ b/xarray/backends/netCDF4_.py
@@ -194,6 +194,15 @@ def _nc4_require_group(ds, group, mode, create_group=_netcdf4_create_group):
         return ds
 
 
+def _ensure_no_forward_slash_in_name(name):
+    if "/" in name:
+        raise ValueError(
+            f"Forward slashes '/' are not allowed in variable and dimension names (got {name!r}). "
+            "Forward slashes are used as hierarchy-separators for "
+            "HDF5-based files ('netcdf4'/'h5netcdf')."
+        )
+
+
 def _ensure_fill_value_valid(data, attributes):
     # work around for netCDF4/scipy issue where _FillValue has the wrong type:
     # https://github.com/Unidata/netcdf4-python/issues/271
@@ -447,6 +456,7 @@ class NetCDF4DataStore(WritableCFDataStore):
         }
 
     def set_dimension(self, name, length, is_unlimited=False):
+        _ensure_no_forward_slash_in_name(name)
         dim_length = length if not is_unlimited else None
         self.ds.createDimension(name, size=dim_length)
 
@@ -470,6 +480,8 @@ class NetCDF4DataStore(WritableCFDataStore):
     def prepare_variable(
         self, name, variable, check_encoding=False, unlimited_dims=None
     ):
+        _ensure_no_forward_slash_in_name(name)
+
         datatype = _get_datatype(
             variable, self.format, raise_on_invalid_encoding=check_encoding
         )


Test patch:
diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py
index 0450e769..dad2d668 100644
--- a/xarray/tests/test_backends.py
+++ b/xarray/tests/test_backends.py
@@ -1611,6 +1611,20 @@ class NetCDF4Base(NetCDFBase):
             assert actual.encoding["unlimited_dims"] == set("y")
             assert_equal(ds, actual)
 
+    def test_raise_on_forward_slashes_in_names(self) -> None:
+        # test for forward slash in variable names and dimensions
+        # see GH 7943
+        data_vars: list[dict[str, Any]] = [
+            {"PASS/FAIL": (["PASSFAIL"], np.array([0]))},
+            {"PASS/FAIL": np.array([0])},
+            {"PASSFAIL": (["PASS/FAIL"], np.array([0]))},
+        ]
+        for dv in data_vars:
+            ds = Dataset(data_vars=dv)
+            with pytest.raises(ValueError, match="Forward slashes '/' are not allowed"):
+                with self.roundtrip(ds):
+                    pass
+
 
 @requires_netCDF4
 class TestNetCDF4Data(NetCDF4Base):