dissect.archive 1.9.dev2__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.
Files changed (86) hide show
  1. dissect_archive-1.9.dev3/.gitattributes +1 -0
  2. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/PKG-INFO +5 -3
  3. dissect_archive-1.9.dev3/dissect/archive/tibx/__init__.py +24 -0
  4. dissect_archive-1.9.dev3/dissect/archive/tibx/c_tibx.py +555 -0
  5. dissect_archive-1.9.dev3/dissect/archive/tibx/c_tibx.pyi +836 -0
  6. dissect_archive-1.9.dev3/dissect/archive/tibx/codec.py +137 -0
  7. dissect_archive-1.9.dev3/dissect/archive/tibx/crypto.py +255 -0
  8. dissect_archive-1.9.dev3/dissect/archive/tibx/exception.py +23 -0
  9. dissect_archive-1.9.dev3/dissect/archive/tibx/lsm.py +466 -0
  10. dissect_archive-1.9.dev3/dissect/archive/tibx/map.py +520 -0
  11. dissect_archive-1.9.dev3/dissect/archive/tibx/page.py +420 -0
  12. dissect_archive-1.9.dev3/dissect/archive/tibx/segment.py +187 -0
  13. dissect_archive-1.9.dev3/dissect/archive/tibx/stream.py +28 -0
  14. dissect_archive-1.9.dev3/dissect/archive/tibx/tibx.py +955 -0
  15. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/dissect.archive.egg-info/PKG-INFO +5 -3
  16. dissect_archive-1.9.dev3/dissect.archive.egg-info/SOURCES.txt +81 -0
  17. dissect_archive-1.9.dev3/dissect.archive.egg-info/requires.txt +14 -0
  18. dissect_archive-1.9.dev3/dissect.archive.egg-info/scm_file_list.json +80 -0
  19. dissect_archive-1.9.dev3/dissect.archive.egg-info/scm_version.json +8 -0
  20. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/pyproject.toml +5 -2
  21. dissect_archive-1.9.dev3/tests/_data/acronis/tibx/ati_2020_disk_enc_lz4_compr.tibx +0 -0
  22. dissect_archive-1.9.dev3/tests/_data/acronis/tibx/ati_2020_disk_lz4_compr.tibx +0 -0
  23. dissect_archive-1.9.dev3/tests/_data/acronis/tibx/t1_chain_diff-0001.tibx +0 -0
  24. dissect_archive-1.9.dev3/tests/_data/acronis/tibx/t1_chain_diff.tibx +0 -0
  25. dissect_archive-1.9.dev3/tests/_data/acronis/tibx/t1_chain_inc.tibx +0 -0
  26. dissect_archive-1.9.dev3/tests/_data/acronis/tibx/t1_disk_gpt.tibx +0 -0
  27. dissect_archive-1.9.dev3/tests/_data/acronis/tibx/t1_disk_mbr.tibx +0 -0
  28. dissect_archive-1.9.dev3/tests/_data/acronis/tibx/t1_empty_ntfs.tibx +0 -0
  29. dissect_archive-1.9.dev3/tests/_data/acronis/tibx/t1_enc_aes.tibx +0 -0
  30. dissect_archive-1.9.dev3/tests/_data/acronis/tibx/t1_legacy_tib.tibx +0 -0
  31. dissect_archive-1.9.dev3/tests/_data/acronis/tibx/t1_multivol.tibx +0 -0
  32. dissect_archive-1.9.dev3/tests/_data/acronis/tibx/t1_simple_ntfs.tibx +0 -0
  33. dissect_archive-1.9.dev3/tests/_data/acronis/tibx/t1_stored.tibx +0 -0
  34. dissect_archive-1.9.dev3/tests/_tools/__init__.py +0 -0
  35. dissect_archive-1.9.dev3/tests/_tools/tibx/__init__.py +0 -0
  36. dissect_archive-1.9.dev3/tests/_tools/tibx/_synth.py +723 -0
  37. dissect_archive-1.9.dev3/tests/tibx/__init__.py +0 -0
  38. dissect_archive-1.9.dev3/tests/tibx/test_codec.py +158 -0
  39. dissect_archive-1.9.dev3/tests/tibx/test_crypto.py +302 -0
  40. dissect_archive-1.9.dev3/tests/tibx/test_lsm.py +279 -0
  41. dissect_archive-1.9.dev3/tests/tibx/test_page.py +150 -0
  42. dissect_archive-1.9.dev3/tests/tibx/test_recovery.py +145 -0
  43. dissect_archive-1.9.dev3/tests/tibx/test_segment.py +138 -0
  44. dissect_archive-1.9.dev3/tests/tibx/test_tibx.py +805 -0
  45. dissect_archive-1.9.dev3/tests/tibx/test_tibx_acronis.py +459 -0
  46. dissect_archive-1.9.dev2/.gitattributes +0 -1
  47. dissect_archive-1.9.dev2/dissect.archive.egg-info/SOURCES.txt +0 -42
  48. dissect_archive-1.9.dev2/dissect.archive.egg-info/requires.txt +0 -10
  49. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/.git-blame-ignore-revs +0 -0
  50. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/COPYRIGHT +0 -0
  51. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/LICENSE +0 -0
  52. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/MANIFEST.in +0 -0
  53. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/README.md +0 -0
  54. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/dissect/archive/__init__.py +0 -0
  55. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/dissect/archive/c_vbk.py +0 -0
  56. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/dissect/archive/c_vma.py +0 -0
  57. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/dissect/archive/c_wim.py +0 -0
  58. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/dissect/archive/exceptions.py +0 -0
  59. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/dissect/archive/tools/__init__.py +0 -0
  60. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/dissect/archive/tools/backup.py +0 -0
  61. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/dissect/archive/vbk.py +0 -0
  62. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/dissect/archive/vma.py +0 -0
  63. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/dissect/archive/wim.py +0 -0
  64. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/dissect/archive/xva.py +0 -0
  65. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/dissect.archive.egg-info/dependency_links.txt +0 -0
  66. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/dissect.archive.egg-info/entry_points.txt +0 -0
  67. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/dissect.archive.egg-info/top_level.txt +0 -0
  68. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/setup.cfg +0 -0
  69. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/tests/__init__.py +0 -0
  70. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/tests/_data/basic16k.wim.gz +0 -0
  71. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/tests/_data/basic32k.wim.gz +0 -0
  72. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/tests/_data/basic4k.wim.gz +0 -0
  73. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/tests/_data/basic8k.wim.gz +0 -0
  74. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/tests/_data/test.vma.gz +0 -0
  75. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/tests/_data/test13.vbk.gz +0 -0
  76. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/tests/_data/test9.vbk.gz +0 -0
  77. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/tests/_docs/Makefile +0 -0
  78. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/tests/_docs/__init__.py +0 -0
  79. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/tests/_docs/conf.py +0 -0
  80. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/tests/_docs/index.rst +0 -0
  81. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/tests/conftest.py +0 -0
  82. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/tests/test_exceptions.py +0 -0
  83. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/tests/test_vbk.py +0 -0
  84. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/tests/test_vma.py +0 -0
  85. {dissect_archive-1.9.dev2 → dissect_archive-1.9.dev3}/tests/test_wim.py +0 -0
  86. {dissect_archive-1.9.dev2 → 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.dev2
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.22
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.22.dev; extra == "dev"
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