5858from collections .abc import Generator , Iterable , Iterator
5959from typing import IO , Any
6060
61- # For device capacity reading in query_device_capacity(). Only supported
62- # on posix systems for now. Will be addressed in issue #52 on GitHub.
61+ # For device capacity reading in query_device_capacity().
6362if os .name == 'posix' :
6463 import stat
6564 import fcntl
6665 import struct
66+ elif os .name == 'nt' :
67+ import ctypes
68+ import ctypes .wintypes
69+ import msvcrt
6770
71+ #: Platforms where :func:`query_device_capacity` is supported.
72+ #: Corresponds to possible values of :data:`os.name`.
73+ SUPPORTED_PLATFORMS = frozenset ({'posix' , 'nt' })
6874
6975__all__ = ['Bit' , 'Byte' , 'KiB' , 'MiB' , 'GiB' , 'TiB' , 'PiB' , 'EiB' , 'ZiB' , 'YiB' ,
7076 'kB' , 'MB' , 'GB' , 'TB' , 'PB' , 'EB' , 'ZB' , 'YB' , 'Kib' ,
@@ -1285,6 +1291,56 @@ def best_prefix(bytes: Bitmath | int | float, system: int = NIST) -> Bitmath:
12851291 return Byte (value ).best_prefix (system = system )
12861292
12871293
1294+ def _query_device_capacity_windows (device_fd : IO [Any ]) -> int :
1295+ """Return device capacity in bytes on Windows via DeviceIoControl.
1296+
1297+ Windows physical disk paths look like ``\\ \\ .\\ PhysicalDrive0``.
1298+ Raises :class:`ValueError` if the file descriptor is not a physical device.
1299+ Raises :class:`OSError` if the DeviceIoControl call fails.
1300+ """
1301+ if not device_fd .name .startswith ('\\ \\ .\\ ' ):
1302+ raise ValueError ("The file descriptor provided is not of a device type" )
1303+
1304+ IOCTL_DISK_GET_DRIVE_GEOMETRY_EX = 0x000700A0
1305+
1306+ class DISK_GEOMETRY (ctypes .Structure ):
1307+ _fields_ = [
1308+ ('Cylinders' , ctypes .c_longlong ),
1309+ ('MediaType' , ctypes .c_uint ),
1310+ ('TracksPerCylinder' , ctypes .c_ulong ),
1311+ ('SectorsPerTrack' , ctypes .c_ulong ),
1312+ ('BytesPerSector' , ctypes .c_ulong ),
1313+ ]
1314+
1315+ class DISK_GEOMETRY_EX (ctypes .Structure ):
1316+ _fields_ = [
1317+ ('Geometry' , DISK_GEOMETRY ),
1318+ ('DiskSize' , ctypes .c_longlong ),
1319+ ('Data' , ctypes .c_byte * 1 ),
1320+ ]
1321+
1322+ geometry = DISK_GEOMETRY_EX ()
1323+ bytes_returned = ctypes .wintypes .DWORD (0 )
1324+ handle = msvcrt .get_osfhandle (device_fd .fileno ())
1325+
1326+ result = ctypes .windll .kernel32 .DeviceIoControl (
1327+ handle ,
1328+ IOCTL_DISK_GET_DRIVE_GEOMETRY_EX ,
1329+ None ,
1330+ 0 ,
1331+ ctypes .byref (geometry ),
1332+ ctypes .sizeof (geometry ),
1333+ ctypes .byref (bytes_returned ),
1334+ None ,
1335+ )
1336+
1337+ if not result :
1338+ error_code = ctypes .windll .kernel32 .GetLastError ()
1339+ raise OSError (f"DeviceIoControl failed with error code: { error_code } " )
1340+
1341+ return geometry .DiskSize
1342+
1343+
12881344def query_device_capacity (device_fd : IO [Any ]) -> Byte :
12891345 """Create bitmath instances of the capacity of a system block device
12901346
@@ -1300,13 +1356,16 @@ def query_device_capacity(device_fd: IO[Any]) -> Byte:
13001356* http://stackoverflow.com/a/9764508/263969
13011357
13021358 :param file device_fd: A ``file`` object of the device to query the
1303- capacity of (as in ``get_device_capacity(open("/dev/sda"))``).
1359+ capacity of. On Linux/macOS: ``open("/dev/sda", "rb")``. On Windows:
1360+ ``open(r'\\ \\ .\\ PhysicalDrive0', 'rb')`` (requires administrator privileges).
13041361
13051362 :return: a bitmath :class:`bitmath.Byte` instance equivalent to the
13061363 capacity of the target device in bytes.
13071364"""
1308- if os .name != 'posix' :
1365+ if os .name not in SUPPORTED_PLATFORMS :
13091366 raise NotImplementedError (f"'bitmath.query_device_capacity' is not supported on this platform: { os .name } " )
1367+ if os .name == 'nt' :
1368+ return Byte (_query_device_capacity_windows (device_fd ))
13101369
13111370 s = os .stat (device_fd .name ).st_mode
13121371 if not stat .S_ISBLK (s ):
0 commit comments