use anyhow::Result;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DatabaseFormat {
LevelDB,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BitcoinCoreNetwork {
Mainnet,
Testnet,
Regtest,
Signet,
}
impl BitcoinCoreNetwork {
fn directory_name(&self) -> &'static str {
match self {
BitcoinCoreNetwork::Mainnet => "",
BitcoinCoreNetwork::Testnet => "testnet3",
BitcoinCoreNetwork::Regtest => "regtest",
BitcoinCoreNetwork::Signet => "signet",
}
}
}
impl std::str::FromStr for BitcoinCoreNetwork {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"mainnet" => Ok(BitcoinCoreNetwork::Mainnet),
"testnet" => Ok(BitcoinCoreNetwork::Testnet),
"regtest" => Ok(BitcoinCoreNetwork::Regtest),
"signet" => Ok(BitcoinCoreNetwork::Signet),
_ => Err(format!("Unknown network: {s}")),
}
}
}
impl std::fmt::Display for BitcoinCoreNetwork {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BitcoinCoreNetwork::Mainnet => write!(f, "mainnet"),
BitcoinCoreNetwork::Testnet => write!(f, "testnet"),
BitcoinCoreNetwork::Regtest => write!(f, "regtest"),
BitcoinCoreNetwork::Signet => write!(f, "signet"),
}
}
}
pub struct BitcoinCoreDetection;
impl BitcoinCoreDetection {
pub fn detect_data_dir(network: BitcoinCoreNetwork) -> Result<Option<PathBuf>> {
let possible_dirs = Self::get_standard_paths(network);
for dir in possible_dirs.into_iter().flatten() {
if Self::is_bitcoin_core_dir(&dir, network) {
return Ok(Some(dir));
}
}
Ok(None)
}
fn get_standard_paths(network: BitcoinCoreNetwork) -> Vec<Option<PathBuf>> {
let mut paths = Vec::new();
if let Some(home) = dirs::home_dir() {
let base = home.join(".bitcoin");
if network == BitcoinCoreNetwork::Mainnet {
paths.push(Some(base.clone()));
} else {
paths.push(Some(base.join(network.directory_name())));
}
}
paths.push(Some(PathBuf::from("/var/lib/bitcoind")));
if network != BitcoinCoreNetwork::Mainnet {
paths.push(Some(
PathBuf::from("/var/lib/bitcoind").join(network.directory_name()),
));
}
paths
}
fn is_bitcoin_core_dir(dir: &Path, network: BitcoinCoreNetwork) -> bool {
let chainstate = dir.join("chainstate");
if !chainstate.exists() {
return false;
}
let blocks = dir.join("blocks");
if !blocks.exists() {
return false;
}
if Self::detect_db_format(&chainstate).is_err() {
return false;
}
true
}
pub fn detect_db_format(data_dir: &Path) -> Result<DatabaseFormat> {
let current_file = data_dir.join("CURRENT");
if !current_file.exists() {
return Err(anyhow::anyhow!(
"CURRENT file not found - not a LevelDB database"
));
}
let contents = std::fs::read_to_string(¤t_file)?;
let trimmed = contents.trim();
if trimmed.starts_with("MANIFEST-") {
let manifest_path = data_dir.join(trimmed);
if manifest_path.exists() {
return Ok(DatabaseFormat::LevelDB);
}
}
Err(anyhow::anyhow!("Invalid LevelDB format"))
}
pub fn detect_network(data_dir: &Path) -> Option<BitcoinCoreNetwork> {
if let Some(dir_name) = data_dir.file_name().and_then(|n| n.to_str()) {
match dir_name {
"testnet3" => return Some(BitcoinCoreNetwork::Testnet),
"regtest" => return Some(BitcoinCoreNetwork::Regtest),
"signet" => return Some(BitcoinCoreNetwork::Signet),
_ => {}
}
}
if let Some(parent) = data_dir.parent() {
if let Some(parent_name) = parent.file_name().and_then(|n| n.to_str()) {
if parent_name == ".bitcoin" {
if let Some(dir_name) = data_dir.file_name().and_then(|n| n.to_str()) {
match dir_name {
"testnet3" => return Some(BitcoinCoreNetwork::Testnet),
"regtest" => return Some(BitcoinCoreNetwork::Regtest),
"signet" => return Some(BitcoinCoreNetwork::Signet),
_ => return Some(BitcoinCoreNetwork::Mainnet),
}
}
}
}
}
Some(BitcoinCoreNetwork::Mainnet)
}
pub fn verify_database(data_dir: &Path) -> Result<()> {
let chainstate = data_dir.join("chainstate");
if !chainstate.exists() {
return Err(anyhow::anyhow!("Chainstate directory not found"));
}
let current = chainstate.join("CURRENT");
if !current.exists() {
return Err(anyhow::anyhow!("CURRENT file not found in chainstate"));
}
let contents = std::fs::read_to_string(¤t)?;
let manifest_name = contents.trim();
if !manifest_name.starts_with("MANIFEST-") {
return Err(anyhow::anyhow!("Invalid CURRENT file format"));
}
let manifest = chainstate.join(manifest_name);
if !manifest.exists() {
return Err(anyhow::anyhow!(
"MANIFEST file not found: {}",
manifest_name
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_detect_network_from_path() {
let temp = TempDir::new().unwrap();
let testnet_path = temp.path().join("testnet3");
std::fs::create_dir_all(&testnet_path).unwrap();
assert_eq!(
BitcoinCoreDetection::detect_network(&testnet_path),
Some(BitcoinCoreNetwork::Testnet)
);
}
#[test]
fn test_get_standard_paths() {
let paths = BitcoinCoreDetection::get_standard_paths(BitcoinCoreNetwork::Mainnet);
assert!(!paths.is_empty());
}
}