14 releases (5 stable)
Uses new Rust 2024
| 2.0.0 | Apr 22, 2026 |
|---|---|
| 1.1.2 | Apr 10, 2026 |
| 0.1.8 | Mar 31, 2026 |
#1883 in Network programming
Used in hostcraft-cli
41KB
616 lines
hostcraft-core
The shared foundation powering the hostcraft ecosystem — a suite of tools for managing your system hosts file (/etc/hosts on macOS/Linux, C:\Windows\System32\drivers\etc\hosts on Windows). This crate contains all parsing, data modelling, and file I/O logic, deliberately free of any presentation or platform concerns so it can be consumed equally by a CLI, a desktop GUI, or any other Rust application.
Ecosystem
| Component | Description | Status | Link |
|---|---|---|---|
hostcraft-core |
Shared library (this crate) | ✅ Published | crates.io |
hostcraft-cli |
Terminal interface | ✅ Published | crates.io and Releases |
hostcraft-gui |
Desktop GUI (Tauri) | 🟡 Beta | Releases |
Every consumer in the ecosystem depends on this crate for its data types and business logic. Presentation — colours, layout, widgets — is always the consumer's responsibility.
Design Philosophy
- No I/O surprises — the library never prints to stdout or stderr. It returns data and
Resulttypes; what you do with them is up to you. - Exact matching by default —
remove,toggle, andeditrequire the full hostname to prevent accidental bulk modifications. - Explicit partial matching — bulk substring operations are available through opt-in helpers:
remove_entries_matchingandtoggle_entries_matching. - Active / Inactive model — entries are never silently deleted to disable them. Inactive entries are preserved as commented-out lines (
# ip hostname) so they can be re-enabled at any time. - Duplicate safety — adding or editing an entry to a combination that already exists is rejected before any write occurs.
- IPv4 and IPv6 — all IP handling goes through Rust's standard
IpAddr, so both address families work out of the box.
Installation
[dependencies]
hostcraft-core = "2.0.0"
Quick Start
use hostcraft_core::HostCraftError;
use hostcraft_core::file::{read_file, write_file};
use hostcraft_core::host::{add_entry, parse_contents, toggle_entry};
use std::net::IpAddr;
// 1. Read and parse
let lines = read_file("/etc/hosts").expect("Failed to read hosts file");
let mut entries = parse_contents(lines);
// 2. Manipulate
let ip: IpAddr = "127.0.0.1".parse().unwrap();
add_entry(&mut entries, ip, "myapp.local").expect("Failed to add entry");
// 3. Write back
write_file("/etc/hosts", &entries).expect("Failed to write hosts file");
Note: Writing to
/etc/hostsrequires elevated privileges (sudoon macOS/Linux). Your application is responsible for requesting or documenting this requirement. See theplatformmodule for permission-aware write helpers.
Usage
Reading and parsing
read_file returns a lazy line iterator — nothing is loaded into memory until parse_contents consumes it.
use hostcraft_core::file::read_file;
use hostcraft_core::host::parse_contents;
let lines = read_file("/etc/hosts").expect("Failed to read hosts file");
let entries = parse_contents(lines);
// HostEntry implements Display: "127.0.0.1: example.com is Active"
for entry in &entries {
println!("{}", entry);
}
Lines that do not match a valid host pattern (pure comments, blank lines, malformed entries) are silently skipped during parsing. This mirrors how the OS itself handles the file.
Adding an entry
New entries are always added as Active. Adding a duplicate (same hostname and same IP) returns Err(HostCraftError::DuplicateEntry) without modifying the list.
use hostcraft_core::HostCraftError;
use hostcraft_core::host::add_entry;
use std::net::IpAddr;
let ip: IpAddr = "192.168.1.10".parse().unwrap();
match add_entry(&mut entries, ip, "myapp.local") {
Ok(()) => println!("Entry added"),
Err(HostCraftError::DuplicateEntry) => println!("Already exists"),
Err(e) => println!("Error: {}", e),
}
Editing an entry
Updates an existing entry's IP address, hostname, or both in a single call. Requires an exact hostname match — partial names are not accepted.
The entry's HostStatus is preserved unchanged.
use hostcraft_core::HostCraftError;
use hostcraft_core::host::edit_entry;
use std::net::IpAddr;
let new_ip: IpAddr = "192.168.1.50".parse().unwrap();
match edit_entry(&mut entries, "myapp.local", new_ip, "myapp.dev") {
Ok(()) => println!("Entry updated"),
Err(HostCraftError::EntryNotFound) => println!("No entry with that exact name"),
Err(HostCraftError::DuplicateEntry) => println!("Another entry already has that IP + name"),
Err(HostCraftError::NoChange) => println!("New values are identical to current ones"),
Err(e) => println!("Error: {}", e),
}
Removing an entry
Requires an exact hostname match. Returns Err(HostCraftError::EntryNotFound) if no exact name matched.
use hostcraft_core::HostCraftError;
use hostcraft_core::host::remove_entry;
match remove_entry(&mut entries, "myapp.local") {
Ok(()) => println!("Removed"),
Err(HostCraftError::EntryNotFound) => println!("No entry with that exact name"),
Err(e) => println!("Error: {}", e),
}
Removing entries by partial match (opt-in)
Use remove_entries_matching to remove all hostnames containing a substring. Returns the number of removed entries.
use hostcraft_core::HostCraftError;
use hostcraft_core::host::remove_entries_matching;
match remove_entries_matching(&mut entries, "myapp") {
Ok(count) => println!("Removed {} entries", count),
Err(HostCraftError::EntryNotFound) => println!("No entries contain that pattern"),
Err(e) => println!("Error: {}", e),
}
Toggling an entry
Toggling flips an entry between active and inactive without removing it:
127.0.0.1 myapp.local → # 127.0.0.1 myapp.local
# 127.0.0.1 myapp.local → 127.0.0.1 myapp.local
Requires an exact hostname match.
use hostcraft_core::HostCraftError;
use hostcraft_core::host::toggle_entry;
match toggle_entry(&mut entries, "myapp.local") {
Ok(()) => println!("Toggled"),
Err(HostCraftError::EntryNotFound) => println!("No entry with that exact name"),
Err(e) => println!("Error: {}", e),
}
Toggling entries by partial match (opt-in)
Use toggle_entries_matching to toggle all hostnames containing a substring. Returns the number of toggled entries.
use hostcraft_core::HostCraftError;
use hostcraft_core::host::toggle_entries_matching;
match toggle_entries_matching(&mut entries, "myapp") {
Ok(count) => println!("Toggled {} entries", count),
Err(HostCraftError::EntryNotFound) => println!("No entries contain that pattern"),
Err(e) => println!("Error: {}", e),
}
Writing back
write_file truncates and rewrites the file. Active entries are written as plain lines; inactive entries are written as commented-out lines.
use hostcraft_core::file::write_file;
write_file("/etc/hosts", &entries).expect("Failed to write hosts file");
Platform module
The platform module provides OS-aware path resolution and permission-aware write helpers. Rather than handling raw io::Error from write_file, these functions return HostCraftError::PermissionDenied with a clear, platform-specific message when the process lacks the necessary privileges.
use hostcraft_core::platform;
// Resolve the hosts file path for the current OS at runtime
let path = platform::get_hosts_path().expect("Unsupported platform");
println!("Hosts file: {}", path.display());
// Write to an explicit path with permission-aware error messages
platform::write_hosts_to(&path, &entries)?;
// Or use the convenience wrapper — resolves the path and writes in one call
platform::write_hosts(&entries)?;
API Reference
HostEntry
The core data type. Represents one parsed line from the hosts file.
pub struct HostEntry {
pub status: HostStatus,
pub ip: IpAddr,
pub name: String,
}
| Field | Type | Description |
|---|---|---|
status |
HostStatus |
Whether the entry is active or inactive |
ip |
IpAddr |
The IP address — IPv4 or IPv6 |
name |
String |
The hostname |
Methods
| Method | Description |
|---|---|
toggle() |
Flips Active ↔ Inactive in place |
Trait implementations: Debug, Clone, PartialEq, Serialize, Display ("ip: name is Status")
HostStatus
pub enum HostStatus {
Active, // Written as: 127.0.0.1 hostname
Inactive, // Written as: # 127.0.0.1 hostname
}
Trait implementations: Debug, Clone, PartialEq, Serialize, Display ("Active" / "Inactive")
HostCraftError
pub enum HostCraftError {
Io(std::io::Error), // Underlying I/O failure from read_file / write_file
PermissionDenied(String), // Write rejected — process lacks privileges
UnsupportedPlatform(String), // get_hosts_path called on an unrecognised OS
DuplicateEntry, // add_entry / edit_entry: same IP + hostname already exists
EntryNotFound, // remove/toggle/edit operations: no name matched
NoChange, // edit_entry: supplied values are identical to current ones
}
| Variant | Raised by | Display message |
|---|---|---|
Io(e) |
read_file, write_file |
"IO error: {e}" |
PermissionDenied(m) |
platform::write_hosts_to |
"{m}" (platform-specific hint included) |
UnsupportedPlatform |
platform::get_hosts_path |
"Unsupported platform: {os}" |
DuplicateEntry |
add_entry, edit_entry |
"You have inserted a duplicate entry." |
EntryNotFound |
remove_entry, remove_entries_matching, toggle_entry, toggle_entries_matching, edit_entry |
"Please check the name and try again." |
NoChange |
edit_entry |
"Entry already exists. No changes made." |
Trait implementations: Debug, Display, std::error::Error
Result<T> type alias
pub type Result<T> = std::result::Result<T, HostCraftError>;
Re-exported at the crate root as hostcraft_core::Result<T>. All fallible functions in this crate return this type.
host module — functions
| Function | Signature | Description |
|---|---|---|
parse_contents |
(impl Iterator<Item = io::Result<String>>) -> Vec<HostEntry> |
Parses a line iterator into a list of entries |
add_entry |
(&mut Vec<HostEntry>, IpAddr, impl Into<String>) -> Result<()> |
Adds an active entry; rejects duplicates |
edit_entry |
(&mut Vec<HostEntry>, impl Into<String>, IpAddr, impl Into<String>) -> Result<()> |
Edits an entry by exact name; preserves status |
remove_entry |
(&mut Vec<HostEntry>, &str) -> Result<()> |
Removes entries matching the exact name |
remove_entries_matching |
(&mut Vec<HostEntry>, &str) -> Result<usize> |
Removes all entries containing the pattern; returns count |
toggle_entry |
(&mut Vec<HostEntry>, &str) -> Result<()> |
Toggles entries matching the exact name |
toggle_entries_matching |
(&mut Vec<HostEntry>, &str) -> Result<usize> |
Toggles all entries containing the pattern; returns count |
file module — functions
| Function | Signature | Description |
|---|---|---|
read_file |
(impl AsRef<Path>) -> io::Result<Lines<BufReader<File>>> |
Opens the hosts file and returns a lazy line iterator |
write_file |
(impl AsRef<Path>, &[HostEntry]) -> io::Result<()> |
Serialises entries and writes them to the file |
platform module — functions
| Function | Signature | Description |
|---|---|---|
get_hosts_path |
() -> Result<PathBuf> |
Returns the OS-appropriate hosts file path |
write_hosts_to |
(&Path, &[HostEntry]) -> Result<()> |
Writes entries to a path; wraps permission errors with a clear hint |
write_hosts |
(&[HostEntry]) -> Result<()> |
Convenience wrapper: resolves the platform path and writes |
Building for Consumers
If you are building a consumer on top of hostcraft-core, the typical pattern is:
- Read with
file::read_file - Parse with
host::parse_contents→Vec<HostEntry> - Mutate using
add_entry/edit_entry/remove_entry/toggle_entry(or opt-in partial helpers when you explicitly want bulk matching) - Present however your layer requires — the types all implement
Display,Debug, andSerialize - Write back with
platform::write_hosts_to(permission-aware) orfile::write_file(raw)
The library holds no global state and makes no assumptions about the platform, runtime, or output format.
License
MIT — see LICENSE for details.
Dependencies
~2–3.5MB
~68K SLoC