commit 06cafa0366d9f051879e7ead5aafc5e2e5d9c63d Author: robojerk Date: Sat Sep 13 20:45:18 2025 -0700 Initial commit: APT-DNF Bridge workspace - Core crate: Minimal shell-out implementation - Advanced crate: Pluggable backends and enhanced features - Main crate: Re-exports core + optional advanced features - Feature flags: Users choose complexity level - Examples: Working demonstrations of both approaches - Documentation: Clear README explaining the structure Implements the refined two-crate approach with workspace + feature flags. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..087c9f5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Rust +/target/ +**/*.rs.bk +Cargo.lock + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Temporary files +*.tmp +*.temp diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..74d3b05 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[workspace] +members = [ + "apt-dnf-bridge-core", + "apt-dnf-bridge", + "apt-dnf-bridge-advanced", +] +resolver = "2" + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "MIT" +repository = "https://github.com/your-org/apt-dnf-bridge" +keywords = ["apt", "dnf", "package-manager", "ostree", "bridge"] +categories = ["os::linux-apis", "development-tools::build-utils"] + +[workspace.dependencies] +anyhow = "1.0" +async-trait = "0.1" +serde = { version = "1.0", features = ["derive"] } +thiserror = "1.0" +tokio = { version = "1.0", features = ["process", "macros"] } +tempfile = "3.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..dd49674 --- /dev/null +++ b/README.md @@ -0,0 +1,180 @@ +# APT-DNF Bridge + +A DNF-like bridge around APT for apt-ostree integration. This workspace provides a transaction-based API that makes APT work like DNF. + +## šŸŽÆ **Two-Crate Approach** + +This workspace implements the refined two-crate approach with feature flags: + +- **`apt-dnf-bridge-core`** - Minimal shell-out implementation +- **`apt-dnf-bridge`** - Main crate that re-exports core + optional advanced features +- **`apt-dnf-bridge-advanced`** - Pluggable backends, caching, and enhanced features + +## šŸš€ **Quick Start** + +### Core (Minimal) +```toml +[dependencies] +apt-dnf-bridge = "0.1" +``` + +### Advanced Features +```toml +[dependencies] +apt-dnf-bridge = { version = "0.1", features = ["advanced"] } +``` + +## šŸ“ **Workspace Structure** + +``` +apt-dnf-bridge-workspace/ +ā”œā”€ā”€ Cargo.toml # Workspace root +ā”œā”€ā”€ apt-dnf-bridge-core/ # Minimal implementation +│ ā”œā”€ā”€ Cargo.toml +│ └── src/ +│ ā”œā”€ā”€ lib.rs +│ ā”œā”€ā”€ command.rs +│ ā”œā”€ā”€ error.rs +│ ā”œā”€ā”€ package.rs +│ ā”œā”€ā”€ repository.rs +│ └── transaction.rs +ā”œā”€ā”€ apt-dnf-bridge/ # Main public crate +│ ā”œā”€ā”€ Cargo.toml +│ └── src/ +│ └── lib.rs +ā”œā”€ā”€ apt-dnf-bridge-advanced/ # Advanced features +│ ā”œā”€ā”€ Cargo.toml +│ └── src/ +│ ā”œā”€ā”€ lib.rs +│ ā”œā”€ā”€ backend.rs +│ ā”œā”€ā”€ package_v2.rs +│ ā”œā”€ā”€ transaction_v2.rs +│ └── backend/ +│ ā”œā”€ā”€ shell_backend.rs +│ ā”œā”€ā”€ mock_backend.rs +│ └── libapt_backend.rs +└── examples/ # Shared examples + ā”œā”€ā”€ basic_usage.rs + ā”œā”€ā”€ package_query.rs + ā”œā”€ā”€ atomicity_notes.rs + └── backend_selection.rs +``` + +## šŸ”§ **Usage Examples** + +### Basic Usage (Core Only) +```rust +use apt_dnf_bridge::{Transaction, Package, Repository}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Create a transaction + let mut tx = Transaction::new(); + + // Add packages to install + let vim = Package::new("vim", "2:8.1.2269-1ubuntu5.14", "amd64"); + tx.add_install(vim).await?; + + // Resolve dependencies (APT handles automatically) + tx.resolve().await?; + + // Commit the transaction + tx.commit().await?; + + Ok(()) +} +``` + +### Advanced Usage (With Backends) +```rust +use apt_dnf_bridge::{TransactionV2, PackageDatabaseV2, BackendFactory}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Create transaction with auto-detected backend + let mut tx = TransactionV2::new().await?; + + // Add packages + let vim = Package::new("vim", "2:8.1.2269-1ubuntu5.14", "amd64"); + tx.add_install(vim).await?; + + // Resolve and commit + tx.resolve().await?; + tx.commit().await?; + + Ok(()) +} +``` + +## šŸ—ļø **Building and Testing** + +### Build All Crates +```bash +cargo build +``` + +### Test Core Features +```bash +cargo test -p apt-dnf-bridge-core +``` + +### Test Advanced Features +```bash +cargo test -p apt-dnf-bridge-advanced +``` + +### Run Examples +```bash +# Core examples +cargo run --example basic_usage +cargo run --example package_query + +# Advanced examples (requires advanced feature) +cargo run --example backend_selection --features advanced +``` + +## šŸŽÆ **Feature Flags** + +### Core Features (Default) +- Shell-out APT commands +- Basic transaction model +- Package querying +- Repository management +- Error handling + +### Advanced Features (`advanced` feature) +- Pluggable backends (shell, mock, libapt) +- Caching system +- Enhanced error handling +- Backend statistics +- Mock backend for testing + +## šŸ”„ **Migration Guide** + +### From Single Crate to Workspace +1. **Core users**: No changes needed - same API +2. **Advanced users**: Add `features = ["advanced"]` to Cargo.toml +3. **New users**: Start with core, add advanced when needed + +### API Compatibility +- Core API remains unchanged +- Advanced API available via feature flag +- Gradual adoption path supported + +## šŸ“š **Documentation** + +- **Core API**: Focus on simplicity and reliability +- **Advanced API**: Focus on power and flexibility +- **Examples**: Demonstrate both core and advanced usage +- **Migration**: Guide for moving between feature levels + +## šŸš€ **Benefits of This Approach** + +1. **Single Crate** - No confusion about which to use +2. **Feature Flags** - Users choose complexity level +3. **Workspace** - Easy version coordination +4. **Re-exports** - Clean API surface +5. **Gradual Adoption** - Start simple, add complexity when needed +6. **Clear Documentation** - One README with feature explanations + +This approach gives us all the benefits of the two-crate strategy with much better UX and maintenance. diff --git a/apt-dnf-bridge-advanced/Cargo.toml b/apt-dnf-bridge-advanced/Cargo.toml new file mode 100644 index 0000000..182a12b --- /dev/null +++ b/apt-dnf-bridge-advanced/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "apt-dnf-bridge-advanced" +version.workspace = true +edition.workspace = true +description = "Advanced features for apt-dnf-bridge: pluggable backends, caching, and more" +license.workspace = true +repository.workspace = true +keywords.workspace = true +categories.workspace = true + +[dependencies] +apt-dnf-bridge-core = { path = "../apt-dnf-bridge-core" } +anyhow.workspace = true +async-trait.workspace = true +serde.workspace = true +tokio.workspace = true + +[features] +libapt-backend = [] diff --git a/apt-dnf-bridge-advanced/src/backend.rs b/apt-dnf-bridge-advanced/src/backend.rs new file mode 100644 index 0000000..ac6d325 --- /dev/null +++ b/apt-dnf-bridge-advanced/src/backend.rs @@ -0,0 +1,182 @@ +//! Pluggable backend trait for different APT implementations. + +use apt_dnf_bridge_core::{Package, Result, Operation}; +use serde::{Deserialize, Serialize}; + +/// Resolution result from a backend. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Resolution { + /// Whether the transaction can be resolved successfully. + pub resolvable: bool, + /// Packages that would be installed. + pub to_install: Vec, + /// Packages that would be removed. + pub to_remove: Vec, + /// Packages that would be upgraded. + pub to_upgrade: Vec, + /// Dependencies that would be installed. + pub dependencies: Vec, + /// Any conflicts or issues found. + pub conflicts: Vec, + /// Human-readable summary of the resolution. + pub summary: String, +} + +/// Repository information. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RepositoryInfo { + /// Repository name. + pub name: String, + /// Repository URL. + pub url: String, + /// Repository components. + pub components: Vec, + /// Whether the repository is enabled. + pub enabled: bool, +} + +/// Package query options. +#[derive(Debug, Clone, Default)] +pub struct QueryOptions { + /// Whether to use caching. + pub use_cache: bool, + /// Whether to enable logging. + pub enable_logging: bool, + /// Maximum number of results to return. + pub max_results: Option, +} + +/// Backend trait for different APT implementations. +/// +/// This trait allows the crate to support multiple backends: +/// - ShellBackend: Uses shell-out to apt commands (simple, reliable) +/// - LibAptBackend: Uses libapt-pkg bindings (more powerful, complex) +/// - MockBackend: For testing (no real APT calls) +#[async_trait::async_trait] +pub trait AptBackend: Send + Sync { + /// Get the name of this backend. + fn name(&self) -> &'static str; + + /// Get the version of this backend. + fn version(&self) -> &'static str; + + /// Check if this backend is available on the current system. + async fn is_available(&self) -> Result; + + /// Resolve a transaction to check if it can be executed. + async fn resolve(&self, operations: &[Operation]) -> Result; + + /// Commit a transaction by executing the operations. + async fn commit(&self, operations: &[Operation]) -> Result<()>; + + /// Search for packages by pattern. + async fn search_packages(&mut self, pattern: &str, options: &QueryOptions) -> Result>; + + /// Get detailed information about a specific package. + async fn get_package_info(&mut self, name: &str, options: &QueryOptions) -> Result>; + + /// Check if a package is installed. + async fn is_package_installed(&self, name: &str) -> Result; + + /// Get all installed packages. + async fn get_installed_packages(&self, options: &QueryOptions) -> Result>; + + /// Add a repository. + async fn add_repository(&self, repo: &RepositoryInfo) -> Result<()>; + + /// Remove a repository. + async fn remove_repository(&self, name: &str) -> Result<()>; + + /// List all repositories. + async fn list_repositories(&self) -> Result>; + + /// Update package cache. + async fn update_cache(&self) -> Result<()>; + + /// Get backend-specific statistics. + async fn get_stats(&self) -> Result; +} + +/// Backend statistics. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BackendStats { + /// Number of commands executed. + pub commands_executed: u64, + /// Number of packages queried. + pub packages_queried: u64, + /// Number of transactions committed. + pub transactions_committed: u64, + /// Cache hit rate (0.0 to 1.0). + pub cache_hit_rate: f64, + /// Average command execution time in milliseconds. + pub avg_command_time_ms: f64, +} + +/// Backend configuration. +#[derive(Debug, Clone)] +pub struct BackendConfig { + /// Whether to enable logging. + pub enable_logging: bool, + /// Whether to use caching. + pub use_cache: bool, + /// Maximum cache size. + pub max_cache_size: usize, + /// Timeout for commands in seconds. + pub command_timeout: Option, +} + +impl Default for BackendConfig { + fn default() -> Self { + Self { + enable_logging: false, + use_cache: true, + max_cache_size: 1000, + command_timeout: Some(300), // 5 minutes + } + } +} + +/// Backend factory for creating different backend implementations. +pub struct BackendFactory; + +impl BackendFactory { + /// Create a shell backend (default, most reliable). + pub fn create_shell_backend(config: BackendConfig) -> Result> { + Ok(Box::new(ShellBackend::new(config))) + } + + /// Create a libapt backend (if available). + #[cfg(feature = "libapt-backend")] + pub fn create_libapt_backend(config: BackendConfig) -> Result> { + Ok(Box::new(LibAptBackend::new(config))) + } + + /// Create a mock backend for testing. + pub fn create_mock_backend(config: BackendConfig) -> Result> { + Ok(Box::new(MockBackend::new(config))) + } + + /// Auto-detect the best available backend. + pub async fn auto_detect(config: BackendConfig) -> Result> { + // Try libapt first if available + #[cfg(feature = "libapt-backend")] + { + let libapt = Self::create_libapt_backend(config.clone())?; + if libapt.is_available().await? { + return Ok(libapt); + } + } + + // Fall back to shell backend + Self::create_shell_backend(config) + } +} + +// Backend implementations +mod shell_backend; +mod libapt_backend; +mod mock_backend; + +pub use shell_backend::ShellBackend; +pub use libapt_backend::LibAptBackend; +pub use mock_backend::MockBackend; diff --git a/apt-dnf-bridge-advanced/src/backend/libapt_backend.rs b/apt-dnf-bridge-advanced/src/backend/libapt_backend.rs new file mode 100644 index 0000000..96b915a --- /dev/null +++ b/apt-dnf-bridge-advanced/src/backend/libapt_backend.rs @@ -0,0 +1,126 @@ +//! LibApt backend implementation using libapt-pkg bindings. +//! +//! This is a placeholder implementation that would use apt-rs or direct libapt-pkg bindings +//! when available. Currently, it falls back to shell commands. + +use crate::backend::{AptBackend, BackendConfig, BackendStats, QueryOptions, RepositoryInfo, Resolution}; +use apt_dnf_bridge_core::{AptError, Operation, Package, Result}; +use async_trait::async_trait; + +/// LibApt backend that uses libapt-pkg bindings. +/// +/// This backend provides more accurate dependency resolution and better performance +/// than the shell backend, but requires libapt-pkg to be available. +pub struct LibAptBackend { + config: BackendConfig, + stats: BackendStats, + // TODO: Add libapt-pkg specific fields when implementing +} + +impl LibAptBackend { + /// Create a new libapt backend. + pub fn new(config: BackendConfig) -> Self { + Self { + config, + stats: BackendStats { + commands_executed: 0, + packages_queried: 0, + transactions_committed: 0, + cache_hit_rate: 0.0, + avg_command_time_ms: 0.0, + }, + } + } +} + +#[async_trait] +impl AptBackend for LibAptBackend { + fn name(&self) -> &'static str { + "libapt" + } + + fn version(&self) -> &'static str { + "0.1.0-placeholder" + } + + async fn is_available(&self) -> Result { + // TODO: Check if libapt-pkg is available + // For now, always return false to indicate it's not implemented + Ok(false) + } + + async fn resolve(&self, _operations: &[Operation]) -> Result { + // TODO: Use libapt-pkg for proper dependency resolution + // For now, return a placeholder response + Err(crate::AptError::Generic(anyhow::anyhow!( + "LibApt backend not yet implemented. Use shell backend instead." + ))) + } + + async fn commit(&self, _operations: &[Operation]) -> Result<()> { + // TODO: Use libapt-pkg for transaction execution + Err(crate::AptError::Generic(anyhow::anyhow!( + "LibApt backend not yet implemented. Use shell backend instead." + ))) + } + + async fn search_packages(&mut self, _pattern: &str, _options: &QueryOptions) -> Result> { + // TODO: Use libapt-pkg for package searching + Err(crate::AptError::Generic(anyhow::anyhow!( + "LibApt backend not yet implemented. Use shell backend instead." + ))) + } + + async fn get_package_info(&mut self, _name: &str, _options: &QueryOptions) -> Result> { + // TODO: Use libapt-pkg for package info + Err(crate::AptError::Generic(anyhow::anyhow!( + "LibApt backend not yet implemented. Use shell backend instead." + ))) + } + + async fn is_package_installed(&self, _name: &str) -> Result { + // TODO: Use libapt-pkg for installation check + Err(crate::AptError::Generic(anyhow::anyhow!( + "LibApt backend not yet implemented. Use shell backend instead." + ))) + } + + async fn get_installed_packages(&self, _options: &QueryOptions) -> Result> { + // TODO: Use libapt-pkg for installed packages + Err(crate::AptError::Generic(anyhow::anyhow!( + "LibApt backend not yet implemented. Use shell backend instead." + ))) + } + + async fn add_repository(&self, _repo: &RepositoryInfo) -> Result<()> { + // TODO: Use libapt-pkg for repository management + Err(crate::AptError::Generic(anyhow::anyhow!( + "LibApt backend not yet implemented. Use shell backend instead." + ))) + } + + async fn remove_repository(&self, _name: &str) -> Result<()> { + // TODO: Use libapt-pkg for repository management + Err(crate::AptError::Generic(anyhow::anyhow!( + "LibApt backend not yet implemented. Use shell backend instead." + ))) + } + + async fn list_repositories(&self) -> Result> { + // TODO: Use libapt-pkg for repository listing + Err(crate::AptError::Generic(anyhow::anyhow!( + "LibApt backend not yet implemented. Use shell backend instead." + ))) + } + + async fn update_cache(&self) -> Result<()> { + // TODO: Use libapt-pkg for cache updates + Err(crate::AptError::Generic(anyhow::anyhow!( + "LibApt backend not yet implemented. Use shell backend instead." + ))) + } + + async fn get_stats(&self) -> Result { + Ok(self.stats.clone()) + } +} diff --git a/apt-dnf-bridge-advanced/src/backend/mock_backend.rs b/apt-dnf-bridge-advanced/src/backend/mock_backend.rs new file mode 100644 index 0000000..49e3a1f --- /dev/null +++ b/apt-dnf-bridge-advanced/src/backend/mock_backend.rs @@ -0,0 +1,176 @@ +//! Mock backend implementation for testing. + +use crate::backend::{AptBackend, BackendConfig, BackendStats, QueryOptions, RepositoryInfo, Resolution}; +use apt_dnf_bridge_core::{Operation, Package, Result}; +use async_trait::async_trait; +use std::collections::HashMap; + +/// Mock backend for testing that doesn't make real APT calls. +pub struct MockBackend { + config: BackendConfig, + stats: BackendStats, + packages: HashMap, + repositories: HashMap, +} + +impl MockBackend { + /// Create a new mock backend. + pub fn new(config: BackendConfig) -> Self { + let mut packages = HashMap::new(); + + // Add some mock packages + packages.insert("vim".to_string(), Package::new("vim", "2:8.1.2269-1ubuntu5.14", "amd64")); + packages.insert("nano".to_string(), Package::new("nano", "2.9.3-2", "amd64")); + packages.insert("emacs".to_string(), Package::new("emacs", "1:27.1+1-3ubuntu2", "amd64")); + + Self { + config, + stats: BackendStats { + commands_executed: 0, + packages_queried: 0, + transactions_committed: 0, + cache_hit_rate: 1.0, // Mock always hits cache + avg_command_time_ms: 0.1, // Very fast + }, + packages, + repositories: HashMap::new(), + } + } + + /// Add a mock package. + pub fn add_package(&mut self, package: Package) { + self.packages.insert(package.name.clone(), package); + } + + /// Remove a mock package. + pub fn remove_package(&mut self, name: &str) { + self.packages.remove(name); + } +} + +#[async_trait] +impl AptBackend for MockBackend { + fn name(&self) -> &'static str { + "mock" + } + + fn version(&self) -> &'static str { + "1.0.0-test" + } + + async fn is_available(&self) -> Result { + Ok(true) + } + + async fn resolve(&self, operations: &[Operation]) -> Result { + let mut to_install = Vec::new(); + let mut to_remove = Vec::new(); + let mut to_upgrade = Vec::new(); + let mut conflicts = Vec::new(); + + for operation in operations { + match operation { + Operation::Install(pkg) => { + if self.packages.contains_key(&pkg.name) { + to_install.push(pkg.clone()); + } else { + conflicts.push(format!("Package {} not found", pkg.name)); + } + } + Operation::Remove(pkg) => { + if self.packages.contains_key(&pkg.name) { + to_remove.push(pkg.clone()); + } + } + Operation::Upgrade(pkg) => { + if self.packages.contains_key(&pkg.name) { + to_upgrade.push(pkg.clone()); + } else { + conflicts.push(format!("Package {} not found for upgrade", pkg.name)); + } + } + } + } + + Ok(Resolution { + resolvable: conflicts.is_empty(), + to_install, + to_remove, + to_upgrade, + dependencies: Vec::new(), + conflicts, + summary: format!("Mock resolution: {} operations", operations.len()), + }) + } + + async fn commit(&self, operations: &[Operation]) -> Result<()> { + if self.config.enable_logging { + eprintln!("Mock commit: {} operations", operations.len()); + for (i, op) in operations.iter().enumerate() { + match op { + Operation::Install(pkg) => eprintln!(" {}: Install {}", i + 1, pkg.name), + Operation::Remove(pkg) => eprintln!(" {}: Remove {}", i + 1, pkg.name), + Operation::Upgrade(pkg) => eprintln!(" {}: Upgrade {}", i + 1, pkg.name), + } + } + } + + // Mock commit always succeeds + Ok(()) + } + + async fn search_packages(&mut self, pattern: &str, _options: &QueryOptions) -> Result> { + self.stats.packages_queried += 1; + + let mut results = Vec::new(); + for (name, package) in &self.packages { + if name.contains(pattern) { + results.push(package.clone()); + } + } + + Ok(results) + } + + async fn get_package_info(&mut self, name: &str, _options: &QueryOptions) -> Result> { + self.stats.packages_queried += 1; + Ok(self.packages.get(name).cloned()) + } + + async fn is_package_installed(&self, name: &str) -> Result { + Ok(self.packages.contains_key(name)) + } + + async fn get_installed_packages(&self, _options: &QueryOptions) -> Result> { + Ok(self.packages.values().cloned().collect()) + } + + async fn add_repository(&self, repo: &RepositoryInfo) -> Result<()> { + if self.config.enable_logging { + eprintln!("Mock add repository: {} at {}", repo.name, repo.url); + } + Ok(()) + } + + async fn remove_repository(&self, name: &str) -> Result<()> { + if self.config.enable_logging { + eprintln!("Mock remove repository: {}", name); + } + Ok(()) + } + + async fn list_repositories(&self) -> Result> { + Ok(self.repositories.values().cloned().collect()) + } + + async fn update_cache(&self) -> Result<()> { + if self.config.enable_logging { + eprintln!("Mock update cache"); + } + Ok(()) + } + + async fn get_stats(&self) -> Result { + Ok(self.stats.clone()) + } +} diff --git a/apt-dnf-bridge-advanced/src/backend/shell_backend.rs b/apt-dnf-bridge-advanced/src/backend/shell_backend.rs new file mode 100644 index 0000000..18e5d40 --- /dev/null +++ b/apt-dnf-bridge-advanced/src/backend/shell_backend.rs @@ -0,0 +1,455 @@ +//! Shell backend implementation using apt commands. + +use crate::backend::{AptBackend, BackendConfig, BackendStats, QueryOptions, RepositoryInfo, Resolution}; +use apt_dnf_bridge_core::{ + command::{AptCommands, validate_package_name, validate_repository_url}, + AptError, Operation, Package, Repository, Result, +}; +use async_trait::async_trait; +use std::collections::HashMap; +use std::time::{Duration, Instant}; + +/// Shell backend that uses apt commands via shell-out. +pub struct ShellBackend { + config: BackendConfig, + stats: BackendStats, + search_cache: HashMap, Instant)>, + info_cache: HashMap, Instant)>, +} + +impl ShellBackend { + /// Create a new shell backend. + pub fn new(config: BackendConfig) -> Self { + Self { + config, + stats: BackendStats { + commands_executed: 0, + packages_queried: 0, + transactions_committed: 0, + cache_hit_rate: 0.0, + avg_command_time_ms: 0.0, + }, + search_cache: HashMap::new(), + info_cache: HashMap::new(), + } + } + + /// Execute a command and update statistics. + async fn execute_command(&mut self, f: F) -> Result + where + F: FnOnce() -> Result, + { + let start = Instant::now(); + let result = f()?; + let duration = start.elapsed(); + + self.stats.commands_executed += 1; + self.stats.avg_command_time_ms = + (self.stats.avg_command_time_ms + duration.as_millis() as f64) / 2.0; + + if self.config.enable_logging { + eprintln!("Command executed in {:?}", duration); + } + + Ok(result) + } + + /// Check if cache entry is still valid. + fn is_cache_valid(&self, timestamp: Instant) -> bool { + timestamp.elapsed() < Duration::from_secs(300) // 5 minutes + } + + /// Update cache hit rate. + fn update_cache_hit_rate(&mut self) { + let total_queries = self.stats.packages_queried; + if total_queries > 0 { + let cache_hits = self.search_cache.len() + self.info_cache.len(); + self.stats.cache_hit_rate = cache_hits as f64 / total_queries as f64; + } + } +} + +#[async_trait] +impl AptBackend for ShellBackend { + fn name(&self) -> &'static str { + "shell" + } + + fn version(&self) -> &'static str { + env!("CARGO_PKG_VERSION") + } + + async fn is_available(&self) -> Result { + // Check if apt is available + let output = std::process::Command::new("apt") + .arg("--version") + .output(); + + Ok(output.is_ok() && output.unwrap().status.success()) + } + + async fn resolve(&self, operations: &[Operation]) -> Result { + if operations.is_empty() { + return Ok(Resolution { + resolvable: true, + to_install: Vec::new(), + to_remove: Vec::new(), + to_upgrade: Vec::new(), + dependencies: Vec::new(), + conflicts: Vec::new(), + summary: "Empty transaction".to_string(), + }); + } + + // Separate operations by type + let mut install_packages = Vec::new(); + let mut remove_packages = Vec::new(); + let mut upgrade_packages = Vec::new(); + + for operation in operations { + match operation { + Operation::Install(pkg) => install_packages.push(pkg.name.as_str()), + Operation::Remove(pkg) => remove_packages.push(pkg.name.as_str()), + Operation::Upgrade(pkg) => upgrade_packages.push(pkg.name.as_str()), + } + } + + // Simulate install/upgrade operations + if !install_packages.is_empty() || !upgrade_packages.is_empty() { + let mut packages = install_packages; + packages.extend(upgrade_packages); + + let mut cmd = AptCommands::simulate_install(&packages); + if self.config.enable_logging { + cmd = cmd.with_logging(); + } + + let output = cmd.output()?; + + if !output.status.success() { + return Ok(Resolution { + resolvable: false, + to_install: Vec::new(), + to_remove: Vec::new(), + to_upgrade: Vec::new(), + dependencies: Vec::new(), + conflicts: vec![String::from_utf8_lossy(&output.stderr).to_string()], + summary: "Transaction cannot be resolved".to_string(), + }); + } + } + + // For remove operations, we can't easily simulate, so we just validate + for package in &remove_packages { + validate_package_name(package)?; + } + + Ok(Resolution { + resolvable: true, + to_install: operations.iter() + .filter_map(|op| match op { + Operation::Install(pkg) => Some(pkg.clone()), + _ => None, + }) + .collect(), + to_remove: operations.iter() + .filter_map(|op| match op { + Operation::Remove(pkg) => Some(pkg.clone()), + _ => None, + }) + .collect(), + to_upgrade: operations.iter() + .filter_map(|op| match op { + Operation::Upgrade(pkg) => Some(pkg.clone()), + _ => None, + }) + .collect(), + dependencies: Vec::new(), // Shell backend can't easily determine dependencies + conflicts: Vec::new(), + summary: format!("Transaction with {} operations", operations.len()), + }) + } + + async fn commit(&self, operations: &[Operation]) -> Result<()> { + if operations.is_empty() { + return Ok(()); + } + + // Separate operations by type + let mut install_packages = Vec::new(); + let mut remove_packages = Vec::new(); + let mut upgrade_packages = Vec::new(); + + for operation in operations { + match operation { + Operation::Install(pkg) => install_packages.push(pkg.name.as_str()), + Operation::Remove(pkg) => remove_packages.push(pkg.name.as_str()), + Operation::Upgrade(pkg) => upgrade_packages.push(pkg.name.as_str()), + } + } + + // Execute remove operations first + if !remove_packages.is_empty() { + let mut cmd = AptCommands::remove(&remove_packages); + if self.config.enable_logging { + cmd = cmd.with_logging(); + } + + let output = cmd.output()?; + + if !output.status.success() { + return Err(crate::AptError::CommitFailed( + String::from_utf8_lossy(&output.stderr).to_string(), + )); + } + } + + // Execute install/upgrade operations + if !install_packages.is_empty() || !upgrade_packages.is_empty() { + let mut packages = install_packages; + packages.extend(upgrade_packages); + + let mut cmd = AptCommands::install(&packages); + if self.config.enable_logging { + cmd = cmd.with_logging(); + } + + let output = cmd.output()?; + + if !output.status.success() { + return Err(crate::AptError::CommitFailed( + String::from_utf8_lossy(&output.stderr).to_string(), + )); + } + } + + Ok(()) + } + + async fn search_packages(&mut self, pattern: &str, options: &QueryOptions) -> Result> { + self.stats.packages_queried += 1; + + // Check cache first + if options.use_cache && self.config.use_cache { + if let Some((cached, timestamp)) = self.search_cache.get(pattern) { + if self.is_cache_valid(*timestamp) { + return Ok(cached.clone()); + } + } + } + + validate_package_name(pattern)?; + + let mut cmd = AptCommands::search(pattern); + if options.enable_logging || self.config.enable_logging { + cmd = cmd.with_logging(); + } + + let output = cmd.output()?; + + if !output.status.success() { + return Err(crate::AptError::CommandExitCode { + code: output.status.code().unwrap_or(-1), + output: String::from_utf8_lossy(&output.stderr).to_string(), + }); + } + + let output_str = String::from_utf8(output.stdout)?; + let mut packages = Vec::new(); + + for line in output_str.lines() { + if let Some((name, _description)) = line.split_once(" - ") { + let package = Package::new(name.trim(), "", "unknown"); + packages.push(package); + } + } + + // Apply max_results limit + if let Some(max) = options.max_results { + packages.truncate(max); + } + + // Cache the results + if options.use_cache && self.config.use_cache { + self.search_cache.insert(pattern.to_string(), (packages.clone(), Instant::now())); + } + + self.update_cache_hit_rate(); + Ok(packages) + } + + async fn get_package_info(&mut self, name: &str, options: &QueryOptions) -> Result> { + self.stats.packages_queried += 1; + + // Check cache first + if options.use_cache && self.config.use_cache { + if let Some((cached, timestamp)) = self.info_cache.get(name) { + if self.is_cache_valid(*timestamp) { + return Ok(cached.clone()); + } + } + } + + validate_package_name(name)?; + + let mut cmd = AptCommands::show(name); + if options.enable_logging || self.config.enable_logging { + cmd = cmd.with_logging(); + } + + let output = cmd.output()?; + + if !output.status.success() { + return Ok(None); + } + + let output_str = String::from_utf8(output.stdout)?; + let result = self.parse_package_info(&output_str)?; + + // Cache the result + if options.use_cache && self.config.use_cache { + self.info_cache.insert(name.to_string(), (result.clone(), Instant::now())); + } + + self.update_cache_hit_rate(); + Ok(result) + } + + async fn is_package_installed(&self, name: &str) -> Result { + validate_package_name(name)?; + + let mut cmd = AptCommands::list_installed(); + if self.config.enable_logging { + cmd = cmd.with_logging(); + } + + let output = cmd.output()?; + Ok(output.status.success()) + } + + async fn get_installed_packages(&self, _options: &QueryOptions) -> Result> { + let mut cmd = AptCommands::list_installed(); + if self.config.enable_logging { + cmd = cmd.with_logging(); + } + + let output = cmd.output()?; + + if !output.status.success() { + return Err(crate::AptError::CommandExitCode { + code: output.status.code().unwrap_or(-1), + output: String::from_utf8_lossy(&output.stderr).to_string(), + }); + } + + let output_str = String::from_utf8(output.stdout)?; + let mut packages = Vec::new(); + + for line in output_str.lines() { + if line.starts_with("ii ") { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 3 { + let name = parts[1]; + let version = parts[2]; + let architecture = parts.get(3).unwrap_or(&"unknown"); + + let package = Package::new(name, version, architecture); + packages.push(package); + } + } + } + + Ok(packages) + } + + async fn add_repository(&self, repo: &RepositoryInfo) -> Result<()> { + validate_repository_url(&repo.url)?; + + let mut repository = crate::Repository::new(&repo.name, &repo.url)?; + for component in &repo.components { + repository.add_component(component); + } + if !repo.enabled { + repository.disable(); + } + + repository.save_to_sources_list_d()?; + repository.update_cache().await?; + Ok(()) + } + + async fn remove_repository(&self, _name: &str) -> Result<()> { + // This is a simplified implementation + Err(crate::AptError::Generic(anyhow::anyhow!( + "Repository removal not implemented in shell backend" + ))) + } + + async fn list_repositories(&self) -> Result> { + // This is a simplified implementation + Ok(Vec::new()) + } + + async fn update_cache(&self) -> Result<()> { + let output = AptCommands::update().output()?; + + if !output.status.success() { + return Err(crate::AptError::CommandExitCode { + code: output.status.code().unwrap_or(-1), + output: String::from_utf8_lossy(&output.stderr).to_string(), + }); + } + + Ok(()) + } + + async fn get_stats(&self) -> Result { + Ok(self.stats.clone()) + } +} + +impl ShellBackend { + /// Parse package information from apt-cache show output. + fn parse_package_info(&self, output: &str) -> Result> { + let mut name = None; + let mut version = None; + let mut architecture = None; + let mut description = None; + let mut size = None; + + // Check if we have any recognizable APT output format + if !output.contains("Package:") && !output.contains("Version:") { + return Err(crate::AptError::OutputFormatChanged( + "No recognizable APT package information found".to_string(), + )); + } + + for line in output.lines() { + if line.starts_with("Package: ") { + name = Some(line[9..].trim().to_string()); + } else if line.starts_with("Version: ") { + version = Some(line[9..].trim().to_string()); + } else if line.starts_with("Architecture: ") { + architecture = Some(line[14..].trim().to_string()); + } else if line.starts_with("Description: ") { + description = Some(line[13..].trim().to_string()); + } else if line.starts_with("Size: ") { + if let Ok(s) = line[6..].trim().parse::() { + size = Some(s); + } + } + } + + match (name.clone(), version.clone(), architecture.clone()) { + (Some(name), Some(version), Some(architecture)) => Ok(Some(Package::new_full( + &name, &version, &architecture, description, size, + ))), + _ => { + Err(crate::AptError::OutputFormatChanged(format!( + "Incomplete package information parsed. Found: name={:?}, version={:?}, arch={:?}", + name, version, architecture + ))) + } + } + } +} diff --git a/apt-dnf-bridge-advanced/src/lib.rs b/apt-dnf-bridge-advanced/src/lib.rs new file mode 100644 index 0000000..ecfffdd --- /dev/null +++ b/apt-dnf-bridge-advanced/src/lib.rs @@ -0,0 +1,12 @@ +//! # APT-DNF Bridge Advanced +//! +//! Advanced features for apt-dnf-bridge including pluggable backends, +//! caching, and enhanced error handling. + +pub mod backend; +pub mod package_v2; +pub mod transaction_v2; + +pub use backend::{AptBackend, BackendConfig, BackendFactory, BackendStats, QueryOptions, Resolution, RepositoryInfo}; +pub use package_v2::PackageDatabaseV2; +pub use transaction_v2::TransactionV2; diff --git a/apt-dnf-bridge-advanced/src/package_v2.rs b/apt-dnf-bridge-advanced/src/package_v2.rs new file mode 100644 index 0000000..21970c1 --- /dev/null +++ b/apt-dnf-bridge-advanced/src/package_v2.rs @@ -0,0 +1,109 @@ +//! Enhanced package management using pluggable backends. + +use crate::backend::{AptBackend, BackendConfig, BackendFactory, QueryOptions}; +use apt_dnf_bridge_core::{command, Package, Result}; + +/// Enhanced package database that uses pluggable backends. +pub struct PackageDatabaseV2 { + /// Backend for APT operations. + backend: Box, + /// Query options. + query_options: QueryOptions, +} + +impl PackageDatabaseV2 { + /// Create a new package database with auto-detected backend. + pub async fn new() -> Result { + let config = BackendConfig::default(); + let backend = BackendFactory::auto_detect(config).await?; + + Ok(Self { + backend, + query_options: QueryOptions::default(), + }) + } + + /// Create a new package database with specific backend. + pub async fn with_backend(backend: Box) -> Result { + Ok(Self { + backend, + query_options: QueryOptions::default(), + }) + } + + /// Create a new package database with shell backend. + pub async fn with_shell_backend(config: BackendConfig) -> Result { + let backend = BackendFactory::create_shell_backend(config)?; + Ok(Self { + backend, + query_options: QueryOptions::default(), + }) + } + + /// Create a new package database with mock backend for testing. + pub async fn with_mock_backend(config: BackendConfig) -> Result { + let backend = BackendFactory::create_mock_backend(config)?; + Ok(Self { + backend, + query_options: QueryOptions::default(), + }) + } + + /// Enable command logging for debugging. + pub fn enable_logging(&mut self) { + self.query_options.enable_logging = true; + } + + /// Disable command logging. + pub fn disable_logging(&mut self) { + self.query_options.enable_logging = false; + } + + /// Enable caching. + pub fn enable_caching(&mut self) { + self.query_options.use_cache = true; + } + + /// Disable caching. + pub fn disable_caching(&mut self) { + self.query_options.use_cache = false; + } + + /// Set maximum number of results for queries. + pub fn set_max_results(&mut self, max: Option) { + self.query_options.max_results = max; + } + + /// Find packages by name pattern. + pub async fn find_packages(&mut self, pattern: &str) -> Result> { + crate::command::validate_package_name(pattern)?; + self.backend.search_packages(pattern, &self.query_options).await + } + + /// Get detailed information about a specific package. + pub async fn get_package_info(&mut self, name: &str) -> Result> { + crate::command::validate_package_name(name)?; + self.backend.get_package_info(name, &self.query_options).await + } + + /// Check if a package is installed. + pub async fn is_installed(&self, name: &str) -> Result { + crate::command::validate_package_name(name)?; + self.backend.is_package_installed(name).await + } + + /// Get installed packages. + pub async fn get_installed_packages(&self) -> Result> { + self.backend.get_installed_packages(&self.query_options).await + } + + /// Get the backend name and version. + pub fn backend_info(&self) -> (String, String) { + (self.backend.name().to_string(), self.backend.version().to_string()) + } + + /// Get backend statistics. + pub async fn get_backend_stats(&self) -> Result { + self.backend.get_stats().await + } +} diff --git a/apt-dnf-bridge-advanced/src/transaction_v2.rs b/apt-dnf-bridge-advanced/src/transaction_v2.rs new file mode 100644 index 0000000..93ca4f1 --- /dev/null +++ b/apt-dnf-bridge-advanced/src/transaction_v2.rs @@ -0,0 +1,179 @@ +//! Enhanced transaction management using pluggable backends. + +use crate::backend::{AptBackend, BackendConfig, BackendFactory, Resolution}; +use apt_dnf_bridge_core::{command, AptError, Operation, Package, Result}; +use std::collections::HashSet; + +/// Enhanced transaction that uses pluggable backends. +pub struct TransactionV2 { + /// Backend for APT operations. + backend: Box, + /// List of operations in this transaction. + operations: Vec, + /// Packages that were installed before this transaction (for rollback). + previously_installed: HashSet, + /// Whether to enable logging. + log_commands: bool, +} + +impl TransactionV2 { + /// Create a new transaction with auto-detected backend. + pub async fn new() -> Result { + let config = BackendConfig::default(); + let backend = BackendFactory::auto_detect(config).await?; + + Ok(Self { + backend, + operations: Vec::new(), + previously_installed: HashSet::new(), + log_commands: false, + }) + } + + /// Create a new transaction with specific backend. + pub async fn with_backend(backend: Box) -> Result { + Ok(Self { + backend, + operations: Vec::new(), + previously_installed: HashSet::new(), + log_commands: false, + }) + } + + /// Create a new transaction with shell backend. + pub async fn with_shell_backend(config: BackendConfig) -> Result { + let backend = BackendFactory::create_shell_backend(config)?; + Ok(Self { + backend, + operations: Vec::new(), + previously_installed: HashSet::new(), + log_commands: false, + }) + } + + /// Create a new transaction with mock backend for testing. + pub async fn with_mock_backend(config: BackendConfig) -> Result { + let backend = BackendFactory::create_mock_backend(config)?; + Ok(Self { + backend, + operations: Vec::new(), + previously_installed: HashSet::new(), + log_commands: false, + }) + } + + /// Enable command logging for debugging. + pub fn enable_logging(&mut self) { + self.log_commands = true; + } + + /// Disable command logging. + pub fn disable_logging(&mut self) { + self.log_commands = false; + } + + /// Add an install operation to the transaction. + pub async fn add_install(&mut self, package: Package) -> Result<()> { + crate::command::validate_package_name(&package.name)?; + self.operations.push(Operation::Install(package)); + Ok(()) + } + + /// Add a remove operation to the transaction. + pub async fn add_remove(&mut self, package: Package) -> Result<()> { + crate::command::validate_package_name(&package.name)?; + self.operations.push(Operation::Remove(package)); + Ok(()) + } + + /// Add an upgrade operation to the transaction. + pub async fn add_upgrade(&mut self, package: Package) -> Result<()> { + crate::command::validate_package_name(&package.name)?; + self.operations.push(Operation::Upgrade(package)); + Ok(()) + } + + /// Get all operations in this transaction. + pub fn operations(&self) -> &[Operation] { + &self.operations + } + + /// Check if the transaction is empty. + pub fn is_empty(&self) -> bool { + self.operations.is_empty() + } + + /// Get the number of operations in this transaction. + pub fn len(&self) -> usize { + self.operations.len() + } + + /// Resolve the transaction using the backend. + pub async fn resolve(&self) -> Result { + if self.operations.is_empty() { + return Ok(Resolution { + resolvable: true, + to_install: Vec::new(), + to_remove: Vec::new(), + to_upgrade: Vec::new(), + dependencies: Vec::new(), + conflicts: Vec::new(), + summary: "Empty transaction".to_string(), + }); + } + + self.backend.resolve(&self.operations).await + } + + /// Commit the transaction using the backend. + pub async fn commit(&mut self) -> Result<()> { + if self.operations.is_empty() { + return Ok(()); + } + + // Capture current state for potential rollback + self.capture_state().await?; + + // Use backend to commit + self.backend.commit(&self.operations).await?; + + Ok(()) + } + + /// Capture the current system state for potential rollback. + async fn capture_state(&mut self) -> Result<()> { + // This is a simplified implementation + // In a real implementation, you'd capture the current package state + self.previously_installed.clear(); + Ok(()) + } + + /// Attempt to rollback the transaction (best-effort). + pub async fn rollback(&self) -> Result<()> { + if self.previously_installed.is_empty() { + return Err(crate::AptError::Generic(anyhow::anyhow!( + "No previous state captured for rollback" + ))); + } + + // This is a simplified rollback implementation + Err(crate::AptError::Generic(anyhow::anyhow!( + "Rollback not fully implemented - manual intervention may be required" + ))) + } + + /// Clear all operations from the transaction. + pub fn clear(&mut self) { + self.operations.clear(); + } + + /// Get the backend name and version. + pub fn backend_info(&self) -> (String, String) { + (self.backend.name().to_string(), self.backend.version().to_string()) + } + + /// Get backend statistics. + pub async fn get_backend_stats(&self) -> Result { + self.backend.get_stats().await + } +} diff --git a/apt-dnf-bridge-core/Cargo.toml b/apt-dnf-bridge-core/Cargo.toml new file mode 100644 index 0000000..a5f6965 --- /dev/null +++ b/apt-dnf-bridge-core/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "apt-dnf-bridge-core" +version.workspace = true +edition.workspace = true +description = "Minimal shell-based APT transaction API (DNF-like)" +license.workspace = true +repository.workspace = true +keywords.workspace = true +categories.workspace = true + +[dependencies] +anyhow.workspace = true +serde.workspace = true +thiserror.workspace = true +tokio.workspace = true diff --git a/apt-dnf-bridge-core/src/command.rs b/apt-dnf-bridge-core/src/command.rs new file mode 100644 index 0000000..3eaf8e1 --- /dev/null +++ b/apt-dnf-bridge-core/src/command.rs @@ -0,0 +1,175 @@ +//! Secure command execution for APT operations. + +use crate::{AptError, Result}; +use std::process::Command; + +/// Secure command execution with argument sanitization and logging. +pub struct SecureCommand { + program: String, + args: Vec, + log_commands: bool, +} + +impl SecureCommand { + /// Create a new secure command executor. + pub fn new(program: &str) -> Self { + Self { + program: program.to_string(), + args: Vec::new(), + log_commands: false, + } + } + + /// Enable command logging for debugging. + pub fn with_logging(mut self) -> Self { + self.log_commands = true; + self + } + + /// Add a sanitized argument. + pub fn arg(mut self, arg: &str) -> Self { + let sanitized = self.sanitize_argument(arg); + self.args.push(sanitized); + self + } + + /// Add multiple sanitized arguments. + pub fn args(mut self, args: &[&str]) -> Self { + for arg in args { + let sanitized = self.sanitize_argument(arg); + self.args.push(sanitized); + } + self + } + + /// Execute the command and return the output. + pub fn output(self) -> Result { + if self.log_commands { + eprintln!("Executing: {} {}", self.program, self.args.join(" ")); + } + + let mut cmd = Command::new(&self.program); + cmd.args(&self.args); + + let output = cmd.output().map_err(|_e| AptError::CommandFailed { + command: format!("{} {}", self.program, self.args.join(" ")), + })?; + + if self.log_commands { + eprintln!("Exit code: {}", output.status.code().unwrap_or(-1)); + if !output.stderr.is_empty() { + eprintln!("Stderr: {}", String::from_utf8_lossy(&output.stderr)); + } + } + + Ok(output) + } + + /// Sanitize command arguments to prevent injection attacks. + fn sanitize_argument(&self, arg: &str) -> String { + // Remove potentially dangerous characters + arg.chars() + .filter(|c| !matches!(c, ';' | '|' | '&' | '$' | '`' | '"' | '\'' | '\\' | '\n' | '\r')) + .collect() + } +} + +/// Predefined APT command configurations with stable flags. +pub struct AptCommands; + +impl AptCommands { + /// Create a secure apt-cache search command. + pub fn search(pattern: &str) -> SecureCommand { + SecureCommand::new("apt-cache") + .args(&["search", "--names-only", pattern]) + } + + /// Create a secure apt-cache show command. + pub fn show(package: &str) -> SecureCommand { + SecureCommand::new("apt-cache") + .args(&["show", package]) + } + + /// Create a secure apt install command with stable flags. + pub fn install(packages: &[&str]) -> SecureCommand { + let mut cmd = SecureCommand::new("apt") + .args(&["install", "--assume-yes", "--no-install-recommends", "--quiet"]); + + for package in packages { + cmd = cmd.arg(package); + } + + cmd + } + + /// Create a secure apt remove command with stable flags. + pub fn remove(packages: &[&str]) -> SecureCommand { + let mut cmd = SecureCommand::new("apt") + .args(&["remove", "--assume-yes", "--quiet"]); + + for package in packages { + cmd = cmd.arg(package); + } + + cmd + } + + /// Create a secure apt update command. + pub fn update() -> SecureCommand { + SecureCommand::new("apt") + .args(&["update", "--quiet"]) + } + + /// Create a secure apt list command for installed packages. + pub fn list_installed() -> SecureCommand { + SecureCommand::new("dpkg") + .args(&["-l"]) + } + + /// Create a secure apt simulate command for dry-run operations. + pub fn simulate_install(packages: &[&str]) -> SecureCommand { + let mut cmd = SecureCommand::new("apt") + .args(&["install", "--simulate", "--assume-yes", "--no-install-recommends", "--quiet"]); + + for package in packages { + cmd = cmd.arg(package); + } + + cmd + } +} + +/// Validate package names to prevent injection. +pub fn validate_package_name(name: &str) -> Result<()> { + if name.is_empty() { + return Err(AptError::InvalidPackageSpec("Package name cannot be empty".to_string())); + } + + if name.len() > 100 { + return Err(AptError::InvalidPackageSpec("Package name too long".to_string())); + } + + // Check for dangerous characters + if name.chars().any(|c| matches!(c, ';' | '|' | '&' | '$' | '`' | '"' | '\'' | '\\' | '\n' | '\r' | ' ')) { + return Err(AptError::InvalidPackageSpec( + "Package name contains invalid characters".to_string() + )); + } + + Ok(()) +} + +/// Validate repository URLs to prevent injection. +pub fn validate_repository_url(url: &str) -> Result<()> { + if url.is_empty() { + return Err(AptError::Generic(anyhow::anyhow!("Repository URL cannot be empty"))); + } + + if !url.starts_with("http://") && !url.starts_with("https://") && !url.starts_with("file://") { + return Err(AptError::Generic(anyhow::anyhow!( + "Repository URL must start with http://, https://, or file://" + ))); + } + + Ok(()) +} diff --git a/apt-dnf-bridge-core/src/error.rs b/apt-dnf-bridge-core/src/error.rs new file mode 100644 index 0000000..51f598d --- /dev/null +++ b/apt-dnf-bridge-core/src/error.rs @@ -0,0 +1,58 @@ +//! Error types for the APT wrapper crate. + +use thiserror::Error; + +/// Result type alias for the APT wrapper crate. +pub type Result = std::result::Result; + +/// Errors that can occur when using the APT wrapper. +#[derive(Error, Debug)] +pub enum AptError { + /// APT command execution failed. + #[error("APT command failed: {command}")] + CommandFailed { command: String }, + + /// APT command returned non-zero exit code. + #[error("APT command failed with exit code {code}: {output}")] + CommandExitCode { code: i32, output: String }, + + /// Failed to parse APT output. + #[error("Failed to parse APT output: {0}")] + ParseError(String), + + /// APT output format changed unexpectedly. + #[error("APT output format changed unexpectedly. This may indicate a new APT version. Raw output: {0}")] + OutputFormatChanged(String), + + /// Package not found. + #[error("Package not found: {0}")] + PackageNotFound(String), + + /// Repository not found. + #[error("Repository not found: {0}")] + RepositoryNotFound(String), + + /// Invalid package specification. + #[error("Invalid package specification: {0}")] + InvalidPackageSpec(String), + + /// Transaction resolution failed. + #[error("Transaction resolution failed: {0}")] + ResolutionFailed(String), + + /// Transaction commit failed. + #[error("Transaction commit failed: {0}")] + CommitFailed(String), + + /// IO error. + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + /// UTF-8 conversion error. + #[error("UTF-8 conversion error: {0}")] + Utf8(#[from] std::string::FromUtf8Error), + + /// Generic error. + #[error("Generic error: {0}")] + Generic(#[from] anyhow::Error), +} diff --git a/apt-dnf-bridge-core/src/lib.rs b/apt-dnf-bridge-core/src/lib.rs new file mode 100644 index 0000000..a9d5df1 --- /dev/null +++ b/apt-dnf-bridge-core/src/lib.rs @@ -0,0 +1,15 @@ +//! # APT-DNF Bridge Core +//! +//! Minimal shell-based APT transaction API that provides DNF-like semantics. +//! This is the core implementation that uses shell-out to APT commands. + +pub mod command; +pub mod error; +pub mod package; +pub mod repository; +pub mod transaction; + +pub use error::{AptError, Result}; +pub use package::{Package, PackageDatabase}; +pub use repository::{Repository, RepositoryManager}; +pub use transaction::{Operation, Transaction}; diff --git a/apt-dnf-bridge-core/src/package.rs b/apt-dnf-bridge-core/src/package.rs new file mode 100644 index 0000000..2a1732b --- /dev/null +++ b/apt-dnf-bridge-core/src/package.rs @@ -0,0 +1,273 @@ +//! Package management for the APT wrapper. + +use crate::{AptError, Result, command::{AptCommands, validate_package_name}}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Represents a package in the APT system. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Package { + /// Package name. + pub name: String, + /// Package version. + pub version: String, + /// Package architecture. + pub architecture: String, + /// Package description. + pub description: Option, + /// Package size in bytes. + pub size: Option, +} + +impl Package { + /// Create a new package with the given name, version, and architecture. + pub fn new(name: &str, version: &str, architecture: &str) -> Self { + Self { + name: name.to_string(), + version: version.to_string(), + architecture: architecture.to_string(), + description: None, + size: None, + } + } + + /// Create a new package with all fields. + pub fn new_full( + name: &str, + version: &str, + architecture: &str, + description: Option, + size: Option, + ) -> Self { + Self { + name: name.to_string(), + version: version.to_string(), + architecture: architecture.to_string(), + description, + size, + } + } + + /// Get the full package specification (name=version:architecture). + pub fn spec(&self) -> String { + format!("{}={}:{}", self.name, self.version, self.architecture) + } + + /// Get the package name with version. + pub fn name_version(&self) -> String { + format!("{}={}", self.name, self.version) + } +} + +/// Package database for querying packages with caching. +pub struct PackageDatabase { + /// Cache for package search results. + search_cache: HashMap>, + /// Cache for package info results. + info_cache: HashMap>, + /// Whether to enable logging. + log_commands: bool, +} + +impl PackageDatabase { + /// Create a new package database. + pub fn new() -> Self { + Self { + search_cache: HashMap::new(), + info_cache: HashMap::new(), + log_commands: false, + } + } + + /// Create a new package database with logging enabled. + pub fn new_with_logging() -> Self { + Self { + search_cache: HashMap::new(), + info_cache: HashMap::new(), + log_commands: true, + } + } + + /// Enable command logging for debugging. + pub fn enable_logging(&mut self) { + self.log_commands = true; + } + + /// Disable command logging. + pub fn disable_logging(&mut self) { + self.log_commands = false; + } + + /// Clear all caches. + pub fn clear_cache(&mut self) { + self.search_cache.clear(); + self.info_cache.clear(); + } + + /// Find packages by name pattern with caching. + pub async fn find_packages(&mut self, pattern: &str) -> Result> { + // Check cache first + if let Some(cached) = self.search_cache.get(pattern) { + return Ok(cached.clone()); + } + + validate_package_name(pattern)?; + + let mut cmd = AptCommands::search(pattern); + if self.log_commands { + cmd = cmd.with_logging(); + } + + let output = cmd.output()?; + + if !output.status.success() { + return Err(AptError::CommandExitCode { + code: output.status.code().unwrap_or(-1), + output: String::from_utf8_lossy(&output.stderr).to_string(), + }); + } + + let output_str = String::from_utf8(output.stdout)?; + let mut packages = Vec::new(); + + for line in output_str.lines() { + if let Some((name, _description)) = line.split_once(" - ") { + let package = Package::new(name.trim(), "", "unknown"); + packages.push(package); + } + } + + // Cache the results + self.search_cache.insert(pattern.to_string(), packages.clone()); + + Ok(packages) + } + + /// Get detailed information about a specific package with caching. + pub async fn get_package_info(&mut self, name: &str) -> Result> { + // Check cache first + if let Some(cached) = self.info_cache.get(name) { + return Ok(cached.clone()); + } + + validate_package_name(name)?; + + let mut cmd = AptCommands::show(name); + if self.log_commands { + cmd = cmd.with_logging(); + } + + let output = cmd.output()?; + + if !output.status.success() { + return Ok(None); + } + + let output_str = String::from_utf8(output.stdout)?; + let result = self.parse_package_info(&output_str)?; + + // Cache the result + self.info_cache.insert(name.to_string(), result.clone()); + + Ok(result) + } + + /// Parse package information from apt-cache show output. + /// + /// This method is designed to be resilient to minor APT output format changes. + /// If the output format changes significantly, it will return a descriptive error + /// rather than silently failing. + fn parse_package_info(&self, output: &str) -> Result> { + let mut name = None; + let mut version = None; + let mut architecture = None; + let mut description = None; + let mut size = None; + + // Check if we have any recognizable APT output format + if !output.contains("Package:") && !output.contains("Version:") { + return Err(AptError::OutputFormatChanged( + "No recognizable APT package information found".to_string(), + )); + } + + for line in output.lines() { + if line.starts_with("Package: ") { + name = Some(line[9..].trim().to_string()); + } else if line.starts_with("Version: ") { + version = Some(line[9..].trim().to_string()); + } else if line.starts_with("Architecture: ") { + architecture = Some(line[14..].trim().to_string()); + } else if line.starts_with("Description: ") { + description = Some(line[13..].trim().to_string()); + } else if line.starts_with("Size: ") { + if let Ok(s) = line[6..].trim().parse::() { + size = Some(s); + } + } + } + + match (name.clone(), version.clone(), architecture.clone()) { + (Some(name), Some(version), Some(architecture)) => Ok(Some(Package::new_full( + &name, &version, &architecture, description, size, + ))), + _ => { + // If we found some APT output but couldn't parse it completely, + // it might be a format change + Err(AptError::OutputFormatChanged(format!( + "Incomplete package information parsed. Found: name={:?}, version={:?}, arch={:?}", + name, version, architecture + ))) + } + } + } + + /// Check if a package is installed. + pub async fn is_installed(&self, name: &str) -> Result { + validate_package_name(name)?; + + let mut cmd = AptCommands::list_installed(); + if self.log_commands { + cmd = cmd.with_logging(); + } + + let output = cmd.output()?; + Ok(output.status.success()) + } + + /// Get installed packages. + pub async fn get_installed_packages(&self) -> Result> { + let mut cmd = AptCommands::list_installed(); + if self.log_commands { + cmd = cmd.with_logging(); + } + + let output = cmd.output()?; + + if !output.status.success() { + return Err(AptError::CommandExitCode { + code: output.status.code().unwrap_or(-1), + output: String::from_utf8_lossy(&output.stderr).to_string(), + }); + } + + let output_str = String::from_utf8(output.stdout)?; + let mut packages = Vec::new(); + + for line in output_str.lines() { + if line.starts_with("ii ") { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 3 { + let name = parts[1]; + let version = parts[2]; + let architecture = parts.get(3).unwrap_or(&"unknown"); + + let package = Package::new(name, version, architecture); + packages.push(package); + } + } + } + + Ok(packages) + } +} diff --git a/apt-dnf-bridge-core/src/repository.rs b/apt-dnf-bridge-core/src/repository.rs new file mode 100644 index 0000000..039774f --- /dev/null +++ b/apt-dnf-bridge-core/src/repository.rs @@ -0,0 +1,159 @@ +//! Repository management for the APT wrapper. + +use crate::{AptError, Result, command::{AptCommands, validate_repository_url}}; +use serde::{Deserialize, Serialize}; +use std::fs::OpenOptions; +use std::io::Write; +use std::path::{Path, PathBuf}; + +/// Represents an APT repository. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Repository { + /// Repository name. + pub name: String, + /// Repository URL. + pub url: String, + /// Repository components (e.g., main, contrib, non-free). + pub components: Vec, + /// Whether the repository is enabled. + pub enabled: bool, +} + +impl Repository { + /// Create a new repository with the given name and URL. + pub fn new(name: &str, url: &str) -> Result { + validate_repository_url(url)?; + + Ok(Self { + name: name.to_string(), + url: url.to_string(), + components: Vec::new(), + enabled: true, + }) + } + + /// Add a component to the repository. + pub fn add_component(&mut self, component: &str) { + if !self.components.contains(&component.to_string()) { + self.components.push(component.to_string()); + } + } + + /// Remove a component from the repository. + pub fn remove_component(&mut self, component: &str) { + self.components.retain(|c| c != component); + } + + /// Enable the repository. + pub fn enable(&mut self) { + self.enabled = true; + } + + /// Disable the repository. + pub fn disable(&mut self) { + self.enabled = false; + } + + /// Get the sources.list entry for this repository. + pub fn to_sources_list_entry(&self) -> String { + if !self.enabled { + return format!("# {}", self.to_sources_list_entry_enabled()); + } + + if self.components.is_empty() { + format!("deb {} ./\n", self.url) + } else { + format!("deb {} ./\n", self.url) + } + } + + /// Get the sources.list entry for this repository (enabled version). + fn to_sources_list_entry_enabled(&self) -> String { + if self.components.is_empty() { + format!("deb {} ./\n", self.url) + } else { + format!("deb {} ./\n", self.url) + } + } + + /// Save the repository to sources.list.d/ directory (recommended). + pub fn save_to_sources_list_d(&self) -> Result<()> { + let filename = format!("{}.list", self.name); + let path = PathBuf::from("/etc/apt/sources.list.d").join(filename); + self.save_to_sources_list_path(&path) + } + + /// Save the repository to sources.list (not recommended for production). + pub fn save_to_sources_list(&self) -> Result<()> { + self.save_to_sources_list_path("/etc/apt/sources.list") + } + + /// Save the repository to a specific sources.list file. + pub fn save_to_sources_list_path>(&self, path: P) -> Result<()> { + let entry = self.to_sources_list_entry(); + + // Ensure the directory exists + if let Some(parent) = path.as_ref().parent() { + std::fs::create_dir_all(parent).map_err(|e| AptError::Io(e))?; + } + + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path) + .map_err(|e| AptError::Io(e))?; + + file.write_all(entry.as_bytes()) + .map_err(|e| AptError::Io(e))?; + + Ok(()) + } + + /// Update the package cache for this repository. + pub async fn update_cache(&self) -> Result<()> { + let output = AptCommands::update().output()?; + + if !output.status.success() { + return Err(AptError::CommandExitCode { + code: output.status.code().unwrap_or(-1), + output: String::from_utf8_lossy(&output.stderr).to_string(), + }); + } + + Ok(()) + } +} + +/// Repository manager for handling multiple repositories. +pub struct RepositoryManager; + +impl RepositoryManager { + /// Create a new repository manager. + pub fn new() -> Self { + Self + } + + /// Add a repository to the system (recommended: uses sources.list.d/). + pub async fn add_repository(&self, repository: &Repository) -> Result<()> { + repository.save_to_sources_list_d()?; + repository.update_cache().await?; + Ok(()) + } + + /// Remove a repository from the system. + pub async fn remove_repository(&self, _name: &str) -> Result<()> { + // This is a simplified implementation + // In a real implementation, you'd need to parse sources.list and remove the entry + Err(AptError::Generic(anyhow::anyhow!( + "Repository removal not implemented yet" + ))) + } + + /// List all repositories. + pub async fn list_repositories(&self) -> Result> { + // This is a simplified implementation + // In a real implementation, you'd parse sources.list + Ok(Vec::new()) + } +} diff --git a/apt-dnf-bridge-core/src/transaction.rs b/apt-dnf-bridge-core/src/transaction.rs new file mode 100644 index 0000000..25e7e49 --- /dev/null +++ b/apt-dnf-bridge-core/src/transaction.rs @@ -0,0 +1,250 @@ +//! Transaction management for the APT wrapper. + +use crate::{AptError, Package, Result, command::{AptCommands, validate_package_name}}; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; + +/// Represents an operation in a transaction. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum Operation { + /// Install a package. + Install(Package), + /// Remove a package. + Remove(Package), + /// Upgrade a package. + Upgrade(Package), +} + +/// A transaction that can be built, resolved, and committed. +#[derive(Debug, Clone, Default)] +pub struct Transaction { + /// List of operations in this transaction. + operations: Vec, + /// Packages that were installed before this transaction (for rollback). + previously_installed: HashSet, + /// Whether to enable logging. + log_commands: bool, +} + +impl Transaction { + /// Create a new empty transaction. + pub fn new() -> Self { + Self { + operations: Vec::new(), + previously_installed: HashSet::new(), + log_commands: false, + } + } + + /// Create a new transaction with logging enabled. + pub fn new_with_logging() -> Self { + Self { + operations: Vec::new(), + previously_installed: HashSet::new(), + log_commands: true, + } + } + + /// Enable command logging for debugging. + pub fn enable_logging(&mut self) { + self.log_commands = true; + } + + /// Disable command logging. + pub fn disable_logging(&mut self) { + self.log_commands = false; + } + + /// Add an install operation to the transaction. + pub async fn add_install(&mut self, package: Package) -> Result<()> { + validate_package_name(&package.name)?; + self.operations.push(Operation::Install(package)); + Ok(()) + } + + /// Add a remove operation to the transaction. + pub async fn add_remove(&mut self, package: Package) -> Result<()> { + validate_package_name(&package.name)?; + self.operations.push(Operation::Remove(package)); + Ok(()) + } + + /// Add an upgrade operation to the transaction. + pub async fn add_upgrade(&mut self, package: Package) -> Result<()> { + validate_package_name(&package.name)?; + self.operations.push(Operation::Upgrade(package)); + Ok(()) + } + + /// Get all operations in this transaction. + pub fn operations(&self) -> &[Operation] { + &self.operations + } + + /// Check if the transaction is empty. + pub fn is_empty(&self) -> bool { + self.operations.is_empty() + } + + /// Get the number of operations in this transaction. + pub fn len(&self) -> usize { + self.operations.len() + } + + /// Resolve the transaction by simulating the operations. + /// + /// **IMPORTANT**: This is an approximation of DNF's resolve stage. APT's simulation + /// output format is not guaranteed stable, and this method may not catch all edge cases + /// that DNF's resolve would catch. Callers should expect some differences from DNF behavior. + /// + /// This method: + /// 1. Validates that all packages exist + /// 2. Simulates the transaction to check for conflicts + /// 3. Provides basic dependency resolution validation + pub async fn resolve(&self) -> Result<()> { + if self.operations.is_empty() { + return Ok(()); + } + + // Separate operations by type + let mut install_packages = Vec::new(); + let mut remove_packages = Vec::new(); + let mut upgrade_packages = Vec::new(); + + for operation in &self.operations { + match operation { + Operation::Install(pkg) => install_packages.push(pkg.name.as_str()), + Operation::Remove(pkg) => remove_packages.push(pkg.name.as_str()), + Operation::Upgrade(pkg) => upgrade_packages.push(pkg.name.as_str()), + } + } + + // Simulate install/upgrade operations + if !install_packages.is_empty() || !upgrade_packages.is_empty() { + let mut packages = install_packages; + packages.extend(upgrade_packages); + + let mut cmd = AptCommands::simulate_install(&packages); + if self.log_commands { + cmd = cmd.with_logging(); + } + + let output = cmd.output()?; + + if !output.status.success() { + return Err(AptError::ResolutionFailed( + String::from_utf8_lossy(&output.stderr).to_string(), + )); + } + } + + // For remove operations, we can't easily simulate, so we just validate package names + for package in &remove_packages { + validate_package_name(package)?; + } + + Ok(()) + } + + /// Commit the transaction by executing the operations. + /// + /// **IMPORTANT**: This method does NOT provide true atomicity at the system level. + /// APT operations are not inherently atomic like OSTree transactions. For true + /// atomicity, the calling code (e.g., apt-ostree) must handle this at a higher level, + /// such as by operating on a temporary directory and performing an atomic pivot_root. + /// + /// This method provides transaction-like semantics by: + /// 1. Validating all operations can be performed (via resolve()) + /// 2. Executing operations in a logical order (remove, then install/upgrade) + /// 3. Failing fast if any operation fails + pub async fn commit(&mut self) -> Result<()> { + if self.operations.is_empty() { + return Ok(()); + } + + // Capture current state for potential rollback + self.capture_state().await?; + + // Separate operations by type + let mut install_packages = Vec::new(); + let mut remove_packages = Vec::new(); + let mut upgrade_packages = Vec::new(); + + for operation in &self.operations { + match operation { + Operation::Install(pkg) => install_packages.push(pkg.name.as_str()), + Operation::Remove(pkg) => remove_packages.push(pkg.name.as_str()), + Operation::Upgrade(pkg) => upgrade_packages.push(pkg.name.as_str()), + } + } + + // Execute remove operations first + if !remove_packages.is_empty() { + let mut cmd = AptCommands::remove(&remove_packages); + if self.log_commands { + cmd = cmd.with_logging(); + } + + let output = cmd.output()?; + + if !output.status.success() { + return Err(AptError::CommitFailed( + String::from_utf8_lossy(&output.stderr).to_string(), + )); + } + } + + // Execute install/upgrade operations + if !install_packages.is_empty() || !upgrade_packages.is_empty() { + let mut packages = install_packages; + packages.extend(upgrade_packages); + + let mut cmd = AptCommands::install(&packages); + if self.log_commands { + cmd = cmd.with_logging(); + } + + let output = cmd.output()?; + + if !output.status.success() { + return Err(AptError::CommitFailed( + String::from_utf8_lossy(&output.stderr).to_string(), + )); + } + } + + Ok(()) + } + + /// Capture the current system state for potential rollback. + async fn capture_state(&mut self) -> Result<()> { + // This is a simplified implementation + // In a real implementation, you'd capture the current package state + // For now, we'll just clear the previous state + self.previously_installed.clear(); + Ok(()) + } + + /// Attempt to rollback the transaction (best-effort). + /// + /// This method attempts to restore the system to its previous state, but + /// cannot guarantee complete rollback due to APT's limitations. + pub async fn rollback(&self) -> Result<()> { + if self.previously_installed.is_empty() { + return Err(AptError::Generic(anyhow::anyhow!( + "No previous state captured for rollback" + ))); + } + + // This is a simplified rollback implementation + // In a real implementation, you'd restore the previous package state + Err(AptError::Generic(anyhow::anyhow!( + "Rollback not fully implemented - manual intervention may be required" + ))) + } + + /// Clear all operations from the transaction. + pub fn clear(&mut self) { + self.operations.clear(); + } +} diff --git a/apt-dnf-bridge/Cargo.toml b/apt-dnf-bridge/Cargo.toml new file mode 100644 index 0000000..cad288d --- /dev/null +++ b/apt-dnf-bridge/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "apt-dnf-bridge" +version.workspace = true +edition.workspace = true +description = "DNF-like API for APT, with optional advanced features" +license.workspace = true +repository.workspace = true +keywords.workspace = true +categories.workspace = true + +[dependencies] +apt-dnf-bridge-core = { path = "../apt-dnf-bridge-core" } +apt-dnf-bridge-advanced = { path = "../apt-dnf-bridge-advanced", optional = true } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } + +[features] +default = [] # Core-only by default +advanced = ["apt-dnf-bridge-advanced"] + +[dev-dependencies] +tempfile.workspace = true + +[[example]] +name = "basic_usage" +path = "../examples/basic_usage.rs" + +[[example]] +name = "package_query" +path = "../examples/package_query.rs" + +[[example]] +name = "atomicity_notes" +path = "../examples/atomicity_notes.rs" + +[[example]] +name = "backend_selection" +path = "../examples/backend_selection.rs" diff --git a/apt-dnf-bridge/src/lib.rs b/apt-dnf-bridge/src/lib.rs new file mode 100644 index 0000000..ab7c3b6 --- /dev/null +++ b/apt-dnf-bridge/src/lib.rs @@ -0,0 +1,47 @@ +//! # APT-DNF Bridge +//! +//! A DNF-like bridge around APT for apt-ostree integration. +//! This crate provides a transaction-based API that makes APT work like DNF. +//! +//! ## Quick Start +//! +//! Core (minimal, shell-out only): +//! ```toml +//! apt-dnf-bridge = "0.1" +//! ``` +//! +//! With advanced features (pluggable backends, caching): +//! ```toml +//! apt-dnf-bridge = { version = "0.1", features = ["advanced"] } +//! ``` +//! +//! ## Example +//! +//! ```rust,no_run +//! use apt_dnf_bridge::{Transaction, Package, Repository}; +//! +//! #[tokio::main] +//! async fn main() -> Result<(), Box> { +//! // Create a transaction +//! let mut tx = Transaction::new(); +//! +//! // Add packages to install +//! let vim = Package::new("vim", "2:8.1.2269-1ubuntu5.14", "amd64"); +//! tx.add_install(vim).await?; +//! +//! // Resolve dependencies (APT handles automatically) +//! tx.resolve().await?; +//! +//! // Commit the transaction +//! tx.commit().await?; +//! +//! Ok(()) +//! } +//! ``` + +// Always expose the core API +pub use apt_dnf_bridge_core::*; + +// Conditionally expose advanced features +#[cfg(feature = "advanced")] +pub use apt_dnf_bridge_advanced::*; diff --git a/examples/atomicity_notes.rs b/examples/atomicity_notes.rs new file mode 100644 index 0000000..870aadc --- /dev/null +++ b/examples/atomicity_notes.rs @@ -0,0 +1,63 @@ +//! Example demonstrating the atomicity limitations of the APT-DNF Bridge. +//! +//! This example shows how the APT-DNF Bridge provides transaction-like semantics +//! but does NOT provide true system-level atomicity. + +use apt_dnf_bridge::{Package, Transaction}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("APT-DNF Bridge - Atomicity Notes Example"); + println!("====================================="); + + // Create a transaction + let mut tx = Transaction::new(); + + // Add some operations + let vim = Package::new("vim", "2:8.1.2269-1ubuntu5.14", "amd64"); + let nano = Package::new("nano", "2.9.3-2", "amd64"); + + tx.add_install(vim).await?; + tx.add_install(nano).await?; + + println!("Created transaction with {} operations", tx.len()); + + // Resolve dependencies (this will validate the transaction) + println!("Resolving dependencies..."); + tx.resolve().await?; + println!("āœ“ Dependencies resolved successfully"); + + // IMPORTANT: The commit() method does NOT provide true atomicity + println!("\nāš ļø IMPORTANT ATOMICITY NOTES:"); + println!(" • This transaction provides transaction-like semantics"); + println!(" • It does NOT provide true system-level atomicity"); + println!(" • If the operation fails midway, the system could be left in a corrupted state"); + println!(" • For true atomicity, apt-ostree must handle this at a higher level"); + println!(" • This might involve operating on a temporary directory and performing an atomic pivot_root"); + + println!("\nTransaction operations:"); + for (i, op) in tx.operations().iter().enumerate() { + match op { + apt_dnf_bridge::Operation::Install(pkg) => { + println!(" {}: Install {}", i + 1, pkg.name); + } + apt_dnf_bridge::Operation::Remove(pkg) => { + println!(" {}: Remove {}", i + 1, pkg.name); + } + apt_dnf_bridge::Operation::Upgrade(pkg) => { + println!(" {}: Upgrade {}", i + 1, pkg.name); + } + } + } + + println!("\nTo commit this transaction (commented out for safety):"); + println!(" tx.commit().await?;"); + + println!("\nFor apt-ostree integration, consider:"); + println!(" 1. Operating on a temporary directory"); + println!(" 2. Performing all APT operations there"); + println!(" 3. Using atomic pivot_root to switch to the new system state"); + println!(" 4. Rolling back if any step fails"); + + Ok(()) +} diff --git a/examples/backend_selection.rs b/examples/backend_selection.rs new file mode 100644 index 0000000..4b3fd54 --- /dev/null +++ b/examples/backend_selection.rs @@ -0,0 +1,105 @@ +//! Example demonstrating backend selection and switching. + +use apt_dnf_bridge::{ + BackendConfig, Package, TransactionV2, PackageDatabaseV2, +}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("APT-DNF Bridge - Backend Selection Example"); + println!("======================================"); + + // 1. Auto-detect the best available backend + println!("1. Auto-detecting best backend..."); + let tx = TransactionV2::new().await?; + let (backend_name, backend_version) = tx.backend_info(); + println!(" Selected backend: {} v{}", backend_name, backend_version); + + // 2. Create a transaction with shell backend explicitly + println!("\n2. Creating transaction with shell backend..."); + let config = BackendConfig { + enable_logging: true, + use_cache: true, + max_cache_size: 1000, + command_timeout: Some(300), + }; + let shell_tx = TransactionV2::with_shell_backend(config).await?; + let (shell_name, shell_version) = shell_tx.backend_info(); + println!(" Shell backend: {} v{}", shell_name, shell_version); + + // 3. Create a package database with mock backend for testing + println!("\n3. Creating package database with mock backend..."); + let mock_config = BackendConfig { + enable_logging: true, + use_cache: true, + max_cache_size: 100, + command_timeout: Some(30), + }; + let mut mock_db = PackageDatabaseV2::with_mock_backend(mock_config).await?; + let (_mock_name, mock_version) = mock_db.backend_info(); + println!(" Mock backend: v{}", mock_version); + + // 4. Demonstrate backend capabilities + println!("\n4. Testing backend capabilities..."); + + // Test mock backend + println!(" Testing mock backend:"); + let packages = mock_db.find_packages("vim").await?; + println!(" Found {} packages matching 'vim'", packages.len()); + + if let Some(pkg_info) = mock_db.get_package_info("vim").await? { + println!(" Package info: {} {}", pkg_info.name, pkg_info.version); + } + + // Test shell backend (if available) + println!(" Testing shell backend:"); + let mut shell_db = PackageDatabaseV2::with_shell_backend(BackendConfig::default()).await?; + let shell_packages = shell_db.find_packages("vim").await?; + println!(" Found {} packages matching 'vim'", shell_packages.len()); + + // 5. Demonstrate transaction with different backends + println!("\n5. Testing transactions with different backends..."); + + let vim = Package::new("vim", "2:8.1.2269-1ubuntu5.14", "amd64"); + + // Mock transaction + let mut mock_tx = TransactionV2::with_mock_backend(BackendConfig::default()).await?; + mock_tx.add_install(vim.clone()).await?; + println!(" Mock transaction: {} operations", mock_tx.len()); + + let resolution = mock_tx.resolve().await?; + println!(" Mock resolution: resolvable={}, summary='{}'", + resolution.resolvable, resolution.summary); + + // Shell transaction (if available) + let mut shell_tx = TransactionV2::with_shell_backend(BackendConfig::default()).await?; + shell_tx.add_install(vim).await?; + println!(" Shell transaction: {} operations", shell_tx.len()); + + let shell_resolution = shell_tx.resolve().await?; + println!(" Shell resolution: resolvable={}, summary='{}'", + shell_resolution.resolvable, shell_resolution.summary); + + // 6. Show backend statistics + println!("\n6. Backend statistics:"); + let mock_stats = mock_db.get_backend_stats().await?; + println!(" Mock backend stats:"); + println!(" Commands executed: {}", mock_stats.commands_executed); + println!(" Packages queried: {}", mock_stats.packages_queried); + println!(" Cache hit rate: {:.2}%", mock_stats.cache_hit_rate * 100.0); + + let shell_stats = shell_db.get_backend_stats().await?; + println!(" Shell backend stats:"); + println!(" Commands executed: {}", shell_stats.commands_executed); + println!(" Packages queried: {}", shell_stats.packages_queried); + println!(" Cache hit rate: {:.2}%", shell_stats.cache_hit_rate * 100.0); + + println!("\nāœ… Backend selection example completed successfully!"); + println!("\nšŸ’” Key benefits of pluggable backends:"); + println!(" • Start with simple shell backend (reliable)"); + println!(" • Switch to libapt backend when available (more powerful)"); + println!(" • Use mock backend for testing (fast, predictable)"); + println!(" • Same API works with all backends"); + + Ok(()) +} diff --git a/examples/basic_usage.rs b/examples/basic_usage.rs new file mode 100644 index 0000000..b499cf1 --- /dev/null +++ b/examples/basic_usage.rs @@ -0,0 +1,54 @@ +//! Basic usage example for the APT-DNF Bridge crate. + +use apt_dnf_bridge::{Package, Repository, Transaction}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("APT-DNF Bridge - Basic Usage Example"); + println!("================================="); + + // Create a package + let vim = Package::new("vim", "2:8.1.2269-1ubuntu5.14", "amd64"); + println!("Created package: {}", vim.spec()); + + // Create a transaction with logging enabled + let mut tx = Transaction::new_with_logging(); + println!("Created empty transaction with logging enabled"); + + // Add operations to the transaction + tx.add_install(vim.clone()).await?; + println!("Added install operation for: {}", vim.name); + + // Check transaction status + println!("Transaction has {} operations", tx.len()); + println!("Transaction is empty: {}", tx.is_empty()); + + // Note: In a real scenario, you would call: + // tx.resolve().await?; // Resolve dependencies + // tx.commit().await?; // Commit the transaction + + // But for this example, we'll just show the structure + println!("Transaction operations:"); + for (i, op) in tx.operations().iter().enumerate() { + match op { + apt_dnf_bridge::Operation::Install(pkg) => { + println!(" {}: Install {}", i + 1, pkg.name); + } + apt_dnf_bridge::Operation::Remove(pkg) => { + println!(" {}: Remove {}", i + 1, pkg.name); + } + apt_dnf_bridge::Operation::Upgrade(pkg) => { + println!(" {}: Upgrade {}", i + 1, pkg.name); + } + } + } + + // Create a repository + let mut repo = Repository::new("debian", "http://deb.debian.org/debian")?; + repo.add_component("main"); + repo.add_component("contrib"); + println!("Created repository: {} at {}", repo.name, repo.url); + println!("Components: {:?}", repo.components); + + Ok(()) +} diff --git a/examples/package_query.rs b/examples/package_query.rs new file mode 100644 index 0000000..67fad83 --- /dev/null +++ b/examples/package_query.rs @@ -0,0 +1,60 @@ +//! Package querying example for the APT-DNF Bridge crate. + +use apt_dnf_bridge::PackageDatabase; + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("APT-DNF Bridge - Package Query Example"); + println!("==================================="); + + let mut db = PackageDatabase::new_with_logging(); + + // Search for packages + println!("Searching for packages matching 'vim'..."); + let packages = db.find_packages("vim").await?; + println!("Found {} packages:", packages.len()); + + for (i, pkg) in packages.iter().take(5).enumerate() { + println!(" {}: {}", i + 1, pkg.name); + } + + if packages.len() > 5 { + println!(" ... and {} more", packages.len() - 5); + } + + // Get detailed information about a specific package + println!("\nGetting detailed info for 'vim'..."); + if let Some(pkg_info) = db.get_package_info("vim").await? { + println!("Package: {}", pkg_info.name); + println!("Version: {}", pkg_info.version); + println!("Architecture: {}", pkg_info.architecture); + if let Some(desc) = &pkg_info.description { + println!("Description: {}", desc); + } + if let Some(size) = pkg_info.size { + println!("Size: {} bytes", size); + } + } else { + println!("Package 'vim' not found"); + } + + // Check if a package is installed + println!("\nChecking if 'vim' is installed..."); + let is_installed = db.is_installed("vim").await?; + println!("vim is installed: {}", is_installed); + + // List some installed packages + println!("\nListing some installed packages..."); + let installed = db.get_installed_packages().await?; + println!("Found {} installed packages", installed.len()); + + for (i, pkg) in installed.iter().take(10).enumerate() { + println!(" {}: {} {}", i + 1, pkg.name, pkg.version); + } + + if installed.len() > 10 { + println!(" ... and {} more", installed.len() - 10); + } + + Ok(()) +}