dissect.archive 1.9.dev1__tar.gz → 1.9.dev3__tar.gz
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.
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/.git-blame-ignore-revs +2 -0
- dissect_archive-1.9.dev3/.gitattributes +1 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/PKG-INFO +5 -3
- dissect_archive-1.9.dev3/dissect/archive/tibx/__init__.py +24 -0
- dissect_archive-1.9.dev3/dissect/archive/tibx/c_tibx.py +555 -0
- dissect_archive-1.9.dev3/dissect/archive/tibx/c_tibx.pyi +836 -0
- dissect_archive-1.9.dev3/dissect/archive/tibx/codec.py +137 -0
- dissect_archive-1.9.dev3/dissect/archive/tibx/crypto.py +255 -0
- dissect_archive-1.9.dev3/dissect/archive/tibx/exception.py +23 -0
- dissect_archive-1.9.dev3/dissect/archive/tibx/lsm.py +466 -0
- dissect_archive-1.9.dev3/dissect/archive/tibx/map.py +520 -0
- dissect_archive-1.9.dev3/dissect/archive/tibx/page.py +420 -0
- dissect_archive-1.9.dev3/dissect/archive/tibx/segment.py +187 -0
- dissect_archive-1.9.dev3/dissect/archive/tibx/stream.py +28 -0
- dissect_archive-1.9.dev3/dissect/archive/tibx/tibx.py +955 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/dissect.archive.egg-info/PKG-INFO +5 -3
- dissect_archive-1.9.dev3/dissect.archive.egg-info/SOURCES.txt +81 -0
- dissect_archive-1.9.dev3/dissect.archive.egg-info/requires.txt +14 -0
- dissect_archive-1.9.dev3/dissect.archive.egg-info/scm_file_list.json +80 -0
- dissect_archive-1.9.dev3/dissect.archive.egg-info/scm_version.json +8 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/pyproject.toml +5 -2
- dissect_archive-1.9.dev3/tests/_data/acronis/tibx/ati_2020_disk_enc_lz4_compr.tibx +0 -0
- dissect_archive-1.9.dev3/tests/_data/acronis/tibx/ati_2020_disk_lz4_compr.tibx +0 -0
- dissect_archive-1.9.dev3/tests/_data/acronis/tibx/t1_chain_diff-0001.tibx +0 -0
- dissect_archive-1.9.dev3/tests/_data/acronis/tibx/t1_chain_diff.tibx +0 -0
- dissect_archive-1.9.dev3/tests/_data/acronis/tibx/t1_chain_inc.tibx +0 -0
- dissect_archive-1.9.dev3/tests/_data/acronis/tibx/t1_disk_gpt.tibx +0 -0
- dissect_archive-1.9.dev3/tests/_data/acronis/tibx/t1_disk_mbr.tibx +0 -0
- dissect_archive-1.9.dev3/tests/_data/acronis/tibx/t1_empty_ntfs.tibx +0 -0
- dissect_archive-1.9.dev3/tests/_data/acronis/tibx/t1_enc_aes.tibx +0 -0
- dissect_archive-1.9.dev3/tests/_data/acronis/tibx/t1_legacy_tib.tibx +0 -0
- dissect_archive-1.9.dev3/tests/_data/acronis/tibx/t1_multivol.tibx +0 -0
- dissect_archive-1.9.dev3/tests/_data/acronis/tibx/t1_simple_ntfs.tibx +0 -0
- dissect_archive-1.9.dev3/tests/_data/acronis/tibx/t1_stored.tibx +0 -0
- dissect_archive-1.9.dev3/tests/_tools/__init__.py +0 -0
- dissect_archive-1.9.dev3/tests/_tools/tibx/__init__.py +0 -0
- dissect_archive-1.9.dev3/tests/_tools/tibx/_synth.py +723 -0
- dissect_archive-1.9.dev3/tests/tibx/__init__.py +0 -0
- dissect_archive-1.9.dev3/tests/tibx/test_codec.py +158 -0
- dissect_archive-1.9.dev3/tests/tibx/test_crypto.py +302 -0
- dissect_archive-1.9.dev3/tests/tibx/test_lsm.py +279 -0
- dissect_archive-1.9.dev3/tests/tibx/test_page.py +150 -0
- dissect_archive-1.9.dev3/tests/tibx/test_recovery.py +145 -0
- dissect_archive-1.9.dev3/tests/tibx/test_segment.py +138 -0
- dissect_archive-1.9.dev3/tests/tibx/test_tibx.py +805 -0
- dissect_archive-1.9.dev3/tests/tibx/test_tibx_acronis.py +459 -0
- dissect_archive-1.9.dev1/.gitattributes +0 -1
- dissect_archive-1.9.dev1/dissect.archive.egg-info/SOURCES.txt +0 -42
- dissect_archive-1.9.dev1/dissect.archive.egg-info/requires.txt +0 -10
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/COPYRIGHT +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/LICENSE +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/MANIFEST.in +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/README.md +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/dissect/archive/__init__.py +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/dissect/archive/c_vbk.py +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/dissect/archive/c_vma.py +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/dissect/archive/c_wim.py +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/dissect/archive/exceptions.py +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/dissect/archive/tools/__init__.py +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/dissect/archive/tools/backup.py +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/dissect/archive/vbk.py +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/dissect/archive/vma.py +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/dissect/archive/wim.py +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/dissect/archive/xva.py +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/dissect.archive.egg-info/dependency_links.txt +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/dissect.archive.egg-info/entry_points.txt +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/dissect.archive.egg-info/top_level.txt +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/setup.cfg +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/tests/__init__.py +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/tests/_data/basic16k.wim.gz +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/tests/_data/basic32k.wim.gz +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/tests/_data/basic4k.wim.gz +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/tests/_data/basic8k.wim.gz +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/tests/_data/test.vma.gz +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/tests/_data/test13.vbk.gz +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/tests/_data/test9.vbk.gz +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/tests/_docs/Makefile +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/tests/_docs/__init__.py +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/tests/_docs/conf.py +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/tests/_docs/index.rst +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/tests/conftest.py +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/tests/test_exceptions.py +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/tests/test_vbk.py +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/tests/test_vma.py +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/tests/test_wim.py +0 -0
- {dissect_archive-1.9.dev1 → dissect_archive-1.9.dev3}/tox.ini +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tests/_data/** filter=lfs diff=lfs merge=lfs -text
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dissect.archive
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.dev3
|
|
4
4
|
Summary: A Dissect module implementing parsers for various archive and backup formats
|
|
5
5
|
Author-email: Dissect Team <dissect@fox-it.com>
|
|
6
6
|
License-Expression: AGPL-3.0-or-later
|
|
@@ -22,13 +22,15 @@ Description-Content-Type: text/markdown
|
|
|
22
22
|
License-File: LICENSE
|
|
23
23
|
License-File: COPYRIGHT
|
|
24
24
|
Requires-Dist: dissect.cstruct<5,>=4
|
|
25
|
-
Requires-Dist: dissect.util<4,>=3.
|
|
25
|
+
Requires-Dist: dissect.util<4,>=3.25.dev18
|
|
26
26
|
Provides-Extra: full
|
|
27
|
+
Requires-Dist: pycryptodome; extra == "full"
|
|
28
|
+
Requires-Dist: backports.zstd; python_version < "3.14" and extra == "full"
|
|
27
29
|
Requires-Dist: rich; extra == "full"
|
|
28
30
|
Provides-Extra: dev
|
|
29
31
|
Requires-Dist: dissect.archive[full]; extra == "dev"
|
|
30
32
|
Requires-Dist: dissect.cstruct<5.0.dev,>=4.0.dev; extra == "dev"
|
|
31
|
-
Requires-Dist: dissect.util<4.0.dev,>=3.
|
|
33
|
+
Requires-Dist: dissect.util<4.0.dev,>=3.25.dev18; extra == "dev"
|
|
32
34
|
Dynamic: license-file
|
|
33
35
|
|
|
34
36
|
# dissect.archive
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dissect.archive.tibx.exception import (
|
|
4
|
+
CorruptArchiveError,
|
|
5
|
+
Error,
|
|
6
|
+
InvalidArchiveError,
|
|
7
|
+
InvalidPasswordError,
|
|
8
|
+
UnsupportedFormatError,
|
|
9
|
+
)
|
|
10
|
+
from dissect.archive.tibx.stream import TibxVolumeStream
|
|
11
|
+
from dissect.archive.tibx.tibx import TIBX, RecoveryPoint, TibxVolume, find_split_parts
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"TIBX",
|
|
15
|
+
"CorruptArchiveError",
|
|
16
|
+
"Error",
|
|
17
|
+
"InvalidArchiveError",
|
|
18
|
+
"InvalidPasswordError",
|
|
19
|
+
"RecoveryPoint",
|
|
20
|
+
"TibxVolume",
|
|
21
|
+
"TibxVolumeStream",
|
|
22
|
+
"UnsupportedFormatError",
|
|
23
|
+
"find_split_parts",
|
|
24
|
+
]
|
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
"""Structure definitions for the Acronis TIBX ("archive3") file format.
|
|
2
|
+
|
|
3
|
+
A ``.tibx`` is a flat sequence of 4096-byte pages. Byte 0 of every page is the marker
|
|
4
|
+
``0x41`` (``'A'``), byte 1 the page type, and a CRC-32C of the page (checksum bytes
|
|
5
|
+
zeroed) is stored at ``+0x04``. The page content ("body") starts after this 8-byte
|
|
6
|
+
envelope.
|
|
7
|
+
|
|
8
|
+
Most multi-byte fields are big-endian, so the definitions are loaded into a big-endian
|
|
9
|
+
cstruct instance. cstruct applies one byte order per instance, so the one little-endian
|
|
10
|
+
field of a TIBX record -- the segment_map ``page_count`` -- is declared as raw bytes and
|
|
11
|
+
decoded where it is used; the LEAF sequence id is little-endian too, but unused. The LSM
|
|
12
|
+
cell-group header only looks little-endian: it is a count byte followed by a big-endian
|
|
13
|
+
24-bit bitmap.
|
|
14
|
+
|
|
15
|
+
:data:`c_boot` holds the few filesystem boot-sector fields the parser reads to size a
|
|
16
|
+
volume, and the MBR partition table it reads to tell a disk's bootstrap region from one.
|
|
17
|
+
Those formats are not TIBX's and are little-endian, so they get their own instance.
|
|
18
|
+
|
|
19
|
+
The format is not documented by Acronis. These definitions encode the format findings of
|
|
20
|
+
``acronis-tibx`` (MIT License, Copyright (c) 2026 mniedermaier) and were confirmed against
|
|
21
|
+
archives written by Acronis Cyber Protect / True Image 2026; fields still marked
|
|
22
|
+
``_reserved`` or ``_pad`` are ones whose meaning is not yet established.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from dissect.cstruct import cstruct
|
|
28
|
+
|
|
29
|
+
tibx_def = """
|
|
30
|
+
#define PAGE_SIZE 0x1000
|
|
31
|
+
|
|
32
|
+
enum PageType : uint8 {
|
|
33
|
+
ARCH = 0x01, /* superblock / commit root (+ continuation pages) */
|
|
34
|
+
ARCI = 0x02, /* commit info */
|
|
35
|
+
LEAF = 0x03, /* LSM tree leaf */
|
|
36
|
+
LDIR = 0x04, /* LSM tree directory */
|
|
37
|
+
GOLOMB = 0x05, /* dedup_map Golomb filter */
|
|
38
|
+
DATA = 0xFF /* data segment (SG header or continuation) */
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
enum EncrAlg : uint8 {
|
|
42
|
+
NONE = 0, // none
|
|
43
|
+
AES_128_CBC = 1, // aes-128-cbc
|
|
44
|
+
AES_192_CBC = 2, // aes-192-cbc
|
|
45
|
+
AES_256_CBC = 3, // aes-256-cbc
|
|
46
|
+
GOST2015 = 4, // gost2015
|
|
47
|
+
AES_128_GCM = 5, // aes-128-gcm
|
|
48
|
+
AES_192_GCM = 6, // aes-192-gcm
|
|
49
|
+
AES_256_GCM = 7 // aes-256-gcm
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
enum ComprLvl : uint8 {
|
|
53
|
+
NONE = 0, // "none"
|
|
54
|
+
LOW = 1, // "low"
|
|
55
|
+
NORMAL = 2, // "normal"
|
|
56
|
+
HIGH = 3 // "high"
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
enum ComprAlg : uint8 {
|
|
60
|
+
NONE = 0,
|
|
61
|
+
LZ4 = 1,
|
|
62
|
+
ZLIB = 2,
|
|
63
|
+
ZSTD = 3
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
struct page_header {
|
|
67
|
+
uint8 marker; /* always 0x41 'A' */
|
|
68
|
+
PageType type;
|
|
69
|
+
uint16 _pad;
|
|
70
|
+
uint32 crc32c; /* CRC-32C of the page, checksum bytes zeroed */
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/* The type of a backup: the low 7 bits of slice_record.flags, which Acronis's ARCH header
|
|
74
|
+
* dump prints as the slice's "type" */
|
|
75
|
+
enum SliceType : uint8 {
|
|
76
|
+
FULL = 0x01,
|
|
77
|
+
INCREMENTAL = 0x06,
|
|
78
|
+
DIFFERENTIAL = 0x0A
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/* TLV[5] "slices" record: one backup of the archive. Every data_map extent carries this
|
|
82
|
+
* key's slice id in its own slice_id, to say which backup wrote it. The guid is the one
|
|
83
|
+
* "acrocmd list backups" prints for the backup, and is stored mixed-endian. */
|
|
84
|
+
struct slice_key {
|
|
85
|
+
uint32 slice_id;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
struct slice_record {
|
|
89
|
+
char guid[16];
|
|
90
|
+
uint64 ctime_ms; /* backup start, ms since the Unix epoch */
|
|
91
|
+
uint64 ftime_ms; /* backup finish, ms since the Unix epoch */
|
|
92
|
+
uint64 user_size; /* uncompressed bytes of the backup */
|
|
93
|
+
uint64 compr_size; /* compressed bytes of the backup */
|
|
94
|
+
uint32 last_full_sid; /* 1 in all samples */
|
|
95
|
+
uint32 base_full_sid; /* 0 in all samples, even for a differential backup */
|
|
96
|
+
uint32 items_added;
|
|
97
|
+
uint32 items_changed;
|
|
98
|
+
uint32 items_removed;
|
|
99
|
+
uint8 flags; /* low 7 bits: the SliceType; bit 7: deleted */
|
|
100
|
+
uint64 last_segment_id; /* last segment id of the backup */
|
|
101
|
+
uint32 features; /* see `slice_features2str()` */
|
|
102
|
+
uint64 data_size; /* data bytes on disk, observed; 0 in True Image 2020 */
|
|
103
|
+
char _reserved[43]; /* zero */
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/* A slice record as the ARCH header carries it, with extra metadata */
|
|
107
|
+
struct arch_slice {
|
|
108
|
+
slice_record slice;
|
|
109
|
+
uint32 sid; /* slice id */
|
|
110
|
+
uint32 encr_key_id;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/* Start of an ARCH header body, i.e. of an ARCH page after its envelope. The TLV
|
|
114
|
+
* directory follows at TLV_DIRECTORY_OFFSET into the body. */
|
|
115
|
+
struct arch_header {
|
|
116
|
+
char magic[4]; /* "ARCH" */
|
|
117
|
+
uint32 header_size; /* total header body size, may span pages */
|
|
118
|
+
uint16 header_version; /* 8 in current archives */
|
|
119
|
+
ComprLvl compr_lvl;
|
|
120
|
+
EncrAlg encr_alg;
|
|
121
|
+
uint8 dedup; /* 0 = "off", 1 = "on" */
|
|
122
|
+
uint8 hash_alg;
|
|
123
|
+
uint8 chunking_alg;
|
|
124
|
+
uint8 hash_window_width;
|
|
125
|
+
uint64 created_ms; /* creation time, ms since Unix epoch */
|
|
126
|
+
uint64 modified_ms; /* commit time, ms since Unix epoch */
|
|
127
|
+
char archive_uuid[16];
|
|
128
|
+
uint64 archive_size; /* ar_io_get_archive_size(): file size at this commit */
|
|
129
|
+
uint64 last_item_id;
|
|
130
|
+
uint64 last_segment_id;
|
|
131
|
+
uint32 next_sid;
|
|
132
|
+
uint32 slice_count;
|
|
133
|
+
uint32 slices_to_delete;
|
|
134
|
+
uint32 first_unfinished_sid; /* observed: the latest backup's sid, even once complete */
|
|
135
|
+
arch_slice cur_slice;
|
|
136
|
+
uint32 cur_extent_count; /* data_map extents the last backup wrote */
|
|
137
|
+
arch_slice last_slice;
|
|
138
|
+
uint32 ar_flags; /* set by `ar_flags_set_write_mode()` */
|
|
139
|
+
uint64 commit_seq;
|
|
140
|
+
uint64 reuse_seq;
|
|
141
|
+
uint64 commit_info_seq;
|
|
142
|
+
uint64 first_ci_offset;
|
|
143
|
+
uint64 first_hdr_offset;
|
|
144
|
+
uint64 prev_hdr_offset; /* the previous commit root, -1 for the first */
|
|
145
|
+
uint64 ci_offset; /* offset to the commit info (CI) page for this ARCH */
|
|
146
|
+
uint64 space_usage_data; /* data bytes on disk */
|
|
147
|
+
uint64 unused_space; /* grows by 0x3000 (three header pages) per commit */
|
|
148
|
+
uint64 deallocated_space;
|
|
149
|
+
uint64 split_size; /* 0xfffffffffffff000 when not split */
|
|
150
|
+
uint64 compr_size; /* compressed bytes */
|
|
151
|
+
uint64 user_size; /* uncompressed bytes */
|
|
152
|
+
uint64 features;
|
|
153
|
+
uint64 features_ro;
|
|
154
|
+
uint64 features_rw;
|
|
155
|
+
uint64 full_begin_seg_id; /* segment id range of the full backup */
|
|
156
|
+
uint64 full_end_seg_id;
|
|
157
|
+
uint64 diff_begin_seg_id;
|
|
158
|
+
uint64 chain_start_pg; /* first page of a differential chain */
|
|
159
|
+
uint8 notary_degree;
|
|
160
|
+
uint8 notary_hash_alg;
|
|
161
|
+
uint8 notary_flags;
|
|
162
|
+
uint64 notary_last_item_id;
|
|
163
|
+
uint64 _unk_223;
|
|
164
|
+
uint64 _unk_22b;
|
|
165
|
+
uint32 reuse_delay;
|
|
166
|
+
uint64 next_reuse_seq;
|
|
167
|
+
uint64 next_reuse_time;
|
|
168
|
+
uint64 encr_mtime;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
struct arch_superblock {
|
|
172
|
+
page_header header;
|
|
173
|
+
arch_header body;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/* A file of a split archive ("volumes" in Acronis's dumps): its index -- 0 for the first
|
|
177
|
+
* file, NNNN for the file named -NNNN after it -- and where it starts in the archive as a
|
|
178
|
+
* whole. Not a backed-up volume, as TIBX.volumes() returns. */
|
|
179
|
+
struct ar_volume {
|
|
180
|
+
uint32 index;
|
|
181
|
+
uint64 start_offs;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
flag ArciFlags : uint32 {
|
|
185
|
+
REWRITE = 0x1, /* ar_mode rewrite; append mode if clear */
|
|
186
|
+
CI0 = 0x2 /* marks the CI0 page: it names the newest commit */
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/* Commit info ("ARCI"), the body of the page written right after each commit's ARCH header.
|
|
190
|
+
* It points back at that header (arch_offset, arch_size), the header's ci_offset points at
|
|
191
|
+
* it, and prev_ci_offset chains the commit info pages back to the first one. */
|
|
192
|
+
struct arci_header {
|
|
193
|
+
char magic[4]; /* "ARCI" */
|
|
194
|
+
uint16 version; /* 8 in current archives */
|
|
195
|
+
uint16 _pad;
|
|
196
|
+
ArciFlags flags;
|
|
197
|
+
uint32 arch_size; /* total size of the ARCH superblock, divide by 0x1000 = number of pages */
|
|
198
|
+
uint64 arch_offset; /* offset to ARCH page */
|
|
199
|
+
uint64 commit_seq; /* commit sequence number */
|
|
200
|
+
uint64 reuse_seq;
|
|
201
|
+
uint64 ci_seq; /* sequence number of the commit info write, CI0 rewrites included */
|
|
202
|
+
uint64 first_ci_offset; /* offset to the first commit info page */
|
|
203
|
+
uint64 prev_ci_offset; /* offset to previous commit info page; the newest in a rewritten CI0 */
|
|
204
|
+
uint64 crc64; /* CRC-64 (Redis) of the page's first 512 bytes, both checksums zeroed */
|
|
205
|
+
uint64 volume_offset; /* offset of this page within its own file of the archive */
|
|
206
|
+
ar_volume volumes[4]; /* files of the archive, in no order and possibly repeated; index 0 = unused */
|
|
207
|
+
uint128 uuid;
|
|
208
|
+
uint128 session;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
struct arci_page {
|
|
212
|
+
page_header header;
|
|
213
|
+
arci_header commit_info;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/* One TLV directory entry: the payload follows, and the next entry starts at the next
|
|
217
|
+
* 4-byte boundary after it */
|
|
218
|
+
struct tlv_header {
|
|
219
|
+
uint32 length;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/* Data segment header, at page offset +0x08 of a DATA page */
|
|
223
|
+
struct segment_header {
|
|
224
|
+
char magic[2]; /* "SG" plaintext / "SE" encrypted */
|
|
225
|
+
uint16 version; /* 0x0001 */
|
|
226
|
+
uint32 length; /* uncompressed payload size */
|
|
227
|
+
uint32 zlength; /* compressed payload size */
|
|
228
|
+
uint32 key_id; /* encryption key id, 0 = plaintext */
|
|
229
|
+
ComprAlg compression; /* compression codec */
|
|
230
|
+
uint8 cache_level; /* 0 data, 1 filesystem metadata, 2 Acronis metadata, 3 device */
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/* Prefix of an encrypted ("SE") segment payload, ahead of the ciphertext. Which one
|
|
234
|
+
* applies is decided by the wrapped key's alg: a CBC variant carries the IV alone and
|
|
235
|
+
* pads the ciphertext, a GCM variant follows the IV with the tag over that ciphertext. */
|
|
236
|
+
struct segment_cbc_header {
|
|
237
|
+
char iv[16];
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
struct segment_gcm_header {
|
|
241
|
+
char iv[16];
|
|
242
|
+
char tag[16]; /* AES-GCM tag, computed with no additional data */
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/* Header of one block of a linked-LZ4 chain (LSM cell streams, mem-tree blobs); the
|
|
246
|
+
* compressed block follows */
|
|
247
|
+
struct lz4_block_header {
|
|
248
|
+
uint32 compressed_size;
|
|
249
|
+
uint32 uncompressed_size;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/* LSM superblock (L-SB), carried as a TLV payload in the ARCH header body */
|
|
253
|
+
struct lsm_superblock {
|
|
254
|
+
char magic[4]; /* "L-SB" */
|
|
255
|
+
uint8 format_version; /* "ver" */
|
|
256
|
+
uint8 nr_ctree; /* ctree slots in use, from the first */
|
|
257
|
+
uint8 nr_max_ctree; /* ctree slots: LSB_CTREE_SLOTS */
|
|
258
|
+
uint8 _padding;
|
|
259
|
+
uint32 seq; /* commit sequence */
|
|
260
|
+
uint32 max_ext_len; /* longest extent: 1 MiB in the data_map, 0x3FFFFFFF in the umap, else 0 */
|
|
261
|
+
uint32 key_length; /* per-record key bytes (0 = variable) */
|
|
262
|
+
uint32 value_length; /* per-record value bytes */
|
|
263
|
+
/* followed by LSB_CTREE_SLOTS x ctree_ref, then the mem-tree header */
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
struct ctree_ref {
|
|
267
|
+
uint64 offset; /* root page byte offset; 0xFF..FF / 0 = empty */
|
|
268
|
+
uint64 tree_sz; /* bytes the ctree takes */
|
|
269
|
+
uint32 seq; /* the L-SB seq the ctree was written at; its pages carry it too */
|
|
270
|
+
uint32 _padding;
|
|
271
|
+
uint64 tree_nr; /* records in the ctree */
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
struct lsm_memtree_header {
|
|
275
|
+
uint8 encoding; /* low 7 bits codec (0 raw, 1 LZ4), bit 7 encrypted */
|
|
276
|
+
uint8 _reserved;
|
|
277
|
+
uint16 node_count; /* "c0_count": records in the mem tree */
|
|
278
|
+
uint32 extra_len; /* extra payload bytes after the fixed L-SB */
|
|
279
|
+
uint32 uncompressed_size; /* size of the decoded cell stream */
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/* Object tag of an LSM page */
|
|
283
|
+
enum LsmTag : uint8 {
|
|
284
|
+
CI = 0x43, /* 'C' ci (commit info) */
|
|
285
|
+
DATA_MAP = 0x44, /* 'D' dmap */
|
|
286
|
+
HEADER = 0x48, /* 'H' header */
|
|
287
|
+
LINK_MAP = 0x4C, /* 'L' nlink_map */
|
|
288
|
+
NAME_MAP = 0x4E, /* 'N' name_map */
|
|
289
|
+
SEGMENT_MAP = 0x53, /* 'S' segment_map */
|
|
290
|
+
UNUSED_MAP = 0x55, /* 'U' umap */
|
|
291
|
+
DEDUP_MAP = 0x64, /* 'd' dedup_map */
|
|
292
|
+
ENCR_MAP = 0x65, /* 'e' keymap */
|
|
293
|
+
GOLOMB = 0x67, /* 'g' golomb */
|
|
294
|
+
NOTARY_MAP = 0x6E, /* 'n' notary */
|
|
295
|
+
OSTOR = 0x6F, /* 'o' ostor */
|
|
296
|
+
SLICE_MAP = 0x73, /* 's' smap */
|
|
297
|
+
DATA = 0xFF /* data */
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
/* Inner header of a LEAF / LDIR page (at the start of the page body) */
|
|
301
|
+
struct lsm_page_header {
|
|
302
|
+
char magic[4]; /* "LEAF" or "LDIR" */
|
|
303
|
+
uint8 version; /* < 2 */
|
|
304
|
+
uint8 encoding; /* low 7 bits codec, bit 7 encrypted */
|
|
305
|
+
uint16 cell_count;
|
|
306
|
+
uint32 uncompressed_size; /* size of the decoded cell stream */
|
|
307
|
+
uint32 on_disk_size; /* size of the stored cell stream */
|
|
308
|
+
uint32 seq; /* seq of the ctree the page belongs to, checked on reading */
|
|
309
|
+
LsmTag tag; /* 'D' on data_map, 'S' on segment_map pages; 0 where not written */
|
|
310
|
+
char _reserved[3];
|
|
311
|
+
/* zero pad up to LSM_CELL_STREAM_OFFSET, where the cell stream starts */
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/* Precedes each group of cells in a compact cell stream */
|
|
315
|
+
struct lsm_cell_group_header {
|
|
316
|
+
uint8 count; /* cells in this group, 1-24 */
|
|
317
|
+
uint24 alive; /* bit i set: cell i carries a value; a tombstone only its key */
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/* LDIR record value: where the child page is */
|
|
321
|
+
struct ldir_value {
|
|
322
|
+
uint64 child_offset; /* byte offset of the child LEAF / LDIR page */
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
/* data_map (TLV[1]) record: 31-byte key + 10-byte value */
|
|
326
|
+
struct data_map_key {
|
|
327
|
+
uint64 volume_id;
|
|
328
|
+
uint64 source_offset; /* byte offset within the volume */
|
|
329
|
+
uint24 extent_length;
|
|
330
|
+
uint32 slice_id; /* backup slice that wrote this extent ("field3") */
|
|
331
|
+
uint64 extent_id;
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
struct data_map_value {
|
|
335
|
+
uint64 segment_id;
|
|
336
|
+
uint16 extent_index; /* 0xFFFF = extent fills the whole segment */
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
/* segment_map (TLV[2]) record: 8-byte key + 32-byte value */
|
|
340
|
+
struct segment_map_key {
|
|
341
|
+
uint64 segment_id;
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
struct segment_map_value {
|
|
345
|
+
uint64 page_count : 8; /* pages the segment takes */
|
|
346
|
+
uint64 page_offset : 56; /* page index of the segment's header page */
|
|
347
|
+
uint32 cache_level : 2; /* equal to the segment header's cache_level in every sample */
|
|
348
|
+
uint32 refs : 30; /* reference count: how many data_map records use the segment */
|
|
349
|
+
char hash[20]; /* SHA-1 of the decompressed segment */
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
flag ItemFlags : uint16 {
|
|
353
|
+
COMPLETE = 0x0001, /* set by archive_item_complete(); cleared while the item is written */
|
|
354
|
+
LINKED = 0x0002, /* the item has more names, see the nlink_map */
|
|
355
|
+
DELETED = 0x0100 /* gone as of this backup; hides the older versions */
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
/* imap (TLV[0], "items") record key: one version of an item, such as a disk, partition or
|
|
359
|
+
* file, as of a backup. The item's path follows, to the end of the key. An item's version
|
|
360
|
+
* in a backup is its newest record with a slice id up to that backup's. */
|
|
361
|
+
struct imap_key {
|
|
362
|
+
uint32 slice_id;
|
|
363
|
+
ItemFlags flags;
|
|
364
|
+
uint8 type;
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
/* imap record value: 1 + extra_streams imap_stream records follow */
|
|
368
|
+
struct imap_value {
|
|
369
|
+
uint8 extra_streams; /* streams after the first */
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
flag StreamFlags : uint8 {
|
|
373
|
+
WRITTEN = 0x01, /* written by the record's backup */
|
|
374
|
+
UNKNOWN_2 = 0x02 /* always set by libarchive3's writer, never read */
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
/* A data stream of an imap item: its content (index 0), or more about it */
|
|
378
|
+
struct imap_stream {
|
|
379
|
+
uint8 index;
|
|
380
|
+
StreamFlags flags;
|
|
381
|
+
uint64 id; /* data_map volume_id the data is stored under */
|
|
382
|
+
uint64 size; /* size of the data */
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
/* Password-wrapped data key, stored in the keymap tree (TLV[7]) mem-tree. The wrapped
|
|
386
|
+
* key itself follows this header and runs to the end of the blob: its length is not
|
|
387
|
+
* carried in the format, only its PKCS#7 padding is. */
|
|
388
|
+
struct wrapped_key {
|
|
389
|
+
uint8 format; /* 0x01 password-wrapped, 0x02 certificate-wrapped */
|
|
390
|
+
uint8 alg; /* segment cipher, see KEY_LENGTH; the wrap is always CBC */
|
|
391
|
+
uint8 iter_log2; /* PBKDF2 iterations = 1 << iter_log2 */
|
|
392
|
+
uint8 _reserved;
|
|
393
|
+
char salt[16]; /* PBKDF2 salt */
|
|
394
|
+
};
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
c_tibx = cstruct(endian=">").load(tibx_def)
|
|
398
|
+
|
|
399
|
+
# Only the fields needed to tell a volume's filesystem apart and read its size, and to
|
|
400
|
+
# recognise a disk's partition table
|
|
401
|
+
boot_def = """
|
|
402
|
+
struct ntfs_boot_sector {
|
|
403
|
+
char jump[3];
|
|
404
|
+
char oem_id[8]; /* "NTFS " */
|
|
405
|
+
uint16 bytes_per_sector;
|
|
406
|
+
char _unused[27];
|
|
407
|
+
uint64 total_sectors; /* +0x28 */
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
struct exfat_boot_sector {
|
|
411
|
+
char jump[3];
|
|
412
|
+
char fs_name[8]; /* "EXFAT " */
|
|
413
|
+
char _must_be_zero[53];
|
|
414
|
+
uint64 partition_offset; /* +0x40 */
|
|
415
|
+
uint64 volume_length; /* +0x48, in sectors */
|
|
416
|
+
char _unused[28];
|
|
417
|
+
uint8 bytes_per_sector_shift; /* +0x6C */
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
struct fat_boot_sector {
|
|
421
|
+
char jump[3];
|
|
422
|
+
char oem_id[8];
|
|
423
|
+
uint16 bytes_per_sector; /* +0x0B */
|
|
424
|
+
uint8 sectors_per_cluster;
|
|
425
|
+
uint16 reserved_sectors;
|
|
426
|
+
uint8 fat_count;
|
|
427
|
+
uint16 root_entries;
|
|
428
|
+
uint16 total_sectors_16; /* +0x13, 0 if the volume needs total_sectors_32 */
|
|
429
|
+
uint8 media;
|
|
430
|
+
uint16 fat_size_16;
|
|
431
|
+
uint16 sectors_per_track;
|
|
432
|
+
uint16 heads;
|
|
433
|
+
uint32 hidden_sectors;
|
|
434
|
+
uint32 total_sectors_32; /* +0x20 */
|
|
435
|
+
char _fat16_ext[18];
|
|
436
|
+
char fs_type_16[8]; /* +0x36, "FAT12 " / "FAT16 " */
|
|
437
|
+
char _fat32_ext[20];
|
|
438
|
+
char fs_type_32[8]; /* +0x52, "FAT32 " */
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
/* ext2/3/4 superblock, EXT_SUPERBLOCK_OFFSET bytes into the volume */
|
|
442
|
+
struct ext_superblock {
|
|
443
|
+
uint32 inodes_count;
|
|
444
|
+
uint32 blocks_count; /* +0x04, low 32 bits */
|
|
445
|
+
char _unused0[16];
|
|
446
|
+
uint32 log_block_size; /* +0x18, block size is 1024 << log_block_size */
|
|
447
|
+
char _unused1[28];
|
|
448
|
+
uint16 magic; /* +0x38, EXT_MAGIC */
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
struct mbr_partition {
|
|
452
|
+
uint8 boot_flag; /* 0x80 active, 0x00 inactive */
|
|
453
|
+
char chs_first[3];
|
|
454
|
+
uint8 type; /* 0x00 for an unused entry */
|
|
455
|
+
char chs_last[3];
|
|
456
|
+
uint32 lba_start;
|
|
457
|
+
uint32 sectors;
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
/* A disk's master boot record */
|
|
461
|
+
struct mbr {
|
|
462
|
+
char bootstrap[446];
|
|
463
|
+
mbr_partition partitions[4]; /* +0x1BE */
|
|
464
|
+
uint16 signature; /* +0x1FE, MBR_SIGNATURE */
|
|
465
|
+
};
|
|
466
|
+
"""
|
|
467
|
+
|
|
468
|
+
c_boot = cstruct(endian="<").load(boot_def)
|
|
469
|
+
|
|
470
|
+
PAGE_SIZE: int = c_tibx.PAGE_SIZE
|
|
471
|
+
ENVELOPE_SIZE = len(c_tibx.page_header)
|
|
472
|
+
PAGE_BODY_SIZE = PAGE_SIZE - ENVELOPE_SIZE
|
|
473
|
+
|
|
474
|
+
PAGE_MARKER = 0x41
|
|
475
|
+
|
|
476
|
+
ZSTD_MAGIC = b"\x28\xb5\x2f\xfd"
|
|
477
|
+
|
|
478
|
+
SEGMENT_MAGIC = b"SG"
|
|
479
|
+
SEGMENT_MAGIC_ENCRYPTED = b"SE"
|
|
480
|
+
SEGMENT_HEADER_OFFSET = ENVELOPE_SIZE
|
|
481
|
+
SEGMENT_PAYLOAD_OFFSET = 0x2C
|
|
482
|
+
# Compressed bytes on the segment's first page; the rest spills onto continuation pages
|
|
483
|
+
SEGMENT_FIRST_PAGE_PAYLOAD = PAGE_SIZE - SEGMENT_PAYLOAD_OFFSET
|
|
484
|
+
|
|
485
|
+
SEGMENT_CBC_HEADER_SIZE = len(c_tibx.segment_cbc_header)
|
|
486
|
+
SEGMENT_GCM_HEADER_SIZE = len(c_tibx.segment_gcm_header)
|
|
487
|
+
|
|
488
|
+
# Extents sharing a segment (extent_index != 0xFFFF) are concatenated in index order,
|
|
489
|
+
# each aligned up to this boundary (empirical: exact fit on all observed segments)
|
|
490
|
+
EXTENT_ALIGNMENT = 16
|
|
491
|
+
|
|
492
|
+
LZ4_BLOCK_HEADER_SIZE = len(c_tibx.lz4_block_header)
|
|
493
|
+
|
|
494
|
+
LSM_MAGIC_SUPERBLOCK = b"L-SB"
|
|
495
|
+
LSM_MAGIC_LEAF = b"LEAF"
|
|
496
|
+
LSM_MAGIC_LDIR = b"LDIR"
|
|
497
|
+
# Cell stream starts this many bytes into a LEAF/LDIR page body
|
|
498
|
+
LSM_CELL_STREAM_OFFSET = 0x34
|
|
499
|
+
LSM_CELL_GROUP_HEADER_SIZE = len(c_tibx.lsm_cell_group_header)
|
|
500
|
+
LSM_CELL_GROUP_MAX = 24
|
|
501
|
+
LDIR_VALUE_SIZE = len(c_tibx.ldir_value)
|
|
502
|
+
# L-SB fixed layout: the lsm_superblock fields, a fixed number of ctree_ref slots, the
|
|
503
|
+
# mem-tree header, then reserved space up to LSB_FIXED_SIZE; the mem-tree blob follows
|
|
504
|
+
LSB_CTREE_SLOTS = 10
|
|
505
|
+
LSB_CTREE_OFFSET = len(c_tibx.lsm_superblock)
|
|
506
|
+
LSB_MEMTREE_OFFSET = LSB_CTREE_OFFSET + LSB_CTREE_SLOTS * len(c_tibx.ctree_ref)
|
|
507
|
+
LSB_FIXED_SIZE = 0x178
|
|
508
|
+
|
|
509
|
+
CTREE_EMPTY_SENTINEL = 0xFFFFFFFFFFFFFFFF
|
|
510
|
+
|
|
511
|
+
# ARCH header body: TLV directory location and slot count
|
|
512
|
+
TLV_DIRECTORY_OFFSET = 0x400
|
|
513
|
+
TLV_HEADER_SIZE = len(c_tibx.tlv_header)
|
|
514
|
+
TLV_SLOT_COUNT = 19
|
|
515
|
+
|
|
516
|
+
TLV_IMAP = 0
|
|
517
|
+
TLV_DATA_MAP = 1
|
|
518
|
+
TLV_SEGMENT_MAP = 2
|
|
519
|
+
TLV_DEDUP_MAP = 3
|
|
520
|
+
TLV_NLINK_MAP = 4
|
|
521
|
+
TLV_SLICES = 5
|
|
522
|
+
TLV_UMAP = 6
|
|
523
|
+
TLV_KEYMAP = 7
|
|
524
|
+
TLV_NOTARY = 8
|
|
525
|
+
TLV_META = 9
|
|
526
|
+
TLV_OSTOR_HISTORY = 13
|
|
527
|
+
TLV_UNUSED = 17
|
|
528
|
+
TLV_VOLUMES = 18
|
|
529
|
+
|
|
530
|
+
# Wrapped-key blob: the fixed header above, then the padded key to the end of the blob
|
|
531
|
+
WRAPPED_KEY_HEADER_SIZE = len(c_tibx.wrapped_key)
|
|
532
|
+
WRAPPED_KEY_SALT_SIZE = 16
|
|
533
|
+
WRAPPED_KEY_FORMAT_PASSWORD = 0x01
|
|
534
|
+
WRAPPED_KEY_FORMAT_PUBKEY = 0x02
|
|
535
|
+
|
|
536
|
+
DATA_MAP_KEY_SIZE = len(c_tibx.data_map_key)
|
|
537
|
+
DATA_MAP_VALUE_SIZE = len(c_tibx.data_map_value)
|
|
538
|
+
# data_map_value.extent_index for "extent fills its segment"
|
|
539
|
+
EXTENT_INDEX_WHOLE_SEGMENT = 0xFFFF
|
|
540
|
+
|
|
541
|
+
SEGMENT_MAP_KEY_SIZE = len(c_tibx.segment_map_key)
|
|
542
|
+
SEGMENT_MAP_VALUE_SIZE = len(c_tibx.segment_map_value)
|
|
543
|
+
|
|
544
|
+
SLICE_TYPE_MASK = 0x7F
|
|
545
|
+
|
|
546
|
+
IMAP_KEY_SIZE = len(c_tibx.imap_key)
|
|
547
|
+
IMAP_VALUE_SIZE = len(c_tibx.imap_value)
|
|
548
|
+
IMAP_STREAM_SIZE = len(c_tibx.imap_stream)
|
|
549
|
+
|
|
550
|
+
EXT_SUPERBLOCK_OFFSET = 0x400
|
|
551
|
+
EXT_MAGIC = 0xEF53
|
|
552
|
+
|
|
553
|
+
MBR_SIGNATURE = 0xAA55
|
|
554
|
+
# mbr_partition.type of the one partition covering a GPT disk in its protective MBR
|
|
555
|
+
MBR_TYPE_GPT_PROTECTIVE = 0xEE
|