Repository: iterative/dvc

Issue:
`plots diff`: fails when given a revision which contains a path separator
# Bug Report

<!--
## Issue name

Issue names must follow the pattern `command: description` where the command is the dvc command that you are trying to run. The description should describe the consequence of the bug. 

Example: `repro: doesn't detect input changes`
-->

## Description

<!--
A clear and concise description of what the bug is.
-->

In the extension, we call `plots diff` with the branch name to fetch the HEAD revision's data, we then show this data in our plots webview. If the user has checked out a branch that contains a path separator this command fails because the required directory does not exist. 

E.g trying to fetch the HEAD revision for a branch called `readme/known-limitations` in the `vscode-dvc` `demo` project yields:

```bash
vscode-dvc/demo readme/known-limitations *1 !2 ?1 ❯ dvc plots diff "readme/known-limitations" -o .dvc/tmp/plots   ✘ INT
ERROR: unexpected error - [Errno 2] No such file or directory: '/vscode-dvc/demo/.dvc/tmp/plots/static/readme/known-limitations_misclassified.jpg'
```

### Reproduce

<!--
Step list of how to reproduce the bug
-->

<!--
Example:

1. dvc init
2. Copy dataset.zip to the directory
3. dvc add dataset.zip
4. dvc run -d dataset.zip -o model ./train.sh
5. modify dataset.zip
6. dvc repro
-->


1. checkout a branch containing a path separator (e.g `readme/known-limitations`)
2. call `plots diff` with the branch name as a revision
3. command will fail because the directory does not exist

### Expected

<!--
A clear and concise description of what you expect to happen.
-->

`plots diff` returns the expected data for revisions which contain path separators.

### Environment information

<!--
This is required to ensure that we can reproduce the bug.
-->

**Output of `dvc doctor`:**

```console
$ dvc doctor
DVC version: 2.11.0 (brew)
---------------------------------
Platform: Python 3.9.13 on macOS-12.4-x86_64-i386-64bit
Supports:
        azure (adlfs = 2022.4.0, knack = 0.9.0, azure-identity = 1.10.0),
        gdrive (pydrive2 = 1.10.1),
        gs (gcsfs = 2022.5.0),
        webhdfs (fsspec = 2022.5.0),
        http (aiohttp = 3.8.1, aiohttp-retry = 2.4.6),
        https (aiohttp = 3.8.1, aiohttp-retry = 2.4.6),
        s3 (s3fs = 2022.5.0, boto3 = 1.21.21),
        ssh (sshfs = 2022.6.0),
        oss (ossfs = 2021.8.0),
        webdav (webdav4 = 0.9.7),
        webdavs (webdav4 = 0.9.7)
Cache types: reflink, hardlink, symlink
Cache directory: apfs on /dev/disk1s5s1
Caches: local
Remotes: s3
Workspace directory: apfs on /dev/disk1s5s1
Repo: dvc (subdir), git
```

**Additional Information (if any):**

<!--
Please check https://github.com/iterative/dvc/wiki/Debugging-DVC on ways to gather more information regarding the issue.

If applicable, please also provide a `--verbose` output of the command, eg: `dvc add --verbose`.
If the issue is regarding the performance, please attach the profiling information and the benchmark comparisons.
-->

Original issue: https://github.com/iterative/vscode-dvc/issues/1946

Partially related to #7933



Comments:
The bug has been present since the introduction of image support. Working on a fix

Patch:
diff --git a/dvc/render/image_converter.py b/dvc/render/image_converter.py
--- a/dvc/render/image_converter.py
+++ b/dvc/render/image_converter.py
@@ -20,7 +20,8 @@ def _write_image(
         image_data: bytes,
     ) -> "StrPath":
         img_path = os.path.join(
-            path, f"{revision}_{filename.replace(os.sep, '_')}"
+            path,
+            f"{revision}_{filename}".replace(os.sep, "_").replace("/", "_"),
         )
         with open(img_path, "wb") as fd:
             fd.write(image_data)


Test patch:
diff --git a/tests/unit/render/test_image_converter.py b/tests/unit/render/test_image_converter.py
--- a/tests/unit/render/test_image_converter.py
+++ b/tests/unit/render/test_image_converter.py
@@ -28,3 +28,19 @@ def test_image_converter_with_out(tmp_dir):
     }
 
     assert (tmp_dir / "foo" / "r_image.png").read_bytes() == b"content"
+
+
+def test_image_converter_with_slash_in_revision(tmp_dir):
+    """Regression test for #7934"""
+    data = b"content"
+    converter = ImageConverter({"out": tmp_dir / "foo"})
+
+    datapoints, _ = converter.convert(data, "feature/r", "image.png")
+
+    assert datapoints[0] == {
+        REVISION_FIELD: "feature/r",
+        "filename": "image.png",
+        SRC_FIELD: str(tmp_dir / "foo" / "feature_r_image.png"),
+    }
+
+    assert (tmp_dir / "foo" / "feature_r_image.png").read_bytes() == b"content"