use iced::advanced::{image as advanced_image, text};
use iced::widget::{container, image, markdown};
use iced::{ContentFit, Element, Font, Length, Pixels, padding};
use std::cell::RefCell;
use std::collections::{HashMap, VecDeque};
use std::path::{Path, PathBuf};
use std::sync::{Arc, LazyLock, Mutex};
use crate::util::media_target;
use crate::util::{
MEDIA_MARKER_RE, MediaMarkerKind, UnwrapPoison, file_name_or_path, parse_media_marker,
};
use ::image as image_crate;
pub(crate) fn preprocess(content: &str) -> String {
let s = replace_transcription(content);
let s = replace_media_markers(&s);
replace_saved_annotations(&s)
}
fn replace_transcription(s: &str) -> String {
static RE: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"\[(Audio|Video) transcription of [^\]]+\]:\s*")
.expect("transcription regex must compile")
});
RE.replace_all(s, |caps: ®ex::Captures| {
match caps.get(1).map(|m| m.as_str()) {
Some("Audio") => "🔊 ",
_ => "🎬 ",
}
})
.to_string()
}
fn replace_media_markers(s: &str) -> String {
MEDIA_MARKER_RE
.replace_all(s, |caps: ®ex::Captures| {
let (kind, path) = parse_media_marker(caps);
match kind {
MediaMarkerKind::Image => {
let p = path.trim();
if p.starts_with(crate::util::DATA_URI_OMITTED_PREFIX) {
"🖼️ image".to_string()
} else if p.starts_with("data:") {
format!("")
} else if media_target::is_valid_remote_url(p)
|| std::path::Path::new(p).is_absolute()
{
format!("")
} else {
caps.get_match().as_str().to_string()
}
}
MediaMarkerKind::Audio => format!("🎵 {}", file_name_or_path(path)),
MediaMarkerKind::File => file_marker_label(path),
MediaMarkerKind::Video => format!("🎬 Video: {}", file_name_or_path(path)),
}
})
.to_string()
}
fn file_marker_label(path: &str) -> String {
if path.trim().starts_with("data:") {
"📄 file".to_string()
} else {
format!("📄 {}", file_name_or_path(path))
}
}
fn replace_saved_annotations(s: &str) -> String {
static SAVED_IMAGE_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"\n*\[Saved image: [^\]]*\]")
.expect("saved image annotation regex must compile")
});
static SAVED_VIDEO_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"\[Saved video: ([^\]]*)\]")
.expect("saved video annotation regex must compile")
});
let s = SAVED_IMAGE_RE.replace_all(s, "").to_string();
SAVED_VIDEO_RE
.replace_all(&s, |caps: ®ex::Captures| {
let path = caps.get(1).map_or("", |m| m.as_str());
format!("🎬 Video: {}", file_name_or_path(path))
})
.to_string()
}
const MAX_IMAGE_LONGEST_SIDE_PX: u32 = 2048;
fn downscale_raster(width: u32, height: u32, rgba: Vec<u8>) -> Option<(u32, u32, Vec<u8>)> {
if width <= MAX_IMAGE_LONGEST_SIDE_PX && height <= MAX_IMAGE_LONGEST_SIDE_PX {
return Some((width, height, rgba));
}
let img = image_crate::DynamicImage::ImageRgba8(image_crate::RgbaImage::from_raw(
width, height, rgba,
)?);
let resized = img
.thumbnail(MAX_IMAGE_LONGEST_SIDE_PX, MAX_IMAGE_LONGEST_SIDE_PX)
.into_rgba8();
Some((resized.width(), resized.height(), resized.into_raw()))
}
#[must_use]
fn render_raster(bytes: &[u8]) -> Option<(u32, u32, Vec<u8>)> {
let (width, height, rgba) =
media_target::decode_raster_bytes(bytes, media_target::raster_decode_limits())?;
downscale_raster(width, height, rgba)
}
#[must_use]
fn decode_data_uri_image(uri: &str) -> Option<(u32, u32, Vec<u8>)> {
let (width, height, rgba) =
media_target::decode_native_data_uri(uri, media_target::raster_decode_limits())?;
downscale_raster(width, height, rgba)
}
#[derive(Debug)]
enum CachedDataUriImage {
Decoded(image::Handle),
Failed,
}
static DATA_URI_IMAGE_CACHE: LazyLock<Mutex<BoundedLruCache<String, CachedDataUriImage>>> =
LazyLock::new(|| Mutex::new(BoundedLruCache::with_capacity(MAX_CACHED_IMAGES)));
struct BoundedLruCache<K, V> {
entries: HashMap<K, V>,
lru: VecDeque<K>,
cap: usize,
}
impl<K, V> BoundedLruCache<K, V>
where
K: std::hash::Hash + Eq + Clone,
{
fn with_capacity(cap: usize) -> Self {
Self {
entries: HashMap::new(),
lru: VecDeque::new(),
cap,
}
}
fn get<Q>(&mut self, key: &Q) -> Option<&V>
where
K: std::borrow::Borrow<Q>,
Q: std::hash::Hash + Eq + ?Sized,
{
let hit = self.entries.get(key);
if hit.is_some() {
if let Some(pos) = self.lru.iter().position(|k| k.borrow() == key) {
let key = self.lru.remove(pos).expect("position found by iteration");
self.lru.push_back(key);
}
}
hit
}
fn insert(&mut self, key: K, value: V) {
if let Some(pos) = self.lru.iter().position(|k| k == &key) {
self.lru.remove(pos);
}
self.entries.insert(key.clone(), value);
self.lru.push_back(key);
while self.entries.len() > self.cap {
let oldest = self
.lru
.pop_front()
.expect("lru mirrors entries, so a front key always exists");
self.entries.remove(&oldest);
}
}
}
#[must_use]
fn data_uri_image_handle(uri: &str) -> Option<image::Handle> {
media_target::data_uri_base64_payload(uri)?;
{
let mut cache = DATA_URI_IMAGE_CACHE.lock().unwrap_poison();
match cache.get(uri) {
Some(CachedDataUriImage::Decoded(handle)) => return Some(handle.clone()),
Some(CachedDataUriImage::Failed) => return None,
None => {}
}
}
let outcome = match decode_data_uri_image(uri) {
Some((width, height, rgba)) => {
CachedDataUriImage::Decoded(image::Handle::from_rgba(width, height, rgba))
}
None => CachedDataUriImage::Failed,
};
let handle = match &outcome {
CachedDataUriImage::Decoded(handle) => Some(handle.clone()),
CachedDataUriImage::Failed => None,
};
DATA_URI_IMAGE_CACHE
.lock()
.unwrap_poison()
.insert(uri.to_string(), outcome);
handle
}
const MAX_CACHED_IMAGES: usize = 32;
#[derive(Debug)]
enum CachedLocalImage {
Decoded(image::Handle),
Failed,
}
static LOCAL_IMAGE_CACHE: LazyLock<
Mutex<BoundedLruCache<(std::path::PathBuf, std::time::SystemTime), CachedLocalImage>>,
> = LazyLock::new(|| Mutex::new(BoundedLruCache::with_capacity(MAX_CACHED_IMAGES)));
#[must_use]
fn local_path_image_handle(path: &str) -> Option<image::Handle> {
let p = std::path::Path::new(path);
if !p.is_absolute() {
return None;
}
let meta = std::fs::metadata(p).ok()?;
if !meta.is_file() || meta.len() > crate::util::INBOUND_IMAGE_MAX_INPUT_BYTES {
return None;
}
let mtime = meta.modified().ok()?;
let canonical = std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
let key = (canonical.clone(), mtime);
{
let mut cache = LOCAL_IMAGE_CACHE.lock().unwrap_poison();
match cache.get(&key) {
Some(CachedLocalImage::Decoded(handle)) => return Some(handle.clone()),
Some(CachedLocalImage::Failed) => return None,
None => {}
}
}
let outcome = match std::fs::read(&canonical)
.ok()
.and_then(|b| render_raster(&b))
{
Some((width, height, rgba)) => {
CachedLocalImage::Decoded(image::Handle::from_rgba(width, height, rgba))
}
None => CachedLocalImage::Failed,
};
let handle = match &outcome {
CachedLocalImage::Decoded(handle) => Some(handle.clone()),
CachedLocalImage::Failed => None,
};
LOCAL_IMAGE_CACHE
.lock()
.unwrap_poison()
.insert(key, outcome);
handle
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct FileMarker {
pub label: String,
pub target: String,
}
const FILE_LINK_PREFIX: &str = "mahbot-file:";
pub(crate) fn file_markers(content: &str) -> Vec<FileMarker> {
MEDIA_MARKER_RE
.captures_iter(content)
.filter_map(|caps| {
let (kind, path) = parse_media_marker(&caps);
if kind != MediaMarkerKind::File {
return None;
}
Some(FileMarker {
label: file_marker_label(path),
target: if path.trim().starts_with("data:") {
String::new()
} else {
path.to_string()
},
})
})
.collect()
}
pub(crate) fn file_link_target(uri: &str) -> Option<&str> {
uri.strip_prefix(FILE_LINK_PREFIX)
}
fn file_link_uri(target: &str) -> String {
format!("{FILE_LINK_PREFIX}{target}")
}
fn resolve_file_marker_target(target: &str, workspace: Option<&Path>) -> Result<PathBuf, String> {
if target.is_empty() {
return Err("The marker does not name a file.".to_string());
}
if media_target::is_valid_remote_url(target) {
return Err("FILE targets are local files, not URLs.".to_string());
}
let raw = Path::new(target);
if raw.is_absolute() {
return Ok(raw.to_path_buf());
}
workspace
.map(|root| root.join(raw))
.ok_or_else(|| format!("Cannot open '{target}': the conversation's workspace is unknown."))
}
pub(crate) fn reveal_file_marker(target: &str, workspace: Option<&Path>) -> Result<(), String> {
let path = resolve_file_marker_target(target, workspace)?;
if !path.exists() {
return Err(format!("File not found: {}", path.display()));
}
if !path.is_file() {
return Err(format!("Not a file: {}", path.display()));
}
super::reveal_in_file_manager(&path)
}
struct SelectableMediaViewer<'a> {
files: &'a [FileMarker],
linked: RefCell<Vec<bool>>,
}
pub(crate) fn selectable_markdown_view<'a>(
items: &'a [markdown::Item],
settings: impl Into<markdown::Settings>,
files: &'a [FileMarker],
) -> Element<'a, markdown::Uri, iced::Theme, iced::Renderer> {
markdown::view_with(items, settings, &SelectableMediaViewer::new(files))
}
impl<'a> SelectableMediaViewer<'a> {
fn new(files: &'a [FileMarker]) -> Self {
Self {
files,
linked: RefCell::new(vec![false; files.len()]),
}
}
fn marker_spans(
&self,
spans: &Arc<[text::Span<'static, markdown::Uri>]>,
) -> Arc<[text::Span<'static, markdown::Uri>]> {
self.linkified_spans(spans)
.map_or_else(|| spans.clone(), Arc::from)
}
fn linkified_spans(
&self,
spans: &[text::Span<'static, markdown::Uri>],
) -> Option<Vec<text::Span<'static, markdown::Uri>>> {
let mut linked = self.linked.borrow_mut();
if !self.files.iter().enumerate().any(|(index, file)| {
!linked[index]
&& spans
.iter()
.any(|span| can_link(span) && span.text.contains(&file.label))
}) {
return None;
}
let mut out = Vec::with_capacity(spans.len() + self.files.len());
for span in spans {
if !can_link(span) {
out.push(span.clone());
continue;
}
let mut rest: &str = span.text.as_ref();
let mut split = false;
while let Some((at, index)) = self
.files
.iter()
.enumerate()
.filter(|(i, _)| !linked[*i])
.filter_map(|(i, file)| rest.find(&file.label).map(|at| (at, i)))
.min_by_key(|(at, i)| (*at, std::cmp::Reverse(self.files[*i].label.len())))
{
split = true;
if at > 0 {
out.push(span_with_text(span, &rest[..at]));
}
let label = &self.files[index].label;
let mut link = span_with_text(span, label);
link.link = Some(file_link_uri(&self.files[index].target));
out.push(link);
linked[index] = true;
rest = &rest[at + label.len()..];
}
if !split {
out.push(span.clone());
} else if !rest.is_empty() {
out.push(span_with_text(span, rest));
}
}
Some(out)
}
}
fn can_link(span: &text::Span<'static, markdown::Uri>) -> bool {
span.link.is_none() && span.highlight.is_none()
}
fn span_with_text(
span: &text::Span<'static, markdown::Uri>,
text: &str,
) -> text::Span<'static, markdown::Uri> {
text::Span {
text: std::borrow::Cow::Owned(text.to_owned()),
..span.clone()
}
}
impl<'a, Theme, Renderer> markdown::Viewer<'a, markdown::Uri, Theme, Renderer>
for SelectableMediaViewer<'a>
where
Theme: markdown::Catalog + iced_selection::text::Catalog + 'a,
Renderer: text::Renderer<Paragraph = iced::advanced::graphics::text::Paragraph, Font = Font>
+ advanced_image::Renderer<Handle = image::Handle>
+ 'a,
{
fn on_link_click(url: markdown::Uri) -> markdown::Uri {
url
}
fn image(
&self,
settings: markdown::Settings,
url: &'a markdown::Uri,
_title: &'a str,
_alt: &markdown::Text,
) -> Element<'a, markdown::Uri, Theme, Renderer> {
let url_str = url.as_str();
if url_str.starts_with("data:") {
return match data_uri_image_handle(url_str) {
Some(handle) => image::Image::new(handle)
.width(Length::Fill)
.content_fit(ContentFit::Contain)
.into(),
None => iced::widget::text("🖼️ image")
.size(settings.text_size)
.into(),
};
}
if let Some(handle) = local_path_image_handle(url_str) {
image::Image::new(handle)
.width(Length::Fill)
.content_fit(ContentFit::Contain)
.into()
} else {
let filename = file_name_or_path(url_str);
iced::widget::text(format!("🖼️ {filename}"))
.size(settings.text_size)
.into()
}
}
fn heading(
&self,
settings: markdown::Settings,
level: &'a markdown::HeadingLevel,
text: &'a markdown::Text,
index: usize,
) -> Element<'a, markdown::Uri, Theme, Renderer> {
let size = match level {
markdown::HeadingLevel::H1 => settings.h1_size,
markdown::HeadingLevel::H2 => settings.h2_size,
markdown::HeadingLevel::H3 => settings.h3_size,
markdown::HeadingLevel::H4 => settings.h4_size,
markdown::HeadingLevel::H5 => settings.h5_size,
markdown::HeadingLevel::H6 => settings.h6_size,
};
let spans = text.spans(settings.style);
let rich = iced_selection::rich_text(self.marker_spans(&spans)).size(size);
container(rich.on_link_click(|url| url))
.padding(padding::top(if index > 0 {
settings.text_size / 2.0
} else {
Pixels::ZERO
}))
.into()
}
fn paragraph(
&self,
settings: markdown::Settings,
text: &markdown::Text,
) -> Element<'a, markdown::Uri, Theme, Renderer> {
let spans = text.spans(settings.style);
iced_selection::rich_text(self.marker_spans(&spans))
.size(settings.text_size)
.on_link_click(|url| url)
.into()
}
fn code_block(
&self,
settings: markdown::Settings,
_language: Option<&'a str>,
_code: &'a str,
lines: &'a [markdown::Text],
) -> Element<'a, markdown::Uri, Theme, Renderer> {
iced_selection::markdown::code_block(settings, lines, |url| url)
}
fn unordered_list(
&self,
settings: markdown::Settings,
bullets: &'a [markdown::Bullet],
) -> Element<'a, markdown::Uri, Theme, Renderer> {
iced_selection::markdown::unordered_list(self, settings, bullets)
}
fn ordered_list(
&self,
settings: markdown::Settings,
start: u64,
bullets: &'a [markdown::Bullet],
) -> Element<'a, markdown::Uri, Theme, Renderer> {
iced_selection::markdown::ordered_list(self, settings, start, bullets)
}
}
#[cfg(test)]
mod tests {
use super::*;
use base64::{Engine as _, engine::general_purpose::STANDARD};
use std::io::Cursor;
#[expect(clippy::too_many_lines)] #[test]
fn preprocess_table() {
let tmp = tempfile::tempdir().unwrap();
let photo = tmp.path().join("photo.png");
let spaced = tmp.path().join("my file.png");
std::fs::write(&photo, tiny_png(2, 1)).unwrap();
std::fs::write(&spaced, tiny_png(2, 1)).unwrap();
let photo_str = photo.to_string_lossy().into_owned();
let spaced_str = spaced.to_string_lossy().into_owned();
let valid_uri = data_uri(&tiny_png(2, 1));
let cleaned = tmp.path().join("cleaned_up.png"); let cleaned_str = cleaned.to_string_lossy().into_owned();
let cases: Vec<(&str, String, String)> = vec![
(
"replaces an image marker",
format!("Look [IMAGE:{photo_str}] here"),
format!("Look  here"),
),
(
"replaces an image marker with spaces in the path",
format!("img [IMAGE:{spaced_str}]"),
format!("img "),
),
(
"renders a cleaned-up absolute temp-file marker as an image item (placeholder in the viewer)",
format!("Look [IMAGE:{cleaned_str}] here"),
format!("Look  here"),
),
(
"renders the omitted-data-URI placeholder as the image affordance, not raw marker text",
format!(
"Look [IMAGE:{} (35 bytes)>] here",
crate::util::DATA_URI_OMITTED_PREFIX
),
"Look 🖼️ image here".into(),
),
(
"replaces an audio marker",
"Listen [AUDIO:/tmp/recording.ogg]".into(),
"Listen 🎵 recording.ogg".into(),
),
(
"replaces an audio marker in a nested path",
"hear [AUDIO:/dir/subdir/rec.ogg]".into(),
"hear 🎵 rec.ogg".into(),
),
(
"replaces a video marker",
"Watch [VIDEO:/tmp/video.mp4]".into(),
"Watch 🎬 Video: video.mp4".into(),
),
(
"replaces a file marker",
"Get [FILE:/tmp/report.pdf]".into(),
"Get 📄 report.pdf".into(),
),
(
"names a data-URI file marker generically instead of dumping its payload",
"Get [FILE:data:text/plain;base64,aGVsbG8=]".into(),
"Get 📄 file".into(),
),
(
"replaces an audio transcription block",
"[Audio transcription of recording.ogg]: Hello world".into(),
"🔊 Hello world".into(),
),
(
"renders every line of a multiline audio transcription under 🔊",
"[Audio transcription of voice.ogg]: Line one\nLine two".into(),
"🔊 Line one\nLine two".into(),
),
(
"renders every line of a multiline video transcription under 🎬 and leaves following message content untouched",
"[Video transcription of clip.mp4]: Line one\nLine two\n\nEdit this".into(),
"🎬 Line one\nLine two\n\nEdit this".into(),
),
(
"video-transcription format contains Video and is handled before the [VIDEO:...] pattern",
"[Video transcription of clip.mp4]: hi there [VIDEO:/tmp/other.mp4]".into(),
"🎬 hi there 🎬 Video: other.mp4".into(),
),
(
"audio-transcription format contains Audio and is handled before the [AUDIO:...] pattern",
"[Audio transcription of msg.ogg]: hi there [AUDIO:/tmp/other.ogg]".into(),
"🔊 hi there 🎵 other.ogg".into(),
),
(
"replaces mixed image/audio/video markers",
format!("![]() [IMAGE:{photo_str}] and [AUDIO:/tmp/b.ogg] end [VIDEO:/tmp/c.mp4]"),
format!("![]()  and 🎵 b.ogg end 🎬 Video: c.mp4"),
),
(
"leaves content without markers unchanged",
"Hello world".into(),
"Hello world".into(),
),
(
"returns the empty string unchanged",
String::new(),
String::new(),
),
(
"strips a saved image annotation and its blank-line separator",
format!("[IMAGE:{valid_uri}]\n\n[Saved image: /uploads/img.png]"),
format!(""),
),
(
"strips a saved image annotation at the start with no preceding separator",
"[Saved image: /uploads/img.png]".into(),
String::new(),
),
(
"turns a saved video annotation into a visible clip placeholder",
"Clip [Saved video: /uploads/clip.mp4] here".into(),
"Clip 🎬 Video: clip.mp4 here".into(),
),
(
"keeps the saved video clip visible alongside its transcription",
"[Video transcription of clip.mp4]: Line one\n\n[Saved video: /uploads/clip.mp4]"
.into(),
"🎬 Line one\n\n🎬 Video: clip.mp4".into(),
),
];
for (i, (name, input, expected)) in cases.iter().enumerate() {
assert_eq!(preprocess(input), *expected, "case {i} ({name})");
}
}
#[test]
fn file_markers_pair_labels_with_targets() {
let body = "Get [FILE:generated/report.pdf] and [FILE:/tmp/notes.txt] then \
[FILE:data:text/plain;base64,aGVsbG8=]";
let markers = file_markers(body);
assert_eq!(
markers,
vec![
FileMarker {
label: "📄 report.pdf".to_string(),
target: "generated/report.pdf".to_string(),
},
FileMarker {
label: "📄 notes.txt".to_string(),
target: "/tmp/notes.txt".to_string(),
},
FileMarker {
label: "📄 file".to_string(),
target: String::new(),
},
]
);
}
#[test]
fn linkified_spans_links_only_the_marker_label() {
let span = text::Span::new("Get 📄 report.pdf now");
let files = [FileMarker {
label: "📄 report.pdf".to_string(),
target: "/tmp/report.pdf".to_string(),
}];
let spans = SelectableMediaViewer::new(&files)
.linkified_spans(&[span])
.unwrap();
let texts: Vec<&str> = spans.iter().map(|s| s.text.as_ref()).collect();
assert_eq!(texts, ["Get ", "📄 report.pdf", " now"]);
assert_eq!(spans[0].link, None);
assert_eq!(spans[1].link, Some(file_link_uri("/tmp/report.pdf")));
assert_eq!(
spans[1].color, None,
"a file marker must keep its plain colour"
);
assert!(
!spans[1].underline,
"a file marker must not be underlined at rest"
);
assert_eq!(spans[2].link, None);
}
#[test]
fn linkified_spans_links_same_named_markers_to_their_own_targets() {
let files = [
FileMarker {
label: "📄 notes.txt".to_string(),
target: "first/notes.txt".to_string(),
},
FileMarker {
label: "📄 notes.txt".to_string(),
target: "second/notes.txt".to_string(),
},
];
let viewer = SelectableMediaViewer::new(&files);
let first = viewer
.linkified_spans(&[text::Span::new("Saved 📄 notes.txt")])
.unwrap();
let second = viewer
.linkified_spans(&[text::Span::new("Again 📄 notes.txt")])
.unwrap();
assert_eq!(
first[1].link,
Some(file_link_uri("first/notes.txt")),
"each block must link the next marker, not always the first"
);
assert_eq!(second[1].link, Some(file_link_uri("second/notes.txt")));
}
#[test]
fn file_marker_targets_resolve_against_the_workspace() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
assert_eq!(
resolve_file_marker_target("generated/report.pdf", Some(root)).unwrap(),
root.join("generated/report.pdf"),
"a relative target resolves against the conversation's workspace"
);
let absolute = root.join("elsewhere/report.pdf");
assert_eq!(
resolve_file_marker_target(&absolute.to_string_lossy(), Some(root)).unwrap(),
absolute,
"an absolute target is used as it is"
);
assert_eq!(
resolve_file_marker_target("report.pdf", None).unwrap_err(),
"Cannot open 'report.pdf': the conversation's workspace is unknown."
);
assert_eq!(
resolve_file_marker_target("https://example.com/report.pdf", Some(root)).unwrap_err(),
"FILE targets are local files, not URLs."
);
assert_eq!(
resolve_file_marker_target("", Some(root)).unwrap_err(),
"The marker does not name a file."
);
}
#[test]
fn reveal_file_marker_refuses_a_target_that_is_not_a_file() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let missing = root.join("generated/report.pdf");
assert_eq!(
reveal_file_marker("generated/report.pdf", Some(root)).unwrap_err(),
format!("File not found: {}", missing.display()),
"a relative target must resolve against the workspace"
);
std::fs::create_dir(root.join("folder")).unwrap();
assert_eq!(
reveal_file_marker("folder", Some(root)).unwrap_err(),
format!("Not a file: {}", root.join("folder").display())
);
}
#[test]
fn file_name_or_path_table() {
let cases: &[(&str, &str, &str)] = &[
("unix path", "/foo/bar.txt", "bar.txt"),
("nested path", "/foo/bar/doc.txt", "doc.txt"),
("no directory component", "bar.txt", "bar.txt"),
("trailing slash is normalized on Unix", "/foo/bar/", "bar"),
];
for (i, (name, input, expected)) in cases.iter().enumerate() {
assert_eq!(file_name_or_path(input), *expected, "case {i} ({name})");
}
}
fn tiny_png(width: u32, height: u32) -> Vec<u8> {
let img = ::image::RgbaImage::from_pixel(width, height, ::image::Rgba([255, 0, 0, 255]));
let mut buf = Vec::new();
img.write_to(&mut Cursor::new(&mut buf), ::image::ImageFormat::Png)
.expect("test PNG must encode");
buf
}
fn data_uri(png: &[u8]) -> String {
format!("data:image/png;base64,{}", STANDARD.encode(png))
}
#[test]
fn decode_data_uri_image_table() {
let valid_uri = data_uri(&tiny_png(2, 1));
let invalid_base64 = "data:image/png;base64,%%%not-base64%%%".to_string();
let non_image = format!("data:text/plain;base64,{}", STANDARD.encode(b"hello world"));
let oversized_payload = format!(
"data:image/png;base64,{}",
"A".repeat(media_target::MAX_DATA_URI_ENCODED_BYTES + 1)
);
let oversized_dimensions = data_uri(&tiny_png(MAX_IMAGE_LONGEST_SIDE_PX + 1, 1));
let missing_marker = "data:image/png,AAAA".to_string();
let mut jbuf = Vec::new();
::image::RgbImage::from_pixel(1, 1, ::image::Rgb([255, 0, 0]))
.write_to(&mut Cursor::new(&mut jbuf), ::image::ImageFormat::Jpeg)
.expect("test JPEG must encode");
let mismatched_uri = format!("data:image/png;base64,{}", STANDARD.encode(&jbuf));
#[expect(clippy::type_complexity)] let cases: &[(&str, String, Option<(u32, u32, Vec<u8>)>)] = &[
(
"decodes a solid-red 2×1 PNG",
valid_uri,
Some((2, 1, vec![255, 0, 0, 255, 255, 0, 0, 255])),
),
("rejects invalid base64", invalid_base64, None),
("rejects non-image bytes", non_image, None),
(
"rejects an oversized payload by a pure length check before any base64 work",
oversized_payload,
None,
),
(
"downscales a valid but over-display-cap raster to the render cap",
oversized_dimensions,
Some((
MAX_IMAGE_LONGEST_SIDE_PX,
1,
[255, 0, 0, 255].repeat(MAX_IMAGE_LONGEST_SIDE_PX as usize),
)),
),
(
"rejects a data URI missing the base64 marker",
missing_marker,
None,
),
(
"rejects a data URI whose declared subtype mismatches the actual bytes",
mismatched_uri,
None,
),
];
for (i, (name, uri, expected)) in cases.iter().enumerate() {
assert_eq!(
decode_data_uri_image(uri).as_ref(),
expected.as_ref(),
"case {i} ({name})"
);
}
}
#[test]
fn data_uri_image_handle_caches_stable_handle() {
let uri = data_uri(&tiny_png(3, 2));
let first = data_uri_image_handle(&uri).expect("valid data URI decodes");
let second = data_uri_image_handle(&uri).expect("cached hit still decodes");
assert_eq!(first.id(), second.id());
}
#[test]
fn data_uri_image_handle_caches_failures() {
let uri = "data:image/png;base64,%%%not-base64%%%";
assert_eq!(data_uri_image_handle(uri), None);
{
let cache = DATA_URI_IMAGE_CACHE.lock().unwrap_poison();
assert!(
matches!(cache.entries.get(uri), Some(CachedDataUriImage::Failed)),
"failed decode must be cached, got {:?}",
cache.entries.get(uri)
);
}
assert_eq!(data_uri_image_handle(uri), None);
}
#[test]
fn data_uri_image_handle_rejects_oversized_payload_without_caching() {
let huge = "A".repeat(media_target::MAX_DATA_URI_ENCODED_BYTES + 1);
let uri = format!("data:image/png;base64,{huge}");
assert_eq!(data_uri_image_handle(&uri), None);
let cache = DATA_URI_IMAGE_CACHE.lock().unwrap_poison();
assert!(
!cache.entries.contains_key(&uri),
"over-cap payload must not be cached"
);
}
#[test]
fn data_uri_marker_survives_markdown_parse() {
let uri = data_uri(&tiny_png(2, 1));
let processed = preprocess(&format!("[IMAGE:{uri}]"));
let items: Vec<_> = markdown::parse(&processed).collect();
let [markdown::Item::Image { url, .. }] = items.as_slice() else {
panic!("expected a single Image item, got {items:?}");
};
assert_eq!(url.as_str(), uri);
}
#[test]
fn bounded_cache_duplicate_insert_keeps_eviction_exact() {
let mut cache = BoundedLruCache::with_capacity(2);
cache.insert(1, "a");
cache.insert(1, "a2"); cache.insert(2, "b");
assert_eq!(cache.lru.len(), 2, "duplicate key must not survive in lru");
assert_eq!(cache.entries.len(), 2);
assert_eq!(cache.get(&2), Some(&"b"));
cache.insert(3, "c");
assert_eq!(cache.lru.len(), 2, "eviction must stay exact");
assert_eq!(cache.get(&1), None, "oldest (1) must be evicted");
assert_eq!(cache.get(&2), Some(&"b"));
assert_eq!(cache.get(&3), Some(&"c"));
}
}