ntdsextract2 1.4.10

Display contents of Active Directory database files (ntds.dit)
use std::collections::{HashMap, HashSet};

use crate::cache::{self, MetaDataCache, RecordId, RecordPointer, Value, WithValue};
use crate::ntds::Error;
use crate::value::FromValue;

use super::{LinkTable, NtdsAttributeId};

pub(crate) struct LinkTableBuilder<'info, 'db> {
    link_table: cache::LinkTable<'info, 'db>,
    data_table: &'db cache::DataTable<'info, 'db>,
    schema_record_id: RecordPointer,
}

impl<'info, 'db> LinkTableBuilder<'info, 'db> {
    pub fn from(
        link_table: cache::LinkTable<'info, 'db>,
        data_table: &'db cache::DataTable<'info, 'db>,
        schema_record_id: RecordPointer,
    ) -> crate::ntds::Result<Self> {
        Ok(Self {
            link_table,
            data_table,
            schema_record_id,
        })
    }

    pub fn build(self, metadata: &MetaDataCache) -> crate::ntds::Result<LinkTable> {
        log::info!("building link table associations");

        let (member_link_id, _member_of_link_id) = self.find_member_link_id_pair()?;
        let link_base = member_link_id / 2;
        let link_dnt_id = self.link_table.link_dnt_id();
        let backlink_dnt_id = self.link_table.backlink_dnt_id();
        let link_base_id = self.link_table.link_base_id();

        let mut forward_map = HashMap::new();
        let mut backward_map = HashMap::new();

        for record in self.link_table.iter().filter(|r| {
            r.with_value(*link_base_id, |value| match value {
                Some(Value::U32(v)) => Ok(*v == member_link_id),
                Some(Value::I32(v)) => Ok(u32::try_from(*v).map_or(false, |v| v == link_base)),
                _ => Ok(false),
            })
            .unwrap_or(false)
        }) {
            if let Ok(Some(forward_link)) = record.with_value(*link_dnt_id, |v| {
                RecordId::from_value(v.unwrap())
                    .map(|id| {
                        metadata.ptr_from_id(&id).or_else(|| {
                            log::warn!("I expected to find an entry for forward link {id}; but there was none. I'll ignore that entry.");
                            None
                        })
                    })
            }) {
                if let Ok(Some(backward_link)) = record.with_value(*backlink_dnt_id, |v| {
                    RecordId::from_value(v.unwrap())
                        .map(|id| {
                            metadata.ptr_from_id(&id).or_else(|| {
                                log::warn!(
                                    "I expected to find an entry for backward link {id}; but there was none. I'll ignore that entry."
                                );
                                None
                            })
                        })
                }) {
                    forward_map
                        .entry(*forward_link.ds_record_id())
                        .or_insert_with(HashSet::new)
                        .insert(*backward_link);
                    backward_map
                        .entry(*backward_link.ds_record_id())
                        .or_insert_with(HashSet::new)
                        .insert(*forward_link);
                }
            }
        }

        for (key, value) in forward_map.iter() {
            log::info!("found link {key} --> {value:?}");
        }

        for (key, value) in backward_map.iter() {
            log::info!("found backlink {key} --> {value:?}");
        }

        log::debug!(
            "found {} forward links and {} backward links",
            forward_map.len(),
            backward_map.len()
        );

        Ok(LinkTable {
            _forward_map: forward_map,
            backward_map,
        })
    }

    fn find_member_link_id_pair(&self) -> crate::ntds::Result<(u32, u32)> {
        log::info!("searching for link attributes 'Member' and 'Is-Member-Of-DL'");

        let member_link_id = self.find_link_id(&String::from("Member"))?;
        log::info!("'Member' has Link-ID '{member_link_id}'");

        let member_of_link_id = self.find_link_id(&String::from("Is-Member-Of-DL"))?;
        log::info!("'Is-Member-Of-DL' has Link-ID '{member_of_link_id}'");

        if member_link_id & 1 != 0 {
            return Err(Error::InvalidForwardLinkId(member_link_id));
        }

        if member_link_id + 1 != member_of_link_id {
            return Err(Error::InvalidLinkIdValues {
                member_link_id, member_of_link_id
            });
        }

        Ok((member_link_id, member_of_link_id))
    }

    fn find_link_id(&self, attribute_name: &String) -> crate::ntds::Result<u32> {
        let entry = self
            .data_table
            .metadata()
            .children_of(&self.schema_record_id)
            .find(|r| r.rdn().name() == attribute_name)
            .unwrap_or_else(|| panic!("found no record by that name: '{attribute_name}'"));

        let link_id_column = NtdsAttributeId::AttLinkId.id(self.data_table.esedbinfo());

        let record = self
            .data_table
            .table()
            .record(entry.record_ptr().esedb_row().inner())?;
        Ok(u32::from_record_opt(&record, link_id_column)?
            .unwrap_or_else(|| panic!("missing link-id attribute in {attribute_name}")))
    }
}