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}
89
90impl StoreDescriptor {
91 /// The store identifier rendered as a canonical GUID string.
92 #[must_use]
93 pub fn store_id_string(&self) -> String {
94 format_guid(&self.store_id)
95 }
96}
97
98/// One decoded catalog entry.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub(crate) enum CatalogEntry {
101 /// Type 0x00 — empty padding slot.
102 Empty,
103 /// Type 0x02 — a snapshot descriptor.
104 Snapshot(StoreDescriptor),
105 /// Type 0x03 — a pointer to a store's block header.
106 StorePointer {
107 store_id: [u8; 16],
108 store_header_offset: u64,
109 },
110 /// Any other entry type (0x01, unknown).
111 Other,
112}
113
114/// Parse a single 128-byte catalog entry.
115pub(crate) fn parse_catalog_entry(buf: &[u8]) -> CatalogEntry {
116 match le_u64(buf, 0) {
117 0x02 => CatalogEntry::Snapshot(StoreDescriptor {
118 store_id: read_guid(buf, 16),
119 volume_size: le_u64(buf, 8),
120 sequence: le_u64(buf, 32),
121 flags: le_u64(buf, 40),
122 creation_time: le_u64(buf, 48),
123 store_header_offset: None,
124 }),
125 0x03 => CatalogEntry::StorePointer {
126 store_id: read_guid(buf, 16),
127 store_header_offset: le_u64(buf, 32),
128 },
129 0x00 => CatalogEntry::Empty,
130 _ => CatalogEntry::Other,
131 }
132}
133
134/// Read the next-block offset (volume-relative) from a catalog block header.
135pub(crate) fn catalog_next_block_offset(block: &[u8]) -> u64 {
136 le_u64(block, 40)
137}
138
139/// Whether a catalog block header carries the VSS identifier and catalog record
140/// type — used to stop walking on a corrupt/foreign block.
141pub(crate) fn is_catalog_block(block: &[u8]) -> bool {
142 read_guid(block, 0) == VSS_IDENTIFIER && le_u32(block, 20) == CATALOG_BLOCK_RECORD_TYPE
143}