Repository: pydicom/pydicom

Issue:
The function generate_uid() generates non-conforming “2.25 .” DICOM UIDs
<!-- Instructions For Filing a Bug: https://github.com/pydicom/pydicom/blob/master/CONTRIBUTING.md#filing-bugs -->

#### Description
It seems there was already a discussion about this function in the past (#125), but the current implementation generates non-conforming DICOM UIDs when called with prefix ‘none’ to trigger that the function generate_uid() should generate a UUID derived UID.

The DICOM Standard requires (see DICOM PS 3.5, B.2 that when a UUID derived UID is constructed it should be in the format “2.25.” + uuid(in its decimal representation string representation)
For example a UUID of f81d4fae-7dec-11d0-a765-00a0c91e6bf6 should become 2.25.329800735698586629295641978511506172918

The current implementation extends the uuid part to the remaining 59 characters. By not following the DICOM formatting rule, receiving systems that are processing DICOM instances created with this library are not capable of converting the generated “2.25” UID back to a UUID. Due to the extra sha512 operation on the UUID, the variant and version info of the UUID are also lost.

#### Steps/Code to Reproduce
- call generate_uid() to generate a "2.25." DICOM UID

#### Expected Results
A conforming unique DICOM UID is returned.

#### Actual Results
Non conforming UID is returned.


Discussion:
Thanks for pointing this out - I wasn't aware of that section of the standard.  Do you know of a solution that respects the privacy issues pointed out in #125? 

> By not following the DICOM formatting rule, receiving systems that are processing DICOM instances created with this library are not capable of converting the generated “2.25” UID back to a UUID

Why would it be necessary for the receiving software to generate a "real UUID" from this (and therefore the variant and the version)?  Just curious, as if we want to be standard compliant it doesn't really matter.



I think the solution proposed by @cancan101 in #125 to use the uuid v4 algorithm is fine. The uuid v1 algorithm leaks the MAC address. The open source dcm4che (Java) implementation for example also uses the uuid v4 algorithm, the two C# implementation I known of use the .NET Guid.NewGuid() method, which will also returns v4 uuids. 

DICOM is all about interoperability, there may be receiving systems that (implicitly) depend on it. A Level 2 (Full) C-STORE SCP may, (but is not required) validate the attributes of an incoming SOP instance. Personally, I have never encountered a DICOM system that had trouble with it, most systems just threat a UID as 64 bytes and are happy with it as long as it is unique.

Possible (performance) scenario
Large DICOM archives need to maintain a relational database to maintain which images are stored in the system. To ensure data integrity, these systems put often a constraint on the uniqueness of the SOP Instance UID column. Most DBMS systems will create a non-cluster index to ensure that this constraint can be met. A uuid is only 16 bytes, compared to a UID that is 64 bytes, which can make the difference of keeping the index in memory or not. Some DBMS systems have a native data type to support uuid columns. The complication is of course that these systems also need to support images with “Organizationally Derived” UIDs and this optimization only makes sense if a majority of UIDs are uuid derived UIDs.

Patch:
diff --git a/pydicom/uid.py b/pydicom/uid.py
--- a/pydicom/uid.py
+++ b/pydicom/uid.py
@@ -250,19 +250,19 @@ def generate_uid(prefix=PYDICOM_ROOT_UID, entropy_srcs=None):
     ----------
     prefix : str or None
         The UID prefix to use when creating the UID. Default is the pydicom
-        root UID '1.2.826.0.1.3680043.8.498.'. If None then a value of '2.25.'
-        will be used (as described on `David Clunie's website
-        <http://www.dclunie.com/medical-image-faq/html/part2.html#UID>`_).
+        root UID '1.2.826.0.1.3680043.8.498.'. If None then a prefix of '2.25.'
+        will be used with the integer form of a UUID generated using the
+        UUID4 algorithm.
     entropy_srcs : list of str or None
-        If a list of str, the prefix will be appended with a SHA512 hash of the
-        list which means the result is deterministic and should make the
-        original data unrecoverable. If None random data will be used
-        (default).
+        If `prefix` is not None, then the prefix will be appended with a
+        SHA512 hash of the list which means the result is deterministic and
+        should make the original data unrecoverable. If None random data will
+        be used (default).
 
     Returns
     -------
     pydicom.uid.UID
-        A 64 character DICOM UID.
+        A DICOM UID of up to 64 characters.
 
     Raises
     ------
@@ -275,17 +275,17 @@ def generate_uid(prefix=PYDICOM_ROOT_UID, entropy_srcs=None):
     >>> generate_uid()
     1.2.826.0.1.3680043.8.498.22463838056059845879389038257786771680
     >>> generate_uid(prefix=None)
-    2.25.12586835699909622925962004639368649121731805922235633382942
+    2.25.167161297070865690102504091919570542144
     >>> generate_uid(entropy_srcs=['lorem', 'ipsum'])
     1.2.826.0.1.3680043.8.498.87507166259346337659265156363895084463
     >>> generate_uid(entropy_srcs=['lorem', 'ipsum'])
     1.2.826.0.1.3680043.8.498.87507166259346337659265156363895084463
     """
-    max_uid_len = 64
-
     if prefix is None:
-        prefix = '2.25.'
+        # UUID -> as 128-bit int -> max 39 characters long
+        return UID('2.25.{}'.format(uuid.uuid4().int))
 
+    max_uid_len = 64
     if len(prefix) > max_uid_len - 1:
         raise ValueError("The prefix must be less than 63 chars")
     if not re.match(RE_VALID_UID_PREFIX, prefix):


Test patch:
diff --git a/pydicom/tests/test_uid.py b/pydicom/tests/test_uid.py
--- a/pydicom/tests/test_uid.py
+++ b/pydicom/tests/test_uid.py
@@ -1,6 +1,8 @@
 # Copyright 2008-2018 pydicom authors. See LICENSE file for details.
 """Test suite for uid.py"""
 
+import uuid
+
 import pytest
 
 from pydicom.uid import UID, generate_uid, PYDICOM_ROOT_UID, JPEGLSLossy
@@ -57,6 +59,24 @@ def test_entropy_src_custom(self):
         assert uid == rf
         assert len(uid) == 64
 
+    def test_none(self):
+        """Test generate_uid(None)."""
+        uid = generate_uid(prefix=None)
+        # Check prefix
+        assert '2.25.' == uid[:5]
+        # Check UUID suffix
+        as_uuid = uuid.UUID(int=int(uid[5:]))
+        assert isinstance(as_uuid, uuid.UUID)
+        assert as_uuid.version == 4
+        assert as_uuid.variant == uuid.RFC_4122
+
+    def test_none_iterate(self):
+        """Test generate_uid(None) generates valid UIDs."""
+        # Generate random UIDs, if a bad method then should eventually fail
+        for ii in range(100000):
+            uid = generate_uid(None)
+            assert uid.is_valid
+
 
 class TestUID(object):
     """Test DICOM UIDs"""