# foobar2000-catbox — Administrator & Developer Guide

## 1. Architecture Overview

**foobar2000-catbox** is a single-binary Rust command-line tool. It accepts one image file path on stdin, processes the image in memory (optional resize, optional re-encode), then performs a multipart HTTP POST to either catbox.moe or litterbox.catbox.moe and writes the returned URL to stdout.

There is no daemon, no server, no database, and no persistent state beyond the `settings.yml` file placed adjacent to the binary.

```
stdin (file path)
        │
        ▼
    main.rs
        │── load settings.yml (sibling to executable)
        │── validate settings
        │── read image file from path
        │── [optional] resize (nearest-neighbor, aspect-preserving)
        │── [optional] re-encode (JPG / PNG / WebP)
        ▼
    consumer.rs
        │── Catbox  → POST https://catbox.moe/user/api.php
        │── Litterbox → POST https://litterbox.catbox.moe/resources/internals/api.php
        ▼
stdout (URL string)
```

> [!NOTE]
> Settings are loaded from the directory of the executable (`env::current_exe().parent()`), not the process working directory. This is a deliberate design choice so the binary and its config can be co-located and invoked from any working directory.

### Component Inventory

| Component | File | Role |
|---|---|---|
| Entry point | `src/main.rs` | Orchestrates the full pipeline: settings load → validate → read → process → upload |
| Upload backends | `src/consumer.rs` | `Catbox` and `Litterbox` structs implementing the `Consumer` trait |
| Configuration | `src/settings.rs` | `Settings`, `EncodeFormat`, `ExpireTime` types; YAML deserialization and validation |
| Config file | `settings.yml` | User-editable YAML configuration (co-located with binary) |
| Sample images | `samples/` | Test images at various resolutions (512–2048 px), used by integration tests |

### Technology Stack

| Crate | Version | Purpose |
|---|---|---|
| `image` | 0.25.6 | Image decoding (format detection) and encoding (JPG, PNG, WebP) |
| `curl` | 0.4.47 | HTTP multipart POST upload |
| `serde` | 1.0.219 | Derive macros for YAML deserialization |
| `serde_yml` | 0.0.12 | YAML parser for `settings.yml` |
| `strum` / `strum_macros` | 0.27.1 | `EnumString` derive for `EncodeFormat` and `ExpireTime` |
| `base64` | 0.22.1 | Used in test fixtures to embed a 1×1 PNG inline |

### Data Flow

1. `main()` resolves `settings.yml` path as `current_exe().parent().join("settings.yml")`.
2. File is read with `fs::read_to_string`; failures fall back to empty string → default `Settings`.
3. `Settings::from_str()` deserializes YAML; failures fall back to `Settings::new()`.
4. `settings.validate()` checks bounds; any violation prints a colored error and exits.
5. One line is read from stdin via `io::stdin().lock().lines().next()`.
6. The file is read into a `Vec<u8>`; format is detected with `image::guess_format`.
7. Optional resize: `DynamicImage::resize()` with `FilterType::Nearest`.
8. Optional encode: `write_with_encoder()` to a `Cursor<Vec<u8>>`.
9. The in-memory buffer is handed to `consumer.upload_image()`.
10. The returned URL string is printed to stdout; curl errors exit with code 6.

---

## 2. System Requirements

| Item | Requirement |
|---|---|
| Rust edition | 2021 |
| Minimum Rust version | Compatible with Rust stable (no nightly features used) |
| libcurl | Required at link time by the `curl` crate (0.4.47) |
| Build target | Any `rustup` target — release binary is statically linked where toolchain supports |
| Network | Outbound HTTPS to `catbox.moe` (port 443) and/or `litterbox.catbox.moe` (port 443) |

> [!NOTE]
> The release profile (`Cargo.toml`) sets `strip = true`, `lto = true`, `codegen-units = 16`. Stripped LTO binaries are significantly smaller and produce no debug symbols — do not attempt to attach a debugger to a release build.

---

## 3. Configuration Reference

### `settings.yml` — Complete Field Reference

The file is YAML, deserialized via `serde_yml`. Unknown keys are silently collected into `unexpected: HashMap<String, Value>` (no error on extra keys).

| Key | Type | Default | Valid Values | Validation |
|---|---|---|---|---|
| `enable_litterbox` | `bool` | `false` | `true`, `false` | None |
| `litterbox_expire_time` | `string` | `"24h"` | `"1h"`, `"12h"`, `"24h"`, `"72h"` | YAML parse error if unrecognized |
| `enable_encode` | `bool` | `false` | `true`, `false` | None |
| `encode_format` | `string` | `"JPG"` | `"JPG"` (also `"JPEG"`), `"PNG"`, `"WEBP"` | YAML parse error if unrecognized |
| `encode_format_quality` | `u8` | `80` | `1`–`100` | Exit code 1 if out of range |
| `enable_resize` | `bool` | `false` | `true`, `false` | None |
| `resize_max_resolution` | `[u32, u32]` | `[1024, 1024]` | Positive powers of two | Exit code 2 (zero) or 3 (not power-of-two) |
| `user_hash` | `string` | `""` | 25-char string or empty | Exit code 4 if non-empty and not 25 chars |
| `user_agent` | `string` | Firefox 133 UA | Any string | None |

> [!IMPORTANT]
> `encode_format` values are case-insensitive at parse time (`ascii_case_insensitive` via strum). `"jpg"`, `"JPG"`, `"jPg"` all map to the JPG variant. However, `litterbox_expire_time` values are case-sensitive — only lowercase `"1h"`, `"12h"`, `"24h"`, `"72h"` are accepted.

### Validation Error Details

The `Settings::validate()` method enforces these constraints and returns a typed `Error` variant:

| Exit Code | Error Variant | Message |
|---|---|---|
| 1 | `EncodeFormatQualityError(u8)` | `encode_format_quality is out-of-bounds (N)` |
| 2 | `ResizeMaxResolutionZeroError([u32; 2])` | `resize_max_resolution values must be larger than 0 (W, H)` |
| 3 | `ResizeMaxResolutionPowerError([u32; 2])` | `both values of resize_max_resolution must be a power-of-two (W, H)` |
| 4 | `UserHashError(u8)` | `user_hash isn't 25 characters (N)` |

> [!NOTE]
> Exit code 4 is shared between `UserHashError` (startup validation) and "file not found" (runtime). Callers cannot distinguish them from the exit code alone — stderr content is the differentiator.

---

## 4. Deployment & Scaling

### Deployment Checklist

- [x] Build release binary: `cargo build --release`
- [x] Place binary and `settings.yml` in the same directory
- [x] Verify `settings.yml` reflects desired defaults
- [x] Test: `echo "/path/to/test.jpg" | ./foobar2000-catbox` → URL printed
- [ ] If using `user_hash`, verify it is exactly 25 characters
- [ ] If firewall-restricted, open outbound HTTPS to `catbox.moe` and `litterbox.catbox.moe`

### Invocation Patterns

**Shell pipe:**
```bash
echo "/mnt/music/album/cover.jpg" | /opt/catbox-uploader/foobar2000-catbox
```

**Capture URL in variable:**
```bash
URL=$(echo "/path/to/image.png" | ./foobar2000-catbox)
```

**Process substitution (foobar2000 component integration):**
The binary is intended to be called from a media player component that writes the current track's album art path to a pipe or file. The component passes the path via stdin; the binary outputs the URL the component uses for display (e.g. Discord Rich Presence image URL).

> [!NOTE]
> The binary is single-shot: one path in, one URL out, then exit. For high-frequency invocation (e.g. track changes), each call is a fresh process — no connection pooling (`easy.forbid_reuse(true)` is set explicitly in the curl backend).

### Concurrent Invocations

No shared state exists. Multiple processes can run simultaneously without interference. Each reads its own copy of `settings.yml` at startup.

---

## 5. Operations

### Monitoring

There is no logging facility. The binary uses:
- `stdout` — success URL
- `stderr` — error messages (colored with ANSI escape codes on non-Windows terminals)
- Exit codes — see the table in Section 3

To capture both streams in a wrapper script:

```bash
OUTPUT=$(echo "$PATH_TO_ART" | ./foobar2000-catbox 2>/tmp/catbox_err.log)
EXIT=$?
if [ $EXIT -ne 0 ]; then
    cat /tmp/catbox_err.log
fi
```

> [!TIP]
> Wrap invocations in a script that checks the exit code and logs stderr. The binary itself has no retry logic — build retries at the integration layer if needed.

### Running Tests

The project includes integration tests that make real HTTP requests:

```bash
cargo test
```

> [!WARNING]
> `test_consumers` in `src/consumer.rs` uploads a real 1×1 PNG to catbox.moe and litterbox.catbox.moe. These tests require internet access and hit the live services. Do not run them in CI environments without network access or rate-limit considerations.

`test_perform` verifies curl connectivity — it asserts `https://example.com/` succeeds and `http://localhost/` fails (connection refused). This also requires internet access.

### Updating `settings.yml`

Changes to `settings.yml` take effect on the next invocation. There is no reload signal — the file is read fresh each time the binary starts.

---

## 6. Security

### Network Exposure

The binary makes outbound POST requests to:
- `https://catbox.moe/user/api.php` (Catbox)
- `https://litterbox.catbox.moe/resources/internals/api.php` (Litterbox)

No inbound ports are opened. No local server is started. All communication is HTTPS.

> [!CAUTION]
> Images uploaded to catbox.moe (non-litterbox) are **permanently public**. Anyone with the URL can access the image. Do not upload images containing sensitive content unless you intend them to be publicly accessible.

### User Hash Sensitivity

The `user_hash` value is a credential that associates uploads with a catbox account. Store `settings.yml` with appropriate filesystem permissions if this value is configured:

```bash
chmod 600 settings.yml
```

### Input Validation

- File path is read from stdin (first line only, trimmed).
- The file's existence is checked before reading.
- Image format is detected from file contents via `image::guess_format`, not the file extension — mitigates extension-spoofing concerns for image format detection.
- `settings.yml` unknown keys are silently ignored (collected into `unexpected` map) — no arbitrary code execution from config.

> [!NOTE]
> The binary does not sanitize the file path beyond checking existence. If the binary is called by a component that constructs paths from user-controlled input, the caller is responsible for path sanitization.

### User-Agent

The default user-agent identifies as Firefox on Linux. It can be changed freely in `settings.yml`. The curl backend sends it as a custom `User-Agent` HTTP header.

---

## 7. Developer Internals

### Repository Structure

```
foobar2000-catbox/
├── Cargo.toml          Package manifest (version 1.0.9, edition 2021)
├── Cargo.lock          Pinned dependency tree
├── LICENSE             License file
├── README.md           (not present in briefing — no repo_docs extracted)
├── settings.yml        Example/default config co-located with binary
├── samples/            Test images: 512.jpg, 1024.jpg, 1205.jpg, 1456.jpg, 1805.jpg, 2048.jpg
└── src/
    ├── main.rs         Entry point, pipeline orchestrator
    ├── consumer.rs     Upload backends (Catbox, Litterbox) + Consumer trait
    └── settings.rs     Settings struct, enums, validation, YAML deserialization
```

### Module Reference

#### `src/main.rs` — Entry Point

`main()` is the sole function. Execution order:

1. Resolve `settings.yml` path via `env::current_exe().parent().join("settings.yml")`
2. Deserialize settings (double fallback: parse error → `Settings::new()`)
3. Validate settings — exits 1–4 on failure
4. Read one line from stdin
5. Verify file exists
6. Read file bytes
7. Decode with `image::load_from_memory`
8. Conditional resize (nearest-neighbor via `DynamicImage::resize`)
9. Conditional encode into `Cursor<Vec<u8>>`
10. Construct `Box<dyn Consumer>` based on `enable_litterbox`
11. Call `consumer.upload_image(bytes)` → print URL or exit 6

#### `src/consumer.rs` — Upload Backends

**`Consumer` trait:**
```rust
pub trait Consumer {
    fn upload_image(&self, data: Vec<u8>) -> Result<String, Error>;
}
```

**`Catbox` struct:**
- Fields: `user_agent: String`, `user_hash: String`
- Endpoint: `https://catbox.moe/user/api.php`
- Form fields: `reqtype=fileupload`, `fileToUpload=<bytes>`, `userhash=<hash>` (if non-empty)
- File name in multipart form: auto-detected from `image::guess_format` → `extensions_str()[0]`

**`Litterbox` struct:**
- Fields: `expire_time: String`, `user_agent: String`, `user_hash: String`
- Endpoint: `https://litterbox.catbox.moe/resources/internals/api.php`
- Form fields: `reqtype=fileupload`, `time=<expire_time>`, `fileToUpload=<bytes>`, `userhash=<hash>` (if non-empty)

**`perform()` (private):**
- Creates `Easy2<ResponseBody>` curl handle
- Sets URL, enables POST, attaches multipart form
- Appends `User-Agent` header via `List`
- Sets `forbid_reuse(true)` (no connection pooling)
- Returns response body as UTF-8 string

#### `src/settings.rs` — Configuration

**`Settings` struct** — deserialized from YAML, all fields have serde defaults:

```rust
pub struct Settings {
    pub enable_litterbox: bool,          // default: false
    pub litterbox_expire_time: ExpireTime, // default: ExpireTime::TWENTYFOUR
    pub enable_encode: bool,             // default: false
    pub encode_format: EncodeFormat,     // default: EncodeFormat::JPG
    pub encode_format_quality: u8,       // default: 80
    pub enable_resize: bool,             // default: false
    pub resize_max_resolution: [u32; 2], // default: [1024, 1024]
    pub user_agent: String,              // default: Firefox 133 UA
    pub user_hash: String,               // default: ""
    pub unexpected: HashMap<String, Value>, // unknown keys, silently collected
}
```

**`EncodeFormat` enum:**

| Variant | Serde/strum serialization | Notes |
|---|---|---|
| `JPG` | `"JPG"`, `"JPEG"` (case-insensitive) | Lossy; quality controlled by `encode_format_quality` |
| `PNG` | `"PNG"` (case-insensitive) | Lossless |
| `WEBP` | `"WEBP"` (case-insensitive) | Always lossless (`WebPEncoder::new_lossless`) |

**`ExpireTime` enum:**

| Variant | Serde key | strum serialization |
|---|---|---|
| `ONE` | `"1h"` | `"1h"` |
| `TWELVE` | `"12h"` | `"12h"` |
| `TWENTYFOUR` | `"24h"` | `"24h"` (default) |
| `SEVENTYTWO` | `"72h"` | `"72h"` |

> [!NOTE]
> `ExpireTime` uses explicit `#[serde(rename = "Nh")]` attributes. The serde deserialization is case-sensitive for these values, unlike `EncodeFormat` which uses `ascii_case_insensitive`.

### Code Patterns & Conventions

- **Double fallback for settings**: YAML parse failures silently fall back to `Settings::new()` rather than hard-failing. This means a corrupt `settings.yml` goes unnoticed until a validation error fires.
- **In-memory pipeline**: all image operations use `Cursor<Vec<u8>>` — no temp files written to disk.
- **Single stdin line**: `io::stdin().lock().lines().next().unwrap().unwrap()` — panics if stdin is closed before a line is received. The binary is designed for programmatic invocation, not interactive use.
- **Trait object dispatch**: `Box<dyn Consumer>` is chosen at runtime based on `enable_litterbox`, keeping `main()` agnostic of upload target.
- **Exit code 4 collision**: both file-not-found and `UserHashError` exit with code 4. This is a current design limitation.

### Testing Strategy

Tests live in `#[cfg(test)]` modules within each source file:

| Test | File | What it tests |
|---|---|---|
| `test_consumers` | `consumer.rs` | Live upload of 1×1 PNG to Catbox and Litterbox (real network) |
| `test_perform` | `consumer.rs` | curl success/failure (hits `example.com` and `localhost`) |
| `test_encode_format` | `settings.rs` | `EncodeFormat` parse, display, equality |
| `test_from_str` | `settings.rs` | YAML deserialization of empty and invalid YAML |
| `test_validation` | `settings.rs` | Full valid settings round-trip |
| `test_encode_format_quality_validation` | `settings.rs` | Quality bounds (0, 100, 110) |
| `test_resize_max_resolution` | `settings.rs` | Zero and non-power-of-two detection |
| `test_error_displays` | `settings.rs` | Error message strings |

All tests require internet access. No mocking layer exists.

### Potential Improvements

- **Exit code 4 collision**: separate file-not-found (currently also 4) from `UserHashError` (4) to allow callers to distinguish them.
- **Stdin validation**: the binary panics on closed stdin. A graceful error with a non-zero exit code would be more robust.
- **Settings parse failure visibility**: a corrupt `settings.yml` falls back silently to defaults. Emitting a warning to stderr on parse failure would aid debugging.
- **Retry logic**: upload failures (exit 6) could benefit from a configurable retry count, given that catbox.moe can be transiently unavailable.
- **PNG quality note**: `encode_format_quality` silently has no effect on PNG and WebP. A warning when `encode_format_quality` is set alongside a lossless format would prevent user confusion.
- **User hash validation note**: the source includes a comment noting the 25-character constraint "needs to be verified" — this should be confirmed against catbox.moe's actual account hash length.

### Contributing

Build:
```bash
cargo build
```

Test (requires internet):
```bash
cargo test
```

Lint:
```bash
cargo clippy
```

Format:
```bash
cargo fmt
```

Release build (stripped, LTO):
```bash
cargo build --release
```

---

_Generated May 2026 from realoksi/foobar2000-catbox at f47db1a. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
