Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-pydicom for openSUSE:Factory 
checked in at 2026-03-23 17:11:33
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-pydicom (Old)
 and      /work/SRC/openSUSE:Factory/.python-pydicom.new.8177 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-pydicom"

Mon Mar 23 17:11:33 2026 rev:18 rq:1341568 version:3.0.2

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-pydicom/python-pydicom.changes    
2025-04-20 20:08:07.309497382 +0200
+++ /work/SRC/openSUSE:Factory/.python-pydicom.new.8177/python-pydicom.changes  
2026-03-23 17:11:54.482029725 +0100
@@ -1,0 +2,7 @@
+Fri Mar 20 10:13:28 UTC 2026 - Markéta Machová <[email protected]>
+
+- update to 3.0.2 (CVE-2026-32711, bsc#1259973)
+  * A crafted DICOMDIR could create a path traversal by setting
+    ReferencedFileID to a path outside the File-set root.
+
+-------------------------------------------------------------------

Old:
----
  pydicom-3.0.1-gh.tar.gz

New:
----
  pydicom-3.0.2-gh.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-pydicom.spec ++++++
--- /var/tmp/diff_new_pack.o80GTf/_old  2026-03-23 17:11:55.166058216 +0100
+++ /var/tmp/diff_new_pack.o80GTf/_new  2026-03-23 17:11:55.166058216 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-pydicom
 #
-# Copyright (c) 2025 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-pydicom
-Version:        3.0.1
+Version:        3.0.2
 Release:        0
 Summary:        Pure python package for DICOM medical file reading and writing
 License:        MIT
@@ -31,9 +31,10 @@
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
 # SECTION test requirements
-BuildRequires:  %{python_module numpy}
 BuildRequires:  %{python_module Pillow}
+BuildRequires:  %{python_module numpy}
 BuildRequires:  %{python_module pydicom-data}
+BuildRequires:  %{python_module pyfakefs >= 6.1.6}
 BuildRequires:  %{python_module pytest}
 BuildRequires:  %{python_module requests}
 # /SECTION

++++++ pydicom-3.0.1-gh.tar.gz -> pydicom-3.0.2-gh.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pydicom-3.0.1/doc/release_notes/index.rst 
new/pydicom-3.0.2/doc/release_notes/index.rst
--- old/pydicom-3.0.1/doc/release_notes/index.rst       2024-09-22 
03:57:27.000000000 +0200
+++ new/pydicom-3.0.2/doc/release_notes/index.rst       2026-03-19 
22:37:06.000000000 +0100
@@ -2,6 +2,7 @@
 Release notes
 =============
 
+.. include:: v3.0.2.rst
 .. include:: v3.0.1.rst
 .. include:: v3.0.0.rst
 .. include:: v2.4.0.rst
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pydicom-3.0.1/doc/release_notes/v3.0.2.rst 
new/pydicom-3.0.2/doc/release_notes/v3.0.2.rst
--- old/pydicom-3.0.1/doc/release_notes/v3.0.2.rst      1970-01-01 
01:00:00.000000000 +0100
+++ new/pydicom-3.0.2/doc/release_notes/v3.0.2.rst      2026-03-19 
22:37:06.000000000 +0100
@@ -0,0 +1,8 @@
+3.0.2
+=====
+
+Fixes
+-----
+
+* Fixed a security issue: a crafted DICOMDIR could set ``ReferencedFileID`` to 
a path outside the File-set root.
+  This addresses CVE-2026-32711.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pydicom-3.0.1/pyproject.toml 
new/pydicom-3.0.2/pyproject.toml
--- old/pydicom-3.0.1/pyproject.toml    2024-09-22 03:57:27.000000000 +0200
+++ new/pydicom-3.0.2/pyproject.toml    2026-03-19 22:37:06.000000000 +0100
@@ -29,7 +29,7 @@
 name = "pydicom"
 readme = "README.md"
 requires-python = ">=3.10"
-version = "3.0.1"
+version = "3.0.2"
 
 
 [project.optional-dependencies]
@@ -55,6 +55,7 @@
     "ruff==0.6.3",
     "types-requests",
     "pre-commit",
+    "pyfakefs>=6.1.6",
 ]
 
 basic = ["numpy", "types-pydicom"]
@@ -90,6 +91,30 @@
 show = "pydicom.cli.show:add_subparser"
 
 
+[dependency-groups]
+docs = [
+    "numpy",
+    "numpydoc",
+    "matplotlib",
+    "pillow",
+    "pydata-sphinx-theme",
+    "sphinx",
+    "sphinx-gallery",
+    "sphinxcontrib-napoleon",
+    "sphinx-copybutton",
+    "sphinx_design",
+]
+
+dev = [
+    "pydicom-data",
+    "pyfakefs",
+    "pytest",
+    "pytest-cov",
+    "types-requests",
+    "pre-commit",
+]
+
+
 [tool.black]
 exclude = ".venv|build|/_.*_dict.py$"
 force-exclude = ".venv|/_.*_dict.py$"  # to not do files pre-commit asks for
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pydicom-3.0.1/src/pydicom/fileset.py 
new/pydicom-3.0.2/src/pydicom/fileset.py
--- old/pydicom-3.0.1/src/pydicom/fileset.py    2024-09-22 03:57:27.000000000 
+0200
+++ new/pydicom-3.0.2/src/pydicom/fileset.py    2026-03-19 22:37:06.000000000 
+0100
@@ -346,24 +346,47 @@
 
         return len(fp.getvalue())
 
-    @property
-    def _file_id(self) -> Path | None:
+    def file_id_path(self, root_path: Path) -> Path | None:
         """Return the *Referenced File ID* as a :class:`~pathlib.Path`.
 
+        Params
+        ------
+        root_path : Path
+            The root path of the parent file set.
+
         Returns
         -------
         pathlib.Path or None
             The *Referenced File ID* from the directory record as a
             :class:`pathlib.Path` or ``None`` if the element value is null.
+
+        Raises
+        ------
+        PermissionError
+            If the file ID points to a path outside the fileset root path.
+
+        AttributeError
+            If the Referenced File ID is missing in the directory record.
+
+        :meta private:
         """
         if "ReferencedFileID" in self._record:
             elem = self._record["ReferencedFileID"]
+            if elem.VM < 1:
+                return None
             if elem.VM == 1:
-                return Path(cast(str, self._record.ReferencedFileID))
-            if elem.VM > 1:
-                return Path(*cast(list[str], self._record.ReferencedFileID))
+                path = Path(cast(str, self._record.ReferencedFileID))
+            else:
+                path = Path(*cast(list[str], self._record.ReferencedFileID))
 
-            return None
+            if path is not None:
+                if path.anchor or not (
+                    (root_path / path).resolve().is_relative_to(root_path)
+                ):
+                    raise PermissionError(
+                        f"ReferencedFileID ('{path}') must be inside the 
DICOMDIR root path"
+                    )
+            return path
 
         raise AttributeError("No 'Referenced File ID' in the directory record")
 
@@ -373,7 +396,7 @@
         return self.root.file_set
 
     def __getitem__(self, key: Union[str, "RecordNode"]) -> "RecordNode":
-        """Return the current node's child using it's
+        """Return the current node's child using its
         :attr:`~pydicom.fileset.RecordNode.key`
         """
         if isinstance(key, RecordNode):
@@ -525,7 +548,7 @@
             indent = indent_char * node.depth
             if node.children:
                 s.append(f"{indent}{node}")
-                # Summarise any leaves at the next level
+                # Summarize any leaves at the next level
                 for child in node.children:
                     if child.has_instance:
                         s.extend(leaf_summary(child, indent_char))
@@ -926,9 +949,8 @@
             return os.fspath(cast(Path, self._stage_path))
 
         # If not staged for addition then File Set must exist on file system
-        return os.fspath(
-            cast(Path, self.file_set.path) / cast(Path, self.node._file_id)
-        )
+        root_path = self.file_set.root_path
+        return os.fspath(root_path / cast(Path, 
self.node.file_id_path(root_path)))
 
     @property
     def SOPClassUID(self) -> UID:
@@ -962,7 +984,7 @@
             to the DICOMDIR file.
         """
         # The nominal path to the root of the File-set
-        self._path: Path | None = None
+        self._root_path: Path | None = None
         # The root node of the record tree used to fill out the DICOMDIR's
         #   *Directory Record Sequence*.
         # The tree for instances currently in the File-set
@@ -1203,7 +1225,7 @@
         """Clear the File-set."""
         self._tree.children = []
         self._instances = []
-        self._path = None
+        self._root_path = None
         self._ds = Dataset()
         self._id = None
         self._uid = generate_uid()
@@ -1635,7 +1657,7 @@
             )
 
         try:
-            path = Path(cast(str, ds.filename)).resolve(strict=True)
+            path = Path(ds.filename).resolve(strict=True)
         except FileNotFoundError:
             raise FileNotFoundError(
                 "Unable to load the File-set as the 'filename' attribute "
@@ -1661,7 +1683,7 @@
         self._charset = cast(
             str | None, ds.get("SpecificCharacterSetOfFileSetDescriptorFile", 
None)
         )
-        self._path = path.parent
+        self._root_path = path.parent
         self._ds = ds
 
         # Create the record tree
@@ -1670,20 +1692,17 @@
         bad_instances = []
         for instance in self:
             # Check that the referenced file exists
-            file_id = instance.node._file_id
-            if file_id is None:
-                bad_instances.append(instance)
-                continue
-
+            file_id = self._file_id_path(instance.node)
+            assert file_id is not None
             try:
                 # self.path is already set at this point
-                (cast(Path, self.path) / file_id).resolve(strict=True)
+                (self.root_path / file_id).resolve(strict=True)
             except FileNotFoundError:
                 bad_instances.append(instance)
                 warn_and_log(
                     "The referenced SOP Instance for the directory record at "
                     f"offset {instance.node._offset} does not exist: "
-                    f"{cast(Path, self.path) / file_id}"
+                    f"{self.root_path / file_id}"
                 )
                 continue
 
@@ -1695,6 +1714,31 @@
         for instance in bad_instances:
             self._instances.remove(instance)
 
+    def _file_id_path(self, node: RecordNode) -> Path | None:
+        """Return the *Referenced File ID* from the given node
+        as a :class:`~pathlib.Path`.
+
+        Parameters
+        ----------
+        node: RecordNode
+            The node where the *Referenced File ID* resides.
+
+        Returns
+        -------
+        pathlib.Path or None
+            The *Referenced File ID* from the directory record as a
+            :class:`pathlib.Path` or ``None`` if the element value is null.
+
+        Raises
+        ------
+        PermissionError
+            If the file ID points to a path outside the fileset root path.
+
+        AttributeError
+            If the Referenced File ID is missing in the directory record.
+        """
+        return node.file_id_path(self.root_path)
+
     def _parse_records(
         self, ds: Dataset, include_orphans: bool, raise_orphans: bool = False
     ) -> None:
@@ -1748,7 +1792,10 @@
                 del node.parent[node]
 
             # The leaf node references the FileInstance
-            if "ReferencedFileID" in node._record:
+            if (
+                "ReferencedFileID" in node._record
+                and self._file_id_path(node) is not None
+            ):
                 node.instance = FileInstance(node)
                 self._instances.append(node.instance)
 
@@ -1781,12 +1828,11 @@
         for node in missing:
             # Get the path to the orphaned instance
             original_value = node._record.ReferencedFileID
-            file_id = node._file_id
-            if file_id is None:
+            if (file_id := self._file_id_path(node)) is None:
                 continue
 
             # self.path is set for an existing File Set
-            path = cast(Path, self.path) / file_id
+            path = self.root_path / file_id
             if node.record_type == "PRIVATE":
                 instance = self.add_custom(path, node)
             else:
@@ -1796,14 +1842,29 @@
             instance.node._record.ReferencedFileID = original_value
 
     @property
+    def root_path(self) -> Path:
+        """Return the absolute path to the File-set root directory as
+        :class:`pathlib.Path`.
+
+        Raises
+        ------
+        AttributeError
+            If the root path is not set.
+        """
+        if self._root_path is None:
+            raise AttributeError("No root path set in the File-set")
+
+        return self._root_path
+
+    @property
     def path(self) -> str | None:
         """Return the absolute path to the File-set root directory as
         :class:`str` (if set) or ``None`` otherwise.
         """
-        if self._path is not None:
-            return os.fspath(self._path)
+        if self._root_path is not None:
+            return os.fspath(self._root_path)
 
-        return self._path
+        return None
 
     def _recordify(self, ds: Dataset) -> Iterator[Dataset]:
         """Yield directory records for a SOP Instance.
@@ -2062,14 +2123,15 @@
             )
 
         if path:
-            self._path = Path(path)
+            self._root_path = Path(path)
 
         # Don't write unless changed or new
         if not self.is_staged:
             return
 
         # Path to the DICOMDIR file
-        p = cast(Path, self._path) / "DICOMDIR"
+        root = self.root_path
+        p = root / "DICOMDIR"
 
         # Re-use the existing directory structure if only moves or removals
         #   are required and `use_existing` is True
@@ -2117,23 +2179,31 @@
         #   and copy any to the stage
         fout = {Path(ii.FileID) for ii in self}
         fin = {
-            ii.node._file_id for ii in self if ii.SOPInstanceUID not in 
self._stage["+"]
+            self._file_id_path(ii.node)
+            for ii in self
+            if ii.SOPInstanceUID not in self._stage["+"]
         }
         collisions = fout & fin
-        for instance in [ii for ii in self if ii.node._file_id in collisions]:
+        for instance in [
+            ii for ii in self if self._file_id_path(ii.node) in collisions
+        ]:
             self._stage["+"][instance.SOPInstanceUID] = instance
             instance._apply_stage("+")
-            shutil.copyfile(self._path / instance.node._file_id, instance.path)
+            shutil.copyfile(
+                root / cast(Path, self._file_id_path(instance.node)),
+                instance.path,
+            )
 
         for instance in self:
-            dst = self._path / instance.FileID
+            dst = root / instance.FileID
             dst.parent.mkdir(parents=True, exist_ok=True)
             fn: Callable
+            src: Path | str
             if instance.SOPInstanceUID in self._stage["+"]:
                 src = instance.path
                 fn = shutil.copyfile
             else:
-                src = self._path / instance.node._file_id
+                src = root / cast(Path, self._file_id_path(instance.node))
                 fn = shutil.move
 
             fn(os.fspath(src), os.fspath(dst))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pydicom-3.0.1/tests/conftest.py 
new/pydicom-3.0.2/tests/conftest.py
--- old/pydicom-3.0.1/tests/conftest.py 2024-09-22 03:57:27.000000000 +0200
+++ new/pydicom-3.0.2/tests/conftest.py 2026-03-19 22:37:06.000000000 +0100
@@ -22,6 +22,14 @@
 
 
 @pytest.fixture
+def ignore_reading_invalid_values():
+    value = config.settings.reading_validation_mode
+    config.settings.reading_validation_mode = config.IGNORE
+    yield
+    config.settings.reading_validation_mode = value
+
+
[email protected]
 def enforce_writing_invalid_values():
     value = config.settings.writing_validation_mode
     config.settings.writing_validation_mode = config.RAISE
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pydicom-3.0.1/tests/test_dataset.py 
new/pydicom-3.0.2/tests/test_dataset.py
--- old/pydicom-3.0.1/tests/test_dataset.py     2024-09-22 03:57:27.000000000 
+0200
+++ new/pydicom-3.0.2/tests/test_dataset.py     2026-03-19 22:37:06.000000000 
+0100
@@ -2987,7 +2987,7 @@
 
         msg = (
             r"Error deepcopying the buffered element \(7FE0,0010\) 'Pixel 
Data': "
-            r"cannot (.*) '_io.BufferedReader' object"
+            r"cannot (.*)BufferedReader"
         )
         with pytest.raises(TypeError, match=msg):
             copy.deepcopy(ds)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pydicom-3.0.1/tests/test_fileset.py 
new/pydicom-3.0.2/tests/test_fileset.py
--- old/pydicom-3.0.1/tests/test_fileset.py     2024-09-22 03:57:27.000000000 
+0200
+++ new/pydicom-3.0.2/tests/test_fileset.py     2026-03-19 22:37:06.000000000 
+0100
@@ -1,4 +1,5 @@
 import os
+import platform
 import sys
 from pathlib import Path
 import shutil
@@ -9,7 +10,7 @@
 from pydicom import dcmread
 from pydicom.data import get_testdata_file
 from pydicom.dataset import Dataset, FileMetaDataset
-from pydicom.filebase import DicomBytesIO
+from pydicom.filebase import DicomBytesIO, DicomFileLike
 from pydicom.fileset import (
     FileSet,
     FileInstance,
@@ -106,6 +107,56 @@
     return TemporaryDirectory()
 
 
+FILESET_ROOT = "/path/to/fileset/"
+ABS_FILE_PATH = "/secret.txt"
+SYMLINK_TO_ABS_FILE = "Pat1/St1/Im2"
+SYMLINK_TO_ABS_DIR = "Pat1/St2"
+DOT_DOT_FILE = "../goback.txt"
+ABS_FILE_CONTENTS = "Top Secret file contents"
+COPY_PATH = "/path/to/copied/"
+
+
[email protected](
+    params=[
+        ABS_FILE_PATH,
+        DOT_DOT_FILE,
+        SYMLINK_TO_ABS_FILE,
+        SYMLINK_TO_ABS_DIR + ABS_FILE_PATH,
+    ]
+)
+def fileset_fs(request, fs, ignore_reading_invalid_values):
+    """Create an in-memory file system with pyfakefs and test DICOMDIRs"""
+    # Simplified version of submitted report from JeongAhn Jang, in pyfakefs
+    orig_dicomdir_root = Path(TEST_FILE).parent
+    dicomdir_root = Path(FILESET_ROOT)
+    fs.add_real_file(
+        orig_dicomdir_root / "77654033/CR1/6154",
+        target_path=dicomdir_root / "Pat1/St1/Im1",
+    )
+    fs.create_file(ABS_FILE_PATH, contents=ABS_FILE_CONTENTS)
+    fs.create_dir(COPY_PATH)
+    fs.create_symlink(dicomdir_root / SYMLINK_TO_ABS_FILE, ABS_FILE_PATH)
+    fs.create_symlink(dicomdir_root / SYMLINK_TO_ABS_DIR, "/")
+    # MAKE DICOMDIR for this simplified file-set
+    fset = FileSet()
+    fset.add(dicomdir_root / "Pat1/St1/Im1")
+    fset.write(dicomdir_root)
+
+    # Create bad DICOMDIR2 file from the simplified one
+    # Modify first referenced file
+    fset = FileSet(dicomdir_root / "DICOMDIR")
+    record = next(
+        rec for rec in fset._ds.DirectoryRecordSequence if "ReferencedFileID" 
in rec
+    )
+    record.ReferencedFileID = request.param
+
+    # Write modified DICOMDIR file
+    with open(dicomdir_root / "DICOMDIR2", "wb") as fp:
+        fset._write_dicomdir(DicomFileLike(fp))
+
+    yield fs
+
+
 @pytest.fixture
 def custom_leaf():
     """Return the leaf node from a custom 4-level record hierarchy"""
@@ -139,7 +190,7 @@
 
 
 @pytest.fixture
-def private(dicomdir):
+def private(dicomdir, request, ignore_reading_invalid_values):
     """Return a DICOMDIR dataset with PRIVATE records."""
 
     def write_record(ds):
@@ -167,13 +218,17 @@
     middle = private_record()
     bottom = private_record()
     bottom.ReferencedSOPClassUIDInFile = "1.2.3.4"
-    bottom.ReferencedFileID = [
-        "TINY_ALPHA",
-        "PT000000",
-        "ST000000",
-        "SE000000",
-        "IM000000",
-    ]
+    if hasattr(request, "param"):
+        file_ids = request.param
+    else:
+        file_ids = [
+            "TINY_ALPHA",
+            "PT000000",
+            "ST000000",
+            "SE000000",
+            "IM000000",
+        ]
+    bottom.ReferencedFileID = file_ids
     bottom.ReferencedSOPInstanceUIDInFile = (
         "1.2.276.0.7230010.3.1.4.0.31906.1359940846.78187"
     )
@@ -683,6 +738,15 @@
         with pytest.raises(AttributeError, match=msg):
             instance.node.key
 
+    @pytest.mark.parametrize("private", [["/", "etc", "passwd"]], 
indirect=True)
+    def test_id_outside_root(self, private):
+        """File ID points to a path outside the root directory."""
+        with pytest.raises(
+            PermissionError,
+            match=r"ReferencedFileID .* must be inside the DICOMDIR root path",
+        ):
+            FileSet(private)
+
     def test_bad_record(self, private):
         """Test a bad directory record raises an exception when loading."""
         del private.DirectoryRecordSequence[0].PatientID
@@ -745,7 +809,33 @@
         item.ReferencedFileID = "01"
         ds.save_as(p / "DICOMDIR", overwrite=True)
         fs = FileSet(ds)
-        assert fs._instances[0].node._file_id == Path("01")
+        assert fs._instances[0].node.file_id_path(fs.root_path) == Path("01")
+
+    def test_absolute_file_id(self, ct, tdir, ignore_reading_invalid_values):
+        """Test a singleton File ID."""
+        fs = FileSet()
+        p = Path(tdir.name)
+        ct.save_as(p / "01")
+        fs.add(p / "01")
+        fs.write(p)
+        ds = dcmread(p / "DICOMDIR")
+        item = ds.DirectoryRecordSequence[-1]
+        item.ReferencedFileID = "/01"
+        ds.save_as(p / "DICOMDIR", overwrite=True)
+        with pytest.raises(
+            PermissionError,
+            match=r"ReferencedFileID .* must be inside the DICOMDIR root path",
+        ):
+            FileSet(ds)
+
+    def test_root_path_missing(self, ct):
+        """Test RecordNode._file_id if no Referenced File ID."""
+        fs = FileSet()
+        instance = fs.add(ct)
+        # del instance.node._record.ReferencedFileID
+        msg = r"No root path set in the File-set"
+        with pytest.raises(AttributeError, match=msg):
+            fs.root_path
 
     def test_file_id_missing(self, ct):
         """Test RecordNode._file_id if no Referenced File ID."""
@@ -754,7 +844,7 @@
         del instance.node._record.ReferencedFileID
         msg = r"No 'Referenced File ID' in the directory record"
         with pytest.raises(AttributeError, match=msg):
-            instance.node._file_id
+            instance.node.file_id_path(Path("/dicom_data"))
 
 
 class TestFileInstance:
@@ -1673,7 +1763,7 @@
         assert "ISO 1" == fs.descriptor_character_set
         assert [] != fs._instances
         assert fs._id is not None
-        assert fs._path is not None
+        assert fs.root_path is not None
         uid = fs._uid
         assert fs._uid is not None
         assert fs._ds is not None
@@ -1684,7 +1774,7 @@
         fs.clear()
         assert [] == fs._instances
         assert fs._id is None
-        assert fs._path is None
+        assert fs._root_path is None
         assert uid != fs._uid
         assert fs._uid.is_valid
         assert fs._ds == Dataset()
@@ -2328,14 +2418,14 @@
         tdir, ds = dicomdir_copy
         assert 52 == len(ds.DirectoryRecordSequence)
         fs = FileSet(ds)
-        orig_paths = [p for p in fs._path.glob("**/*") if p.is_file()]
+        orig_paths = [p for p in fs.root_path.glob("**/*") if p.is_file()]
         instance = fs._instances[0]
         assert Path(instance.path) in orig_paths
         fs.remove(instance)
         orig_file_ids = [ii.ReferencedFileID for ii in fs]
         fs.write(use_existing=True)
         assert 50 == len(fs._ds.DirectoryRecordSequence)
-        paths = [p for p in fs._path.glob("**/*") if p.is_file()]
+        paths = [p for p in fs.root_path.glob("**/*") if p.is_file()]
         assert orig_file_ids == [ii.ReferencedFileID for ii in fs]
         assert Path(instance.path) not in paths
         assert sorted(orig_paths)[1:] == sorted(paths)
@@ -2487,6 +2577,18 @@
     def teardown_method(self):
         FileSet.__len__ = self.orig
 
+    @pytest.mark.skipif(
+        platform.python_implementation() == "PyPy",
+        reason="pyfakefs does not work with generate_uid() in PyPy",
+    )
+    def test_constrained_to_fileset_root(self, fileset_fs):
+        """Ensure files cannot be copied outside the FileSet root"""
+        with pytest.raises(
+            PermissionError,
+            match=r"ReferencedFileID .* must be inside the DICOMDIR root path",
+        ):
+            FileSet(Path(FILESET_ROOT) / "DICOMDIR2")
+
     def test_copy(self, dicomdir, tdir):
         """Test FileSet.copy()"""
         orig_root = Path(dicomdir.filename).parent

Reply via email to