pub(crate) mod error;
pub(crate) mod html;
pub(crate) mod http;
pub(crate) mod json;
pub(crate) mod macros;
pub(crate) mod model_state;
#[cfg(test)]
pub(crate) mod test;
pub(crate) mod tree_sitter;
pub(crate) mod upload_bridge;
use directories::UserDirs;
use regex::Regex;
use regex::RegexBuilder;
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use anyhow::{Context as _, Result};
use base64::{Engine as _, engine::general_purpose::STANDARD};
use rand::RngExt;
#[cfg(feature = "voice-tests")]
use rand::SeedableRng;
use sha2::{Digest, Sha256};
use std::fs::File;
use std::io::Read;
use tracing::error;
pub trait UnwrapPoison {
type Inner;
#[must_use]
fn unwrap_poison(self) -> Self::Inner;
}
impl<T> UnwrapPoison for Result<T, std::sync::PoisonError<T>> {
type Inner = T;
fn unwrap_poison(self) -> T {
self.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
const MEDIA_MARKER_PATTERN: &str = r"\[(?P<kind>IMAGE|AUDIO|VIDEO):(?P<path>[^\]]+)\]";
pub(crate) static MEDIA_MARKER_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(MEDIA_MARKER_PATTERN).expect("MEDIA_MARKER_RE must compile"));
pub(crate) static TELEGRAM_MEDIA_MARKER_RE: LazyLock<Regex> = LazyLock::new(|| {
RegexBuilder::new(MEDIA_MARKER_PATTERN)
.case_insensitive(true)
.build()
.expect("TELEGRAM_MEDIA_MARKER_RE must compile")
});
#[must_use]
pub(crate) fn parse_media_marker<'h>(caps: ®ex::Captures<'h>) -> (&'h str, &'h str) {
let kind = caps
.name("kind")
.expect("parse_media_marker: expected 'kind' group")
.as_str();
let path = caps
.name("path")
.expect("parse_media_marker: expected 'path' group")
.as_str();
(kind, path)
}
#[must_use]
pub fn truncate(input: &str, max_chars: usize) -> String {
match input.char_indices().nth(max_chars) {
Some((idx, _)) => format!("{}…", input[..idx].trim_end()),
None => input.to_string(),
}
}
#[must_use]
pub(crate) fn truncate_bytes(s: &str, max_bytes: usize) -> &str {
if s.len() <= max_bytes {
s
} else {
&s[..s.floor_char_boundary(max_bytes)]
}
}
#[must_use]
pub(crate) fn none_if_empty(s: &str) -> Option<String> {
if s.is_empty() {
None
} else {
Some(s.to_string())
}
}
#[must_use]
pub(crate) fn is_word_char(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
#[must_use]
#[expect(clippy::cast_possible_truncation)]
pub(crate) fn unix_millis() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
#[must_use]
pub(crate) fn env_duration_secs(name: &str, default_secs: u64) -> std::time::Duration {
let secs = std::env::var(name)
.ok()
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(default_secs);
std::time::Duration::from_secs(secs)
}
#[must_use]
pub(crate) fn hex_string(bytes: &[u8]) -> String {
bytes
.iter()
.fold(String::with_capacity(bytes.len() * 2), |mut acc, b| {
use std::fmt::Write;
let _ = write!(acc, "{b:02x}");
acc
})
}
pub(crate) fn verify_sha256(path: &Path, expected: &str) -> Result<()> {
if expected.is_empty() {
return Ok(()); }
let mut hasher = Sha256::new();
let mut file = File::open(path)
.with_context(|| format!("Failed to open {} for SHA256 verification", path.display()))?;
let mut buf = vec![0u8; 65536]; loop {
let n = file.read(&mut buf)?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
}
let actual = hex_string(&hasher.finalize());
if actual != expected {
anyhow::bail!(
"SHA256 mismatch for {}: expected {expected}, got {actual}",
path.display()
);
}
Ok(())
}
#[must_use]
pub(crate) fn expand_tilde(path: &str) -> PathBuf {
if let Some(stripped) = path.strip_prefix('~') {
let home = std::env::var("HOME").or_else(|_| std::env::var("USERPROFILE"));
if let Ok(home) = home {
return PathBuf::from(home).join(stripped.trim_start_matches('/'));
}
}
PathBuf::from(path)
}
#[must_use]
pub(crate) fn models_dir() -> Option<PathBuf> {
crate::config::CONFIG
.try_storage_root()
.map(|root| root.join("models"))
}
#[must_use]
pub(crate) fn with_block_in_place<T>(f: impl FnOnce() -> T) -> T {
if let Ok(handle) = tokio::runtime::Handle::try_current()
&& handle.runtime_flavor() == tokio::runtime::RuntimeFlavor::MultiThread
{
return tokio::task::block_in_place(f);
}
f()
}
#[must_use]
pub fn summarize_args(args: &serde_json::Value) -> String {
match args {
serde_json::Value::Object(map) => {
let parts: Vec<String> = map
.iter()
.map(|(k, v)| {
let val = match v {
serde_json::Value::String(s) => truncate(s, 80),
other => truncate(&other.to_string(), 80),
};
format!("{k}: {val}")
})
.collect();
parts.join(", ")
}
other => truncate(&other.to_string(), 120),
}
}
#[must_use]
pub fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
if let Some(msg) = payload.downcast_ref::<&str>() {
msg.to_string()
} else if let Some(msg) = payload.downcast_ref::<String>() {
msg.clone()
} else {
"unknown panic".to_string()
}
}
pub(crate) fn log_join_failures(
results: Vec<Result<(), tokio::task::JoinError>>,
panic_log: &str,
cancelled_log: &str,
) {
for result in results {
if let Err(e) = result {
if e.is_panic() {
let payload = e.into_panic();
error!(error = %panic_message(&*payload), "{panic_log}");
} else {
error!("{cancelled_log}");
}
}
}
}
pub(crate) const FAILURE_DETAIL_CAP: usize = 24_000;
#[must_use]
pub fn truncate_sandwich(s: &str, max_bytes: usize, label: &str) -> String {
if s.len() <= max_bytes {
return s.to_string();
}
let head_bytes = max_bytes * 2 / 3;
let tail_bytes = max_bytes / 3;
let head_end = s.floor_char_boundary(head_bytes);
let tail_start = s.floor_char_boundary(s.len().saturating_sub(tail_bytes));
if head_end < tail_start {
let omitted = s[head_end..tail_start].len();
format!(
"{}... ({} bytes omitted at {label} truncation)\n{}",
&s[..head_end],
omitted,
&s[tail_start..]
)
} else {
let boundary = s.floor_char_boundary(max_bytes);
let mut out = s[..boundary].to_string();
let _ = std::fmt::Write::write_fmt(
&mut out,
format_args!("\n... [{label} truncated at {max_bytes} bytes]"),
);
out
}
}
pub(crate) const TOOL_OUTPUT_BUDGET_BYTES: usize = 5_000;
#[must_use]
pub fn truncate_tool_output(output: &str) -> String {
truncate_sandwich(output, TOOL_OUTPUT_BUDGET_BYTES, "tool output")
}
pub(crate) async fn local_image_to_data_uri(path: &std::path::Path) -> anyhow::Result<String> {
let bytes = tokio::fs::read(path).await?;
let mime = mime_for_extension(path);
Ok(format!("data:{mime};base64,{}", STANDARD.encode(&bytes)))
}
const MAX_REFERENCE_INPUT_BYTES: u64 = 50 * 1024 * 1024;
pub(crate) const MAX_TOTAL_REFERENCE_INPUT_BYTES: u64 = 100 * 1024 * 1024;
const REFERENCE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_mins(1);
const JPEG_DATA_URI_PREFIX: &str = "data:image/jpeg;base64,";
const REFERENCE_COMPRESSION_LADDER: &[(f32, u8)] = &[
(1.0, 85),
(1.0, 70),
(1.0, 55),
(0.75, 70),
(0.5, 70),
(0.5, 45),
];
pub(crate) struct ReferenceImage {
data_uri: String,
source_bytes: Vec<u8>,
next_step: usize,
released: bool,
}
impl ReferenceImage {
#[must_use]
pub(crate) fn data_uri(&self) -> &str {
&self.data_uri
}
#[must_use]
pub(crate) fn has_compression_left(&self) -> bool {
!self.released && self.next_step < REFERENCE_COMPRESSION_LADDER.len()
}
pub(crate) fn compress_more(&mut self) -> anyhow::Result<()> {
if self.released {
anyhow::bail!("Reference image is final — its source bytes were released");
}
if self.next_step >= REFERENCE_COMPRESSION_LADDER.len() {
anyhow::bail!("Reference image compression ladder exhausted");
}
let out =
with_block_in_place(|| compress_reference_step(&self.source_bytes, self.next_step))?;
self.next_step += 1;
self.data_uri = format!("{JPEG_DATA_URI_PREFIX}{}", STANDARD.encode(&out));
Ok(())
}
pub(crate) fn release_source_bytes(&mut self) {
self.released = true;
self.source_bytes = Vec::new();
}
}
pub(crate) async fn load_reference_image(
path: &std::path::Path,
max_bytes: u64,
) -> anyhow::Result<ReferenceImage> {
let meta = tokio::fs::metadata(path)
.await
.with_context(|| format!("Failed to access reference image {}", path.display()))?;
if !meta.is_file() {
anyhow::bail!(
"Reference image {} is not a regular file — refusing to read it.",
path.display(),
);
}
if meta.len() > MAX_REFERENCE_INPUT_BYTES {
anyhow::bail!(
"Reference image {} is limited to 50 MB, got {} bytes. Use a smaller image.",
path.display(),
meta.len(),
);
}
check_reference_extension(path)?;
let bytes = tokio::time::timeout(REFERENCE_READ_TIMEOUT, tokio::fs::read(path))
.await
.map_err(|_| anyhow::anyhow!("Timed out reading reference image {}", path.display()))?
.map_err(|e| anyhow::anyhow!("Failed to read reference image {}: {e}", path.display()))?;
let format = sniff_reference_content(path, &bytes)?;
if bytes.len() as u64 <= max_bytes {
return Ok(ReferenceImage {
data_uri: format!(
"data:{};base64,{}",
format.to_mime_type(),
STANDARD.encode(&bytes)
),
source_bytes: bytes,
next_step: 0,
released: false,
});
}
let mut step = 0;
loop {
if step >= REFERENCE_COMPRESSION_LADDER.len() {
#[expect(clippy::cast_precision_loss)]
let cap = format!(
"{} bytes ({:.1} MB)",
max_bytes,
max_bytes as f64 / 1_000_000.0
);
anyhow::bail!(
"Reference image {} is {} bytes and cannot be compressed under the {} cap \
after {} bounded steps. Use a smaller or simpler image.",
path.display(),
bytes.len(),
cap,
step,
);
}
let out = with_block_in_place(|| compress_reference_step(&bytes, step))?;
if out.len() as u64 <= max_bytes {
return Ok(ReferenceImage {
data_uri: format!("{JPEG_DATA_URI_PREFIX}{}", STANDARD.encode(&out)),
source_bytes: bytes,
next_step: step + 1,
released: false,
});
}
step += 1;
}
}
pub(crate) async fn check_reference_total_input(paths: &[PathBuf]) -> anyhow::Result<()> {
let mut total: u64 = 0;
for path in paths {
total += tokio::fs::metadata(path)
.await
.with_context(|| format!("Failed to access reference image {}", path.display()))?
.len();
}
if total > MAX_TOTAL_REFERENCE_INPUT_BYTES {
anyhow::bail!(
"Combined reference images are limited to 100 MB, got {total} bytes total. \
Use fewer or smaller images.",
);
}
Ok(())
}
fn check_reference_extension(path: &Path) -> anyhow::Result<()> {
let ext = path
.extension()
.and_then(|e| e.to_str())
.map(str::to_ascii_lowercase);
if !matches!(ext.as_deref(), Some("png" | "jpg" | "jpeg" | "webp")) {
anyhow::bail!(
"Reference image {}: unsupported format ({}). Only PNG, JPEG, or WebP \
images are accepted.",
path.display(),
ext.as_deref().unwrap_or("unknown extension"),
);
}
Ok(())
}
fn sniff_reference_content(path: &Path, bytes: &[u8]) -> anyhow::Result<image::ImageFormat> {
let format = image::guess_format(bytes).map_err(|_| {
anyhow::anyhow!(
"Reference image {}: content is not a decodable image (PNG/JPEG/WebP). \
HEIC/HEIF and other unsupported formats are not accepted.",
path.display(),
)
})?;
match format {
image::ImageFormat::Png | image::ImageFormat::Jpeg | image::ImageFormat::WebP => Ok(format),
other => anyhow::bail!(
"Reference image {}: unsupported image format ({other:?}). Only PNG, JPEG, \
or WebP images are accepted.",
path.display(),
),
}
}
fn compress_reference_step(bytes: &[u8], step: usize) -> anyhow::Result<Vec<u8>> {
use image::GenericImageView;
let (scale, quality) = REFERENCE_COMPRESSION_LADDER[step];
let mut img = image::load_from_memory(bytes).context("Failed to decode reference image")?;
if let Some(orientation) = exif_orientation(bytes) {
img.apply_orientation(orientation);
}
let img = if scale < 1.0 {
let (w, h) = img.dimensions();
#[expect(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
let nw = (w as f32 * scale).round().max(1.0) as u32;
#[expect(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
let nh = (h as f32 * scale).round().max(1.0) as u32;
img.resize(nw, nh, image::imageops::FilterType::Triangle)
} else {
img
};
let rgb = flatten_alpha_onto_white(&img);
let mut out = Vec::new();
{
let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut out, quality);
encoder
.encode(
rgb.as_raw(),
rgb.width(),
rgb.height(),
image::ExtendedColorType::Rgb8,
)
.context("Failed to encode compressed reference image")?;
}
Ok(out)
}
fn exif_orientation(bytes: &[u8]) -> Option<image::metadata::Orientation> {
use image::ImageDecoder;
let reader = image::ImageReader::new(std::io::Cursor::new(bytes))
.with_guessed_format()
.ok()?;
let mut decoder = reader.into_decoder().ok()?;
match decoder.orientation().ok()? {
image::metadata::Orientation::NoTransforms => None,
orientation => Some(orientation),
}
}
fn flatten_alpha_onto_white(img: &image::DynamicImage) -> image::RgbImage {
if !img.color().has_alpha() {
return img.to_rgb8();
}
let rgba = img.to_rgba8();
let mut rgb = image::RgbImage::new(rgba.width(), rgba.height());
for (x, y, px) in rgba.enumerate_pixels() {
let [r, g, b, a] = px.0;
let alpha = f32::from(a) / 255.0;
#[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let blend = |c: u8| (f32::from(c) * alpha + 255.0 * (1.0 - alpha)).round() as u8;
rgb.put_pixel(x, y, image::Rgb([blend(r), blend(g), blend(b)]));
}
rgb
}
pub(crate) const VIDEO_EXTENSIONS: &[&str] = &["mp4", "mov", "mkv", "avi", "webm"];
pub(crate) const TRANSCRIBABLE_VIDEO_EXTENSIONS: &[&str] = &["mp4", "mpeg", "mov", "webm"];
pub(crate) const IMAGE_EXTENSIONS: &[&str] = &["jpg", "jpeg", "png", "webp", "heic", "heif"];
pub(crate) const TELEGRAM_FILES_DIR: &str = "mahbot_telegram_files";
#[must_use]
pub(crate) fn has_extension(path: &std::path::Path, table: &[&str]) -> bool {
path.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| table.contains(&ext.to_ascii_lowercase().as_str()))
}
#[must_use]
pub(crate) fn is_video_extension(path: &std::path::Path) -> bool {
has_extension(path, VIDEO_EXTENSIONS)
}
#[must_use]
pub(crate) fn is_transcribable_video(path: &std::path::Path) -> bool {
has_extension(path, TRANSCRIBABLE_VIDEO_EXTENSIONS)
}
#[must_use]
pub(crate) fn is_image_extension(path: &std::path::Path) -> bool {
has_extension(path, IMAGE_EXTENSIONS)
}
#[must_use]
pub(crate) fn is_http_url(target: &str) -> bool {
target.starts_with("http://") || target.starts_with("https://")
}
pub(crate) fn mime_for_extension(path: &std::path::Path) -> &'static str {
match path
.extension()
.and_then(|e| e.to_str())
.map(str::to_ascii_lowercase)
.as_deref()
{
Some("png") => "image/png",
Some("jpg" | "jpeg") => "image/jpeg",
Some("webp") => "image/webp",
Some("heic") => "image/heic",
Some("heif") => "image/heif",
Some("mp4") => "video/mp4",
Some("mpeg") => "video/mpeg",
Some("mov") => "video/quicktime",
Some("webm") => "video/webm",
_ => "application/octet-stream",
}
}
#[must_use]
pub(crate) fn file_name_or_path(path: &str) -> &str {
Path::new(path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(path)
}
#[must_use]
pub(crate) fn strip_ansi_escapes(input: &str) -> String {
static RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"\x1B\[[0-9;]*[a-zA-Z]|\x1B\][0-9;]*[^\x1B]*\x1B\\|\x1B[\(\)\[\]KM]|\x1B\][0-9;]*\x07",
)
.unwrap()
});
RE.replace_all(input, "").to_string()
}
static SENSITIVE_KV_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r#"(?i)(token|api[_-]?key|password|secret|user[_-]?key|bearer|credential)["']?\s*[:=]\s*(?:"([^"]{8,})"|'([^']{8,})'|([a-zA-Z0-9_\-\./+=]{8,}))"#).expect("hardcoded regex is valid")
});
#[must_use]
pub fn scrub_credentials(input: &str) -> String {
SENSITIVE_KV_REGEX
.replace_all(input, |caps: ®ex::Captures| {
let full_match = &caps[0];
let key = &caps[1];
let val = caps
.get(2)
.or(caps.get(3))
.or(caps.get(4))
.map_or("", |m| m.as_str());
debug_assert!(val.len() >= 8, "regex guarantees values >= 8 chars");
let prefix = val
.char_indices()
.nth(4)
.map_or(val, |(byte_idx, _)| &val[..byte_idx]);
let quote = if caps.get(2).is_some() {
Some('"')
} else if caps.get(3).is_some() {
Some('\'')
} else {
None
};
let redacted = format!("{prefix}*[REDACTED]");
if full_match.contains(':') {
match quote {
Some('"') => format!("\"{key}\": \"{redacted}\""),
Some('\'') => format!("{key}: '{redacted}'"),
_ => format!("{key}: {redacted}"),
}
} else {
match quote {
Some('"') => format!("{key}=\"{redacted}\""),
Some('\'') => format!("{key}='{redacted}'"),
_ => format!("{key}={redacted}"),
}
}
})
.to_string()
}
#[must_use]
pub fn extract_provider_error_detail(error: &serde_json::Value) -> Option<String> {
if let serde_json::Value::String(s) = error
&& !s.trim().is_empty()
{
return Some(s.clone());
}
match error.get("error") {
Some(serde_json::Value::String(s)) if !s.trim().is_empty() => return Some(s.clone()),
Some(inner) if !inner.is_null() => {
if let Some(detail) = extract_provider_error_detail(inner) {
return Some(detail);
}
}
_ => {}
}
let text = |v: Option<&serde_json::Value>| -> Option<String> {
v.and_then(|v| v.as_str())
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
};
if let Some(msg) = text(error.get("message")) {
return Some(msg);
}
if let Some(raw) = text(error.get("metadata").and_then(|m| m.get("raw"))) {
if let Ok(raw_json) = serde_json::from_str::<serde_json::Value>(&raw)
&& let Some(detail) = extract_provider_error_detail(&raw_json)
{
return Some(detail);
}
return Some(raw);
}
if let Some(code) = text(
error
.get("metadata")
.and_then(|m| m.get("provider_error_code")),
) {
return Some(code);
}
if let Some(code) = text(error.get("code")) {
return Some(code);
}
text(error.get("type"))
}
pub(crate) fn is_executable(path: &Path) -> bool {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
path.is_file() && std::fs::metadata(path).is_ok_and(|m| m.permissions().mode() & 0o111 != 0)
}
#[cfg(not(unix))]
{
path.is_file()
&& path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("exe"))
}
}
#[must_use]
pub(crate) fn cargo_bin_dir() -> Option<PathBuf> {
if let Ok(cargo_home) = std::env::var("CARGO_HOME")
&& !cargo_home.is_empty()
{
return Some(PathBuf::from(cargo_home).join("bin"));
}
let dirs = UserDirs::new()?;
Some(dirs.home_dir().join(".cargo").join("bin"))
}
#[must_use]
pub fn unquote_c_style(raw: &str) -> Option<String> {
if let Some(inner) = raw.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
unescape_c_style(inner)
} else {
Some(raw.to_string())
}
}
fn unescape_c_style(input: &str) -> Option<String> {
let mut result = String::with_capacity(input.len());
let bytes = input.as_bytes();
let mut i: usize = 0;
while i < bytes.len() {
if bytes[i] == b'\\' {
i += 1; if i >= bytes.len() {
tracing::warn!(
input = %input,
"unescape_c_style: dangling backslash at end of string"
);
return None;
}
match bytes[i] {
b'"' => result.push('"'),
b'\\' => result.push('\\'),
b't' => result.push('\t'),
b'n' => result.push('\n'),
b'a' => result.push('\x07'),
b'b' => result.push('\x08'),
b'f' => result.push('\x0c'),
b'r' => result.push('\r'),
b'v' => result.push('\x0b'),
b'0'..=b'3' => {
let digits_start = i;
i += 1;
let mut digit_count = 1;
while digit_count < 3 && i < bytes.len() && (b'0'..=b'7').contains(&bytes[i]) {
i += 1;
digit_count += 1;
}
let octal_str = std::str::from_utf8(&bytes[digits_start..i]).ok()?;
let Ok(byte_val) = u8::from_str_radix(octal_str, 8) else {
tracing::warn!(
input = %input, octal = %octal_str,
"unescape_c_style: invalid octal escape"
);
return None;
};
result.push_str(&String::from_utf8_lossy(&[byte_val]));
continue; }
b'4'..=b'7' => {
if i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit() {
tracing::warn!(
input = %input,
ch = %(bytes[i] as char),
"unescape_c_style: invalid octal prefix \\4–\\7 followed by digit"
);
return None;
}
result.push(bytes[i] as char);
}
_ => {
tracing::warn!(
input = %input,
ch = %(bytes[i] as char),
"unescape_c_style: unrecognized escape sequence"
);
return None;
}
}
} else {
result.push(bytes[i] as char);
}
i += 1;
}
Some(result)
}
#[must_use]
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
pub(crate) fn resample_audio(samples: &[f32], from_rate: u32, to_rate: u32) -> Vec<f32> {
if from_rate == to_rate {
return samples.to_vec();
}
let ratio = f64::from(to_rate) / f64::from(from_rate);
let output_len = (samples.len() as f64 * ratio).ceil() as usize;
let filtered: Vec<f32> = if from_rate > to_rate && samples.len() >= 3 {
let mut out = Vec::with_capacity(samples.len());
out.push(samples[0] * 0.75 + samples[1] * 0.25);
for i in 1..samples.len() - 1 {
out.push(samples[i - 1] * 0.25 + samples[i] * 0.5 + samples[i + 1] * 0.25);
}
out.push(samples[samples.len() - 2] * 0.25 + samples[samples.len() - 1] * 0.75);
out
} else {
samples.to_vec()
};
let mut output = Vec::with_capacity(output_len);
for i in 0..output_len {
let src_pos = i as f64 / ratio;
let src_idx = src_pos as usize;
let frac = src_pos - src_idx as f64;
if src_idx + 1 < filtered.len() {
output.push(
(f64::from(filtered[src_idx]) * (1.0 - frac)
+ f64::from(filtered[src_idx + 1]) * frac) as f32,
);
} else if src_idx < filtered.len() {
output.push(filtered[src_idx]);
} else {
output.push(0.0);
}
}
output
}
#[cfg(test)]
mod truncate_tests {
use super::*;
#[test]
fn passthrough_under_limit() {
let input = "hello world";
let result = truncate_sandwich(input, 5_000, "test");
assert_eq!(
result, input,
"should pass through unchanged when under limit"
);
}
#[test]
fn passthrough_at_exact_limit() {
let input = "a".repeat(5_000);
assert_eq!(input.len(), 5_000);
let result = truncate_sandwich(&input, 5_000, "test");
assert_eq!(result, input, "exact limit should pass through unchanged");
}
#[test]
fn sandwich_just_over_limit() {
let input = "x".repeat(5_001);
let result = truncate_sandwich(&input, 5_000, "test");
assert!(
result.starts_with("xxx"),
"head portion should be preserved"
);
assert!(
result.contains("bytes omitted at test truncation"),
"should contain the omission marker"
);
assert!(result.ends_with('x'), "tail should contain input suffix");
}
#[test]
fn sandwich_large_input() {
let line = "hello world\n".repeat(200_000);
assert!(line.len() > 1_048_576, "input should exceed 1MB");
let result = truncate_sandwich(&line, 1_048_576, "output");
assert!(result.len() < line.len(), "should truncate");
assert!(
result.contains("bytes omitted at output truncation"),
"should contain label in omission marker"
);
assert!(
result.starts_with("hello world"),
"head should be preserved"
);
let last_line = result.lines().last().unwrap_or("");
assert_eq!(last_line, "hello world", "tail should be preserved");
}
#[test]
fn sandwich_preserves_utf8_boundaries() {
let mut input = String::new();
input.push_str(&"x".repeat(3_329));
input.push('🐱'); input.push_str(&"y".repeat(20_000));
let result = truncate_sandwich(&input, 5_000, "test");
assert!(
result.contains('🐱'),
"multibyte char at boundary should survive intact"
);
}
#[test]
fn sandwich_line_boundaries_intact() {
let line = "hello world!\n".repeat(100_000);
let result = truncate_sandwich(&line, 500_000, "test");
assert!(result.len() < line.len(), "should truncate");
for l in result.lines().filter(|l| !l.starts_with("...")) {
assert!(
!l.contains("hello world!hello"),
"lines should not be concatenated"
);
}
}
#[test]
fn custom_label_appears_in_marker() {
let input = "x".repeat(10_000);
let result = truncate_sandwich(&input, 5_000, "my custom label");
assert!(
result.contains("bytes omitted at my custom label truncation"),
"custom label should appear verbatim in marker"
);
}
#[test]
fn empty_label() {
let input = "x".repeat(10_000);
let result = truncate_sandwich(&input, 5_000, "");
assert!(
result.contains("bytes omitted at truncation"),
"empty label should still produce coherent marker"
);
}
#[test]
fn truncate_tool_output_appends_correct_label() {
let input = "abc".repeat(2_000); let result = truncate_tool_output(&input);
assert!(result.len() < input.len(), "should truncate");
assert!(
result.contains("bytes omitted at tool output truncation"),
"should use 'tool output' label"
);
assert!(result.starts_with("abcabc"), "head should be preserved");
}
}
#[cfg(test)]
mod scrub_tests {
use super::scrub_credentials;
#[test]
fn scrub_redacts_credentials() {
const CASES: &[(&str, &str, &str, &str)] = &[
(
"alphanumeric unquoted value",
"API_KEY=sk-1234567890abcdef",
"1234567890abcdef",
"API_KEY=sk-1",
),
(
"Base64 unquoted value with plus and slash",
"api_key=u2FsdGVkX1+h/wZ/L3Y+Q==",
"u2FsdGVkX1+h/wZ/L3Y+Q==",
"api_key=u2Fs",
),
(
"double-quoted value with colon separator",
r#"token: "abcdefgh1234567890""#,
"1234567890",
"",
),
(
"bearer colon-separated value",
"bearer: eyJhbGciOiJIUzI1NiJ9",
"eyJhbG",
"",
),
(
"hyphen-key variant",
"user-key=abcdefgh12345678",
"12345678",
"user-key=abcd",
),
];
for &(name, input, not_contains, prefix) in CASES {
let out = scrub_credentials(input);
assert!(out.contains("[REDACTED]"), "{name}: should redact: {out}");
if !not_contains.is_empty() {
assert!(
!out.contains(not_contains),
"{name}: should not leak value: {out}"
);
}
if !prefix.is_empty() {
assert!(out.starts_with(prefix), "{name}: should keep prefix: {out}");
}
}
}
#[test]
fn scrub_exact_output() {
const CASES: &[(&str, &str, &str)] = &[
(
"single-quoted value with colon separator",
"password: 's3cr3t_p@ssw0rd!!'",
"password: 's3cr*[REDACTED]'",
),
(
"single-quoted value with equals separator",
"password='mysecretvalue123'",
"password='myse*[REDACTED]'",
),
(
"double-quoted key with single-quoted value",
r#""password": 'secretvalue123'"#,
"\"password: 'secr*[REDACTED]'",
),
];
for &(name, input, expected) in CASES {
assert_eq!(scrub_credentials(input), expected, "{name}");
}
}
#[test]
fn scrub_passthrough() {
const CASES: &[(&str, &str)] = &[
("short unquoted values (under 8 chars)", "key=short"),
(
"non-secret lines with = and /",
"normal line with = equals and / slash",
),
];
for &(name, input) in CASES {
assert_eq!(scrub_credentials(input), input, "{name}");
}
}
}
#[cfg(test)]
mod extract_provider_error_detail_tests {
use super::extract_provider_error_detail;
use serde_json::json;
#[test]
fn standard_message_field() {
let body = json!({"error": {"message": "upstream busy"}});
assert_eq!(
extract_provider_error_detail(&body).as_deref(),
Some("upstream busy")
);
let body = json!({"message": "bare detail"});
assert_eq!(
extract_provider_error_detail(&body).as_deref(),
Some("bare detail")
);
}
#[test]
fn string_error_field() {
let body = json!({"error": "plain message"});
assert_eq!(
extract_provider_error_detail(&body).as_deref(),
Some("plain message")
);
}
#[test]
fn nested_envelope_raw_field() {
let body = json!({
"error": {
"code": "data_inspection_failed",
"metadata": {"raw": "Input image data may contain inappropriate content."}
}
});
assert_eq!(
extract_provider_error_detail(&body).as_deref(),
Some("Input image data may contain inappropriate content.")
);
}
#[test]
fn nested_raw_is_itself_json_preferred() {
let body = json!({
"error": {
"message": "generic",
"metadata": {"raw": r#"{"error":{"message":"deep upstream detail"}}"#}
}
});
assert_eq!(
extract_provider_error_detail(&body).as_deref(),
Some("generic")
);
let body = json!({
"error": {
"metadata": {"raw": r#"{"error":{"message":"deep upstream detail"}}"#}
}
});
assert_eq!(
extract_provider_error_detail(&body).as_deref(),
Some("deep upstream detail")
);
}
#[test]
fn provider_error_code_field() {
let body = json!({
"error": {
"metadata": {"provider_error_code": "upstream_shared_pool"}
}
});
assert_eq!(
extract_provider_error_detail(&body).as_deref(),
Some("upstream_shared_pool")
);
}
#[test]
fn bare_code_then_type_fallbacks() {
let body = json!({"error": {"code": "invalid_request_error"}});
assert_eq!(
extract_provider_error_detail(&body).as_deref(),
Some("invalid_request_error")
);
let body = json!({"error": {"type": "rate_limit_exceeded"}});
assert_eq!(
extract_provider_error_detail(&body).as_deref(),
Some("rate_limit_exceeded")
);
}
#[test]
fn empty_and_absent_fields_yield_none() {
assert_eq!(extract_provider_error_detail(&json!({})), None);
assert_eq!(extract_provider_error_detail(&json!({"error": null})), None);
assert_eq!(extract_provider_error_detail(&json!({"error": {}})), None);
assert_eq!(
extract_provider_error_detail(&json!({"error": {"message": " "}})),
None
);
}
}
#[cfg(test)]
mod unescape_c_style_tests {
use super::unescape_c_style;
#[test]
fn test_unescape_c_style() {
let cases: &[(&str, Option<&str>)] = &[
(
r#"hello\"world\\test\nline\there"#,
Some("hello\"world\\test\nline\there"),
),
(r"\a\b\f\r\v", Some("\x07\x08\x0c\r\x0b")),
(r"\0\1", Some("\0\x01")),
(r"\12\37", Some("\n\x1f")),
(r"\101\377", Some("A\u{FFFD}")),
(r"\12x", Some("\nx")),
(r"\18", Some("\x018")),
("plain/path.rs", Some("plain/path.rs")),
("", Some("")),
(r"path\", None),
(r"\x", None),
(r"\q", None),
(r"\40", None),
(r"\77", None),
(r"\70", None),
(r"\4", Some("4")),
(r"\7x", Some("7x")),
];
for (i, (input, expected)) in cases.iter().enumerate() {
let result = unescape_c_style(input);
assert_eq!(
result.as_deref(),
*expected,
"case {i}: unescape_c_style({input:?})"
);
}
}
}
#[cfg(feature = "voice-tests")]
pub(crate) fn generate_pink_noise(len: usize, mut rng: impl rand::Rng) -> Vec<f32> {
const NUM_OCTAVES: usize = 16;
let mut values = [0.0f32; NUM_OCTAVES];
let mut outputs = [0.0f32; NUM_OCTAVES];
let mut sample_count = 0u64;
let mut noise = Vec::with_capacity(len);
for _ in 0..len {
sample_count += 1;
let mut sum = 0.0;
for octave in 0..NUM_OCTAVES {
if sample_count.is_multiple_of(1u64 << octave) {
values[octave] = rng.random::<f32>() * 2.0 - 1.0;
}
let new_val = values[octave];
let delta = new_val - outputs[octave];
outputs[octave] = new_val;
sum += delta;
}
noise.push(sum);
}
let rms = compute_rms(&noise).max(1e-10);
for s in &mut noise {
*s /= rms;
}
noise
}
#[cfg(feature = "voice-tests")]
pub(crate) fn add_noise(pcm: &[f32], snr_db: f32, seed: u64) -> Vec<f32> {
add_noise_color(pcm, snr_db, NoiseColor::Pink, seed)
}
#[cfg(feature = "voice-tests")]
pub(crate) fn apply_gain(pcm: &[f32], gain_db: f32) -> Vec<f32> {
let amp = 10.0_f32.powf(gain_db / 20.0);
pcm.iter().map(|&s| s * amp).collect()
}
#[cfg(feature = "voice-tests")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum NoiseColor {
Pink,
Brown,
}
#[cfg(feature = "voice-tests")]
pub(crate) fn add_noise_color(pcm: &[f32], snr_db: f32, color: NoiseColor, seed: u64) -> Vec<f32> {
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
let signal_rms = compute_rms(pcm).max(1e-10);
let noise: Vec<f32> = match color {
NoiseColor::Pink => generate_pink_noise(pcm.len(), &mut rng),
NoiseColor::Brown => {
let mut acc = 0.0f32;
(0..pcm.len())
.map(|_| {
acc = 0.999 * acc + (rng.random::<f32>() * 2.0 - 1.0);
acc
})
.collect()
}
};
let noise_rms_target = signal_rms * 10.0_f32.powf(-snr_db / 20.0);
let noise_rms_current = compute_rms(&noise).max(1e-10);
let scale = noise_rms_target / noise_rms_current;
pcm.iter()
.zip(noise.iter())
.map(|(&s, &n)| (s + n * scale).clamp(-1.0, 1.0))
.collect()
}
#[expect(clippy::cast_precision_loss)]
pub(crate) fn compute_rms(samples: &[f32]) -> f32 {
if samples.is_empty() {
return 0.0;
}
let sum_sq: f32 = samples.iter().map(|&s| s * s).sum();
(sum_sq / samples.len() as f32).sqrt()
}
#[must_use]
#[inline]
pub(crate) fn gaussian_pair_from_uniforms(u1: f32, u2: f32) -> (f32, f32) {
let r = (-2.0 * u1.ln()).sqrt();
let theta = 2.0 * core::f32::consts::PI * u2;
(r * theta.cos(), r * theta.sin())
}
#[cfg_attr(not(any(feature = "voice-tests", test)), allow(dead_code))]
pub(crate) fn sample_gaussian_pair_clamped(rng: &mut impl rand::Rng) -> (f32, f32) {
let u1: f32 = rng.random::<f32>().max(f32::EPSILON);
let u2: f32 = rng.random::<f32>().max(f32::EPSILON);
gaussian_pair_from_uniforms(u1, u2)
}
#[must_use]
#[cfg(feature = "voice-tests")]
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
pub(crate) fn speed_perturbation(samples: &[f32], sample_rate: u32, factor: f32) -> Vec<f32> {
if samples.is_empty() || (factor - 1.0).abs() < 1e-6 {
return samples.to_vec();
}
let effective_rate = (sample_rate as f32 * factor) as u32;
resample_audio(samples, effective_rate, sample_rate)
}
#[cfg(test)]
mod strip_ansi_escapes_tests {
use super::strip_ansi_escapes;
#[test]
fn test_ansi_escape_cases() {
let cases: &[(&str, &str)] = &[
("\x1B[31mred\x1B[0m \x1B[1mbold\x1B[22m", "red bold"),
("hello world", "hello world"),
("\x1B[32mgreen\x1B[0m", "green"),
("no escapes here", "no escapes here"),
("", ""),
];
for (input, expected) in cases {
assert_eq!(strip_ansi_escapes(input), *expected, "input: {input:?}");
}
}
}
#[cfg(test)]
mod verify_sha256_tests {
use super::verify_sha256;
fn sha256_hex(data: &[u8]) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(data);
super::hex_string(&hasher.finalize())
}
#[test]
fn matching_hash_passes() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.bin");
std::fs::write(&path, b"test data").unwrap();
let hash = sha256_hex(b"test data");
assert!(verify_sha256(&path, &hash).is_ok());
}
#[test]
fn mismatching_hash_fails() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.bin");
std::fs::write(&path, b"different data").unwrap();
let hash = sha256_hex(b"test data");
assert!(verify_sha256(&path, &hash).is_err());
}
#[test]
fn empty_hash_skips_verification() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("nonexistent.bin");
assert!(
verify_sha256(&missing, "").is_ok(),
"empty SHA256 expected hash should skip verification"
);
}
#[test]
fn file_not_found_with_non_empty_hash_fails() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("nonexistent.bin");
let hash = sha256_hex(b"anything");
assert!(verify_sha256(&missing, &hash).is_err());
}
}
#[cfg(all(test, feature = "voice-tests"))]
mod audio_util_tests {
use super::{add_noise, apply_gain, generate_pink_noise, speed_perturbation};
use rand::SeedableRng;
#[test]
fn test_speed_perturbation_identity() {
#[expect(clippy::cast_precision_loss)] let pcm: Vec<f32> = (0..100).map(|i| (i as f32) / 100.0).collect();
let result = speed_perturbation(&pcm, 16000, 1.0);
assert_eq!(result.len(), pcm.len(), "identity should preserve length");
for (a, b) in pcm.iter().zip(result.iter()) {
assert!((a - b).abs() < 1e-5, "identity should preserve values");
}
}
#[test]
fn test_speed_perturbation_rates() {
#[expect(clippy::cast_precision_loss)] let pcm: Vec<f32> = (0..100).map(|i| (i as f32) / 100.0).collect();
let slowed = speed_perturbation(&pcm, 16000, 0.5);
assert!(
slowed.len() > pcm.len(),
"rate < 1 should increase sample count"
);
let sped_up = speed_perturbation(&pcm, 16000, 2.0);
assert!(
sped_up.len() < pcm.len(),
"rate > 1 should decrease sample count"
);
}
#[test]
fn test_speed_perturbation_determinism() {
#[expect(clippy::cast_precision_loss)] let pcm: Vec<f32> = (0..100).map(|i| (i as f32) / 100.0).collect();
let a = speed_perturbation(&pcm, 16000, 0.95);
let b = speed_perturbation(&pcm, 16000, 0.95);
assert_eq!(a, b, "deterministic speed perturbation");
}
#[test]
fn test_apply_gain_determinism() {
#[expect(clippy::cast_precision_loss)] let pcm: Vec<f32> = (0..50).map(|i| (i as f32 - 25.0) / 25.0).collect();
let a = apply_gain(&pcm, -3.0);
let b = apply_gain(&pcm, -3.0);
assert_eq!(a, b, "apply_gain must be deterministic");
}
#[test]
fn test_apply_gain_db_conversion() {
let pcm: Vec<f32> = vec![0.5, -0.3, 0.1, -0.7, 0.9];
let unity = apply_gain(&pcm, 0.0);
for (a, b) in pcm.iter().zip(unity.iter()) {
assert!((a - b).abs() < 1e-6, "0 dB gain should be unity");
}
let attenuated = apply_gain(&pcm, -6.0);
let expected_amp = 10.0_f32.powf(-6.0 / 20.0);
for (orig, atten) in pcm.iter().zip(attenuated.iter()) {
assert!(
(atten - orig * expected_amp).abs() < 1e-6,
"-6 dB gain should multiply by {expected_amp}"
);
}
let amplified = apply_gain(&pcm, 6.0);
let expected_amp2 = 10.0_f32.powf(6.0 / 20.0);
for (orig, amp) in pcm.iter().zip(amplified.iter()) {
assert!(
(amp - orig * expected_amp2).abs() < 1e-6,
"+6 dB gain should multiply by {expected_amp2}"
);
}
}
#[test]
fn test_add_noise_determinism() {
#[expect(clippy::cast_precision_loss)] let pcm: Vec<f32> = (0..200).map(|i| (i as f32 - 100.0) / 100.0).collect();
let a = add_noise(&pcm, 25.0, 42);
let b = add_noise(&pcm, 25.0, 42);
assert_eq!(a.len(), b.len(), "add_noise output length should match");
assert_eq!(a, b, "add_noise with same seed must be deterministic");
}
#[test]
fn test_add_noise_seed_variation() {
#[expect(clippy::cast_precision_loss)] let pcm: Vec<f32> = (0..200).map(|i| (i as f32 - 100.0) / 100.0).collect();
let a = add_noise(&pcm, 25.0, 42);
let b = add_noise(&pcm, 25.0, 99);
assert_ne!(a, b, "different seeds should produce different output");
}
#[test]
fn test_add_noise_snr_approximation() {
#[expect(clippy::cast_precision_loss)] let pcm: Vec<f32> = (0..1000).map(|i| (i as f32 - 500.0) / 500.0).collect();
let noisy = add_noise(&pcm, 100.0, 42);
let signal_power: f32 = pcm.iter().map(|&s| s * s).sum();
let noise_power: f32 = pcm
.iter()
.zip(noisy.iter())
.map(|(&s, &n)| (n - s) * (n - s))
.sum();
let actual_snr_db = 10.0 * (signal_power / noise_power.max(1e-10)).log10();
assert!(
actual_snr_db > 80.0,
"at 100 dB target, actual SNR should be high (was {actual_snr_db:.1} dB)"
);
}
#[test]
fn test_generate_pink_noise_properties() {
let noise = generate_pink_noise(1000, rand::rngs::StdRng::seed_from_u64(42));
assert_eq!(noise.len(), 1000);
let has_positive = noise.iter().any(|&s| s > 0.0);
let has_negative = noise.iter().any(|&s| s < 0.0);
assert!(has_positive, "pink noise should have positive values");
assert!(has_negative, "pink noise should have negative values");
let rms = super::compute_rms(&noise);
assert!(
(rms - 1.0).abs() < 0.1,
"pink noise RMS should be near 1.0 (was {rms})"
);
}
#[test]
fn test_augmentation_variants_are_different() {
#[expect(clippy::cast_precision_loss)] let pcm: Vec<f32> = (0..500).map(|i| (i as f32 - 250.0) / 250.0).collect();
let speed_down = speed_perturbation(&pcm, 16000, 0.95);
let speed_up = speed_perturbation(&pcm, 16000, 1.05);
let volume_down = apply_gain(&pcm, -3.0);
let noise = add_noise(&pcm, 25.0, 42);
assert_ne!(speed_down, pcm, "speed-down should differ from original");
assert_ne!(speed_up, pcm, "speed-up should differ from original");
assert_ne!(volume_down, pcm, "volume-down should differ from original");
assert_ne!(noise, pcm, "noise should differ from original");
assert_ne!(
speed_down, speed_up,
"speed-down and speed-up should differ"
);
assert_ne!(
speed_down, volume_down,
"speed-down and volume-down should differ"
);
assert_ne!(speed_down, noise, "speed-down and noise should differ");
}
}
#[cfg(test)]
mod gaussian_sampler_tests {
use super::{gaussian_pair_from_uniforms, sample_gaussian_pair_clamped};
use rand::RngExt;
use rand::SeedableRng;
fn reference_bench_pair(rng: &mut impl rand::Rng) -> (f32, f32) {
let u1: f32 = rng.random::<f32>().max(f32::EPSILON);
let u2: f32 = rng.random::<f32>().max(f32::EPSILON);
let z1 = (-2.0 * u1.ln()).sqrt() * (2.0 * core::f32::consts::PI * u2).cos();
let z2 = (-2.0 * u1.ln()).sqrt() * (2.0 * core::f32::consts::PI * u2).sin();
(z1, z2)
}
#[test]
fn seeded_bench_sequence_byte_identical() {
const DRAW_PAIRS: usize = 4000;
let mut shared_rng = rand::rngs::StdRng::seed_from_u64(43);
let mut ref_rng = rand::rngs::StdRng::seed_from_u64(43);
for i in 0..DRAW_PAIRS {
let shared = sample_gaussian_pair_clamped(&mut shared_rng);
let reference = reference_bench_pair(&mut ref_rng);
#[expect(clippy::float_cmp)] {
assert_eq!(shared.0, reference.0, "bench z1 divergence at pair {i}");
}
#[expect(clippy::float_cmp)] {
assert_eq!(shared.1, reference.1, "bench z2 divergence at pair {i}");
}
}
}
#[test]
fn pair_math_matches_inline_formulas() {
for &(u1, u2) in &[(0.1, 0.2), (0.5, 0.9), (0.001, 0.999), (0.25, 0.75)] {
let (z1, z2) = gaussian_pair_from_uniforms(u1, u2);
let r = (-2.0 * u1.ln()).sqrt();
let theta = 2.0 * std::f32::consts::PI * u2;
#[expect(clippy::float_cmp)]
{
assert_eq!(z1, r * theta.cos(), "z1 for ({u1}, {u2})");
}
#[expect(clippy::float_cmp)]
{
assert_eq!(z2, r * theta.sin(), "z2 for ({u1}, {u2})");
}
}
}
}
#[cfg(test)]
mod reference_image_tests {
use super::*;
use crate::util::test::noisy_png;
#[tokio::test]
async fn under_cap_passes_through_unchanged() {
let tmp = tempfile::TempDir::new().unwrap();
let path = tmp.path().join("small.png");
let bytes = noisy_png(64, 64);
std::fs::write(&path, &bytes).unwrap();
let img = load_reference_image(&path, 1_500_000).await.unwrap();
assert_eq!(
img.data_uri(),
format!("data:image/png;base64,{}", STANDARD.encode(&bytes))
);
}
#[tokio::test]
async fn over_cap_is_compressed_under_cap() {
let tmp = tempfile::TempDir::new().unwrap();
let path = tmp.path().join("big.png");
let bytes = noisy_png(1376, 768);
assert!(
bytes.len() as u64 > 1_500_000,
"noise PNG should exceed the cap (got {} bytes)",
bytes.len()
);
std::fs::write(&path, &bytes).unwrap();
let img = load_reference_image(&path, 1_500_000).await.unwrap();
assert!(img.data_uri().starts_with("data:image/jpeg;base64,"));
let decoded = STANDARD
.decode(img.data_uri().split(',').nth(1).unwrap())
.unwrap();
assert!(decoded.len() as u64 <= 1_500_000);
}
#[tokio::test]
async fn unsupported_extension_rejected() {
let tmp = tempfile::TempDir::new().unwrap();
let path = tmp.path().join("photo.heic");
std::fs::write(&path, b"\x00\x00\x00\x18ftypheic").unwrap();
let Err(err) = load_reference_image(&path, 1_500_000).await else {
panic!("HEIC reference should be rejected");
};
assert!(err.to_string().contains("unsupported format"), "{err}");
}
#[tokio::test]
async fn undecodable_content_rejected() {
let tmp = tempfile::TempDir::new().unwrap();
let path = tmp.path().join("fake.png");
std::fs::write(&path, b"this is not an image").unwrap();
let Err(err) = load_reference_image(&path, 1_500_000).await else {
panic!("undecodable reference should be rejected");
};
assert!(err.to_string().contains("not a decodable image"), "{err}");
}
fn jpeg_with_exif_orientation(jpeg: &[u8]) -> Vec<u8> {
assert!(jpeg.starts_with(&[0xFF, 0xD8]));
let mut out = jpeg[..2].to_vec();
out.extend_from_slice(&[0xFF, 0xE1, 0x00, 0x22]);
out.extend_from_slice(b"Exif\0\0");
out.extend_from_slice(&[
0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x12, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]);
out.extend_from_slice(&jpeg[2..]);
out
}
#[test]
fn over_cap_jpeg_exif_orientation_applied() {
use image::GenericImageView;
use image::ImageDecoder;
let src = image::RgbImage::from_fn(2, 1, |x, _| {
if x == 0 {
image::Rgb([255, 0, 0])
} else {
image::Rgb([0, 0, 255])
}
});
let mut jpeg = Vec::new();
image::codecs::jpeg::JpegEncoder::new(&mut jpeg)
.encode_image(&image::DynamicImage::ImageRgb8(src))
.unwrap();
let oriented = jpeg_with_exif_orientation(&jpeg);
let reader = image::ImageReader::new(std::io::Cursor::new(&oriented))
.with_guessed_format()
.unwrap();
assert_eq!(
reader.into_decoder().unwrap().orientation().unwrap(),
image::metadata::Orientation::Rotate90
);
let out = compress_reference_step(&oriented, 0).unwrap();
let decoded = image::load_from_memory(&out).unwrap();
assert_eq!(decoded.dimensions(), (1, 2));
}
}