digit-cli 0.2.0

A finger protocol client (RFC 1288 / RFC 742)
Documentation
# digit -- Finger Protocol Client Design Spec

**Date:** 2026-04-11
**Status:** Draft

## Overview

`digit` is a Rust command-line finger client implementing both RFC 1288 and RFC 742. It emphasizes code clarity, documentation, and test coverage over absolute efficiency. The name is a synonym for "finger" -- fitting for a finger protocol client.

## CLI Interface

Uses `clap` (derive API) for argument parsing.

```
digit [OPTIONS] [QUERY]

Arguments:
  [QUERY]    Finger query (e.g. "user@host", "@host", "user@host1@host2")

Options:
  -l, --long             Request verbose/long output (sends /W)
  -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
```

### Query forms

| Input | User | Host(s) | Behavior |
|---|---|---|---|
| `digit user@host` | `user` | `host` | Query user at host |
| `digit @host` | (none) | `host` | List all users at host |
| `digit user` | `user` | `localhost` | Query user at localhost |
| `digit` | (none) | `localhost` | List users at localhost |
| `digit user@host1@host2` | `user` | `host1`, `host2` | Forwarded query |

### Forwarding queries

The `@host1@host2` chaining syntax from RFC 1288 is fully supported. The query is sent to the last host in the chain, asking it to forward to the preceding hosts. Both help text and documentation note that many servers disable forwarding, so this feature depends on server support.

## Architecture

Library + binary split (`src/lib.rs` re-exports modules, `src/main.rs` is the CLI entry point).

### Modules

#### `query` (`src/query.rs`)

Parsing user input into a structured finger query. No I/O.

```rust
pub struct Query {
    pub user: Option<String>,    // None means "list all users"
    pub hosts: Vec<String>,      // At least one; multiple means forwarding
    pub long: bool,              // Whether to send /W
    pub port: u16,               // Default 79
}
```

Parsing rules:
- Split query string on `@` -- first segment is user (if non-empty), rest are hosts
- `user@host` -> user=Some("user"), hosts=["host"]
- `@host` -> user=None, hosts=["host"]
- `user@host1@host2` -> user=Some("user"), hosts=["host1", "host2"]
- `user` (no @) -> user=Some("user"), hosts=["localhost"]
- Empty/no query -> user=None, hosts=["localhost"]

#### `protocol` (`src/protocol.rs`)

Constructs the RFC-compliant wire-format query string and executes the TCP request/response.

**Query string construction (per RFC 1288):**
- Base: `{user}` or empty string for listing
- Verbose: prepend `/W ` (with trailing space) when `--long` is set
- Forwarding: append `@host1@host2...` for all but the last host (last host is the connection target)
- Terminate with `\r\n`

**Wire format examples:**

| CLI invocation | Sent to | Query string |
|---|---|---|
| `digit user@host` | `host:79` | `user\r\n` |
| `digit -l user@host` | `host:79` | `/W user\r\n` |
| `digit user@host1@host2` | `host2:79` | `user@host1\r\n` |
| `digit -l @host` | `host:79` | `/W \r\n` |

**TCP communication:**
1. Resolve hostname and connect to `host:port` with the configured timeout
2. Send the constructed query string
3. Read the full response until the server closes the connection (finger has no content-length)
4. Decode response bytes with `String::from_utf8_lossy`
5. Return the response string

#### `main.rs`

Thin entry point: parses CLI args with clap, constructs a `Query`, calls `protocol::finger()`, prints the result or error, and exits with the appropriate code.

## Error Handling

**Error type** (using `thiserror`):

```rust
pub enum FingerError {
    DnsResolution { host: String, source: io::Error },
    ConnectionFailed { host: String, port: u16, source: io::Error },
    Timeout { host: String, port: u16 },
    SendFailed { source: io::Error },
    ReadFailed { source: io::Error },
}
```

Each variant carries enough context for helpful error messages (e.g. "could not resolve host 'example.com': ...").

**Exit codes:**
- `0` -- success
- `1` -- finger protocol error (connection failed, timeout, etc.)
- `2` -- usage error (bad arguments, handled by clap)

**Output conventions:**
- Response goes to **stdout**
- Errors go to **stderr** via `eprintln!`
- No color or formatting -- raw finger response, pipe-friendly

## Dependencies

| Crate | Purpose |
|---|---|
| `clap` (with `derive` feature) | CLI argument parsing |
| `thiserror` | Ergonomic error type derivation |

No async runtime. Standard library handles TCP (`std::net`), timeouts (`std::time::Duration`), and I/O.

## Testing Strategy

**Unit tests** (inline `#[cfg(test)]` modules):
- `query.rs`: exhaustive parsing tests for all input patterns, edge cases, empty strings
- `protocol.rs`: query string construction tests (no network -- testing the string builder function)

**Integration tests** (`tests/integration.rs`):
- CLI argument parsing validation (correct args produce correct behavior, bad args produce helpful errors)
- Optional: mock TCP listener on localhost for end-to-end protocol exchange testing

## Project Structure

```
digit/
├── Cargo.toml
├── LICENSE           # MIT
├── docs/
│   └── spec.md       # This file
├── src/
│   ├── main.rs       # CLI entry point
│   ├── lib.rs        # Re-exports query and protocol modules
│   ├── query.rs      # Query struct and parsing
│   └── protocol.rs   # Wire format and TCP I/O
└── tests/
    └── integration.rs
```

## Configuration

- **Rust edition:** 2021
- **License:** MIT
- **MSRV:** Not pinned (current stable)

## RFCs

- [RFC 742]https://datatracker.ietf.org/doc/html/rfc742 -- Name/Finger Protocol (1977, original spec)
- [RFC 1288]https://datatracker.ietf.org/doc/html/rfc1288 -- The Finger User Information Protocol (1991, updated spec)