1import binascii
2import io
3from math import ceil
4from typing import Optional
5
6from unblob.extractors import Command
7from unblob.file_utils import File, InvalidInputFormat, get_endian_short
8from unblob.models import (
9 HandlerDoc,
10 HandlerType,
11 Reference,
12 Regex,
13 StructHandler,
14 ValidChunk,
15)
16
17C_DEFINITIONS = r"""
18 typedef struct partclone_header{
19 char magic[16];
20 char partclone_version[14];
21 char image_version_txt[4];
22 char endian[2];
23 char fs_type[16];
24 uint64 fs_size;
25 uint64 fs_total_block_count;
26 uint64 fs_used_block_count_superblock;
27 uint64 fs_used_block_count_bitmap;
28 uint32 fs_block_size;
29 uint32 feature_size;
30 uint16 image_version;
31 uint16 number_of_bits_for_CPU;
32 uint16 checksum_mode;
33 uint16 checksum_size;
34 uint32 blocks_per_checksum;
35 uint8 reseed_checksum;
36 uint8 bitmap_mode;
37 uint32 crc32;
38 } partclone_header_t;
39"""
40
41HEADER_STRUCT = "partclone_header_t"
42BIG_ENDIAN_MAGIC = 0xC0DE
43ENDIAN_OFFSET = 34
44
45
46class PartcloneHandler(StructHandler):
47 NAME = "partclone"
48 PATTERNS = [Regex(r"partclone-image\x00\d+\.\d+\.\d+.*?0002(\xde\xc0|\xc0\xde)")]
49 HEADER_STRUCT = HEADER_STRUCT
50 C_DEFINITIONS = C_DEFINITIONS
51 EXTRACTOR = Command(
52 "partclone.restore",
53 "-W",
54 "-s",
55 "{inpath}",
56 "-o",
57 "{outdir}/partclone.restored",
58 "-L",
59 "/dev/stdout",
60 )
61 DOC = HandlerDoc(
62 name="Partclone",
63 description="Partclone is a utility used for backing up and restoring partitions. Many cloning tools (such as Clonezilla) rely on it to create block-level images that include filesystem metadata.",
64 handler_type=HandlerType.ARCHIVE,
65 vendor=None,
66 references=[
67 Reference(
68 title="Partclone GitHub Repository",
69 url="https://github.com/Thomas-Tsai/partclone",
70 ),
71 Reference(
72 title="Clonezilla Official Documentation",
73 url="https://clonezilla.org/",
74 ),
75 ],
76 limitations=[],
77 )
78
79 def is_valid_header(self, header) -> bool:
80 calculated_crc = binascii.crc32(header.dumps()[0:-4])
81 return (
82 header.crc32 ^ 0xFFFFFFFF
83 ) == calculated_crc # partclone does not final XOR
84
85 def calculate_chunk(self, file: File, start_offset: int) -> Optional[ValidChunk]:
86 file.seek(start_offset + ENDIAN_OFFSET, io.SEEK_SET) # go to endian
87 endian = get_endian_short(file, BIG_ENDIAN_MAGIC)
88 file.seek(start_offset, io.SEEK_SET) # go to beginning of file
89 header = self.parse_header(file, endian)
90
91 if not self.is_valid_header(header):
92 raise InvalidInputFormat("Invalid partclone header.")
93
94 end_offset = start_offset + len(header) # header
95 end_offset += header.checksum_size # checksum size
96 end_offset += ceil(header.fs_total_block_count / 8) # bitmap, as bytes
97
98 if header.checksum_mode != 0:
99 checksum_blocks = ceil(
100 header.fs_used_block_count_bitmap / header.blocks_per_checksum
101 )
102 end_offset += checksum_blocks * header.checksum_size
103
104 end_offset += header.fs_used_block_count_bitmap * header.fs_block_size # Data
105 return ValidChunk(start_offset=start_offset, end_offset=end_offset)