# digit Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build a Rust CLI finger client implementing RFC 1288 and RFC 742 with clear code, documentation, and thorough tests.
**Architecture:** Library + binary split. `src/query.rs` handles input parsing into a `Query` struct. `src/protocol.rs` builds the wire-format string and performs TCP I/O. `src/main.rs` is a thin CLI entry point using clap. Error handling uses `thiserror`.
**Tech Stack:** Rust 2021 edition, clap (derive), thiserror, std::net for TCP.
---
### Task 1: Project Scaffolding
**Files:**
- Create: `Cargo.toml`
- Create: `LICENSE`
- Create: `src/main.rs` (placeholder)
- Create: `src/lib.rs` (placeholder)
- [ ] **Step 1: Initialize the Cargo project**
Run: `cargo init --name digit`
This creates `Cargo.toml` and `src/main.rs`.
- [ ] **Step 2: Set up Cargo.toml with dependencies and metadata**
Edit `Cargo.toml` to contain:
```toml
[package]
name = "digit"
version = "0.1.0"
edition = "2021"
description = "A finger protocol client (RFC 1288 / RFC 742)"
license = "MIT"
[dependencies]
clap = { version = "4", features = ["derive"] }
thiserror = "2"
```
- [ ] **Step 3: Create the MIT LICENSE file**
Create `LICENSE` with the MIT license text, copyright 2026.
- [ ] **Step 4: Create src/lib.rs with module declarations**
Create `src/lib.rs`:
```rust
pub mod protocol;
pub mod query;
```
- [ ] **Step 5: Create placeholder module files**
Create `src/query.rs`:
```rust
// Finger query parsing.
```
Create `src/protocol.rs`:
```rust
// Finger protocol wire format and TCP communication.
```
- [ ] **Step 6: Replace src/main.rs placeholder**
Replace `src/main.rs` with:
```rust
fn main() {
println!("digit - finger client");
}
```
- [ ] **Step 7: Verify the project compiles**
Run: `cargo build`
Expected: Compiles successfully with no errors.
- [ ] **Step 8: Commit**
```bash
git add Cargo.toml Cargo.lock LICENSE src/
git commit -m "feat: scaffold project with dependencies and module structure"
```
---
### Task 2: Query Parsing -- Struct and Tests
**Files:**
- Create: `src/query.rs`
- Test: `src/query.rs` (inline `#[cfg(test)]` module)
- [ ] **Step 1: Write failing tests for Query parsing**
Write the full contents of `src/query.rs`:
```rust
/// A parsed finger query.
///
/// Represents the structured result of parsing a finger query string
/// like `"user@host"`, `"@host"`, or `"user@host1@host2"`.
#[derive(Debug, Clone, PartialEq)]
pub struct Query {
/// The user to query. `None` means list all users.
pub user: Option<String>,
/// The host(s) to query. The last host is the connection target.
/// Multiple hosts indicate a forwarding chain (RFC 1288).
pub hosts: Vec<String>,
/// Whether to request verbose output (sends `/W` prefix).
pub long: bool,
/// TCP port to connect on. Default is 79.
pub port: u16,
}
/// The default finger protocol port.
pub const DEFAULT_PORT: u16 = 79;
impl Query {
/// Parse a query string into a `Query`.
///
/// # Examples
///
/// - `"user@host"` -> user=Some("user"), hosts=["host"]
/// - `"@host"` -> user=None, hosts=["host"]
/// - `"user@host1@host2"` -> user=Some("user"), hosts=["host1", "host2"]
/// - `"user"` -> user=Some("user"), hosts=["localhost"]
/// - `""` or `None` -> user=None, hosts=["localhost"]
pub fn parse(input: Option<&str>, long: bool, port: u16) -> Query {
todo!()
}
/// Returns the host to connect to (the last host in the chain).
pub fn target_host(&self) -> &str {
todo!()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_user_at_host() {
let q = Query::parse(Some("user@host"), false, 79);
assert_eq!(q.user, Some("user".to_string()));
assert_eq!(q.hosts, vec!["host".to_string()]);
assert!(!q.long);
assert_eq!(q.port, 79);
}
#[test]
fn parse_at_host_lists_users() {
let q = Query::parse(Some("@host"), false, 79);
assert_eq!(q.user, None);
assert_eq!(q.hosts, vec!["host".to_string()]);
}
#[test]
fn parse_user_only_defaults_to_localhost() {
let q = Query::parse(Some("user"), false, 79);
assert_eq!(q.user, Some("user".to_string()));
assert_eq!(q.hosts, vec!["localhost".to_string()]);
}
#[test]
fn parse_empty_string_defaults_to_localhost() {
let q = Query::parse(Some(""), false, 79);
assert_eq!(q.user, None);
assert_eq!(q.hosts, vec!["localhost".to_string()]);
}
#[test]
fn parse_none_defaults_to_localhost() {
let q = Query::parse(None, false, 79);
assert_eq!(q.user, None);
assert_eq!(q.hosts, vec!["localhost".to_string()]);
}
#[test]
fn parse_forwarding_chain() {
let q = Query::parse(Some("user@host1@host2"), false, 79);
assert_eq!(q.user, Some("user".to_string()));
assert_eq!(q.hosts, vec!["host1".to_string(), "host2".to_string()]);
}
#[test]
fn parse_forwarding_chain_no_user() {
let q = Query::parse(Some("@host1@host2"), false, 79);
assert_eq!(q.user, None);
assert_eq!(q.hosts, vec!["host1".to_string(), "host2".to_string()]);
}
#[test]
fn parse_long_flag_preserved() {
let q = Query::parse(Some("user@host"), true, 79);
assert!(q.long);
}
#[test]
fn parse_custom_port_preserved() {
let q = Query::parse(Some("user@host"), false, 7979);
assert_eq!(q.port, 7979);
}
#[test]
fn target_host_returns_last_host() {
let q = Query::parse(Some("user@host1@host2"), false, 79);
assert_eq!(q.target_host(), "host2");
}
#[test]
fn target_host_single_host() {
let q = Query::parse(Some("user@host"), false, 79);
assert_eq!(q.target_host(), "host");
}
#[test]
fn parse_three_host_chain() {
let q = Query::parse(Some("user@a@b@c"), false, 79);
assert_eq!(q.user, Some("user".to_string()));
assert_eq!(
q.hosts,
vec!["a".to_string(), "b".to_string(), "c".to_string()]
);
assert_eq!(q.target_host(), "c");
}
}
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `cargo test --lib query`
Expected: FAIL -- all tests fail with "not yet implemented" panics.
- [ ] **Step 3: Commit the failing tests**
```bash
git add src/query.rs
git commit -m "test: add query parsing tests (red)"
```
---
### Task 3: Query Parsing -- Implementation
**Files:**
- Modify: `src/query.rs`
- [ ] **Step 1: Implement Query::parse and Query::target_host**
Replace the two `todo!()` bodies in `src/query.rs`:
Replace `Query::parse`:
```rust
pub fn parse(input: Option<&str>, long: bool, port: u16) -> Query {
let input = input.unwrap_or("");
if input.is_empty() {
return Query {
user: None,
hosts: vec!["localhost".to_string()],
long,
port,
};
}
// Split on '@'. First part is the user (if non-empty), rest are hosts.
let parts: Vec<&str> = input.splitn(2, '@').collect();
if parts.len() == 1 {
// No '@' found -- user only, default to localhost.
return Query {
user: Some(parts[0].to_string()),
hosts: vec!["localhost".to_string()],
long,
port,
};
}
// Has at least one '@'.
let user = if parts[0].is_empty() {
None
} else {
Some(parts[0].to_string())
};
let hosts: Vec<String> = parts[1].split('@').map(|s| s.to_string()).collect();
Query {
user,
hosts,
long,
port,
}
}
```
Replace `Query::target_host`:
```rust
pub fn target_host(&self) -> &str {
self.hosts.last().expect("hosts must not be empty")
}
```
- [ ] **Step 2: Run tests to verify they pass**
Run: `cargo test --lib query`
Expected: All 13 tests pass.
- [ ] **Step 3: Commit**
```bash
git add src/query.rs
git commit -m "feat: implement query parsing (green)"
```
---
### Task 4: Protocol -- Wire Format and Tests
**Files:**
- Create: `src/protocol.rs`
- Test: `src/protocol.rs` (inline `#[cfg(test)]` module)
- [ ] **Step 1: Write the error type, wire format builder, and failing tests**
Write the full contents of `src/protocol.rs`:
```rust
use std::io;
use crate::query::Query;
/// Errors that can occur during a finger protocol exchange.
#[derive(Debug, thiserror::Error)]
pub enum FingerError {
/// Failed to resolve the hostname.
#[error("could not resolve host '{host}': {source}")]
DnsResolution {
host: String,
#[source]
source: io::Error,
},
/// Failed to connect to the remote host.
#[error("could not connect to {host}:{port}: {source}")]
ConnectionFailed {
host: String,
port: u16,
#[source]
source: io::Error,
},
/// Connection timed out.
#[error("connection to {host}:{port} timed out")]
Timeout { host: String, port: u16 },
/// Failed to send the query.
#[error("failed to send query: {source}")]
SendFailed {
#[source]
source: io::Error,
},
/// Failed to read the response.
#[error("failed to read response: {source}")]
ReadFailed {
#[source]
source: io::Error,
},
}
/// Build the wire-format query string to send over the TCP connection.
///
/// Per RFC 1288:
/// - Verbose queries prepend `/W ` (with trailing space).
/// - Forwarding appends `@host1@host2...` for all hosts except the last
/// (the last host is the connection target, not part of the query string).
/// - The query is terminated with `\r\n`.
pub fn build_query_string(query: &Query) -> String {
todo!()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::query::Query;
#[test]
fn query_string_user_at_host() {
let q = Query::parse(Some("user@host"), false, 79);
assert_eq!(build_query_string(&q), "user\r\n");
}
#[test]
fn query_string_list_users() {
let q = Query::parse(Some("@host"), false, 79);
assert_eq!(build_query_string(&q), "\r\n");
}
#[test]
fn query_string_verbose_user() {
let q = Query::parse(Some("user@host"), true, 79);
assert_eq!(build_query_string(&q), "/W user\r\n");
}
#[test]
fn query_string_verbose_list() {
let q = Query::parse(Some("@host"), true, 79);
assert_eq!(build_query_string(&q), "/W \r\n");
}
#[test]
fn query_string_forwarding() {
let q = Query::parse(Some("user@host1@host2"), false, 79);
assert_eq!(build_query_string(&q), "user@host1\r\n");
}
#[test]
fn query_string_forwarding_verbose() {
let q = Query::parse(Some("user@host1@host2"), true, 79);
assert_eq!(build_query_string(&q), "/W user@host1\r\n");
}
#[test]
fn query_string_forwarding_no_user() {
let q = Query::parse(Some("@host1@host2"), false, 79);
assert_eq!(build_query_string(&q), "@host1\r\n");
}
#[test]
fn query_string_three_host_chain() {
let q = Query::parse(Some("user@a@b@c"), false, 79);
assert_eq!(build_query_string(&q), "user@a@b\r\n");
}
#[test]
fn query_string_localhost_user() {
let q = Query::parse(Some("user"), false, 79);
assert_eq!(build_query_string(&q), "user\r\n");
}
#[test]
fn query_string_localhost_list() {
let q = Query::parse(None, false, 79);
assert_eq!(build_query_string(&q), "\r\n");
}
}
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `cargo test --lib protocol`
Expected: FAIL -- all tests fail with "not yet implemented" panics.
- [ ] **Step 3: Commit the failing tests**
```bash
git add src/protocol.rs
git commit -m "test: add wire format query string tests (red)"
```
---
### Task 5: Protocol -- Wire Format Implementation
**Files:**
- Modify: `src/protocol.rs`
- [ ] **Step 1: Implement build_query_string**
Replace the `todo!()` body in `build_query_string`:
```rust
pub fn build_query_string(query: &Query) -> String {
let mut result = String::new();
// Verbose prefix per RFC 1288.
if query.long {
result.push_str("/W ");
}
// User portion.
if let Some(ref user) = query.user {
result.push_str(user);
}
// Forwarding: include all hosts except the last (the connection target).
// These become @host1@host2... in the query string.
if query.hosts.len() > 1 {
for host in &query.hosts[..query.hosts.len() - 1] {
result.push('@');
result.push_str(host);
}
}
result.push_str("\r\n");
result
}
```
- [ ] **Step 2: Run tests to verify they pass**
Run: `cargo test --lib protocol`
Expected: All 10 tests pass.
- [ ] **Step 3: Commit**
```bash
git add src/protocol.rs
git commit -m "feat: implement wire format query string builder (green)"
```
---
### Task 6: Protocol -- TCP Communication
**Files:**
- Modify: `src/protocol.rs`
- [ ] **Step 1: Implement the finger function**
Add the following imports to the top of `src/protocol.rs` (alongside existing imports):
```rust
use std::io::{Read, Write};
use std::net::TcpStream;
use std::time::Duration;
```
Add the `finger` function after `build_query_string`:
```rust
/// Execute a finger query over TCP.
///
/// Connects to the target host, sends the query string, reads the full
/// response until the server closes the connection, and returns the
/// response as a string. Invalid UTF-8 bytes are replaced with U+FFFD.
pub fn finger(query: &Query, timeout: Duration) -> Result<String, FingerError> {
let host = query.target_host();
let addr = format!("{}:{}", host, query.port);
// Resolve and connect with timeout.
let mut stream = TcpStream::connect_timeout(
&addr.parse().map_err(|_| {
// If the address doesn't parse as a SocketAddr, try DNS resolution.
// This path handles hostnames (not raw IPs).
return FingerError::DnsResolution {
host: host.to_string(),
source: io::Error::new(io::ErrorKind::InvalidInput, "invalid address"),
};
})?,
timeout,
)
.map_err(|e| {
if e.kind() == io::ErrorKind::TimedOut {
FingerError::Timeout {
host: host.to_string(),
port: query.port,
}
} else {
FingerError::ConnectionFailed {
host: host.to_string(),
port: query.port,
source: e,
}
}
})?;
// Set read/write timeouts on the connected socket.
stream.set_read_timeout(Some(timeout)).ok();
stream.set_write_timeout(Some(timeout)).ok();
// Send the query.
let query_string = build_query_string(query);
stream
.write_all(query_string.as_bytes())
.map_err(|e| FingerError::SendFailed { source: e })?;
// Read the full response.
let mut buf = Vec::new();
stream
.read_to_end(&mut buf)
.map_err(|e| FingerError::ReadFailed { source: e })?;
Ok(String::from_utf8_lossy(&buf).into_owned())
}
```
- [ ] **Step 2: Verify it compiles**
Run: `cargo build`
Expected: Compiles successfully.
- [ ] **Step 3: Commit**
```bash
git add src/protocol.rs
git commit -m "feat: implement TCP finger protocol exchange"
```
---
### Task 7: Protocol -- DNS Resolution Fix
The `finger` function in Task 6 uses `TcpStream::connect_timeout` which requires a `SocketAddr`. This doesn't support hostnames directly. We need to resolve hostnames first using `std::net::ToSocketAddrs`.
**Files:**
- Modify: `src/protocol.rs`
- [ ] **Step 1: Rewrite the connection logic to support hostnames**
Replace the `finger` function body with:
```rust
pub fn finger(query: &Query, timeout: Duration) -> Result<String, FingerError> {
let host = query.target_host();
let addr_str = format!("{}:{}", host, query.port);
// Resolve hostname to socket addresses.
use std::net::ToSocketAddrs;
let addr = addr_str
.to_socket_addrs()
.map_err(|e| FingerError::DnsResolution {
host: host.to_string(),
source: e,
})?
.next()
.ok_or_else(|| FingerError::DnsResolution {
host: host.to_string(),
source: io::Error::new(io::ErrorKind::NotFound, "no addresses found"),
})?;
// Connect with timeout.
let mut stream = TcpStream::connect_timeout(&addr, timeout).map_err(|e| {
if e.kind() == io::ErrorKind::TimedOut {
FingerError::Timeout {
host: host.to_string(),
port: query.port,
}
} else {
FingerError::ConnectionFailed {
host: host.to_string(),
port: query.port,
source: e,
}
}
})?;
// Set read/write timeouts on the connected socket.
stream.set_read_timeout(Some(timeout)).ok();
stream.set_write_timeout(Some(timeout)).ok();
// Send the query.
let query_string = build_query_string(query);
stream
.write_all(query_string.as_bytes())
.map_err(|e| FingerError::SendFailed { source: e })?;
// Read the full response.
let mut buf = Vec::new();
stream
.read_to_end(&mut buf)
.map_err(|e| FingerError::ReadFailed { source: e })?;
Ok(String::from_utf8_lossy(&buf).into_owned())
}
```
- [ ] **Step 2: Verify it compiles**
Run: `cargo build`
Expected: Compiles successfully.
- [ ] **Step 3: Commit**
```bash
git add src/protocol.rs
git commit -m "fix: use ToSocketAddrs for proper hostname resolution"
```
---
### Task 8: CLI Entry Point
**Files:**
- Modify: `src/main.rs`
- [ ] **Step 1: Implement the CLI with clap**
Replace the contents of `src/main.rs`:
```rust
use std::process::ExitCode;
use std::time::Duration;
use clap::Parser;
use digit::protocol::{finger, FingerError};
use digit::query::{Query, DEFAULT_PORT};
/// digit - a finger protocol client (RFC 1288 / RFC 742)
///
/// Query user information from finger servers. Supports standard queries,
/// user listings, and forwarding chains.
///
/// Note: Forwarding queries (user@host1@host2) depend on server support.
/// Many modern finger servers disable forwarding for security reasons.
#[derive(Parser, Debug)]
#[command(version, about)]
struct Cli {
/// Finger query (e.g. "user@host", "@host", "user@host1@host2")
query: Option<String>,
/// Request verbose/long output (sends /W)
#[arg(short, long)]
long: bool,
/// Port to connect on
#[arg(short, long, default_value_t = DEFAULT_PORT)]
port: u16,
/// Connection timeout in seconds
#[arg(short, long, default_value_t = 10)]
timeout: u64,
}
fn main() -> ExitCode {
let cli = Cli::parse();
let query = Query::parse(cli.query.as_deref(), cli.long, cli.port);
let timeout = Duration::from_secs(cli.timeout);
match finger(&query, timeout) {
Ok(response) => {
print!("{}", response);
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("digit: {}", e);
ExitCode::FAILURE
}
}
}
```
- [ ] **Step 2: Verify it compiles**
Run: `cargo build`
Expected: Compiles successfully.
- [ ] **Step 3: Verify help text**
Run: `cargo run -- --help`
Expected: Shows usage with query, --long, --port, --timeout options. The about text should mention forwarding and RFC numbers.
- [ ] **Step 4: Commit**
```bash
git add src/main.rs
git commit -m "feat: implement CLI entry point with clap"
```
---
### Task 9: Integration Tests
**Files:**
- Create: `tests/integration.rs`
- [ ] **Step 1: Write integration tests with a mock TCP server**
Create `tests/integration.rs`:
```rust
use std::io::{Read, Write};
use std::net::TcpListener;
use std::thread;
use std::time::Duration;
use digit::protocol::{finger, FingerError};
use digit::query::Query;
/// Start a mock finger server that accepts one connection, reads the query,
/// and responds with `response`. Returns the port it's listening on and
/// a join handle. The `on_query` callback receives the raw query bytes.
fn mock_finger_server(
response: &str,
on_query: impl FnOnce(String) + Send + 'static,
) -> (u16, thread::JoinHandle<()>) {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind to ephemeral port");
let port = listener.local_addr().unwrap().port();
let response = response.to_string();
let handle = thread::spawn(move || {
let (mut stream, _) = listener.accept().expect("accept connection");
stream
.set_read_timeout(Some(Duration::from_secs(2)))
.ok();
let mut buf = Vec::new();
let mut tmp = [0u8; 1024];
// Read until we see \r\n (end of finger query).
loop {
match stream.read(&mut tmp) {
Ok(0) => break,
Ok(n) => {
buf.extend_from_slice(&tmp[..n]);
if buf.windows(2).any(|w| w == b"\r\n") {
break;
}
}
Err(_) => break,
}
}
let query_str = String::from_utf8_lossy(&buf).into_owned();
on_query(query_str);
stream.write_all(response.as_bytes()).expect("write response");
// Server closes connection to signal end of response.
});
(port, handle)
}
#[test]
fn end_to_end_user_query() {
let (port, handle) = mock_finger_server("Login: user\r\nName: Test User\r\n", |query| {
assert_eq!(query, "user\r\n");
});
let q = Query::parse(Some(&format!("user@127.0.0.1")), false, port);
let result = finger(&q, Duration::from_secs(5)).expect("finger should succeed");
assert!(result.contains("Login: user"));
assert!(result.contains("Test User"));
handle.join().expect("server thread");
}
#[test]
fn end_to_end_list_users() {
let (port, handle) = mock_finger_server("user1\r\nuser2\r\n", |query| {
assert_eq!(query, "\r\n");
});
let q = Query::parse(Some(&format!("@127.0.0.1")), false, port);
let result = finger(&q, Duration::from_secs(5)).expect("finger should succeed");
assert!(result.contains("user1"));
assert!(result.contains("user2"));
handle.join().expect("server thread");
}
#[test]
fn end_to_end_verbose_query() {
let (port, handle) = mock_finger_server("Verbose info\r\n", |query| {
assert_eq!(query, "/W user\r\n");
});
let q = Query::parse(Some(&format!("user@127.0.0.1")), true, port);
let result = finger(&q, Duration::from_secs(5)).expect("finger should succeed");
assert!(result.contains("Verbose info"));
handle.join().expect("server thread");
}
#[test]
fn connection_refused_returns_error() {
// Connect to a port that nothing is listening on.
let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
let port = listener.local_addr().unwrap().port();
drop(listener); // Close it so the port is free but nothing is listening.
let q = Query::parse(Some(&format!("user@127.0.0.1")), false, port);
let result = finger(&q, Duration::from_secs(2));
assert!(result.is_err());
let err = result.unwrap_err();
let msg = format!("{}", err);
assert!(
msg.contains("could not connect"),
"unexpected error: {}",
msg
);
}
#[test]
fn utf8_lossy_handles_invalid_bytes() {
// Server sends bytes that aren't valid UTF-8.
let response_bytes: Vec<u8> = vec![72, 101, 108, 108, 111, 0xFF, 0xFE, 10];
let response_str = unsafe { String::from_utf8_unchecked(response_bytes) };
let (port, handle) = mock_finger_server(&response_str, |_| {});
let q = Query::parse(Some(&format!("user@127.0.0.1")), false, port);
let result = finger(&q, Duration::from_secs(5)).expect("finger should succeed");
// The valid "Hello" portion should be present.
assert!(result.contains("Hello"));
// Invalid bytes should be replaced with the replacement character.
assert!(result.contains('\u{FFFD}'));
handle.join().expect("server thread");
}
```
- [ ] **Step 2: Run the integration tests**
Run: `cargo test --test integration`
Expected: All 5 tests pass.
- [ ] **Step 3: Commit**
```bash
git add tests/integration.rs
git commit -m "test: add integration tests with mock TCP finger server"
```
---
### Task 10: Run Full Test Suite and Lint
**Files:**
- No new files.
- [ ] **Step 1: Run all tests**
Run: `cargo test`
Expected: All unit tests (query + protocol) and integration tests pass.
- [ ] **Step 2: Run clippy**
Run: `cargo clippy -- -D warnings`
Expected: No warnings.
- [ ] **Step 3: Check formatting**
Run: `cargo fmt -- --check`
Expected: No formatting issues.
- [ ] **Step 4: Fix any issues found**
If clippy or fmt flag anything, fix them and re-run.
- [ ] **Step 5: Commit any fixes**
```bash
git add -A
git commit -m "chore: fix clippy warnings and formatting"
```
(Skip this step if there were no issues.)
---
### Task 11: Documentation
**Files:**
- Create: `README.md`
- [ ] **Step 1: Write the README**
Create `README.md`:
````markdown
# digit
A finger protocol client implementing [RFC 1288](https://datatracker.ietf.org/doc/html/rfc1288) and [RFC 742](https://datatracker.ietf.org/doc/html/rfc742), written in Rust.
## Installation
```
cargo install --path .
```
## Usage
```
digit [OPTIONS] [QUERY]
```
### Examples
```bash
# Query a user at a host
digit user@example.com
# List all users at a host
digit @example.com
# Query a user with verbose/long output
digit -l user@example.com
# Use a non-standard port
digit -p 7979 user@example.com
# Set a connection timeout (in seconds)
digit -t 5 user@example.com
# Query a user at localhost
digit user
# List users at localhost
digit
```
### Forwarding queries
The finger protocol supports forwarding queries through a chain of hosts using the `@host1@host2` syntax (RFC 1288, section 2.5.1). For example:
```bash
digit user@host1@host2
```
This connects to `host2` and asks it to forward the query to `host1`. Note that many modern finger servers disable forwarding for security reasons, so this feature depends on server support.
### Options
| `-l, --long` | Request verbose/long output (sends `/W` prefix) |
| `-p, --port <PORT>` | Port to connect on (default: 79) |
| `-t, --timeout <SECS>` | Connection timeout in seconds (default: 10) |
| `-h, --help` | Print help |
| `-V, --version` | Print version |
## License
MIT
````
- [ ] **Step 2: Commit**
```bash
git add README.md
git commit -m "docs: add README with usage examples"
```