hostcraft-core 1.1.2

Core library for the hostcraft ecosystem — parsing, manipulation and file I/O for system hosts
Documentation

hostcraft-core

Crates.io License: MIT Rust

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 Result types; what you do with them is up to you.
  • Partial name matchingremove and toggle operate on substrings, so "myapp" matches myapp.local, myapp.dev, and so on.
  • Exact matching for editsedit requires the full hostname to prevent accidental bulk modifications.
  • 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 = "1.1.2"

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/hosts requires elevated privileges (sudo on macOS/Linux). Your application is responsible for requesting or documenting this requirement. See the platform module 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

Matches by substring — all entries whose hostname contains partial_name are removed. Returns Err(HostCraftError::EntryNotFound) if nothing matched.

use hostcraft_core::HostCraftError;
use hostcraft_core::host::remove_entry;

match remove_entry(&mut entries, "myapp") {
    Ok(())                                  => println!("Removed"),
    Err(HostCraftError::EntryNotFound)      => println!("No match found"),
    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

All entries matching the partial name are toggled in one call.

use hostcraft_core::HostCraftError;
use hostcraft_core::host::toggle_entry;

match toggle_entry(&mut entries, "myapp") {
    Ok(())                                  => println!("Toggled"),
    Err(HostCraftError::EntryNotFound)      => println!("No match found"),
    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 ActiveInactive 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_entry / toggle_entry / edit_entry: 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, toggle_entry, 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 all entries matching the partial name
toggle_entry (&mut Vec<HostEntry>, &str) -> Result<()> Toggles all entries matching the partial name

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:

  1. Read with file::read_file
  2. Parse with host::parse_contentsVec<HostEntry>
  3. Mutate using add_entry / edit_entry / remove_entry / toggle_entry
  4. Present however your layer requires — the types all implement Display, Debug, and Serialize
  5. Write back with platform::write_hosts_to (permission-aware) or file::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.