vshadow 0.2.0

Pure Rust parser for Windows Volume Shadow Copy (VSS) snapshots. Read-only access to VSS stores from forensic disk images without Windows APIs.
Documentation
use std::collections::BTreeMap;
use std::io::{Read, Seek, SeekFrom};
use crate::catalog::{StoreMeta, StoreLocation};
use crate::VssError;

const BLOCK_SIZE: u64 = 0x4000; // 16 KiB
const BLOCK_HEADER_SIZE: usize = 128;
const DESCRIPTOR_SIZE: usize = 32;

/// High-level store information.
#[derive(Debug, Clone)]
pub struct StoreInfo {
    /// Store GUID
    pub store_id: [u8; 16],
    /// Volume size at snapshot time
    pub volume_size: u64,
    /// Creation time (Windows FILETIME)
    pub creation_time: u64,
    /// Sequence number
    pub sequence: u64,
    /// Block list offset
    pub block_list_offset: u64,
    /// Store header offset
    pub store_header_offset: u64,
}

impl StoreInfo {
    pub fn from_meta_and_location(meta: &StoreMeta, loc: &StoreLocation) -> Self {
        Self {
            store_id: meta.store_id,
            volume_size: meta.volume_size,
            creation_time: meta.creation_time,
            sequence: meta.sequence,
            block_list_offset: loc.block_list_offset,
            store_header_offset: loc.store_header_offset,
        }
    }

    /// Convert FILETIME to a human-readable UTC date string (YYYY-MM-DD HH:MM:SS UTC).
    pub fn creation_time_utc(&self) -> String {
        if self.creation_time == 0 {
            return "unknown".to_string();
        }
        // FILETIME: 100-nanosecond intervals since 1601-01-01
        let secs_since_1601 = self.creation_time / 10_000_000;
        let unix_secs = secs_since_1601.saturating_sub(11_644_473_600);

        let time_of_day = unix_secs % 86400;
        let hours = time_of_day / 3600;
        let minutes = (time_of_day % 3600) / 60;
        let seconds = time_of_day % 60;

        // Calculate year/month/day from unix timestamp
        let mut days = (unix_secs / 86400) as i64;
        let mut year = 1970i64;

        loop {
            let days_in_year = if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) { 366 } else { 365 };
            if days < days_in_year { break; }
            days -= days_in_year;
            year += 1;
        }

        let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
        let month_days = [31, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
        let mut month = 0usize;
        for (i, &md) in month_days.iter().enumerate() {
            if days < md as i64 { month = i; break; }
            days -= md as i64;
        }

        format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02} UTC",
            year, month + 1, days + 1, hours, minutes, seconds)
    }
}

/// A block descriptor mapping: original_offset -> store_data_offset
#[derive(Debug, Clone)]
pub struct BlockDescriptor {
    /// Offset of the original data block on the volume
    pub original_offset: u64,
    /// Offset of the stored (old) data block on the volume
    pub store_data_offset: u64,
    /// Flags
    pub flags: u32,
}

/// Parse block descriptors for a store.
/// Returns a BTreeMap from original_offset -> store_data_offset.
/// This map tells us: "to read what was at original_offset at snapshot time,
/// read from store_data_offset instead."
pub fn parse_block_descriptors<R: Read + Seek>(
    reader: &mut R,
    first_block_offset: u64,
) -> Result<BTreeMap<u64, BlockDescriptor>, VssError> {
    let mut map = BTreeMap::new();
    let mut current_offset = first_block_offset;

    loop {
        if current_offset == 0 {
            break;
        }

        reader.seek(SeekFrom::Start(current_offset))
            .map_err(VssError::Io)?;

        let mut block = vec![0u8; BLOCK_SIZE as usize];
        reader.read_exact(&mut block).map_err(VssError::Io)?;

        // Parse block header
        let record_type = u32::from_le_bytes(block[20..24].try_into().unwrap());
        if record_type != 0x03 {
            break;
        }

        let next_offset = u64::from_le_bytes(block[40..48].try_into().unwrap());

        // Parse descriptors after the header
        let mut pos = BLOCK_HEADER_SIZE;
        while pos + DESCRIPTOR_SIZE <= BLOCK_SIZE as usize {
            let desc = &block[pos..pos + DESCRIPTOR_SIZE];

            let original_offset = u64::from_le_bytes(desc[0..8].try_into().unwrap());
            let _relative_offset = u64::from_le_bytes(desc[8..16].try_into().unwrap());
            let store_data_offset = u64::from_le_bytes(desc[16..24].try_into().unwrap());
            let flags = u32::from_le_bytes(desc[24..28].try_into().unwrap());

            // Skip empty or "not used" descriptors
            if original_offset == 0 && store_data_offset == 0 {
                break;
            }
            if flags & 0x04 != 0 {
                // "Not used" flag
                pos += DESCRIPTOR_SIZE;
                continue;
            }

            map.insert(original_offset, BlockDescriptor {
                original_offset,
                store_data_offset,
                flags,
            });

            pos += DESCRIPTOR_SIZE;
        }

        current_offset = next_offset;
    }

    Ok(map)
}