rawblock-io 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rawblock_io/__init__.py +41 -0
- rawblock_io/_backing_file.py +84 -0
- rawblock_io/_dd.py +59 -0
- rawblock_io/_direct_io.py +17 -0
- rawblock_io/_io.py +43 -0
- rawblock_io/_resolve.py +57 -0
- rawblock_io/_resolve_darwin.py +42 -0
- rawblock_io/_resolve_linux.py +30 -0
- rawblock_io/_strategies.py +80 -0
- rawblock_io-0.1.0.dist-info/METADATA +54 -0
- rawblock_io-0.1.0.dist-info/RECORD +13 -0
- rawblock_io-0.1.0.dist-info/WHEEL +5 -0
- rawblock_io-0.1.0.dist-info/top_level.txt +1 -0
rawblock_io/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Raw block device I/O with automatic strategy fallback.
|
|
2
|
+
|
|
3
|
+
Provides a pluggable I/O strategy chain for reading/writing raw block
|
|
4
|
+
devices and disk image files, along with cross-platform device and
|
|
5
|
+
mount-point resolution.
|
|
6
|
+
|
|
7
|
+
Layers (one file per layer):
|
|
8
|
+
``_resolve`` — Block device / mount point resolution
|
|
9
|
+
``_strategies`` — Pluggable I/O strategy base class & helpers
|
|
10
|
+
``_direct_io`` — ``DirectIOStrategy``
|
|
11
|
+
``_backing_file`` — ``BackingFileStrategy``
|
|
12
|
+
``_dd`` — ``DDStrategy``
|
|
13
|
+
``_io`` — ``RawBlockIO`` (strategy chain)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from rawblock_io._strategies import (
|
|
17
|
+
IOStrategy,
|
|
18
|
+
DirectIOStrategy,
|
|
19
|
+
BackingFileStrategy,
|
|
20
|
+
DDStrategy,
|
|
21
|
+
BLOCK_SIZE,
|
|
22
|
+
_block_align,
|
|
23
|
+
_try_pread,
|
|
24
|
+
_try_pwrite,
|
|
25
|
+
)
|
|
26
|
+
from rawblock_io._io import RawBlockIO
|
|
27
|
+
from rawblock_io._resolve import resolve_device, resolve_mount_point
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
'IOStrategy',
|
|
31
|
+
'DirectIOStrategy',
|
|
32
|
+
'BackingFileStrategy',
|
|
33
|
+
'DDStrategy',
|
|
34
|
+
'BLOCK_SIZE',
|
|
35
|
+
'_block_align',
|
|
36
|
+
'_try_pread',
|
|
37
|
+
'_try_pwrite',
|
|
38
|
+
'RawBlockIO',
|
|
39
|
+
'resolve_device',
|
|
40
|
+
'resolve_mount_point',
|
|
41
|
+
]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Backing-file strategy — reads/writes via the loop-device backing file."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import plistlib
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
from rawblock_io._strategies import IOStrategy, _try_pread, _try_pwrite
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BackingFileStrategy(IOStrategy):
|
|
12
|
+
"""Resolve the loop-device backing file and operate on that.
|
|
13
|
+
|
|
14
|
+
When the *device* is a loop device (e.g. ``/dev/loop0``) this
|
|
15
|
+
reads/writes the underlying backing file directly, bypassing
|
|
16
|
+
the kernel block layer entirely.
|
|
17
|
+
|
|
18
|
+
On macOS resolves via ``hdiutil info -plist`` instead.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self._backing_cache: dict[str, str | None] = {}
|
|
23
|
+
self._is_darwin = platform.system() == 'Darwin'
|
|
24
|
+
|
|
25
|
+
def _resolve(self, device: str) -> str | None:
|
|
26
|
+
if device not in self._backing_cache:
|
|
27
|
+
if self._is_darwin:
|
|
28
|
+
self._backing_cache[device] = self._resolve_darwin(device)
|
|
29
|
+
else:
|
|
30
|
+
self._backing_cache[device] = self._resolve_linux(device)
|
|
31
|
+
return self._backing_cache[device]
|
|
32
|
+
|
|
33
|
+
def _resolve_linux(self, device: str) -> str | None:
|
|
34
|
+
dev_name = device.lstrip('/dev/')
|
|
35
|
+
for cmd in (
|
|
36
|
+
['cat', f'/sys/block/{dev_name}/loop/backing_file'],
|
|
37
|
+
['sudo', 'cat', f'/sys/block/{dev_name}/loop/backing_file'],
|
|
38
|
+
['losetup', '-n', '-O', 'BACK-FILE', device],
|
|
39
|
+
['sudo', 'losetup', '-n', '-O', 'BACK-FILE', device],
|
|
40
|
+
):
|
|
41
|
+
try:
|
|
42
|
+
r = subprocess.run(
|
|
43
|
+
cmd, capture_output=True, text=True, timeout=5)
|
|
44
|
+
if r.returncode == 0:
|
|
45
|
+
return r.stdout.strip() or None
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
def _resolve_darwin(self, device: str) -> str | None:
|
|
51
|
+
try:
|
|
52
|
+
r = subprocess.run(
|
|
53
|
+
['hdiutil', 'info', '-plist'],
|
|
54
|
+
capture_output=True, text=True, timeout=10)
|
|
55
|
+
if r.returncode != 0:
|
|
56
|
+
return None
|
|
57
|
+
plist = plistlib.loads(r.stdout.encode())
|
|
58
|
+
for img in plist.get('images', []):
|
|
59
|
+
if not isinstance(img, dict):
|
|
60
|
+
continue
|
|
61
|
+
for ent in img.get('system-entities', []):
|
|
62
|
+
if isinstance(ent, dict) and ent.get('dev-entry') == device:
|
|
63
|
+
return img.get('image-path')
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
def read(self, device: str, offset: int, size: int) -> bytes | None:
|
|
69
|
+
backing = self._resolve(device)
|
|
70
|
+
if backing and os.access(backing, os.R_OK):
|
|
71
|
+
return _try_pread(backing, offset, size)
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
def write(self, device: str, offset: int, data: bytes) -> bool:
|
|
75
|
+
backing = self._resolve(device)
|
|
76
|
+
if backing and os.access(backing, os.W_OK):
|
|
77
|
+
return _try_pwrite(backing, offset, data)
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
def clear_cache(self, device: str | None = None):
|
|
81
|
+
if device is None:
|
|
82
|
+
self._backing_cache.clear()
|
|
83
|
+
else:
|
|
84
|
+
self._backing_cache.pop(device, None)
|
rawblock_io/_dd.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""DD strategy — falls back to ``sudo dd`` for raw block device I/O."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import tempfile
|
|
5
|
+
|
|
6
|
+
from rawblock_io._strategies import IOStrategy, BLOCK_SIZE, _block_align
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DDStrategy(IOStrategy):
|
|
10
|
+
"""Fall back to ``sudo dd`` for read/write on physical block devices.
|
|
11
|
+
|
|
12
|
+
Used when the process lacks direct access to the device and must
|
|
13
|
+
elevate privileges via ``sudo``.
|
|
14
|
+
|
|
15
|
+
I/O is always done in multiples of ``BLOCK_SIZE`` (512 bytes) to
|
|
16
|
+
support platforms where the device requires sector-aligned access
|
|
17
|
+
(e.g. macOS ``/dev/rdisk*`` raw devices).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def read(self, device: str, offset: int, size: int) -> bytes | None:
|
|
21
|
+
try:
|
|
22
|
+
aligned_off, total, skip = _block_align(offset, size)
|
|
23
|
+
cmd = ['sudo', 'dd', f'if={device}', f'bs={BLOCK_SIZE}',
|
|
24
|
+
f'skip={aligned_off // BLOCK_SIZE}',
|
|
25
|
+
f'count={total // BLOCK_SIZE}']
|
|
26
|
+
r = subprocess.run(cmd, stdout=subprocess.PIPE,
|
|
27
|
+
stderr=subprocess.DEVNULL)
|
|
28
|
+
if r.returncode != 0:
|
|
29
|
+
return None
|
|
30
|
+
return r.stdout[skip:skip + size]
|
|
31
|
+
except FileNotFoundError:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
def write(self, device: str, offset: int, data: bytes) -> bool:
|
|
35
|
+
try:
|
|
36
|
+
aligned_off, total, skip = _block_align(offset, len(data))
|
|
37
|
+
if skip == 0 and total == len(data):
|
|
38
|
+
return self._write_blocks(device, aligned_off, data)
|
|
39
|
+
buf = self.read(device, aligned_off, total)
|
|
40
|
+
if buf is None or len(buf) < total:
|
|
41
|
+
return False
|
|
42
|
+
buf = bytearray(buf)
|
|
43
|
+
buf[skip:skip + len(data)] = data
|
|
44
|
+
return self._write_blocks(device, aligned_off, bytes(buf))
|
|
45
|
+
except FileNotFoundError:
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
def _write_blocks(self, device: str, offset: int, data: bytes) -> bool:
|
|
49
|
+
with tempfile.NamedTemporaryFile() as tf:
|
|
50
|
+
tf.write(data)
|
|
51
|
+
tf.flush()
|
|
52
|
+
cmd = ['sudo', 'dd', f'if={tf.name}', f'of={device}',
|
|
53
|
+
f'bs={BLOCK_SIZE}',
|
|
54
|
+
f'seek={offset // BLOCK_SIZE}',
|
|
55
|
+
f'count={len(data) // BLOCK_SIZE}',
|
|
56
|
+
'conv=fsync']
|
|
57
|
+
subprocess.run(cmd, check=True, stdout=subprocess.PIPE,
|
|
58
|
+
stderr=subprocess.DEVNULL)
|
|
59
|
+
return True
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Direct I/O strategy — reads/writes via ``os.pread``/``os.pwrite``."""
|
|
2
|
+
|
|
3
|
+
from rawblock_io._strategies import IOStrategy, _try_pread, _try_pwrite
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DirectIOStrategy(IOStrategy):
|
|
7
|
+
"""Read/write directly on *device* via ``os.pread``/``os.pwrite``.
|
|
8
|
+
|
|
9
|
+
Works for regular files (e.g. disk images) and any block device
|
|
10
|
+
the process has permission to access.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def read(self, device: str, offset: int, size: int) -> bytes | None:
|
|
14
|
+
return _try_pread(device, offset, size)
|
|
15
|
+
|
|
16
|
+
def write(self, device: str, offset: int, data: bytes) -> bool:
|
|
17
|
+
return _try_pwrite(device, offset, data)
|
rawblock_io/_io.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Raw block I/O — delegates to a chain of pluggable strategies."""
|
|
2
|
+
|
|
3
|
+
from rawblock_io._strategies import (
|
|
4
|
+
IOStrategy,
|
|
5
|
+
DirectIOStrategy,
|
|
6
|
+
BackingFileStrategy,
|
|
7
|
+
DDStrategy,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _default_strategies() -> list[IOStrategy]:
|
|
12
|
+
return [DirectIOStrategy(), BackingFileStrategy(), DDStrategy()]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RawBlockIO:
|
|
16
|
+
"""Raw block I/O — delegates to a chain of pluggable strategies.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
strategies
|
|
21
|
+
Ordered list of ``IOStrategy`` instances. Defaults to
|
|
22
|
+
``[DirectIOStrategy(), BackingFileStrategy(), DDStrategy()]``
|
|
23
|
+
on Linux; ``[DirectIOStrategy(), DDStrategy()]`` on macOS.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, strategies: list[IOStrategy] | None = None):
|
|
27
|
+
self._strategies = strategies or _default_strategies()
|
|
28
|
+
|
|
29
|
+
def clear_cache(self, device: str | None = None):
|
|
30
|
+
for s in self._strategies:
|
|
31
|
+
s.clear_cache(device)
|
|
32
|
+
|
|
33
|
+
def read(self, device: str, offset: int, size: int) -> bytes:
|
|
34
|
+
for s in self._strategies:
|
|
35
|
+
result = s.read(device, offset, size)
|
|
36
|
+
if result is not None:
|
|
37
|
+
return result
|
|
38
|
+
return b''
|
|
39
|
+
|
|
40
|
+
def write(self, device: str, offset: int, data: bytes):
|
|
41
|
+
for s in self._strategies:
|
|
42
|
+
if s.write(device, offset, data):
|
|
43
|
+
return
|
rawblock_io/_resolve.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Platform-specific helpers to resolve a block device and mount point from a
|
|
2
|
+
file path.
|
|
3
|
+
|
|
4
|
+
Dispatches to an OS-specific submodule (``_resolve_linux`` or
|
|
5
|
+
``_resolve_darwin``) at module-load time and re-exports the public API.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import importlib
|
|
9
|
+
import platform
|
|
10
|
+
import subprocess
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
SYSTEM = platform.system()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _df_output(path: str) -> tuple[str, str, str] | None:
|
|
17
|
+
"""Return ``(device, mount_point, fstype)`` for *path* via ``df``."""
|
|
18
|
+
try:
|
|
19
|
+
if SYSTEM == 'Darwin':
|
|
20
|
+
r = subprocess.run(
|
|
21
|
+
['df', str(path)],
|
|
22
|
+
capture_output=True, text=True, timeout=5)
|
|
23
|
+
if r.returncode != 0:
|
|
24
|
+
return None
|
|
25
|
+
lines = r.stdout.strip().splitlines()
|
|
26
|
+
if len(lines) < 2:
|
|
27
|
+
return None
|
|
28
|
+
parts = lines[1].split()
|
|
29
|
+
if len(parts) < 3:
|
|
30
|
+
return None
|
|
31
|
+
device = parts[0]
|
|
32
|
+
mount_point = parts[-1]
|
|
33
|
+
stat_r = subprocess.run(
|
|
34
|
+
['stat', '-f', '%T', str(path)],
|
|
35
|
+
capture_output=True, text=True, timeout=5)
|
|
36
|
+
fstype = stat_r.stdout.strip() if stat_r.returncode == 0 else ''
|
|
37
|
+
return device, mount_point, fstype
|
|
38
|
+
else:
|
|
39
|
+
r = subprocess.run(
|
|
40
|
+
['df', '--output=fstype,target,source', str(path)],
|
|
41
|
+
capture_output=True, text=True, timeout=5)
|
|
42
|
+
if r.returncode != 0:
|
|
43
|
+
return None
|
|
44
|
+
lines = r.stdout.strip().splitlines()
|
|
45
|
+
if len(lines) < 2:
|
|
46
|
+
return None
|
|
47
|
+
cols = lines[1].split(None, 2)
|
|
48
|
+
if len(cols) < 3:
|
|
49
|
+
return None
|
|
50
|
+
return cols[2], cols[1], cols[0]
|
|
51
|
+
except (FileNotFoundError, OSError, subprocess.TimeoutExpired):
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_mod = importlib.import_module(f'._resolve_{SYSTEM.lower()}', __package__)
|
|
56
|
+
resolve_device = _mod.resolve_device
|
|
57
|
+
resolve_mount_point = _mod.resolve_mount_point
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""macOS-specific device/mount resolution."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import plistlib
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
from rawblock_io._resolve import _df_output
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def resolve_device(path: str) -> str | None:
|
|
11
|
+
info = _df_output(path)
|
|
12
|
+
if info:
|
|
13
|
+
dev = info[0]
|
|
14
|
+
backing = _resolve_backing_file_darwin(dev)
|
|
15
|
+
if backing and os.path.isfile(backing):
|
|
16
|
+
return backing
|
|
17
|
+
return dev
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _resolve_backing_file_darwin(dev_entry: str) -> str | None:
|
|
22
|
+
try:
|
|
23
|
+
r = subprocess.run(
|
|
24
|
+
['hdiutil', 'info', '-plist'],
|
|
25
|
+
capture_output=True, text=True, timeout=10)
|
|
26
|
+
if r.returncode != 0:
|
|
27
|
+
return None
|
|
28
|
+
plist = plistlib.loads(r.stdout.encode())
|
|
29
|
+
for img in plist.get('images', []):
|
|
30
|
+
if not isinstance(img, dict):
|
|
31
|
+
continue
|
|
32
|
+
for ent in img.get('system-entities', []):
|
|
33
|
+
if isinstance(ent, dict) and ent.get('dev-entry') == dev_entry:
|
|
34
|
+
return img.get('image-path')
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def resolve_mount_point(path: str) -> str | None:
|
|
41
|
+
info = _df_output(path)
|
|
42
|
+
return info[1] if info else None
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Linux-specific device/mount resolution."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def resolve_device(path: str) -> str | None:
|
|
8
|
+
st = os.stat(path)
|
|
9
|
+
major = os.major(st.st_dev)
|
|
10
|
+
minor = os.minor(st.st_dev)
|
|
11
|
+
with open('/proc/partitions') as f:
|
|
12
|
+
for line in f:
|
|
13
|
+
parts = line.split()
|
|
14
|
+
if len(parts) == 4 and parts[0].isdigit():
|
|
15
|
+
if int(parts[0]) == major and int(parts[1]) == minor:
|
|
16
|
+
return f'/dev/{parts[3]}'
|
|
17
|
+
try:
|
|
18
|
+
link = os.readlink(f'/sys/dev/block/{major}:{minor}')
|
|
19
|
+
return os.path.join('/dev', os.path.basename(link))
|
|
20
|
+
except OSError:
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def resolve_mount_point(path: str) -> str | None:
|
|
25
|
+
r = subprocess.run(
|
|
26
|
+
['findmnt', '-n', '-o', 'TARGET', '--target', str(path)],
|
|
27
|
+
capture_output=True, text=True, timeout=5)
|
|
28
|
+
if r.returncode == 0 and r.stdout.strip():
|
|
29
|
+
return r.stdout.strip()
|
|
30
|
+
return None
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Pluggable I/O strategy classes for raw block I/O.
|
|
2
|
+
|
|
3
|
+
Each strategy implements a different mechanism for reading/writing raw
|
|
4
|
+
blocks on a device path. Strategies are tried in order until one succeeds.
|
|
5
|
+
|
|
6
|
+
The base class and shared helpers live here; each concrete strategy is
|
|
7
|
+
defined in its own submodule and re-exported below.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class IOStrategy(ABC):
|
|
15
|
+
"""Pluggable read/write strategy for raw block I/O."""
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def read(self, device: str, offset: int, size: int) -> bytes | None:
|
|
19
|
+
"""Read *size* bytes from *device* at *offset*.
|
|
20
|
+
|
|
21
|
+
Return bytes on success, or ``None`` to fall through to the
|
|
22
|
+
next strategy in the chain.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def write(self, device: str, offset: int, data: bytes) -> bool | None:
|
|
27
|
+
"""Write *data* to *device* at *offset*.
|
|
28
|
+
|
|
29
|
+
Return ``True`` when handled, or ``None``/``False`` to fall
|
|
30
|
+
through to the next strategy.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def clear_cache(self, device: str | None = None):
|
|
34
|
+
"""Drop cached state for *device* (or all if ``None``)."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _try_pread(path: str, offset: int, size: int) -> bytes | None:
|
|
38
|
+
try:
|
|
39
|
+
fd = os.open(path, os.O_RDONLY)
|
|
40
|
+
try:
|
|
41
|
+
return os.pread(fd, size, offset)
|
|
42
|
+
finally:
|
|
43
|
+
os.close(fd)
|
|
44
|
+
except OSError:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _try_pwrite(path: str, offset: int, data: bytes) -> bool:
|
|
49
|
+
try:
|
|
50
|
+
fd = os.open(path, os.O_WRONLY)
|
|
51
|
+
try:
|
|
52
|
+
n = os.pwrite(fd, data, offset)
|
|
53
|
+
assert n == len(data)
|
|
54
|
+
os.fsync(fd)
|
|
55
|
+
finally:
|
|
56
|
+
os.close(fd)
|
|
57
|
+
return True
|
|
58
|
+
except OSError:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
BLOCK_SIZE = 512
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _block_align(offset: int, size: int) -> tuple[int, int, int]:
|
|
66
|
+
"""Return ``(aligned_offset, total_bytes, prefix_skip)``.
|
|
67
|
+
|
|
68
|
+
Rounds *offset* down and *size* up so the resulting region is aligned
|
|
69
|
+
to ``BLOCK_SIZE``. *prefix_skip* is the number of bytes before the
|
|
70
|
+
caller's data in the first block.
|
|
71
|
+
"""
|
|
72
|
+
aligned = (offset // BLOCK_SIZE) * BLOCK_SIZE
|
|
73
|
+
end = offset + size
|
|
74
|
+
aligned_end = ((end + BLOCK_SIZE - 1) // BLOCK_SIZE) * BLOCK_SIZE
|
|
75
|
+
return aligned, aligned_end - aligned, offset - aligned
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
from rawblock_io._direct_io import DirectIOStrategy # noqa: E402
|
|
79
|
+
from rawblock_io._backing_file import BackingFileStrategy # noqa: E402
|
|
80
|
+
from rawblock_io._dd import DDStrategy # noqa: E402
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rawblock-io
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Raw block device I/O with automatic strategy fallback and cross-platform device/mount resolution
|
|
5
|
+
Author-email: Michael Banucu <michael.banucu@googlemail.com>
|
|
6
|
+
License: GPL-3.0-only
|
|
7
|
+
Project-URL: Homepage, https://github.com/MBanucu/rawblock-io
|
|
8
|
+
Project-URL: Repository, https://github.com/MBanucu/rawblock-io
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: System :: Filesystems
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# rawblock-io
|
|
24
|
+
|
|
25
|
+
Raw block device I/O with automatic strategy fallback and cross-platform
|
|
26
|
+
device/mount point resolution.
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
- **Pluggable I/O strategies** — tries direct access first, falls back
|
|
31
|
+
through loop-device backing file to `sudo dd`
|
|
32
|
+
- **`DirectIOStrategy`** — `os.pread`/`os.pwrite` on regular files and
|
|
33
|
+
accessible block devices
|
|
34
|
+
- **`BackingFileStrategy`** — resolves loop-device backing files (`/sys/block`,
|
|
35
|
+
`losetup`, or `hdiutil` on macOS)
|
|
36
|
+
- **`DDStrategy`** — `sudo dd` fallback for physical block devices
|
|
37
|
+
- **`resolve_device`** — find the underlying block device for any file path
|
|
38
|
+
(Linux `/proc/partitions` + `/sys/dev/block`, macOS `hdiutil`)
|
|
39
|
+
- **`resolve_mount_point`** — find the mount point for any file path
|
|
40
|
+
(Linux `findmnt`, macOS `df`)
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from rawblock_io import RawBlockIO, resolve_device
|
|
46
|
+
|
|
47
|
+
io = RawBlockIO()
|
|
48
|
+
device = resolve_device('/some/file')
|
|
49
|
+
data = io.read(device, 0, 512) # read first sector
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
GPL-3.0-only
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
rawblock_io/__init__.py,sha256=r_LfYlw9YZ2GHCFDe_BWpjUVt3_ZGAxqfxNuTTi3rg8,1106
|
|
2
|
+
rawblock_io/_backing_file.py,sha256=PnNvlAHeRycXauUeprNTlFjEbFid6o_efcwg3F_FWeM,3076
|
|
3
|
+
rawblock_io/_dd.py,sha256=eCRqhQb_E_Hv1Nh_-3rMAEaYML7FHJyTW9zPFGQaUTs,2379
|
|
4
|
+
rawblock_io/_direct_io.py,sha256=tKTcqJL_HhqgK8pRbgHMldmqnNT_yf-FksmVLoOw0_8,616
|
|
5
|
+
rawblock_io/_io.py,sha256=Lyrrt25iwM2POZghyEK155H3qdgFArwmbLow8E3xT2I,1309
|
|
6
|
+
rawblock_io/_resolve.py,sha256=eYkz7eNnvmyp2kqNsRnmit26uF1j32e8mO1jhYkLly4,1950
|
|
7
|
+
rawblock_io/_resolve_darwin.py,sha256=001wlHUtp5zpaC3vKuAzMY9vcEjO6S1WDuYW4Q1LDOo,1177
|
|
8
|
+
rawblock_io/_resolve_linux.py,sha256=MTkjYHsRYL9SabyP1Ke5RlXARdzajaHnz5Q9B_u7Um8,935
|
|
9
|
+
rawblock_io/_strategies.py,sha256=cMZ2WirkF7spLGF3aJrqzxJ6sjuZ2C-xdF4iCoBQyq8,2442
|
|
10
|
+
rawblock_io-0.1.0.dist-info/METADATA,sha256=uUNfxI0Y3KA8dReD5lPgoKC97ji6F5AjsVaH00n4hng,2022
|
|
11
|
+
rawblock_io-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
12
|
+
rawblock_io-0.1.0.dist-info/top_level.txt,sha256=KhrVE6ZaJnwEW2wS1JoMZV751u2DzR1QV26qM7QU_e8,12
|
|
13
|
+
rawblock_io-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rawblock_io
|