ETPM (crate name: etpm) is a small, embeddable package manager library for Rust focused on securely downloading, verifying, and installing self-contained package archives from HTTP repositories. It is intentionally minimal – it does not perform dependency resolution or system package management – instead, it provides a simple, auditable workflow for delivering application/artifact overlays and package metadata.
- Minimal, single-responsibility design.
- Packages are standard
tar.gzarchives with a strict layout (package/overlay+package/addition). Files inoverlayare extracted into the configuredroot; files inadditionare stored under the package directory and tracked. - Cryptographic verification with Ed25519 signatures. Packages must include a Base64-encoded
.sigfile and are verified against registered trusted public keys. - Safe unpacking: built-in Path Traversal (Tar Slip) protection.
- Pre-install inspection: ability to read arbitrary files from the
additiondirectory before unpacking (e.g., for metadata validation). - Fully asynchronous API powered by
tokioand typed error handling viathiserror. - C FFI (
cdylib) for embedding in other languages – header available asetpm.h. - Uses
tracingmacros for structured logging (the library does not install a global subscriber; initialize logging in your application to see logs).
- Structured archives:
overlayandadditiondirectories inside each package. - Signature verification using Ed25519 (multiple trusted keys supported).
- Atomic writes for downloaded files (temporary file renamed on successful verification).
- Filelist generation to support safe uninstall.
- C API for embedding in non-Rust environments.
From crates.io (recommended once published):
[dependencies]
etpm = "0.1.1"
tokio = { version = "1", features = ["full"] }Or use the current repository directly:
[dependencies]
etpm = { git = "https://codeberg.org/vstor08/etpm.git" }If you want to see library logs, add tracing-subscriber to your application and initialize it (see Logging section).
The library emits structured logs using the tracing crate. The library itself does NOT initialize a global subscriber – this should be done by the consuming application so it can control formatting and filtering.
Example minimal initialization in your binary:
use tracing_subscriber;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize a simple subscriber which respects RUST_LOG
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();
// ... start tokio runtime or call async main
Ok(())
}Then control log level with RUST_LOG, e.g., RUST_LOG=info cargo run or RUST_LOG=debug cargo run.
ETPM expects .tp files to be standard tar.gz archives whose top-level directory is package/ and which contain two subtrees:
package/
├── overlay/ # Files to extract into the configured root directory
└── addition/ # Files to place under packages/<name>-<version>/ (metadata, filelists, custom configs, etc.)
The package/ prefix is stripped during extraction. The addition/ directory can contain files in any format (JSON, TOML, RON, binary, etc.), allowing the host application to define its own metadata schema.
A repository must expose an index.ron file at its root describing available packages and versions. Each url field is relative to the repository root, and the corresponding signature file must be available at {url}.sig.
Example repository layout:
repository/
├── index.ron
├── my-app-1.0.0.tp
├── my-app-1.0.0.tp.sig
├── my-app-1.1.0.tp
└── my-app-1.1.0.tp.sig
Example index.ron structure:
(
packages: {
"my-app": [
(version: "1.0.0", url: "my-app-1.0.0.tp"),
(version: "1.1.0", url: "my-app-1.1.0.tp")
]
}
)This example demonstrates initialization, adding a repository, fetching, inspecting metadata before installation, installing, and uninstalling a package.
use etpm::PackageManager;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize logging in the application if you want to see library logs
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();
let mut manager = PackageManager::new();
manager.set_root("./packages")?;
manager.set_packages("./packages")?;
manager.add_repository("https://repo.example.com/")?;
// Register the Base64-encoded Ed25519 public key used for signing packages
manager.add_trusted_key("REPLACE_WITH_BASE64_PUBLIC_KEY==")?;
// 1. Fetch the package (downloads, verifies signature, returns local path)
let package_path = manager.fetch_package("my-package", "1.0.0", "./downloads").await?;
// 2. Read metadata from the addition directory BEFORE installation
// This allows the host application to validate compatibility (e.g., game revision)
let metadata_bytes = manager.read_addition_file(&package_path, "metadata.json")?;
let metadata_str = String::from_utf8(metadata_bytes)?;
// (In a real application, you would parse this with serde_json, toml, or ron)
if metadata_str.contains("\"compatible\": true") {
println!("Metadata check passed. Proceeding with installation...");
// 3. Install (unpacks overlay to root and addition to package dir)
manager.install_package(&package_path, "my-package", "1.0.0").await?;
} else {
println!("Incompatible package. Aborting installation.");
std::fs::remove_file(&package_path)?; // Clean up
}
// 4. Uninstall later if needed
// manager.uninstall_package("my-package", "1.0.0").await?;
Ok(())
}PackageManager::new()– create a new manager instance.set_root(path)– set the installation root foroverlayfiles.set_packages(path)– set the directory to store package metadata and filelists.add_repository(url)– add a repository URL (trailing slash will be normalized).add_trusted_key(base64_pubkey)– register a Base64-encoded Ed25519 public key for signature verification.set_allow_unsigned(bool)– allow downloading unsigned packages (use with caution).fetch_package(name, version, dest)– download and verify a package, returning the local path to the.tpfile.read_addition_file(package_path, file_path)– read raw bytes of a specific file from theadditiondirectory of a downloaded.tparchive without extracting the whole archive.install_package(path, name, version)– unpack and install the downloaded package.uninstall_package(name, version)– remove files recorded in the packagefilelistand delete the package directory.check_update(name, current_version)– asynchronously checks configured repositories for newer versions; returnstrueif an update exists.get_latest_version(name)– returnsOption<String>with the latest version found across repositories.
The C header etpm.h (in the repository root) describes the exported functions and status codes. Sample usage in C:
#include <stdio.h>
#include <stdlib.h>
#include "etpm.h"
int main() {
EtpmManager* manager = etpm_manager_new();
etpm_set_root(manager, "/opt/my-application");
etpm_set_packages(manager, "/var/lib/etpm");
etpm_add_repository(manager, "https://repo.example.com/");
// Register trusted public key
etpm_add_trusted_key(manager, "REPLACE_WITH_BASE64_PUBLIC_KEY==");
char* pkg_path = NULL;
EtpmStatus status = etpm_fetch_package(manager, "my-app", "1.0.0", "/tmp", &pkg_path);
if (status == ETPM_OK) {
printf("Downloaded & verified: %s\n", pkg_path);
// Optional: Read a metadata file before installation
uint8_t* metadata = NULL;
size_t metadata_len = 0;
EtpmStatus meta_status = etpm_read_addition_file(manager, pkg_path, "metadata.json", &metadata, &metadata_len);
if (meta_status == ETPM_OK) {
printf("Read %zu bytes of metadata.\n", metadata_len);
// Process metadata here...
etpm_free_buffer(metadata, metadata_len); // Free the buffer
}
etpm_install_package(manager, pkg_path, "my-app", "1.0.0");
etpm_free_string(pkg_path);
etpm_uninstall_package(manager, "my-app", "1.0.0");
} else if (status == ETPM_ERR_INVALID_SIGNATURE) {
printf("Security error: package signature is invalid or missing!\n");
} else {
char* err = etpm_get_last_error(manager);
printf("Error: %s\n", err);
etpm_free_string(err);
}
etpm_manager_free(manager);
return 0;
}Build the Rust library and the C dynamic library (cdylib):
cargo build --releaseArtifacts will be available in target/release/ (e.g., libetpm.so, libetpm.dylib, etpm.dll).
- Keep private signing keys secret. Only distribute public keys to clients that should trust packages.
set_allow_unsigned(true)disables signature enforcement – use only in trusted environments.- The
read_addition_filemethod includes built-in path traversal protection. It will reject absolute paths or paths containing..to prevent reading files outside theadditiondirectory.
ETPM is licensed under the MIT License. See the LICENSE file for details.