Skip to main content

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}