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.
This commit is contained in:
robojerk 2025-09-13 20:45:18 -07:00
commit 06cafa0366
24 changed files with 2790 additions and 0 deletions

18
.gitignore vendored Normal file
View file

@ -0,0 +1,18 @@
# Rust
/target/
**/*.rs.bk
Cargo.lock
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Temporary files
*.tmp
*.temp

23
Cargo.toml Normal file
View file

@ -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"

180
README.md Normal file
View file

@ -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<dyn std::error::Error>> {
// 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<dyn std::error::Error>> {
// 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.

View file

@ -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 = []

View file

@ -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<Package>,
/// Packages that would be removed.
pub to_remove: Vec<Package>,
/// Packages that would be upgraded.
pub to_upgrade: Vec<Package>,
/// Dependencies that would be installed.
pub dependencies: Vec<Package>,
/// Any conflicts or issues found.
pub conflicts: Vec<String>,
/// 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<String>,
/// 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<usize>,
}
/// 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<bool>;
/// Resolve a transaction to check if it can be executed.
async fn resolve(&self, operations: &[Operation]) -> Result<Resolution>;
/// 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<Vec<Package>>;
/// Get detailed information about a specific package.
async fn get_package_info(&mut self, name: &str, options: &QueryOptions) -> Result<Option<Package>>;
/// Check if a package is installed.
async fn is_package_installed(&self, name: &str) -> Result<bool>;
/// Get all installed packages.
async fn get_installed_packages(&self, options: &QueryOptions) -> Result<Vec<Package>>;
/// 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<Vec<RepositoryInfo>>;
/// Update package cache.
async fn update_cache(&self) -> Result<()>;
/// Get backend-specific statistics.
async fn get_stats(&self) -> Result<BackendStats>;
}
/// 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<u64>,
}
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<Box<dyn AptBackend>> {
Ok(Box::new(ShellBackend::new(config)))
}
/// Create a libapt backend (if available).
#[cfg(feature = "libapt-backend")]
pub fn create_libapt_backend(config: BackendConfig) -> Result<Box<dyn AptBackend>> {
Ok(Box::new(LibAptBackend::new(config)))
}
/// Create a mock backend for testing.
pub fn create_mock_backend(config: BackendConfig) -> Result<Box<dyn AptBackend>> {
Ok(Box::new(MockBackend::new(config)))
}
/// Auto-detect the best available backend.
pub async fn auto_detect(config: BackendConfig) -> Result<Box<dyn AptBackend>> {
// 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;

View file

@ -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<bool> {
// 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<Resolution> {
// 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<Vec<Package>> {
// 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<Option<Package>> {
// 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<bool> {
// 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<Vec<Package>> {
// 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<Vec<RepositoryInfo>> {
// 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<BackendStats> {
Ok(self.stats.clone())
}
}

View file

@ -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<String, Package>,
repositories: HashMap<String, RepositoryInfo>,
}
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<bool> {
Ok(true)
}
async fn resolve(&self, operations: &[Operation]) -> Result<Resolution> {
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<Vec<Package>> {
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<Option<Package>> {
self.stats.packages_queried += 1;
Ok(self.packages.get(name).cloned())
}
async fn is_package_installed(&self, name: &str) -> Result<bool> {
Ok(self.packages.contains_key(name))
}
async fn get_installed_packages(&self, _options: &QueryOptions) -> Result<Vec<Package>> {
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<Vec<RepositoryInfo>> {
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<BackendStats> {
Ok(self.stats.clone())
}
}

View file

@ -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<String, (Vec<Package>, Instant)>,
info_cache: HashMap<String, (Option<Package>, 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<F, T>(&mut self, f: F) -> Result<T>
where
F: FnOnce() -> Result<T>,
{
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<bool> {
// 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<Resolution> {
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<Vec<Package>> {
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<Option<Package>> {
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<bool> {
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<Vec<Package>> {
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<Vec<RepositoryInfo>> {
// 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<BackendStats> {
Ok(self.stats.clone())
}
}
impl ShellBackend {
/// Parse package information from apt-cache show output.
fn parse_package_info(&self, output: &str) -> Result<Option<Package>> {
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::<u64>() {
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
)))
}
}
}
}

View file

@ -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;

View file

@ -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<dyn AptBackend>,
/// Query options.
query_options: QueryOptions,
}
impl PackageDatabaseV2 {
/// Create a new package database with auto-detected backend.
pub async fn new() -> Result<Self> {
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<dyn AptBackend>) -> Result<Self> {
Ok(Self {
backend,
query_options: QueryOptions::default(),
})
}
/// Create a new package database with shell backend.
pub async fn with_shell_backend(config: BackendConfig) -> Result<Self> {
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<Self> {
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<usize>) {
self.query_options.max_results = max;
}
/// Find packages by name pattern.
pub async fn find_packages(&mut self, pattern: &str) -> Result<Vec<Package>> {
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<Option<Package>> {
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<bool> {
crate::command::validate_package_name(name)?;
self.backend.is_package_installed(name).await
}
/// Get installed packages.
pub async fn get_installed_packages(&self) -> Result<Vec<Package>> {
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<crate::backend::BackendStats> {
self.backend.get_stats().await
}
}

View file

@ -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<dyn AptBackend>,
/// List of operations in this transaction.
operations: Vec<Operation>,
/// Packages that were installed before this transaction (for rollback).
previously_installed: HashSet<String>,
/// Whether to enable logging.
log_commands: bool,
}
impl TransactionV2 {
/// Create a new transaction with auto-detected backend.
pub async fn new() -> Result<Self> {
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<dyn AptBackend>) -> Result<Self> {
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<Self> {
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<Self> {
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<Resolution> {
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<crate::backend::BackendStats> {
self.backend.get_stats().await
}
}

View file

@ -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

View file

@ -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<String>,
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<std::process::Output> {
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(())
}

View file

@ -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<T> = std::result::Result<T, AptError>;
/// 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),
}

View file

@ -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};

View file

@ -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<String>,
/// Package size in bytes.
pub size: Option<u64>,
}
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<String>,
size: Option<u64>,
) -> 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<String, Vec<Package>>,
/// Cache for package info results.
info_cache: HashMap<String, Option<Package>>,
/// 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<Vec<Package>> {
// 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<Option<Package>> {
// 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<Option<Package>> {
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::<u64>() {
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<bool> {
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<Vec<Package>> {
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)
}
}

View file

@ -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<String>,
/// 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<Self> {
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<P: AsRef<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<Vec<Repository>> {
// This is a simplified implementation
// In a real implementation, you'd parse sources.list
Ok(Vec::new())
}
}

View file

@ -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<Operation>,
/// Packages that were installed before this transaction (for rollback).
previously_installed: HashSet<String>,
/// 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();
}
}

37
apt-dnf-bridge/Cargo.toml Normal file
View file

@ -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"

47
apt-dnf-bridge/src/lib.rs Normal file
View file

@ -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<dyn std::error::Error>> {
//! // 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::*;

View file

@ -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<dyn std::error::Error>> {
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(())
}

View file

@ -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<dyn std::error::Error>> {
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(())
}

54
examples/basic_usage.rs Normal file
View file

@ -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<dyn std::error::Error>> {
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(())
}

60
examples/package_query.rs Normal file
View file

@ -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<dyn std::error::Error>> {
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(())
}