use oximg::pipeline;
use std::collections::HashMap;
use std::path::PathBuf;
#[cfg(feature = "mimalloc")]
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
use std::sync::{Arc, Mutex};
use axum::Router;
use axum::body::Bytes;
use axum::extract::{FromRequestParts, Path, State};
use axum::http::{HeaderValue, StatusCode, header, request::Parts};
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use tokio::sync::{Semaphore, watch};
use oximg::pipeline::ImageFormat;
type FlightKey = (u32, u32, String, Option<ImageFormat>, Option<u8>);
type FlightResult = Result<(Bytes, &'static str), (StatusCode, String)>;
type FlightMap = Mutex<HashMap<FlightKey, watch::Receiver<Option<FlightResult>>>>;
#[derive(Clone)]
struct App {
log_requests: bool,
images_dir: Arc<PathBuf>,
source_base: Option<Arc<str>>,
cpu_slots: Arc<Semaphore>,
workers: usize,
quality: f32,
encoder: pipeline::Encoder,
resize_threads: usize,
inflight: Arc<FlightMap>,
signing: Option<Arc<Signing>>,
auto_format: Arc<[ImageFormat]>,
options_prefix: Option<Arc<str>>,
}
#[derive(Clone)]
struct Signing {
key: Vec<u8>,
salt: Vec<u8>,
}
impl Signing {
fn from_env() -> Result<Option<Self>, String> {
Self::from_values(
std::env::var("OXIMG_KEY").ok().as_deref(),
std::env::var("OXIMG_SALT").ok().as_deref(),
)
}
fn from_values(key: Option<&str>, salt: Option<&str>) -> Result<Option<Self>, String> {
fn decode(name: &str, v: Option<&str>) -> Result<Option<Vec<u8>>, String> {
let Some(v) = v.map(str::trim).filter(|v| !v.is_empty()) else {
return Ok(None);
};
if v.len() % 2 != 0 {
return Err(format!("{name} is not valid hex (odd length)"));
}
(0..v.len())
.step_by(2)
.map(|i| {
u8::from_str_radix(&v[i..i + 2], 16)
.map_err(|_| format!("{name} is not valid hex"))
})
.collect::<Result<Vec<u8>, String>>()
.map(Some)
}
match (decode("OXIMG_KEY", key)?, decode("OXIMG_SALT", salt)?) {
(Some(key), Some(salt)) => Ok(Some(Signing { key, salt })),
(None, None) => Ok(None),
_ => Err("OXIMG_KEY and OXIMG_SALT must both be set to enable signing".into()),
}
}
fn verify(&self, signature: &str, path: &str) -> bool {
use hmac::Mac;
use hmac::digest::KeyInit;
let Ok(mut mac) = hmac::Hmac::<sha2::Sha256>::new_from_slice(&self.key) else {
return false;
};
mac.update(&self.salt);
mac.update(path.as_bytes());
let Some(sig) = base64url_decode(signature) else {
return false;
};
mac.verify_slice(&sig).is_ok()
}
}
fn base64url_decode(s: &str) -> Option<Vec<u8>> {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
let mut rev = [255u8; 256];
for (i, &c) in ALPHABET.iter().enumerate() {
rev[c as usize] = i as u8;
}
let s = s.trim_end_matches('=');
let mut out = Vec::with_capacity(s.len() * 3 / 4);
let mut acc = 0u32;
let mut bits = 0u32;
for &c in s.as_bytes() {
let v = rev[c as usize];
if v == 255 {
return None;
}
acc = (acc << 6) | v as u32;
bits += 6;
if bits >= 8 {
bits -= 8;
out.push((acc >> bits) as u8);
}
}
Some(out)
}
fn env_or<T: std::str::FromStr>(key: &str, default: T) -> T {
match std::env::var(key) {
Err(_) => default,
Ok(v) if v.trim().is_empty() => default,
Ok(v) => v.trim().parse().unwrap_or_else(|_| {
eprintln!("oximg: fatal: {key}={v:?} is not a valid value");
std::process::exit(2);
}),
}
}
mod cli;
mod metrics;
mod options;
fn main() -> anyhow::Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
match args.first().map(String::as_str) {
None | Some("serve") => {
if args.len() > 1 {
eprintln!(
"oximg: serve takes no arguments (configuration is via \
environment variables; try --help)"
);
std::process::exit(2);
}
}
Some("resize") => return cli::resize(&args[1..]),
Some("probe") => return cli::probe(&args[1..]),
Some("-V" | "--version") => {
println!("oximg {}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
Some("-h" | "--help") => {
cli::print_help();
return Ok(());
}
Some(other) => {
eprintln!("oximg: unknown command {other:?} (try --help)");
std::process::exit(2);
}
}
if let Err(e) = oximg::config_validate() {
eprintln!("oximg: fatal: {e}");
std::process::exit(2);
}
let workers = std::thread::available_parallelism()?.get();
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.max_blocking_threads(workers + 4)
.build()?
.block_on(async_main(workers))
}
async fn async_main(workers: usize) -> anyhow::Result<()> {
let port: u16 = env_or("PORT", 8081);
let images_dir =
PathBuf::from(std::env::var("IMAGES_DIR").unwrap_or_else(|_| "./images".to_string()));
let app = App {
images_dir: Arc::new(images_dir.clone()),
source_base: std::env::var("OXIMG_SOURCE_BASE_URL")
.ok()
.map(|s| Arc::from(s.trim_end_matches('/'))),
cpu_slots: Arc::new(Semaphore::new(workers)),
workers,
quality: env_or("QUALITY", 80.0),
encoder: pipeline::Encoder::from_preset(
std::env::var("PRESET").as_deref().unwrap_or("jpegli"),
),
resize_threads: env_or("OXIMG_PAR", 1),
inflight: Arc::new(Mutex::new(HashMap::new())),
log_requests: std::env::var("OXIMG_LOG").as_deref() == Ok("request"),
signing: Signing::from_env()
.unwrap_or_else(|e| {
eprintln!("oximg: fatal: {e}");
std::process::exit(2);
})
.map(Arc::new),
auto_format: auto_format_from_env().into(),
options_prefix: options_prefix_from_env().map(Arc::from),
};
if app.signing.is_some() {
eprintln!("oximg: URL signing enabled");
}
if !app.auto_format.is_empty() {
eprintln!(
"oximg: Accept negotiation enabled ({})",
app.auto_format
.iter()
.map(|f| f.content_type())
.collect::<Vec<_>>()
.join(", ")
);
}
let mut router = Router::new()
.route("/health", get(async || "ok"))
.route("/resize/{w}/{h}/{*file}", get(handle_resize))
.route("/{sig}/resize/{w}/{h}/{*file}", get(handle_signed_resize));
if let Some(prefix) = app.options_prefix.as_deref() {
eprintln!("oximg: options route enabled at {prefix}/{{options}}/{{file}}");
router = router
.route(
&format!("{prefix}/{{options}}/{{*file}}"),
get(handle_options),
)
.route(
&format!("/{{sig}}{prefix}/{{options}}/{{*file}}"),
get(handle_signed_options),
);
}
if std::env::var("OXIMG_METRICS").as_deref() == Ok("1") {
eprintln!("oximg: /metrics enabled");
router = router.route("/metrics", get(handle_metrics));
}
let router = router.with_state(app);
let listener = tokio::net::TcpListener::bind(("0.0.0.0", port)).await?;
let shutdown = shutdown_signal();
let bound = listener.local_addr()?.port();
eprintln!(
"oximg listening on :{bound} (images: {}, workers: {workers})",
images_dir.display()
);
axum::serve(listener, router)
.with_graceful_shutdown(shutdown)
.await?;
eprintln!("oximg: shutdown complete");
Ok(())
}
#[cfg(unix)]
fn shutdown_signal() -> impl std::future::Future<Output = ()> {
use tokio::signal::unix::{SignalKind, signal};
let mut term = signal(SignalKind::terminate()).expect("install SIGTERM handler");
let mut int = signal(SignalKind::interrupt()).expect("install SIGINT handler");
async move {
let name = tokio::select! {
_ = term.recv() => "SIGTERM",
_ = int.recv() => "SIGINT",
};
eprintln!("oximg: {name} received, draining in-flight requests");
}
}
#[cfg(not(unix))]
fn shutdown_signal() -> impl std::future::Future<Output = ()> {
async {
tokio::signal::ctrl_c()
.await
.expect("install ctrl-C handler");
eprintln!("oximg: ctrl-C received, draining in-flight requests");
}
}
fn options_prefix_from_env() -> Option<String> {
let raw = std::env::var("OXIMG_OPTIONS_PREFIX").ok()?;
let v = raw.trim().trim_end_matches('/').to_string();
let fatal = |why: &str| -> ! {
eprintln!("oximg: fatal: OXIMG_OPTIONS_PREFIX={raw:?} {why}");
std::process::exit(2);
};
if v.is_empty() {
return None;
}
if !v.starts_with('/') {
fatal("must start with '/'");
}
if v.split('/').skip(1).any(|seg| {
seg.is_empty() || seg.contains(['{', '}', '\\', '?', '#']) || seg == "." || seg == ".."
}) {
fatal("must be plain path segments");
}
for reserved in ["/health", "/metrics", "/resize"] {
if v == reserved || v.starts_with(&format!("{reserved}/")) {
fatal("collides with a built-in route");
}
}
Some(v)
}
fn auto_format_from_env() -> Vec<ImageFormat> {
let Ok(list) = std::env::var("OXIMG_AUTO_FORMAT") else {
return Vec::new();
};
list.split(',')
.map(str::trim)
.filter(|t| !t.is_empty())
.filter_map(|t| {
let fmt = ImageFormat::from_token(t);
match fmt {
Some(ImageFormat::Avif) if cfg!(not(feature = "avif")) => {
eprintln!("oximg: OXIMG_AUTO_FORMAT: avif not enabled in this build; skipped");
None
}
Some(f) => Some(f),
None => {
eprintln!("oximg: OXIMG_AUTO_FORMAT: unknown format {t:?}; skipped");
None
}
}
})
.collect()
}
struct AcceptHeader(Option<HeaderValue>);
impl<S: Send + Sync> FromRequestParts<S> for AcceptHeader {
type Rejection = std::convert::Infallible;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
Ok(AcceptHeader(parts.headers.get(header::ACCEPT).cloned()))
}
}
async fn handle_signed_resize(
State(app): State<App>,
Path((sig, w, h, file)): Path<(String, u32, u32, String)>,
accept: AcceptHeader,
) -> Result<impl IntoResponse, (StatusCode, String)> {
let Some(signing) = app.signing.as_ref() else {
metrics::METRICS.record_request(404, metrics::FormatLabel::Unresolved);
return Err((StatusCode::NOT_FOUND, "signing not configured".into()));
};
let path = format!("/resize/{w}/{h}/{file}");
if !signing.verify(&sig, &path) {
metrics::METRICS.record_request(403, metrics::FormatLabel::Unresolved);
return Err((StatusCode::FORBIDDEN, "invalid signature".into()));
}
serve_resize(app, w, h, file, accept).await
}
async fn handle_resize(
State(app): State<App>,
Path((w, h, file)): Path<(u32, u32, String)>,
accept: AcceptHeader,
) -> Result<impl IntoResponse, (StatusCode, String)> {
if app.signing.is_some() {
metrics::METRICS.record_request(403, metrics::FormatLabel::Unresolved);
return Err((StatusCode::FORBIDDEN, "signature required".into()));
}
serve_resize(app, w, h, file, accept).await
}
async fn handle_options(
State(app): State<App>,
Path((options, file)): Path<(String, String)>,
accept: AcceptHeader,
) -> Result<impl IntoResponse, (StatusCode, String)> {
if app.signing.is_some() {
metrics::METRICS.record_request(403, metrics::FormatLabel::Unresolved);
return Err((StatusCode::FORBIDDEN, "signature required".into()));
}
serve_options(app, options, file, accept).await
}
async fn handle_signed_options(
State(app): State<App>,
Path((sig, options, file)): Path<(String, String, String)>,
accept: AcceptHeader,
) -> Result<impl IntoResponse, (StatusCode, String)> {
let Some(signing) = app.signing.as_ref() else {
metrics::METRICS.record_request(404, metrics::FormatLabel::Unresolved);
return Err((StatusCode::NOT_FOUND, "signing not configured".into()));
};
let prefix = app.options_prefix.as_deref().unwrap_or_default();
let path = format!("{prefix}/{options}/{file}");
if !signing.verify(&sig, &path) {
metrics::METRICS.record_request(403, metrics::FormatLabel::Unresolved);
return Err((StatusCode::FORBIDDEN, "invalid signature".into()));
}
serve_options(app, options, file, accept).await
}
async fn serve_options(
app: App,
options: String,
file: String,
accept: AcceptHeader,
) -> Result<Response, (StatusCode, String)> {
let parsed = match options::parse(&options) {
Ok(p) => p,
Err(msg) => {
metrics::METRICS.record_request(400, metrics::FormatLabel::Unresolved);
return Err((StatusCode::BAD_REQUEST, msg));
}
};
let prefix = app.options_prefix.as_deref().unwrap_or_default();
let path = format!("{prefix}/{options}/{file}");
let task = ResizeTask {
w: parsed.width,
h: parsed.height,
file,
quality: parsed.quality,
spec: FormatSpec::Explicit(parsed.format),
path,
};
serve_logged(app, task, accept).await
}
fn validate_source_path(file: &str) -> Result<(), (StatusCode, String)> {
let bad_component = |c: &str| c.is_empty() || c == "." || c == "..";
if file.contains(['\\', '?', '#'])
|| file.bytes().any(|b| b < 0x20 || b == 0x7f)
|| file.split('/').any(bad_component)
{
return Err((StatusCode::BAD_REQUEST, "invalid source path".into()));
}
Ok(())
}
async fn handle_metrics(State(app): State<App>) -> impl IntoResponse {
let inflight = match app.inflight.lock() {
Ok(g) => g.len(),
Err(poisoned) => poisoned.into_inner().len(),
};
let body = metrics::METRICS.render(app.workers, app.cpu_slots.available_permits(), inflight);
([(header::CONTENT_TYPE, "text/plain; version=0.0.4")], body)
}
fn split_format(file: &str) -> Result<(&str, Option<ImageFormat>), (StatusCode, String)> {
let last_start = file.rfind('/').map_or(0, |i| i + 1);
let Some((seg_base, token)) = file[last_start..].rsplit_once('@') else {
return Ok((file, None));
};
if seg_base.is_empty() {
return Ok((file, None));
}
let base = &file[..last_start + seg_base.len()];
match ImageFormat::from_token(token) {
Some(ImageFormat::Avif) if cfg!(not(feature = "avif")) => Err((
StatusCode::BAD_REQUEST,
"avif output is not enabled in this build".into(),
)),
Some(fmt) => Ok((base, Some(fmt))),
None if token == "jxl" => Err((
StatusCode::BAD_REQUEST,
"jxl output is not supported in this build".into(),
)),
None => Ok((file, None)),
}
}
fn negotiate(auto: &[ImageFormat], accept: &AcceptHeader) -> Option<ImageFormat> {
if auto.is_empty() {
return None;
}
let accept = accept.0.as_ref()?.to_str().ok()?;
auto.iter()
.copied()
.find(|f| accept.contains(f.content_type()))
}
enum FormatSpec {
FromSuffix,
Explicit(Option<ImageFormat>),
}
struct ResizeTask {
w: u32,
h: u32,
file: String,
quality: Option<u8>,
spec: FormatSpec,
path: String,
}
async fn serve_resize(
app: App,
w: u32,
h: u32,
file: String,
accept: AcceptHeader,
) -> Result<Response, (StatusCode, String)> {
let path = format!("/resize/{w}/{h}/{file}");
let task = ResizeTask {
w,
h,
file,
quality: None,
spec: FormatSpec::FromSuffix,
path,
};
serve_logged(app, task, accept).await
}
async fn serve_logged(
app: App,
task: ResizeTask,
accept: AcceptHeader,
) -> Result<Response, (StatusCode, String)> {
static REQ_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let req = REQ_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let log_requests = app.log_requests;
let t0 = std::time::Instant::now();
let path = task.path.clone();
let mut fmt = metrics::FormatLabel::Unresolved;
let result = serve_resize_inner(app, task, accept, &mut fmt).await;
let ms = t0.elapsed().as_secs_f64() * 1e3;
match &result {
Err((status, msg)) => {
metrics::METRICS.record_request(status.as_u16(), fmt);
eprintln!("oximg: req={req} status={status} ms={ms:.1} path={path:?} err={msg:?}");
}
Ok(_) => {
metrics::METRICS.record_request(200, fmt);
if log_requests {
eprintln!("oximg: req={req} status=200 ms={ms:.1} path={path:?}");
}
}
}
result
}
async fn serve_resize_inner(
app: App,
task: ResizeTask,
accept: AcceptHeader,
fmt: &mut metrics::FormatLabel,
) -> Result<Response, (StatusCode, String)> {
let ResizeTask {
w,
h,
file,
quality,
spec,
path: _,
} = task;
if (w == 0 && h == 0) || w > 8192 || h > 8192 {
return Err((StatusCode::BAD_REQUEST, "invalid dimensions".into()));
}
validate_source_path(&file)?;
let (file, explicit) = match spec {
FormatSpec::FromSuffix => {
let (base, explicit) = split_format(&file)?;
let base_len = base.len();
let mut file = file;
file.truncate(base_len);
(file, explicit)
}
FormatSpec::Explicit(explicit) => (file, explicit),
};
let target = explicit.or_else(|| negotiate(&app.auto_format, &accept));
*fmt = metrics::FormatLabel::Resolved(target);
let vary_accept = !app.auto_format.is_empty();
let (out, content_type) = singleflight(&app, (w, h, file, target, quality)).await?;
let headers = [
(header::CONTENT_TYPE, content_type),
(header::CACHE_CONTROL, "public, max-age=31536000"),
];
if vary_accept {
Ok((headers, [(header::VARY, "Accept")], out).into_response())
} else {
Ok((headers, out).into_response())
}
}
struct FlightGuard {
map: Arc<FlightMap>,
key: FlightKey,
}
impl Drop for FlightGuard {
fn drop(&mut self) {
let mut map = match self.map.lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
};
map.remove(&self.key);
}
}
async fn singleflight(app: &App, key: FlightKey) -> FlightResult {
for _ in 0..3 {
let leader_tx = {
let mut map = match app.inflight.lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
};
match map.get(&key) {
Some(rx) => Err(rx.clone()),
None => {
let (tx, rx) = watch::channel(None);
map.insert(key.clone(), rx);
Ok(tx)
}
}
};
match leader_tx {
Ok(tx) => {
metrics::METRICS.record_leader();
let guard = FlightGuard {
map: Arc::clone(&app.inflight),
key: key.clone(),
};
let result = process_one(app, &key).await;
drop(guard);
tx.send_replace(Some(result.clone()));
return result;
}
Err(mut rx) => {
metrics::METRICS.record_follower();
loop {
if let Some(result) = rx.borrow_and_update().as_ref() {
return result.clone();
}
if rx.changed().await.is_err() {
break; }
}
}
}
}
Err((
StatusCode::SERVICE_UNAVAILABLE,
"request coalescing failed repeatedly".into(),
))
}
fn encode_upstream_path(file: &str) -> String {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
let mut out = String::with_capacity(file.len());
for &b in file.as_bytes() {
let pchar = b.is_ascii_alphanumeric()
|| matches!(
b,
b'-' | b'.'
| b'_'
| b'~'
| b'!'
| b'$'
| b'&'
| b'\''
| b'('
| b')'
| b'*'
| b'+'
| b','
| b';'
| b'='
| b':'
| b'@'
| b'/'
);
if pchar {
out.push(b as char);
} else {
out.push('%');
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0xf) as usize] as char);
}
}
out
}
fn verify_within_root(
root: &std::path::Path,
path: &std::path::Path,
) -> Result<(), (StatusCode, String)> {
let (Ok(resolved), Ok(root)) = (path.canonicalize(), root.canonicalize()) else {
return Ok(());
};
if resolved.starts_with(&root) {
Ok(())
} else {
Err((StatusCode::NOT_FOUND, "image not found".into()))
}
}
async fn process_one(app: &App, key: &FlightKey) -> FlightResult {
let (w, h, file, output, quality) = key;
let path = app.images_dir.join(file);
let t_queue = std::time::Instant::now();
let permit = app
.cpu_slots
.clone()
.acquire_owned()
.await
.expect("semaphore closed");
metrics::METRICS.observe_queue(t_queue.elapsed().as_secs_f64());
let unbounded = |d: u32| if d == 0 { u32::MAX } else { d };
let mut params = pipeline::Params {
max_width: unbounded(*w),
max_height: unbounded(*h),
quality: app.quality,
encoder: app.encoder,
parallel: app.resize_threads,
output: *output,
..Default::default()
};
if let Some(q) = quality {
params.quality = f32::from(*q);
params.webp_quality = Some(f32::from(*q));
#[cfg(feature = "avif")]
{
params.avif_quality = Some(*q);
}
}
let source_url = app
.source_base
.as_ref()
.map(|base| format!("{base}/{}", encode_upstream_path(file)));
let images_root = Arc::clone(&app.images_dir);
let remote = app.source_base.is_some();
let t_process = std::time::Instant::now();
type Processed = Result<(Vec<u8>, ImageFormat), pipeline::Error>;
let out = tokio::task::spawn_blocking(move || -> Result<Processed, (StatusCode, String)> {
let _permit = permit; match source_url {
Some(url) => Ok(pipeline::process_url(&url, ¶ms)),
None => {
verify_within_root(&images_root, &path)?;
Ok(pipeline::process_path(&path, ¶ms))
}
}
})
.await
.map_err(|e| {
eprintln!("oximg: error status=500 file={file:?} panic={e}");
(
StatusCode::INTERNAL_SERVER_ERROR,
"image processing failed".to_string(),
)
})?
.inspect(|_| metrics::METRICS.observe_process(t_process.elapsed().as_secs_f64()))?
.map_err(|e| {
use pipeline::ErrorKind;
if remote {
metrics::METRICS.record_upstream(match e.kind() {
ErrorKind::SourceNotFound => "not_found",
ErrorKind::UpstreamTimeout => "timeout",
ErrorKind::Upstream => "error",
_ => "ok",
});
}
match e.kind() {
ErrorKind::SourceNotFound => (StatusCode::NOT_FOUND, "image not found".to_string()),
ErrorKind::SourceTooLarge => (
StatusCode::PAYLOAD_TOO_LARGE,
"source image exceeds the configured size limit".to_string(),
),
ErrorKind::SourceUnreadable => {
eprintln!("oximg: error status=500 file={file:?} err={e:#}");
(
StatusCode::INTERNAL_SERVER_ERROR,
"source could not be read".to_string(),
)
}
ErrorKind::Upstream => {
eprintln!("oximg: error status=502 file={file:?} err={e:#}");
(
StatusCode::BAD_GATEWAY,
"upstream image fetch failed".to_string(),
)
}
ErrorKind::UpstreamTimeout => {
eprintln!("oximg: error status=504 file={file:?} err={e:#}");
(
StatusCode::GATEWAY_TIMEOUT,
"upstream image fetch timed out".to_string(),
)
}
ErrorKind::Internal => {
eprintln!("oximg: error status=500 file={file:?} err={e:#}");
(
StatusCode::INTERNAL_SERVER_ERROR,
"internal image-processing error".to_string(),
)
}
ErrorKind::Undecodable => (StatusCode::UNPROCESSABLE_ENTITY, e.to_string()),
_ => {
eprintln!("oximg: error status=500 file={file:?} err={e:#}");
(
StatusCode::INTERNAL_SERVER_ERROR,
"internal image-processing error".to_string(),
)
}
}
})?;
if remote {
metrics::METRICS.record_upstream("ok");
}
let (bytes, format) = out;
Ok((Bytes::from(bytes), format.content_type()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base64url_decodes_known_vectors() {
assert_eq!(
base64url_decode("aGVsbG8").as_deref(),
Some(b"hello".as_slice())
);
assert_eq!(
base64url_decode("aGVsbG8=").as_deref(),
Some(b"hello".as_slice())
);
assert_eq!(
base64url_decode("-_8").as_deref(),
Some([0xfb, 0xff].as_slice())
);
assert_eq!(base64url_decode("bad!"), None);
}
fn test_signing() -> Signing {
let hex = |s: &str| -> Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
.collect()
};
Signing {
key: hex(&"deadbeef".repeat(8)),
salt: hex(&"cafebabe".repeat(8)),
}
}
#[test]
fn signing_config_fails_closed() {
assert!(
Signing::from_values(Some("deadbeef"), Some("cafebabe"))
.unwrap()
.is_some()
);
assert!(Signing::from_values(None, None).unwrap().is_none());
assert!(
Signing::from_values(Some(""), Some(" "))
.unwrap()
.is_none()
);
assert!(Signing::from_values(Some("xyz!"), Some("cafebabe")).is_err());
assert!(Signing::from_values(Some("abc"), Some("cafebabe")).is_err()); assert!(Signing::from_values(Some("xyz!"), Some("also-bad")).is_err());
assert!(Signing::from_values(Some("deadbeef"), None).is_err());
assert!(Signing::from_values(None, Some("cafebabe")).is_err());
}
#[test]
fn signature_verifies_precomputed_vector() {
let sig = "lrio_2A_EDYOogJybA7hm-AfXAr5YhjYhXwJ7_K93-U";
assert!(test_signing().verify(sig, "/resize/100/100/x.jpg"));
}
#[test]
fn split_format_token_grammar() {
assert_eq!(split_format("photo.jpg"), Ok(("photo.jpg", None)));
assert_eq!(split_format("photo@2x.jpg"), Ok(("photo@2x.jpg", None)));
assert_eq!(
split_format("photo.jpg@bogus"),
Ok(("photo.jpg@bogus", None))
);
assert_eq!(split_format("@webp"), Ok(("@webp", None)));
for (token, fmt) in [
("jpg", ImageFormat::Jpeg),
("jpeg", ImageFormat::Jpeg),
("png", ImageFormat::Png),
("webp", ImageFormat::Webp),
] {
assert_eq!(
split_format(&format!("photo.png@{token}")),
Ok(("photo.png", Some(fmt))),
"@{token}"
);
}
assert_eq!(
split_format("photo.jpg@jxl").unwrap_err().0,
StatusCode::BAD_REQUEST
);
#[cfg(feature = "avif")]
assert_eq!(
split_format("photo.jpg@avif"),
Ok(("photo.jpg", Some(ImageFormat::Avif)))
);
#[cfg(not(feature = "avif"))]
assert_eq!(
split_format("photo.jpg@avif").unwrap_err().0,
StatusCode::BAD_REQUEST
);
}
#[test]
fn source_path_validation_table() {
let ok = |p: &str| validate_source_path(p).is_ok();
assert!(ok("photo.jpg"));
assert!(ok("albums/2026/photo.jpg"));
assert!(ok("attachment/public_image/uuid-1/uuid-2.png"));
assert!(ok("my..file.jpg"));
assert!(ok("dir.d/...jpg"));
assert!(ok("ver@2/photo.jpg"));
assert!(!ok(".."));
assert!(!ok("../secret.jpg"));
assert!(!ok("a/../secret.jpg"));
assert!(!ok("a/b/.."));
assert!(!ok("./a.jpg"));
assert!(!ok("a/./b.jpg"));
assert!(!ok("/etc/passwd"));
assert!(!ok("a//b.jpg"));
assert!(!ok("a/b/"));
assert!(!ok("a\\b.jpg"));
assert!(!ok("a/b?.jpg"));
assert!(!ok("a/b#.jpg"));
assert!(!ok("a/b\x00.jpg"));
assert!(!ok("a/b\x7f.jpg"));
}
#[test]
fn upstream_path_encoding() {
assert_eq!(encode_upstream_path("photo.jpg"), "photo.jpg");
assert_eq!(
encode_upstream_path("albums/2026/photo@2x.jpg"),
"albums/2026/photo@2x.jpg"
);
assert_eq!(encode_upstream_path("a b.jpg"), "a%20b.jpg");
assert_eq!(encode_upstream_path("a%2e%2e/x.jpg"), "a%252e%252e/x.jpg");
assert_eq!(encode_upstream_path("café.jpg"), "caf%C3%A9.jpg");
}
#[test]
fn split_format_on_nested_paths() {
assert_eq!(
split_format("a/b/photo.png@webp"),
Ok(("a/b/photo.png", Some(ImageFormat::Webp)))
);
assert_eq!(
split_format("ver@2/photo.jpg"),
Ok(("ver@2/photo.jpg", None))
);
assert_eq!(
split_format("ver@webp/photo.jpg"),
Ok(("ver@webp/photo.jpg", None))
);
assert_eq!(split_format("dir/@webp"), Ok(("dir/@webp", None)));
#[cfg(feature = "avif")]
assert_eq!(
split_format("a/b/photo.jpg@avif"),
Ok(("a/b/photo.jpg", Some(ImageFormat::Avif)))
);
#[cfg(not(feature = "avif"))]
assert_eq!(
split_format("a/b/photo.jpg@avif").unwrap_err().0,
StatusCode::BAD_REQUEST
);
}
#[test]
fn negotiate_picks_first_acceptable() {
let auto = [ImageFormat::Avif, ImageFormat::Webp];
let accept = |v: &str| AcceptHeader(Some(HeaderValue::from_str(v).unwrap()));
assert_eq!(
negotiate(&auto, &accept("image/avif,image/webp,*/*")),
Some(ImageFormat::Avif)
);
assert_eq!(
negotiate(&auto, &accept("image/webp,*/*")),
Some(ImageFormat::Webp)
);
assert_eq!(negotiate(&auto, &accept("image/apng,*/*")), None);
assert_eq!(negotiate(&auto, &AcceptHeader(None)), None);
assert_eq!(negotiate(&[], &accept("image/webp")), None);
}
#[test]
fn signature_rejects_wrong_path_and_garbage() {
let s = test_signing();
let sig = "lrio_2A_EDYOogJybA7hm-AfXAr5YhjYhXwJ7_K93-U";
assert!(!s.verify(sig, "/resize/100/101/x.jpg"));
assert!(!s.verify("AAAA", "/resize/100/100/x.jpg"));
assert!(!s.verify("!!!not-base64!!!", "/resize/100/100/x.jpg"));
assert!(!s.verify("", "/resize/100/100/x.jpg"));
}
}