vsc/catalog.rs
1//! VSS volume header (offset 0x1E00) and catalog block / entry decoding.
2//!
3//! The volume header names the catalog; the catalog is one or more 16 KiB
4//! blocks, each a 128-byte header followed by 128-byte entries. A type-0x02
5//! entry is a snapshot descriptor; the paired type-0x03 entry points at the
6//! store's block header so its store-information can be read.
7
8use crate::bytes::{le_u32, le_u64, read_guid};
9use crate::guid::{format_guid, VSS_IDENTIFIER};
10
11/// Byte offset of the VSS volume header within the NTFS volume.
12pub const VSS_VOLUME_HEADER_OFFSET: u64 = 0x1E00;
13
14/// VSS block size (catalog block and store block), in bytes.
15pub const BLOCK_SIZE: usize = 16_384;
16
17/// Length of a catalog block header, in bytes.
18pub const CATALOG_BLOCK_HEADER_LEN: usize = 128;
19
20/// Length of a single catalog entry, in bytes.
21pub const CATALOG_ENTRY_LEN: usize = 128;
22
23/// Record type of the VSS volume header (offset 20).
24pub const VOLUME_HEADER_RECORD_TYPE: u32 = 0x01;
25
26/// Record type of a catalog block header (offset 20).
27pub const CATALOG_BLOCK_RECORD_TYPE: u32 = 0x02;
28
29/// Upper bound on catalog blocks walked, to bound a corrupt/looping chain.
30pub const MAX_CATALOG_BLOCKS: usize = 4096;
31
32/// The decoded VSS volume header at [`VSS_VOLUME_HEADER_OFFSET`].
33#[derive(Debug, Clone)]
34pub struct VolumeHeader {
35 /// Whether the 16 bytes at offset 0 are the VSS identifier GUID — i.e. this
36 /// volume carries VSS metadata at all.
37 pub has_vss_identifier: bool,
38 /// Format version.
39 pub version: u32,
40 /// Record type (expected [`VOLUME_HEADER_RECORD_TYPE`]).
41 pub record_type: u32,
42 /// Current offset field (expected [`VSS_VOLUME_HEADER_OFFSET`]).
43 pub current_offset: u64,
44 /// Catalog offset (volume-relative); 0 when there is no catalog.
45 pub catalog_offset: u64,
46 /// Maximum store size, or 0 when unbounded.
47 pub maximum_size: u64,
48 /// Volume identifier GUID.
49 pub volume_identifier: [u8; 16],
50 /// Shadow-copy storage volume identifier GUID.
51 pub storage_volume_identifier: [u8; 16],
52}
53
54impl VolumeHeader {
55 /// Parse a volume header from a byte slice positioned at offset 0x1E00.
56 #[must_use]
57 pub fn parse(buf: &[u8]) -> Self {
58 VolumeHeader {
59 has_vss_identifier: read_guid(buf, 0) == VSS_IDENTIFIER,
60 version: le_u32(buf, 16),
61 record_type: le_u32(buf, 20),
62 current_offset: le_u64(buf, 24),
63 catalog_offset: le_u64(buf, 48),
64 maximum_size: le_u64(buf, 56),
65 volume_identifier: read_guid(buf, 64),
66 storage_volume_identifier: read_guid(buf, 80),
67 }
68 }
69}
70
71/// A shadow-copy store as described by a catalog type-0x02 entry, augmented with
72/// the store-header offset from its paired type-0x03 entry.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct StoreDescriptor {
75 /// Store identifier GUID (also the store's on-disk filename).
76 pub store_id: [u8; 16],
77 /// Shadow-copy volume size at snapshot time.
78 pub volume_size: u64,
79 /// Catalog sequence number.
80 pub sequence: u64,
81 /// Snapshot flags (0x40 = Vista/7, 0x440 = Win8 file backup).
82 pub flags: u64,
83 /// Shadow-copy creation time, a raw Windows FILETIME (100 ns since 1601, UTC).
84 pub creation_time: u64,
85 /// Volume-relative offset of the store block header (from the type-0x03
86 /// entry), or `None` when the catalog has no type-0x03 entry (Win 2003 R2).
87 pub store_header_offset: Option<u64>,
88 /// Volume-relative offset of the store bitmap chain (0x0006), from the
89 /// type-0x03 entry at offset 48, or `None` when there is no type-0x03 entry.
90 /// Phase-2 reconstruction walks this chain to tell unallocated blocks apart.
91 pub store_bitmap_offset: Option<u64>,
92}
93
94impl StoreDescriptor {
95 /// The store identifier rendered as a canonical GUID string.
96 #[must_use]
97 pub fn store_id_string(&self) -> String {
98 format_guid(&self.store_id)
99 }
100}
101
102/// One decoded catalog entry.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub(crate) enum CatalogEntry {
105 /// Type 0x00 — empty padding slot.
106 Empty,
107 /// Type 0x02 — a snapshot descriptor.
108 Snapshot(StoreDescriptor),
109 /// Type 0x03 — a pointer to a store's block header and bitmap chains.
110 StorePointer {
111 store_id: [u8; 16],
112 store_header_offset: u64,
113 store_bitmap_offset: u64,
114 },
115 /// Any other entry type (0x01, unknown).
116 Other,
117}
118
119/// Parse a single 128-byte catalog entry.
120pub(crate) fn parse_catalog_entry(buf: &[u8]) -> CatalogEntry {
121 match le_u64(buf, 0) {
122 0x02 => CatalogEntry::Snapshot(StoreDescriptor {
123 store_id: read_guid(buf, 16),
124 volume_size: le_u64(buf, 8),
125 sequence: le_u64(buf, 32),
126 flags: le_u64(buf, 40),
127 creation_time: le_u64(buf, 48),
128 store_header_offset: None,
129 store_bitmap_offset: None,
130 }),
131 0x03 => CatalogEntry::StorePointer {
132 store_id: read_guid(buf, 16),
133 store_header_offset: le_u64(buf, 32),
134 store_bitmap_offset: le_u64(buf, 48),
135 },
136 0x00 => CatalogEntry::Empty,
137 _ => CatalogEntry::Other,
138 }
139}
140
141/// Read the next-block offset (volume-relative) from a catalog block header.
142pub(crate) fn catalog_next_block_offset(block: &[u8]) -> u64 {
143 le_u64(block, 40)
144}
145
146/// Whether a catalog block header carries the VSS identifier and catalog record
147/// type — used to stop walking on a corrupt/foreign block.
148pub(crate) fn is_catalog_block(block: &[u8]) -> bool {
149 read_guid(block, 0) == VSS_IDENTIFIER && le_u32(block, 20) == CATALOG_BLOCK_RECORD_TYPE
150}