this is a pdb parsing project that does not use any external libraries besides byteorder
no pdb parsing lib either. fully custom
There is an issue with the DBI Parsing
Fix it. please.
Add color via windows color codes maybe?
Parsing PDB file: .\RobloxStudioBeta.pdb
--- PDB File Information ---
Page Size: 4096
Number of Pages: 223165
Root Stream Size: 918732
Total Streams: 6859

--- Project/File Directory ---
Failed to parse DBI stream: invalid utf-8 sequence of 1 bytes from index 0

--- Types and Structures ---
Total Type Records: 3896880

use std::collections::HashMap;
use std::fs::File;
use std::io::{self, BufReader, Cursor, Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};

use byteorder::{LittleEndian, ReadBytesExt};

fn main() -> io::Result<()> {
    // Entry point of the PDB parsing program
    let args: Vec<String> = std::env::args().collect();
    if args.len() != 2 {
        eprintln!("Usage: {} <path_to_pdb_file>", args[0]);
        std::process::exit(1);
    }

    let pdb_path = &args[1];

    println!("Parsing PDB file: {}", pdb_path);

    // Open the PDB file
    let file = match File::open(pdb_path) {
        Ok(f) => f,
        Err(e) => {
            eprintln!("Failed to open file {}: {}", pdb_path, e);
            std::process::exit(1);
        }
    };
    let mut reader = BufReader::new(file);

    // Parse the PDB file
    let pdb = match PdbFile::parse(&mut reader) {
        Ok(pdb) => pdb,
        Err(e) => {
            eprintln!("Failed to parse PDB file: {}", e);
            std::process::exit(1);
        }
    };

    // Output the parsed information
    pdb.print_info();

    Ok(())
}

/// Represents a PDB file
struct PdbFile {
    page_size: u32,
    fpm_page: u32,
    num_pages: u32,
    root_stream_size: u32,
    root_stream_pages: Vec<u32>,
    streams: Vec<PdbStream>,
}

/// Represents a stream within the PDB file
struct PdbStream {
    size: u32,
    pages: Vec<u32>,
    data: Vec<u8>,
}

impl PdbFile {
    /// Parses the PDB file from the given reader
    fn parse(reader: &mut BufReader<File>) -> io::Result<Self> {
        // Read and parse the SuperBlock (first 64 bytes)
        let mut superblock = [0u8; 64];
        reader.read_exact(&mut superblock)?;

        // Verify the PDB signature
        let signature = &superblock[0..32];
        let expected_signature = b"Microsoft C/C++ MSF 7.00";
        if &signature[0..expected_signature.len()] != expected_signature {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                "Invalid PDB signature.",
            ));
        }

        // Parse SuperBlock fields
        let mut cursor = Cursor::new(&superblock);

        // Skip the signature (32 bytes)
        cursor.seek(SeekFrom::Start(32))?;

        // Read PDB header fields
        let page_size = cursor.read_u32::<LittleEndian>()?;
        let fpm_page = cursor.read_u32::<LittleEndian>()?;
        let num_pages = cursor.read_u32::<LittleEndian>()?;
        let root_stream_size = cursor.read_u32::<LittleEndian>()?;
        let _reserved = cursor.read_u32::<LittleEndian>()?;

        // Read Root Directory Index entries from SuperBlock
        let mut rdi_entries = Vec::new();
        while cursor.position() < 64 {
            let page_number = cursor.read_u32::<LittleEndian>()?;
            rdi_entries.push(page_number);
        }

        // Calculate the number of pages the directory stream spans
        let directory_size = root_stream_size;
        let directory_pages = Self::calculate_page_count(directory_size, page_size);

        // Calculate the number of entries needed in the Root Directory Index
        let entries_per_page = page_size / 4; // Number of u32s per page
        let num_rdi_entries = Self::calculate_page_count(directory_pages * 4, page_size);

        // Read the full Root Directory Index
        let root_dir_index_pages = if num_rdi_entries <= rdi_entries.len() as u32 {
            // Root Directory Index entries fit in SuperBlock
            rdi_entries[0..num_rdi_entries as usize].to_vec()
        } else {
            // Root Directory Index spans multiple levels
            Self::read_page_number_array(reader, page_size, &rdi_entries, num_rdi_entries)?
        };

        // Read the directory pages list from the Root Directory Index Pages
        let directory_pages_list = Self::read_page_number_array(
            reader,
            page_size,
            &root_dir_index_pages,
            directory_pages,
        )?;

        // Read the root directory stream data
        let root_stream_data =
            Self::read_stream(reader, page_size, &directory_pages_list, root_stream_size)?;

        // Parse the root directory stream
        let (streams, _stream_pages) =
            Self::parse_root_stream(&root_stream_data, page_size, reader)?;

        // Load the data for each stream
        let mut pdb_streams = Vec::new();
        for (i, (size, pages)) in streams.into_iter().enumerate() {
            if size == u32::MAX {
                // Stream does not exist
                pdb_streams.push(PdbStream {
                    size,
                    pages,
                    data: Vec::new(),
                });
                continue;
            }

            let data = Self::read_stream(reader, page_size, &pages, size)?;
            pdb_streams.push(PdbStream { size, pages, data });
        }

        Ok(PdbFile {
            page_size,
            fpm_page,
            num_pages,
            root_stream_size,
            root_stream_pages: directory_pages_list,
            streams: pdb_streams,
        })
    }

    /// Prints information about the PDB file
    fn print_info(&self) {
        println!("--- PDB File Information ---");
        println!("Page Size: {}", self.page_size);
        println!("Number of Pages: {}", self.num_pages);
        println!("Root Stream Size: {}", self.root_stream_size);
        println!("Total Streams: {}", self.streams.len());
        println!();

        // Parse and print the project/file directory
        if let Some(stream) = self.streams.get(3) {
            println!("--- Project/File Directory ---");
            self.parse_dbi_stream(stream).unwrap_or_else(|e| {
                eprintln!("Failed to parse DBI stream: {}", e);
            });
            println!();
        } else {
            println!("DBI Stream (Stream 3) not found.");
        }

        // Parse and print the types, structures, and classes
        if let Some(stream) = self.streams.get(2) {
            println!("--- Types and Structures ---");
            self.parse_tpi_stream(stream).unwrap_or_else(|e| {
                eprintln!("Failed to parse TPI stream: {}", e);
            });
            println!();
        } else {
            println!("TPI Stream (Stream 2) not found.");
        }
    }

    /// Parses the DBI (Debug Information) Stream (Stream 3)
    fn parse_dbi_stream(&self, stream: &PdbStream) -> io::Result<()> {
        let mut cursor = Cursor::new(&stream.data);

        // Read DBI Stream Header
        let _signature = cursor.read_u32::<LittleEndian>()?;
        let _version = cursor.read_u32::<LittleEndian>()?;
        let _age = cursor.read_u32::<LittleEndian>()?;
        let _global_stream_index = cursor.read_u16::<LittleEndian>()?;
        let _build_number = cursor.read_u16::<LittleEndian>()?;
        let _public_stream_index = cursor.read_u16::<LittleEndian>()?;
        let _pdb_dll_version = cursor.read_u16::<LittleEndian>()?;
        let _sym_record_stream = cursor.read_u16::<LittleEndian>()?;
        let _pdb_dll_rbld = cursor.read_u16::<LittleEndian>()?;

        let module_info_size = cursor.read_i32::<LittleEndian>()?;
        let _section_contribution_size = cursor.read_i32::<LittleEndian>()?;
        let _section_map_size = cursor.read_i32::<LittleEndian>()?;
        let file_info_size = cursor.read_i32::<LittleEndian>()?;
        let _type_server_map_size = cursor.read_i32::<LittleEndian>()?;
        let _mfc_type_server_index = cursor.read_u32::<LittleEndian>()?;
        let _optional_dbg_header_size = cursor.read_i32::<LittleEndian>()?;
        let _ec_substream_size = cursor.read_i32::<LittleEndian>()?;
        let _flags = cursor.read_u16::<LittleEndian>()?;
        let _machine = cursor.read_u16::<LittleEndian>()?;
        let _reserved = cursor.read_u32::<LittleEndian>()?;

        // Read Module Info Substream
        let module_info_offset = cursor.position();
        let module_info_data = &stream.data
            [module_info_offset as usize..(module_info_offset + module_info_size as u64) as usize];
        let module_infos = self.parse_module_info_substream(module_info_data)?;

        // Move cursor past module_info
        cursor.seek(SeekFrom::Current(module_info_size as i64))?;

        // Skip Section Contribution, Section Map, and Type Server Map Substreams
        cursor.seek(SeekFrom::Current(
            _section_contribution_size as i64
                + _section_map_size as i64
                + _type_server_map_size as i64,
        ))?;

        // Read File Info Substream
        let file_info_offset = cursor.position();
        let file_info_data = &stream.data
            [file_info_offset as usize..(file_info_offset + file_info_size as u64) as usize];
        let file_info = self.parse_file_info_substream(file_info_data)?;

        // Build the directory tree
        let mut root = DirNode::new("".to_string());
        for module_file_info in &file_info.module_file_infos {
            let start = module_file_info.start_file_index as usize;
            let end = start + module_file_info.num_files as usize;
            if end > file_info.file_names.len() {
                return Err(io::Error::new(
                    io::ErrorKind::InvalidData,
                    "File index out of bounds",
                ));
            }
            for file_name in &file_info.file_names[start..end] {
                let path = Path::new(file_name);
                let components: Vec<&str> = path
                    .components()
                    .map(|comp| comp.as_os_str().to_str().unwrap_or(""))
                    .collect();
                root.add_path(&components);
            }
        }

        // Print the directory tree
        root.print("", true);

        Ok(())
    }

    /// Parses the Module Info Substream
    fn parse_module_info_substream(&self, data: &[u8]) -> io::Result<Vec<ModuleInfo>> {
        let mut cursor = Cursor::new(data);
        let mut module_infos = Vec::new();

        while (cursor.position() as usize) < data.len() {
            // Read the ModInfo structure
            let _unused = cursor.read_u32::<LittleEndian>()?;
            let _section = cursor.read_u16::<LittleEndian>()?;
            let _padding = cursor.read_u16::<LittleEndian>()?;
            let _flags = cursor.read_u16::<LittleEndian>()?;
            let _module_sym_stream = cursor.read_u16::<LittleEndian>()?;
            let _sym_bytes = cursor.read_u32::<LittleEndian>()?;
            let _c11_bytes = cursor.read_u32::<LittleEndian>()?;
            let _c13_bytes = cursor.read_u32::<LittleEndian>()?;
            let num_files = cursor.read_u16::<LittleEndian>()?;
            let _pdb_name_index = cursor.read_u16::<LittleEndian>()?;

            // Read null-terminated module name
            let module_name = read_null_terminated_string_from_cursor(&mut cursor)?;

            // Read null-terminated object file name
            let obj_file_name = read_null_terminated_string_from_cursor(&mut cursor)?;

            // Align cursor to next 4-byte boundary
            let pos = cursor.position();
            let aligned_pos = (pos + 3) & !3;
            if aligned_pos > pos {
                cursor.seek(SeekFrom::Start(aligned_pos))?;
            }

            module_infos.push(ModuleInfo {
                module_name,
                obj_file_name,
                num_files,
            });
        }

        Ok(module_infos)
    }

    /// Parses the File Info Substream
    fn parse_file_info_substream(&self, data: &[u8]) -> io::Result<FileInfo> {
        let mut cursor = Cursor::new(data);

        let num_modules = cursor.read_u16::<LittleEndian>()?;
        let num_source_files = cursor.read_u16::<LittleEndian>()?;

        // Read Module File Info entries
        let mut module_file_infos: Vec<ModuleFileInfo> = Vec::with_capacity(num_modules as usize);
        for _ in 0..num_modules {
            let num_files = cursor.read_u16::<LittleEndian>()?;
            let start_file_index = cursor.read_u16::<LittleEndian>()?;
            module_file_infos.push(ModuleFileInfo {
                num_files,
                start_file_index,
            });
        }

        // Read File Name Offsets
        let mut file_name_offsets: Vec<u32> = Vec::with_capacity(num_source_files as usize);
        for _ in 0..num_source_files {
            let offset = cursor.read_u32::<LittleEndian>()?;
            file_name_offsets.push(offset);
        }

        // The rest is the names buffer
        let name_buffer_offset = cursor.position() as usize;
        let name_buffer = &data[name_buffer_offset..];

        // Build a Vec of file names
        let mut file_names = Vec::with_capacity(num_source_files as usize);

        for &offset in &file_name_offsets {
            let name_offset = offset as usize;
            if name_offset >= name_buffer.len() {
                return Err(io::Error::new(
                    io::ErrorKind::InvalidData,
                    "File name offset out of bounds",
                ));
            }
            let name = read_null_terminated_string(&name_buffer[name_offset..])?;
            file_names.push(name);
        }

        Ok(FileInfo {
            module_file_infos,
            file_names,
        })
    }

    /// Parses the TPI (Type Information) Stream (Stream 2)
    fn parse_tpi_stream(&self, stream: &PdbStream) -> io::Result<()> {
        let mut cursor = Cursor::new(&stream.data);

        // Read TPI Stream Header
        let version = cursor.read_u32::<LittleEndian>()?;
        let _header_size = cursor.read_u32::<LittleEndian>()?;
        let type_index_begin = cursor.read_u32::<LittleEndian>()?;
        let type_index_end = cursor.read_u32::<LittleEndian>()?;
        let type_record_bytes = cursor.read_u32::<LittleEndian>()?;
        let _hash_stream_index = cursor.read_u16::<LittleEndian>()?;
        let _hash_aux_stream_index = cursor.read_u16::<LittleEndian>()?;
        let _hash_key_size = cursor.read_u32::<LittleEndian>()?;
        let _num_hash_buckets = cursor.read_u32::<LittleEndian>()?;
        let _hash_value_buffer_offset = cursor.read_u32::<LittleEndian>()?;
        let _hash_value_buffer_length = cursor.read_u32::<LittleEndian>()?;
        let _index_offset_buffer_offset = cursor.read_u32::<LittleEndian>()?;
        let _index_offset_buffer_length = cursor.read_u32::<LittleEndian>()?;
        let _hash_adj_buffer_offset = cursor.read_u32::<LittleEndian>()?;
        let _hash_adj_buffer_length = cursor.read_u32::<LittleEndian>()?;

        println!("Total Type Records: {}", type_index_end - type_index_begin);

        // Read Type Records
        let mut type_records = Vec::new();
        let type_records_start = cursor.position() as usize;
        let type_records_end = type_records_start + type_record_bytes as usize;
        let type_records_data = &stream.data[type_records_start..type_records_end];
        let mut type_cursor = Cursor::new(type_records_data);

        while (type_cursor.position() as usize) < type_records_data.len() {
            let record_start = type_cursor.position();
            let record_length = type_cursor.read_u16::<LittleEndian>()?;
            let kind = type_cursor.read_u16::<LittleEndian>()?;

            let record_data = if record_length >= 2 {
                let data_length = (record_length - 2) as usize;
                let mut data = vec![0u8; data_length];
                type_cursor.read_exact(&mut data)?;
                data
            } else {
                Vec::new()
            };

            type_records.push(TypeRecord {
                kind,
                data: record_data,
            });

            // Handle special type records if needed

            // Align to even boundary
            let pos = type_cursor.position();
            if pos % 4 != 0 {
                let aligned_pos = ((pos + 3) / 4) * 4;
                if aligned_pos > pos {
                    type_cursor.seek(SeekFrom::Start(aligned_pos))?;
                }
            }
        }

        // Print types
        for record in &type_records {
            // For demonstration, print the type kind
            println!("Type Kind: 0x{:04X}", record.kind);
            // Additional parsing can be implemented here to interpret type records
        }

        Ok(())
    }

    /// Reads an array of page numbers from the given pages
    fn read_page_number_array(
        reader: &mut BufReader<File>,
        page_size: u32,
        pages: &[u32],
        num_items: u32,
    ) -> io::Result<Vec<u32>> {
        let mut result = Vec::new();
        let mut items_to_read = num_items;
        let entries_per_page = page_size / 4;
        for &page in pages {
            let page_offset = page as u64 * page_size as u64;
            reader.seek(SeekFrom::Start(page_offset))?;

            // Read the entire page
            let mut buffer = vec![0u8; page_size as usize];
            reader.read_exact(&mut buffer)?;

            let mut cursor = Cursor::new(buffer);

            for _ in 0..entries_per_page {
                if items_to_read == 0 {
                    break;
                }
                let page_number = cursor.read_u32::<LittleEndian>()?;
                result.push(page_number);
                items_to_read -= 1;
            }

            if items_to_read == 0 {
                break;
            }
        }

        if items_to_read > 0 {
            return Err(io::Error::new(
                io::ErrorKind::UnexpectedEof,
                "Not enough data to read all page number entries.",
            ));
        }

        Ok(result)
    }

    /// Reads a stream from the file
    fn read_stream(
        reader: &mut BufReader<File>,
        page_size: u32,
        pages: &[u32],
        stream_size: u32,
    ) -> io::Result<Vec<u8>> {
        let mut stream_data = Vec::with_capacity(stream_size as usize);
        let mut remaining = stream_size as usize;

        for &page in pages {
            let page_offset = page as u64 * page_size as u64;
            reader.seek(SeekFrom::Start(page_offset))?;

            let bytes_to_read = std::cmp::min(page_size as usize, remaining);
            let mut buffer = vec![0u8; bytes_to_read];
            reader.read_exact(&mut buffer)?;
            stream_data.extend_from_slice(&buffer);
            remaining -= bytes_to_read;

            if remaining == 0 {
                break;
            }
        }

        Ok(stream_data)
    }

    /// Calculates the number of pages needed to store the given size
    fn calculate_page_count(size: u32, page_size: u32) -> u32 {
        (size + page_size - 1) / page_size
    }

    /// Parses the Root Stream Directory
    fn parse_root_stream(
        _root_stream_data: &[u8],
        _page_size: u32,
        _reader: &mut BufReader<File>,
    ) -> io::Result<(Vec<(u32, Vec<u32>)>, Vec<Vec<u32>>)> {
        let mut cursor = Cursor::new(_root_stream_data);

        // Read the number of streams
        let num_streams = cursor.read_u32::<LittleEndian>()?;

        // Read the sizes of each stream
        let mut stream_sizes = Vec::new();
        for _ in 0..num_streams {
            let size = cursor.read_u32::<LittleEndian>()?;
            stream_sizes.push(size);
        }

        // Read the page lists for each stream
        let mut stream_pages: Vec<Vec<u32>> = Vec::new();
        for &size in &stream_sizes {
            if size == u32::MAX {
                // Stream does not exist
                stream_pages.push(Vec::new());
                continue;
            }

            // Calculate the number of pages for this stream
            let num_pages = Self::calculate_page_count(size, _page_size);

            let mut pages = Vec::new();
            for _ in 0..num_pages {
                let page_number = cursor.read_u32::<LittleEndian>()?;
                pages.push(page_number);
            }
            stream_pages.push(pages);
        }

        // Pair sizes with their pages
        let streams: Vec<(u32, Vec<u32>)> =
            stream_sizes.into_iter().zip(stream_pages.clone()).collect();

        Ok((streams, stream_pages))
    }
}

/// Represents information about a module in the PDB
struct ModuleInfo {
    module_name: String,
    obj_file_name: String,
    num_files: u16,
}

/// Represents parsed file information from the File Info Substream
struct FileInfo {
    module_file_infos: Vec<ModuleFileInfo>,
    file_names: Vec<String>,
}

/// Represents module file information entry
struct ModuleFileInfo {
    num_files: u16,
    start_file_index: u16,
}

/// Represents a directory node in the directory tree
#[derive(Debug)]
struct DirNode {
    name: String,
    subdirs: HashMap<String, DirNode>,
    files: Vec<String>,
}

impl DirNode {
    fn new(name: String) -> Self {
        DirNode {
            name,
            subdirs: HashMap::new(),
            files: Vec::new(),
        }
    }

    fn add_path(&mut self, components: &[&str]) {
        if components.is_empty() {
            return;
        }
        if components.len() == 1 {
            self.files.push(components[0].to_string());
        } else {
            let subdir_name = components[0].to_string();
            let subdir = self
                .subdirs
                .entry(subdir_name.clone())
                .or_insert_with(|| DirNode::new(subdir_name));
            subdir.add_path(&components[1..]);
        }
    }

    fn print(&self, prefix: &str, is_last: bool) {
        if !self.name.is_empty() {
            let connector = if is_last { "└── " } else { "├── " };
            println!("{}{}{}", prefix, connector, self.name);
        }
        let new_prefix = if is_last {
            format!("{}    ", prefix)
        } else {
            format!("{}│   ", prefix)
        };

        let mut entries: Vec<&String> = self.subdirs.keys().collect();
        entries.sort();

        let total = entries.len() + self.files.len();
        let mut index = 0;

        for name in &entries {
            index += 1;
            let is_last = index == total;
            self.subdirs[*name].print(&new_prefix, is_last);
        }

        for (i, file) in self.files.iter().enumerate() {
            let is_last = index + i + 1 == total;
            let connector = if is_last { "└── " } else { "├── " };
            println!("{}{}{}", new_prefix, connector, file);
        }
    }
}

/// Represents a type record in the TPI stream
struct TypeRecord {
    kind: u16,
    data: Vec<u8>,
}

/// Reads a null-terminated string from a cursor
fn read_null_terminated_string_from_cursor(cursor: &mut Cursor<&[u8]>) -> io::Result<String> {
    let mut buf = Vec::new();
    loop {
        let byte = cursor.read_u8()?;
        if byte == 0 {
            break;
        }
        buf.push(byte);
    }
    String::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}

/// Reads a null-terminated string from a byte slice
fn read_null_terminated_string(data: &[u8]) -> io::Result<String> {
    if let Some(pos) = data.iter().position(|&c| c == 0) {
        let s = std::str::from_utf8(&data[0..pos])
            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
        Ok(s.to_string())
    } else {
        Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "String not null terminated",
        ))
    }
}