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
main()resolvessettings.ymlpath ascurrent_exe().parent().join("settings.yml").- File is read with
fs::read_to_string; failures fall back to empty string → defaultSettings. Settings::from_str()deserializes YAML; failures fall back toSettings::new().settings.validate()checks bounds; any violation prints a colored error and exits.- One line is read from stdin via
io::stdin().lock().lines().next(). - The file is read into a
Vec<u8>; format is detected withimage::guess_format. - Optional resize:
DynamicImage::resize()withFilterType::Nearest. - Optional encode:
write_with_encoder()to aCursor<Vec<u8>>. - The in-memory buffer is handed to
consumer.upload_image(). - 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) setsstrip = 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_formatvalues are case-insensitive at parse time (ascii_case_insensitivevia strum)."jpg","JPG","jPg"all map to the JPG variant. However,litterbox_expire_timevalues 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.ymlin the same directory - [x] Verify
settings.ymlreflects 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.moeandlitterbox.catbox.moe
Invocation Patterns
Shell pipe:
echo "/mnt/music/album/cover.jpg" | /opt/catbox-uploader/foobar2000-catbox
Capture URL in variable:
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 URLstderr— 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:
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:
cargo test
Warning
test_consumersinsrc/consumer.rsuploads 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:
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.ymlunknown keys are silently ignored (collected intounexpectedmap) — 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:
- Resolve
settings.ymlpath viaenv::current_exe().parent().join("settings.yml") - Deserialize settings (double fallback: parse error →
Settings::new()) - Validate settings — exits 1–4 on failure
- Read one line from stdin
- Verify file exists
- Read file bytes
- Decode with
image::load_from_memory - Conditional resize (nearest-neighbor via
DynamicImage::resize) - Conditional encode into
Cursor<Vec<u8>> - Construct
Box<dyn Consumer>based onenable_litterbox - Call
consumer.upload_image(bytes)→ print URL or exit 6
src/consumer.rs — Upload Backends
Consumer trait:
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-Agentheader viaList - 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:
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
ExpireTimeuses explicit#[serde(rename = "Nh")]attributes. The serde deserialization is case-sensitive for these values, unlikeEncodeFormatwhich usesascii_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 corruptsettings.ymlgoes 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 onenable_litterbox, keepingmain()agnostic of upload target. - Exit code 4 collision: both file-not-found and
UserHashErrorexit 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.ymlfalls 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_qualitysilently has no effect on PNG and WebP. A warning whenencode_format_qualityis 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:
cargo build
Test (requires internet):
cargo test
Lint:
cargo clippy
Format:
cargo fmt
Release build (stripped, LTO):
cargo build --release