# hostcraft-core
[](https://crates.io/crates/hostcraft-core)
[](LICENSE)
[](https://www.rust-lang.org)
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
| `hostcraft-core` | Shared library (this crate) | ✅ Published | [crates.io](https://crates.io/crates/hostcraft-core) |
| `hostcraft-cli` | Terminal interface | ✅ Published | [crates.io](https://crates.io/crates/hostcraft-cli) and [Releases](https://github.com/Zaberahmed/hostcraft/releases/tag/cli-v2.1.1) |
| `hostcraft-gui` | Desktop GUI (Tauri) | 🟡 Beta | [Releases](https://github.com/Zaberahmed/hostcraft/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 matching** — `remove` and `toggle` operate on substrings, so `"myapp"` matches `myapp.local`, `myapp.dev`, and so on.
- **Exact matching for edits** — `edit` 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
```toml
[dependencies]
hostcraft-core = "1.1.2"
```
---
## Quick Start
```rust
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](#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.
```rust
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.
```rust
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.
```rust
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.
```rust
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.
```rust
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.
```rust
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.
```rust
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.
```rust
pub struct HostEntry {
pub status: HostStatus,
pub ip: IpAddr,
pub name: String,
}
```
| `status` | `HostStatus` | Whether the entry is active or inactive |
| `ip` | `IpAddr` | The IP address — IPv4 or IPv6 |
| `name` | `String` | The hostname |
**Methods**
| `toggle()` | Flips `Active` ↔ `Inactive` in place |
**Trait implementations:** `Debug`, `Clone`, `PartialEq`, `Serialize`, `Display` (`"ip: name is Status"`)
---
### `HostStatus`
```rust
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`
```rust
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
}
```
| `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
```rust
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
| `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
| `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
| `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_contents` → `Vec<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](LICENSE) for details.