|
| 1 | +import binascii |
| 2 | +import io |
| 3 | +from math import ceil |
| 4 | +from typing import Optional |
| 5 | + |
| 6 | +from unblob.extractors import Command |
| 7 | +from unblob.file_utils import File, InvalidInputFormat, get_endian |
| 8 | +from unblob.models import Regex, StructHandler, ValidChunk |
| 9 | + |
| 10 | +C_DEFINITIONS = r""" |
| 11 | + typedef struct partclone_header{ |
| 12 | + char magic[16]; |
| 13 | + char partclone_version[14]; |
| 14 | + char image_version_txt[4]; |
| 15 | + char endian[2]; |
| 16 | + char fs_type[16]; |
| 17 | + uint64 fs_size; |
| 18 | + uint64 fs_total_block_count; |
| 19 | + uint64 fs_used_block_count_superblock; |
| 20 | + uint64 fs_used_block_count_bitmap; |
| 21 | + uint32 fs_block_size; |
| 22 | + uint32 feature_size; |
| 23 | + uint16 image_version; |
| 24 | + uint16 number_of_bits_for_CPU; |
| 25 | + uint16 checksum_mode; |
| 26 | + uint16 checksum_size; |
| 27 | + uint32 blocks_per_checksum; |
| 28 | + uint8 reseed_checksum; |
| 29 | + uint8 bitmap_mode; |
| 30 | + uint32 crc32; |
| 31 | + } partclone_header_t; |
| 32 | +""" |
| 33 | + |
| 34 | +HEADER_STRUCT = "partclone_header_t" |
| 35 | +BIG_ENDIAN_MAGIC = 0xC0DE |
| 36 | +ENDIAN_OFFSET = 34 |
| 37 | + |
| 38 | + |
| 39 | +class PartcloneHandler(StructHandler): |
| 40 | + NAME = "partclone" |
| 41 | + PATTERNS = [Regex(r"partclone-image\x00\d+\.\d+\.\d+.*?0002(\xde\xc0|\xc0\xde)")] |
| 42 | + HEADER_STRUCT = HEADER_STRUCT |
| 43 | + C_DEFINITIONS = C_DEFINITIONS |
| 44 | + EXTRACTOR = Command( |
| 45 | + "partclone.restore", |
| 46 | + "-W", |
| 47 | + "-s", |
| 48 | + "{inpath}", |
| 49 | + "-o", |
| 50 | + "{outdir}/partclone.restored", |
| 51 | + "-L", |
| 52 | + "/dev/stdout", |
| 53 | + ) |
| 54 | + |
| 55 | + def is_valid_header(self, header) -> bool: |
| 56 | + calculated_crc = binascii.crc32(header.dumps()[0:-4]) |
| 57 | + return ( |
| 58 | + header.crc32 ^ 0xFFFFFFFF |
| 59 | + ) == calculated_crc # partclone does not final XOR |
| 60 | + |
| 61 | + def calculate_chunk(self, file: File, start_offset: int) -> Optional[ValidChunk]: |
| 62 | + file.seek(start_offset + ENDIAN_OFFSET, io.SEEK_SET) # go to endian |
| 63 | + endian = get_endian(file, BIG_ENDIAN_MAGIC, endian_len=2) |
| 64 | + file.seek(start_offset, io.SEEK_SET) # go to beginning of file |
| 65 | + header = self.parse_header(file, endian) |
| 66 | + |
| 67 | + if not self.is_valid_header(header): |
| 68 | + raise InvalidInputFormat("Invalid partclone header.") |
| 69 | + |
| 70 | + end_offset = start_offset + len(header) # header |
| 71 | + end_offset += header.checksum_size # checksum size |
| 72 | + end_offset += ceil(header.fs_total_block_count / 8) # bitmap, as bytes |
| 73 | + |
| 74 | + if header.checksum_mode != 0: |
| 75 | + checksum_blocks = ceil( |
| 76 | + header.fs_used_block_count_bitmap / header.blocks_per_checksum |
| 77 | + ) |
| 78 | + end_offset += checksum_blocks * header.checksum_size |
| 79 | + |
| 80 | + end_offset += header.fs_used_block_count_bitmap * header.fs_block_size # Data |
| 81 | + return ValidChunk(start_offset=start_offset, end_offset=end_offset) |
0 commit comments