initial commit
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.idea/
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.img
|
||||
12
Pipfile
Normal file
12
Pipfile
Normal file
@@ -0,0 +1,12 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[requires]
|
||||
python_version = "3.11"
|
||||
python_full_version = "3.11.3"
|
||||
0
diskutil/__init__.py
Normal file
0
diskutil/__init__.py
Normal file
49
diskutil/blockdev.py
Normal file
49
diskutil/blockdev.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import math
|
||||
from typing import IO
|
||||
from threading import Lock
|
||||
|
||||
|
||||
class BlockDevice:
|
||||
sector_size: int
|
||||
|
||||
def read_sector(self, lba: int, count: int) -> bytes:
|
||||
raise NotImplementedError()
|
||||
|
||||
def read_bytes(self, offset: int, count: int):
|
||||
lba_start = math.floor(offset/self.sector_size)
|
||||
lba_end = math.ceil(offset + count / self.sector_size)
|
||||
lba_count = lba_end - lba_start
|
||||
|
||||
sectors = self.read_sector(lba_start, lba_count)
|
||||
|
||||
return sectors[offset % self.sector_size:count]
|
||||
|
||||
|
||||
class OffsetBlockDevice(BlockDevice):
|
||||
def __init__(self, actual_block_device: BlockDevice, lba_offset: int):
|
||||
self.sector_size = actual_block_device.sector_size
|
||||
self._actual_block_device = actual_block_device
|
||||
self._lba_offset = lba_offset
|
||||
|
||||
def read_sector(self, lba: int, count: int) -> bytes:
|
||||
return self._actual_block_device.read_sector(lba + self._lba_offset, count)
|
||||
|
||||
|
||||
class IOBlockDevice(BlockDevice):
|
||||
def __init__(self, io: IO[bytes], sector_size: int):
|
||||
self.sector_size = sector_size
|
||||
self._io = io
|
||||
self._lock = Lock()
|
||||
|
||||
def read_sector(self, lba: int, count: int) -> bytes:
|
||||
with self._lock:
|
||||
self._io.seek(self.sector_size * lba)
|
||||
return self._io.read(self.sector_size * count)
|
||||
|
||||
|
||||
def block_device_from_file(filename: str) -> IOBlockDevice:
|
||||
f = open(filename, mode='rb')
|
||||
return IOBlockDevice(f, 512)
|
||||
|
||||
|
||||
__all__ = ['BlockDevice', 'IOBlockDevice', 'block_device_from_file']
|
||||
56
diskutil/manager.py
Normal file
56
diskutil/manager.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import string
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, List
|
||||
|
||||
from .blockdev import BlockDevice, OffsetBlockDevice
|
||||
from .part import partition_schemes
|
||||
from .vfs import vfs_schemes, Vfs
|
||||
|
||||
|
||||
@dataclass
|
||||
class Drive:
|
||||
name: str
|
||||
parent: Optional['Drive']
|
||||
block_device: BlockDevice
|
||||
pass
|
||||
|
||||
|
||||
class Manager:
|
||||
def __init__(self):
|
||||
self.drives: List[Drive] = []
|
||||
self.filesystems: List[Vfs] = []
|
||||
self.last_drive_letter = 0
|
||||
|
||||
def _get_next_drive_name(self) -> str:
|
||||
next_letter = string.ascii_lowercase[self.last_drive_letter]
|
||||
self.last_drive_letter += 1
|
||||
return f'sd{next_letter}'
|
||||
|
||||
def add_device(self, block_device: BlockDevice):
|
||||
drive = Drive(self._get_next_drive_name(), None, block_device)
|
||||
self.drives.append(drive)
|
||||
|
||||
self.scan_drive(drive)
|
||||
|
||||
def scan_drive(self, drive: Drive):
|
||||
partition_found = False
|
||||
for part_scheme in partition_schemes:
|
||||
try:
|
||||
part = part_scheme.from_blockdevice(drive.block_device)
|
||||
except ValueError:
|
||||
continue
|
||||
for i, partition in enumerate(part.get_partitions()):
|
||||
part_drive = Drive(f'{drive.name}{i}', drive, OffsetBlockDevice(drive.block_device, partition.offset_lba))
|
||||
self.drives.append(
|
||||
part_drive
|
||||
)
|
||||
self.scan_drive(part_drive)
|
||||
partition_found = True
|
||||
if not partition_found:
|
||||
for vfs in vfs_schemes:
|
||||
try:
|
||||
fs = vfs.from_blockdevice(drive.block_device)
|
||||
self.filesystems.append(fs)
|
||||
except ValueError as e:
|
||||
print(e)
|
||||
continue
|
||||
73
diskutil/offsetio.py
Normal file
73
diskutil/offsetio.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import os
|
||||
from types import TracebackType
|
||||
from typing import IO, AnyStr, Type, Iterator, Iterable
|
||||
|
||||
|
||||
class OffsetIO(IO[bytes]):
|
||||
def __init__(self, actual: IO[bytes], offset: int):
|
||||
self._offset = offset
|
||||
self._actual = actual
|
||||
|
||||
def close(self) -> None:
|
||||
self._actual.close()
|
||||
|
||||
def fileno(self) -> int:
|
||||
# todo ok?
|
||||
return self._actual.fileno()
|
||||
|
||||
def flush(self) -> None:
|
||||
self._actual.flush()
|
||||
|
||||
def isatty(self) -> bool:
|
||||
return self._actual.isatty()
|
||||
|
||||
def read(self, __n: int = ...) -> AnyStr:
|
||||
return self._actual.read(__n)
|
||||
|
||||
def readable(self) -> bool:
|
||||
return self._actual.readable()
|
||||
|
||||
def readline(self, __limit: int = ...) -> AnyStr:
|
||||
return self._actual.readline(__limit)
|
||||
|
||||
def readlines(self, __hint: int = ...) -> list[AnyStr]:
|
||||
return self._actual.readlines(__hint)
|
||||
|
||||
def seek(self, __offset: int, __whence: int = ...) -> int:
|
||||
if __whence == os.SEEK_SET or __whence is None:
|
||||
return self._actual.seek(self._offset + __offset, __whence)
|
||||
elif __whence == os.SEEK_CUR:
|
||||
return self._actual.seek(__offset, __whence)
|
||||
else:
|
||||
raise NotImplementedError(f'__whence is not supported by offset io {__whence}')
|
||||
|
||||
def seekable(self) -> bool:
|
||||
return self._actual.seekable()
|
||||
|
||||
def tell(self) -> int:
|
||||
return self._actual.tell() - self._offset
|
||||
|
||||
def truncate(self, __size: int | None = ...) -> int:
|
||||
return self._actual.truncate(__size)
|
||||
|
||||
def writable(self) -> bool:
|
||||
return self._actual.writable()
|
||||
|
||||
def write(self, __s: AnyStr) -> int:
|
||||
return self._actual.write(__s)
|
||||
|
||||
def writelines(self, __lines: Iterable[AnyStr]) -> None:
|
||||
return self._actual.writelines(__lines)
|
||||
|
||||
def __next__(self) -> AnyStr:
|
||||
return self._actual.__next__()
|
||||
|
||||
def __iter__(self) -> Iterator[AnyStr]:
|
||||
return self.__iter__()
|
||||
|
||||
def __enter__(self) -> IO[AnyStr]:
|
||||
return self.__enter__()
|
||||
|
||||
def __exit__(self, __t: Type[BaseException] | None, __value: BaseException | None,
|
||||
__traceback: TracebackType | None) -> None:
|
||||
return self.__exit__(__t, __value, __traceback)
|
||||
12
diskutil/part/__init__.py
Normal file
12
diskutil/part/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from typing import List, Type
|
||||
|
||||
from .generic import PartitionScheme
|
||||
from .msdos import Mbr
|
||||
|
||||
|
||||
partition_schemes: List[Type[PartitionScheme]] = [
|
||||
Mbr
|
||||
]
|
||||
|
||||
__all__ = ['partition_schemes', 'Mbr', 'PartitionScheme']
|
||||
|
||||
25
diskutil/part/generic.py
Normal file
25
diskutil/part/generic.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Dict
|
||||
|
||||
from ..blockdev import BlockDevice
|
||||
|
||||
|
||||
@dataclass
|
||||
class Partition:
|
||||
offset_lba: int
|
||||
offset_bytes: int
|
||||
count_lba: int
|
||||
count_bytes: int
|
||||
metadata: Dict[str, str]
|
||||
|
||||
|
||||
class PartitionScheme:
|
||||
@staticmethod
|
||||
def from_blockdevice(dev: BlockDevice) -> 'PartitionScheme':
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_partitions(self) -> List[Partition]:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
__all__ = ['PartitionScheme', 'Partition']
|
||||
76
diskutil/part/msdos.py
Normal file
76
diskutil/part/msdos.py
Normal file
@@ -0,0 +1,76 @@
|
||||
from dataclasses import dataclass
|
||||
from struct import unpack_from
|
||||
from typing import List
|
||||
|
||||
from .generic import PartitionScheme, Partition
|
||||
|
||||
from ..blockdev import BlockDevice
|
||||
|
||||
|
||||
MBR_SIZE = 512
|
||||
MBR_MAGIC = b'\x55\xaa'
|
||||
|
||||
|
||||
@dataclass
|
||||
class MbrPartitionTableEntry:
|
||||
drive_attributes: int
|
||||
chs_address: bytes
|
||||
partition_type: int
|
||||
chs_address_end: bytes
|
||||
lba_partition_start: int
|
||||
number_of_sectors: int
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data: bytes) -> "MbrPartitionTableEntry":
|
||||
return MbrPartitionTableEntry(*unpack_from("<B3sb3sII", data))
|
||||
|
||||
def present(self) -> bool:
|
||||
return not self.absent()
|
||||
|
||||
def absent(self) -> bool:
|
||||
return self.drive_attributes == 0 \
|
||||
and self.chs_address == b'\0\0\0' \
|
||||
and self.partition_type == 0 \
|
||||
and self.chs_address_end == b'\0\0\0' \
|
||||
and self.lba_partition_start == 0 \
|
||||
and self.number_of_sectors == 0
|
||||
|
||||
def get_partition(self) -> Partition:
|
||||
if not self.lba_partition_start or not self.number_of_sectors:
|
||||
raise NotImplementedError('CHS to LBA not implemented')
|
||||
|
||||
return Partition(self.lba_partition_start, 0, self.number_of_sectors, 0, {
|
||||
'mbr_drive_attributes': str(self.drive_attributes),
|
||||
'mbr_partition_type': str(self.partition_type),
|
||||
})
|
||||
|
||||
|
||||
@dataclass
|
||||
class Mbr(PartitionScheme):
|
||||
bootstrap: bytes
|
||||
unique_disk_id: bytes
|
||||
partition_table: List[MbrPartitionTableEntry]
|
||||
magic: bytes
|
||||
|
||||
def get_partitions(self) -> List[Partition]:
|
||||
return [part.get_partition() for part in self.partition_table if part.present()]
|
||||
|
||||
@staticmethod
|
||||
def from_blockdevice(dev: BlockDevice):
|
||||
return Mbr.from_bytes(dev.read_bytes(0, MBR_SIZE))
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data: bytes) -> "Mbr":
|
||||
bootstrap, disk_id, \
|
||||
part_1, part_2, part_3, part_4, \
|
||||
magic = unpack_from("<440sI2x16s16s16s16s2s", data)
|
||||
if magic != MBR_MAGIC:
|
||||
raise ValueError('Not a valid MBR')
|
||||
return Mbr(
|
||||
bootstrap, disk_id,
|
||||
[MbrPartitionTableEntry.from_bytes(p) for p in [part_1, part_2, part_3, part_4]],
|
||||
magic,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ['Mbr']
|
||||
8
diskutil/vfs/__init__.py
Normal file
8
diskutil/vfs/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from typing import List, Type
|
||||
|
||||
from .ext import ExtFs
|
||||
from .generic import Vfs
|
||||
|
||||
vfs_schemes: List[Type[Vfs]] = [
|
||||
ExtFs
|
||||
]
|
||||
134
diskutil/vfs/ext.py
Normal file
134
diskutil/vfs/ext.py
Normal file
@@ -0,0 +1,134 @@
|
||||
from dataclasses import dataclass
|
||||
from struct import unpack_from
|
||||
|
||||
from .generic import Vfs
|
||||
from ..blockdev import BlockDevice
|
||||
|
||||
EXT_HEADER_OFFSET = 1024
|
||||
EXT_HEADER_SIZE = 1024
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExtFs(Vfs):
|
||||
no_inodes: int
|
||||
no_blocks: int
|
||||
no_reserved_super: int
|
||||
no_unallocated_blocks: int
|
||||
no_unallocated_inodes: int
|
||||
super_block: int
|
||||
block_size_log2_m10: int
|
||||
fragment_size_log2_m10: int
|
||||
no_blocks_per_group: int
|
||||
no_fragments_per_group: int
|
||||
no_inodes_per_group: int
|
||||
last_mounted: int
|
||||
last_written: int
|
||||
mount_count_since_fschk: int
|
||||
mount_count_since_fschk_max: int
|
||||
signature: int
|
||||
fs_state: int
|
||||
error_behavior: int
|
||||
minor_version: int
|
||||
last_fschk: int
|
||||
interval_fschk: int
|
||||
os_id: int
|
||||
major_version: int
|
||||
reserved_uid: int
|
||||
reserved_gid: int
|
||||
first_nonreserved_inode: int
|
||||
inode_size: int
|
||||
bg_superblock: int
|
||||
optional_features: int
|
||||
required_features: int
|
||||
ro_features: int
|
||||
fsid: bytes
|
||||
fsname: str
|
||||
last_mount_path: str
|
||||
compression: int
|
||||
no_prealloc_file: int
|
||||
no_prealloc_dir: int
|
||||
journal_id: int
|
||||
journal_inode: int
|
||||
journal_device: int
|
||||
orphan_list: int
|
||||
|
||||
@staticmethod
|
||||
def from_blockdevice(device: BlockDevice) -> 'Vfs':
|
||||
return ExtFs.from_bytes(device.read_bytes(EXT_HEADER_OFFSET, EXT_HEADER_SIZE))
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data: bytes) -> 'Vfs':
|
||||
(
|
||||
no_inodes, no_blocks,
|
||||
no_reserved_super,
|
||||
no_unallocated_blocks, no_unallocated_inodes,
|
||||
super_block,
|
||||
block_size_log2_m10, fragment_size_log2_m10,
|
||||
no_blocks_per_group, no_fragments_per_group, no_inodes_per_group,
|
||||
last_mounted, last_written,
|
||||
mount_count_since_fschk, mount_count_since_fschk_max,
|
||||
signature, fs_state,
|
||||
error_behavior, minor_version,
|
||||
last_fschk, interval_fschk,
|
||||
os_id, major_version,
|
||||
reserved_uid, reserved_gid,
|
||||
first_nonreserved_inode,
|
||||
inode_size, bg_superblock,
|
||||
optional_features, required_features, ro_features,
|
||||
fsid,
|
||||
fsname,
|
||||
last_mount_path,
|
||||
compression,
|
||||
no_prealloc_file, no_prealloc_dir,
|
||||
journal_id, journal_inode, journal_device,
|
||||
orphan_list) = unpack_from('<II'
|
||||
'I'
|
||||
'II'
|
||||
'I'
|
||||
'II'
|
||||
'III'
|
||||
'II'
|
||||
'HH'
|
||||
'HH'
|
||||
'HH'
|
||||
'II'
|
||||
'II'
|
||||
'HH'
|
||||
'I'
|
||||
'HH'
|
||||
'III'
|
||||
'16s'
|
||||
'16s'
|
||||
'64s'
|
||||
'I'
|
||||
'BB'
|
||||
'xx'
|
||||
'16s'
|
||||
'II'
|
||||
'I'
|
||||
'787x', data)
|
||||
return ExtFs(
|
||||
no_inodes, no_blocks,
|
||||
no_reserved_super,
|
||||
no_unallocated_blocks, no_unallocated_inodes,
|
||||
super_block,
|
||||
block_size_log2_m10, fragment_size_log2_m10,
|
||||
no_blocks_per_group, no_fragments_per_group, no_inodes_per_group,
|
||||
last_mounted, last_written,
|
||||
mount_count_since_fschk, mount_count_since_fschk_max,
|
||||
signature, fs_state,
|
||||
error_behavior, minor_version,
|
||||
last_fschk, interval_fschk,
|
||||
os_id, major_version,
|
||||
reserved_uid, reserved_gid,
|
||||
first_nonreserved_inode,
|
||||
inode_size, bg_superblock,
|
||||
optional_features, required_features, ro_features,
|
||||
fsid,
|
||||
fsname,
|
||||
last_mount_path,
|
||||
compression,
|
||||
no_prealloc_file, no_prealloc_dir,
|
||||
journal_id, journal_inode, journal_device,
|
||||
orphan_list
|
||||
)
|
||||
9
diskutil/vfs/generic.py
Normal file
9
diskutil/vfs/generic.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from typing import Type, List
|
||||
|
||||
from ..blockdev import BlockDevice
|
||||
|
||||
|
||||
class Vfs:
|
||||
@staticmethod
|
||||
def from_blockdevice(device: BlockDevice) -> 'Vfs':
|
||||
raise NotImplementedError()
|
||||
Reference in New Issue
Block a user