# 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

| Option | Description |
|---|---|
| `-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"
```
