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