Skip to content

Commit c1732d1

Browse files
feat: add permissive uid validation for DicomWebClient
1 parent ded157f commit c1732d1

File tree

2 files changed

+55
-6
lines changed

2 files changed

+55
-6
lines changed

src/dicomweb_client/web.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737

3838
logger = logging.getLogger(__name__)
3939

40+
# For DICOM Standard spec validation of UID components in `DICOMwebClient`.
41+
_REGEX_UID = re.compile(r'[0-9]+([.][0-9]+)*')
42+
_REGEX_PERMISSIVE_UID = re.compile(r'[^/@]+')
43+
4044

4145
def _load_xml_dataset(dataset: Element) -> pydicom.dataset.Dataset:
4246
"""Load DICOM Data Set in DICOM XML format.
@@ -200,7 +204,8 @@ def __init__(
200204
proxies: Optional[Dict[str, str]] = None,
201205
headers: Optional[Dict[str, str]] = None,
202206
callback: Optional[Callable] = None,
203-
chunk_size: int = 10**6
207+
chunk_size: int = 10**6,
208+
permissive: bool = False
204209
) -> None:
205210
"""Instatiate client.
206211
@@ -235,6 +240,14 @@ def __init__(
235240
when streaming data from the server using chunked transfer encoding
236241
(used by ``iter_*()`` methods as well as the ``store_instances()``
237242
method); defaults to ``10**6`` bytes (1MB)
243+
permissive: bool, optional
244+
If ``True``, relaxes the DICOM Standard validation for UIDs (see
245+
main docstring for details). This option is made available since
246+
users may be occasionally forced to work with DICOMs or services
247+
that may be in violation of the standard. Unless required, use of
248+
this flag is **not** recommended, since non-conformant UIDs may
249+
lead to unexpected errors downstream, e.g., rejection by a DICOMweb
250+
server, etc.
238251
239252
Warning
240253
-------
@@ -314,6 +327,7 @@ def __init__(
314327
if callback is not None:
315328
self._session.hooks = {'response': [callback, ]}
316329
self._chunk_size = chunk_size
330+
self._permissive = permissive
317331
self.set_http_retry_params()
318332

319333
def _get_transaction_url(self, transaction: _Transaction) -> str:
@@ -2176,15 +2190,18 @@ def _assert_uid_format(self, uid: str) -> None:
21762190
TypeError
21772191
When `uid` is not a string
21782192
ValueError
2179-
When `uid` doesn't match the regular expression pattern
2180-
``"^[.0-9]+$"``
2193+
When `uid` doesn't match the regular expression pattern for
2194+
DICOM UIDs (strict or permissive regex)
21812195
21822196
"""
21832197
if not isinstance(uid, str):
21842198
raise TypeError('DICOM UID must be a string.')
2185-
pattern = re.compile('^[.0-9]+$')
2186-
if not pattern.search(uid):
2187-
raise ValueError('DICOM UID has invalid format.')
2199+
if not self._permissive and _REGEX_UID.fullmatch(uid) is None:
2200+
raise ValueError(f'UID {uid!r} must match regex {_REGEX_UID!r} in '
2201+
'conformance with the DICOM Standard.')
2202+
elif self._permissive and _REGEX_PERMISSIVE_UID.fullmatch(uid) is None:
2203+
raise ValueError(f'Permissive mode is enabled. UID {uid!r} must match '
2204+
f'regex {_REGEX_PERMISSIVE_UID!r}.')
21882205

21892206
def search_for_series(
21902207
self,

tests/test_web.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1608,3 +1608,35 @@ def test_load_xml_response(httpserver, client, cache_dir):
16081608
dataset = _load_xml_dataset(tree)
16091609
assert dataset.RetrieveURL.startswith('https://wadors.hospital.com')
16101610
assert len(dataset.ReferencedSOPSequence) == 2
1611+
1612+
1613+
@pytest.mark.parametrize('invalid_uid', [
1614+
'1.2/3.4', '[email protected]', '1.2.a.4', '1.2.A.4', '.23.4', '1.2..4', '1.2.', '.'
1615+
])
1616+
def test_uid_strict_validation_fail(httpserver, invalid_uid):
1617+
client = DICOMwebClient(httpserver.url, permissive=False)
1618+
with pytest.raises(ValueError,
1619+
match=f'UID {invalid_uid!r} must match regex'):
1620+
client.search_for_series(study_instance_uid=invalid_uid)
1621+
1622+
1623+
@pytest.mark.parametrize('uid', ['13-abc', 'hello', 'is it', "you're me"])
1624+
def test_uid_permissive_validation_pass(httpserver, uid):
1625+
client_strict = DICOMwebClient(httpserver.url, permissive=False)
1626+
client_permissive = DICOMwebClient(httpserver.url, permissive=True)
1627+
1628+
# Should fail in strict mode
1629+
with pytest.raises(ValueError, match=f'UID {uid!r} must match regex'):
1630+
client_strict.search_for_series(study_instance_uid=uid)
1631+
1632+
# Should not raise exception in permissive mode
1633+
resp = client_permissive.search_for_series(study_instance_uid=uid)
1634+
assert isinstance(resp, list)
1635+
1636+
1637+
@pytest.mark.parametrize('uid', ['1.23.5@', '1/23.4'])
1638+
def test_uid_permissive_validation_fail(httpserver, uid):
1639+
client = DICOMwebClient(httpserver.url, permissive=True)
1640+
with pytest.raises(ValueError,
1641+
match=f'UID {uid!r} must match regex'):
1642+
client.search_for_series(study_instance_uid=uid)

0 commit comments

Comments
 (0)