use std::collections::{BTreeMap, BTreeSet};
use std::io::{BufRead, Read, Seek};
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use std::time::{Duration, Instant};
use bcode_markdown_render::MarkdownDestination;
use bmux_tui::geometry::Rect;
use bmux_tui::image::{
ImageContribution, ImageKey, ImageLifecycle, ImagePayload, ImagePixelFormat, ImagePlacement,
};
use image::ImageDecoder;
pub const MAX_ENCODED_BYTES: usize = 8 * 1024 * 1024;
pub const MAX_DIMENSION: u32 = 4096;
pub const MAX_DECODED_PIXELS: u64 = 32_000_000;
pub const MAX_CACHE_ENTRIES: usize = 128;
pub const MAX_CACHE_BYTES: usize = 64 * 1024 * 1024;
pub const MAX_REDIRECTS: usize = 5;
pub const LOAD_TIMEOUT: Duration = Duration::from_secs(10);
pub const MAX_PREFETCH_CONTRIBUTIONS: usize = 8;
pub const MAX_CONCURRENT_LOADS: usize = 4;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MarkdownImageLoadDecision {
Remote(url::Url),
Local(std::path::PathBuf),
Reject,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MarkdownImagePresentationPolicy {
pub interactive_resident_frame: bool,
pub network_enabled: bool,
pub terminal_supported: bool,
}
#[derive(Debug)]
pub struct MarkdownPresentationRuntime {
pub images: MarkdownImagePresentationStore,
pub image_cache: MarkdownImageCache,
pub image_inflight: MarkdownImageInflight,
pub image_loader: MarkdownImageLoader,
}
impl MarkdownPresentationRuntime {
pub fn new() -> Result<Self, MarkdownImageLoadError> {
Ok(Self {
images: MarkdownImagePresentationStore::default(),
image_cache: MarkdownImageCache::default(),
image_inflight: MarkdownImageInflight::default(),
image_loader: MarkdownImageLoader::new()?,
})
}
pub fn reconcile_and_start(
&mut self,
inputs: &[MarkdownImagePresentationInput],
policy: MarkdownImagePresentationPolicy,
) -> (
Vec<String>,
Vec<tokio::task::JoinHandle<MarkdownImageLoadCompletion>>,
) {
let removed = self
.images
.reconcile_with_inflight(inputs, policy, &mut self.image_inflight);
self.images.hydrate_from_cache(&mut self.image_cache);
let requests = self
.images
.schedule_loads(inputs, policy, &mut self.image_inflight);
let tasks = spawn_image_loads(&self.image_loader, requests);
(removed, tasks)
}
pub fn complete(&mut self, completion: MarkdownImageLoadCompletion) -> bool {
self.images.complete_load(
&completion.cache_key,
completion.result,
&mut self.image_cache,
&mut self.image_inflight,
)
}
pub fn cancel_all(&mut self) {
self.image_inflight.cancel_all();
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MarkdownImagePresentationState {
Idle,
Loading,
Ready(DecodedMarkdownImage),
Failed(String),
NetworkDisabled,
TerminalUnsupported,
}
pub const RESERVED_IMAGE_ROWS: u16 = 4;
#[must_use]
pub const fn markdown_image_reserved_rows(_state: &MarkdownImagePresentationState) -> u16 {
RESERVED_IMAGE_ROWS
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MarkdownImagePresentationInput {
pub contribution_id: String,
pub cache_key: MarkdownImageCacheKey,
pub destination: MarkdownDestination,
pub residency: MarkdownImageResidency,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MarkdownImageResidency {
Hidden,
Visible,
Prefetch,
}
#[derive(Debug, Clone)]
pub struct MarkdownImageLoadRequest {
pub contribution_id: String,
pub cache_key: MarkdownImageCacheKey,
pub destination: MarkdownDestination,
pub cancellation: MarkdownImageCancellationToken,
}
#[derive(Debug)]
pub struct MarkdownImageLoadCompletion {
pub cache_key: MarkdownImageCacheKey,
pub result: Result<DecodedMarkdownImage, MarkdownImageLoadFailure>,
}
#[must_use]
pub fn spawn_image_loads(
loader: &MarkdownImageLoader,
requests: Vec<MarkdownImageLoadRequest>,
) -> Vec<tokio::task::JoinHandle<MarkdownImageLoadCompletion>> {
requests
.into_iter()
.map(|request| {
let loader = loader.clone();
tokio::spawn(async move {
let result = loader
.load_cancellable(&request.destination, &request.cancellation)
.await;
MarkdownImageLoadCompletion {
cache_key: request.cache_key,
result,
}
})
})
.collect()
}
#[derive(Debug, Default)]
pub struct MarkdownImagePresentationStore {
entries: BTreeMap<String, MarkdownImagePresentationEntry>,
}
#[derive(Debug)]
struct MarkdownImagePresentationEntry {
cache_key: MarkdownImageCacheKey,
state: MarkdownImagePresentationState,
}
impl MarkdownImagePresentationStore {
pub fn reconcile(
&mut self,
inputs: &[MarkdownImagePresentationInput],
policy: MarkdownImagePresentationPolicy,
) -> Vec<String> {
let previous = self.entries.keys().cloned().collect::<BTreeSet<_>>();
let active = inputs
.iter()
.map(|input| input.contribution_id.clone())
.collect::<BTreeSet<_>>();
let mut removed = previous
.difference(&active)
.cloned()
.collect::<BTreeSet<_>>();
self.entries
.retain(|contribution_id, _| active.contains(contribution_id));
for input in inputs {
let reset = self
.entries
.get(&input.contribution_id)
.is_none_or(|entry| entry.cache_key != input.cache_key);
if reset {
if self.entries.contains_key(&input.contribution_id) {
removed.insert(input.contribution_id.clone());
}
self.entries.insert(
input.contribution_id.clone(),
MarkdownImagePresentationEntry {
cache_key: input.cache_key.clone(),
state: MarkdownImagePresentationState::initial(&input.destination, policy),
},
);
} else if let Some(entry) = self.entries.get_mut(&input.contribution_id) {
entry.state.reconcile_policy(&input.destination, policy);
}
}
removed.into_iter().collect()
}
pub fn reconcile_with_inflight(
&mut self,
inputs: &[MarkdownImagePresentationInput],
policy: MarkdownImagePresentationPolicy,
inflight: &mut MarkdownImageInflight,
) -> Vec<String> {
let removed = self.reconcile(inputs, policy);
let active_keys = inputs
.iter()
.filter(|input| {
policy.interactive_resident_frame
&& policy.terminal_supported
&& (policy.network_enabled
|| !matches!(
markdown_image_load_decision(&input.destination),
MarkdownImageLoadDecision::Remote(_)
))
})
.map(|input| input.cache_key.clone())
.collect::<BTreeSet<_>>();
inflight.retain(&active_keys);
removed
}
#[must_use]
pub fn schedule_loads(
&mut self,
inputs: &[MarkdownImagePresentationInput],
policy: MarkdownImagePresentationPolicy,
inflight: &mut MarkdownImageInflight,
) -> Vec<MarkdownImageLoadRequest> {
let mut requests = Vec::new();
let mut prefetched = 0_usize;
for input in inputs.iter().filter(|input| match input.residency {
MarkdownImageResidency::Visible => true,
MarkdownImageResidency::Prefetch if prefetched < MAX_PREFETCH_CONTRIBUTIONS => {
prefetched = prefetched.saturating_add(1);
true
}
MarkdownImageResidency::Hidden | MarkdownImageResidency::Prefetch => false,
}) {
let Some(entry) = self.entries.get_mut(&input.contribution_id) else {
continue;
};
if !matches!(entry.state, MarkdownImagePresentationState::Idle)
|| !entry.state.start_loading(&input.destination, policy)
{
continue;
}
if !inflight.start(input.cache_key.clone()) {
entry.state = MarkdownImagePresentationState::Idle;
continue;
}
let Some(cancellation) = inflight.cancellation_token(&input.cache_key) else {
entry.state = MarkdownImagePresentationState::Idle;
continue;
};
requests.push(MarkdownImageLoadRequest {
contribution_id: input.contribution_id.clone(),
cache_key: input.cache_key.clone(),
destination: input.destination.clone(),
cancellation,
});
}
requests
}
pub fn complete_load(
&mut self,
key: &MarkdownImageCacheKey,
result: Result<DecodedMarkdownImage, MarkdownImageLoadFailure>,
cache: &mut MarkdownImageCache,
inflight: &mut MarkdownImageInflight,
) -> bool {
if !inflight.finish(key) {
return false;
}
match result {
Ok(image) => {
cache.insert(key.clone(), image.clone());
for entry in self
.entries
.values_mut()
.filter(|entry| &entry.cache_key == key)
{
entry.state.ready(image.clone());
}
}
Err(failure) => {
for entry in self
.entries
.values_mut()
.filter(|entry| &entry.cache_key == key)
{
entry.state.failed(&failure);
}
}
}
true
}
pub fn hydrate_from_cache(&mut self, cache: &mut MarkdownImageCache) {
for entry in self
.entries
.values_mut()
.filter(|entry| matches!(entry.state, MarkdownImagePresentationState::Idle))
{
if let Some(image) = cache.get(&entry.cache_key) {
entry.state.ready(image);
}
}
}
pub fn present_ready(
&self,
contribution_id: &str,
destination: Rect,
clip: Rect,
frame: &mut bmux_tui::paint::PaintCx<'_, '_>,
) -> bool {
let Some(MarkdownImagePresentationState::Ready(image)) = self.state(contribution_id) else {
return false;
};
let clipped = destination.intersection(clip);
if clipped.is_empty() {
return false;
}
frame.push_image(image.bmux_contribution(
format!("markdown:{contribution_id}"),
destination,
clip,
));
true
}
pub fn remove_from_frame(contribution_id: &str, frame: &mut bmux_tui::paint::PaintCx<'_, '_>) {
frame.push_image(ImageContribution::Remove(ImageKey::new(format!(
"markdown:{contribution_id}"
))));
}
#[must_use]
pub fn state(&self, contribution_id: &str) -> Option<&MarkdownImagePresentationState> {
self.entries.get(contribution_id).map(|entry| &entry.state)
}
#[must_use]
pub fn fallback(
&self,
contribution_id: &str,
alt: &str,
source: &MarkdownDestination,
linked_destination: Option<&MarkdownDestination>,
) -> Option<String> {
let state = self.state(contribution_id)?;
Some(if linked_destination.is_some() {
state.linked_badge_fallback(alt, source, linked_destination)
} else {
state.fallback(alt, source)
})
}
pub fn state_mut(
&mut self,
contribution_id: &str,
) -> Option<&mut MarkdownImagePresentationState> {
self.entries
.get_mut(contribution_id)
.map(|entry| &mut entry.state)
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
impl MarkdownImagePresentationState {
fn reconcile_policy(
&mut self,
destination: &MarkdownDestination,
policy: MarkdownImagePresentationPolicy,
) {
let policy_state = Self::initial(destination, policy);
match policy_state {
Self::TerminalUnsupported | Self::NetworkDisabled => *self = policy_state,
Self::Idle if matches!(self, Self::TerminalUnsupported | Self::NetworkDisabled) => {
*self = Self::Idle;
}
Self::Idle | Self::Loading | Self::Ready(_) | Self::Failed(_) => {}
}
}
#[must_use]
pub fn initial(
destination: &MarkdownDestination,
policy: MarkdownImagePresentationPolicy,
) -> Self {
if !policy.interactive_resident_frame {
return Self::Idle;
}
if !policy.terminal_supported {
return Self::TerminalUnsupported;
}
if !policy.network_enabled
&& matches!(
markdown_image_load_decision(destination),
MarkdownImageLoadDecision::Remote(_)
)
{
return Self::NetworkDisabled;
}
Self::Idle
}
pub fn start_loading(
&mut self,
destination: &MarkdownDestination,
policy: MarkdownImagePresentationPolicy,
) -> bool {
if !policy.interactive_resident_frame {
*self = Self::Idle;
return false;
}
let initial = Self::initial(destination, policy);
if !matches!(initial, Self::Idle)
|| markdown_image_load_decision(destination) == MarkdownImageLoadDecision::Reject
{
*self = initial;
return false;
}
*self = Self::Loading;
true
}
pub fn ready(&mut self, image: DecodedMarkdownImage) {
*self = Self::Ready(image);
}
pub fn failed(&mut self, failure: &MarkdownImageLoadFailure) {
*self = Self::Failed(failure.to_string());
}
#[must_use]
pub fn fallback(&self, alt: &str, source: &MarkdownDestination) -> String {
let state = match self {
Self::Idle => "image idle",
Self::Loading => "image loading",
Self::Failed(_) => "image failed",
Self::NetworkDisabled => "image network disabled",
Self::TerminalUnsupported => "image unsupported by terminal",
Self::Ready(_) => return String::new(),
};
let alt = if alt.trim().is_empty() {
"image"
} else {
alt.trim()
};
let source = markdown_image_source_fallback(source);
match (self, source) {
(Self::Failed(error), Some(source)) => {
format!("[{alt} — {state}: {error}; source: {source}]")
}
(Self::Failed(error), None) => format!("[{alt} — {state}: {error}]"),
(_, Some(source)) => format!("[{alt} — {state}; source: {source}]"),
(_, None) => format!("[{alt} — {state}]"),
}
}
#[must_use]
pub fn linked_badge_fallback(
&self,
alt: &str,
source: &MarkdownDestination,
linked_destination: Option<&MarkdownDestination>,
) -> String {
let image = self.fallback(alt, source);
let Some(destination) = linked_destination.and_then(markdown_image_source_fallback) else {
return image;
};
let label = if alt.trim().is_empty() {
"image"
} else {
alt.trim()
};
if image.is_empty() {
format!("[{label} → {destination}]")
} else {
format!("{image} → {destination}")
}
}
}
fn markdown_image_source_fallback(source: &MarkdownDestination) -> Option<String> {
match source {
MarkdownDestination::Web(url) => Some(url.as_str().to_owned()),
MarkdownDestination::LocalPath(path) => Some(path.to_string_lossy().into_owned()),
MarkdownDestination::Fragment(fragment) => Some(format!("#{fragment}")),
MarkdownDestination::UnresolvedRelative(source) => Some(source.clone()),
MarkdownDestination::Inert { .. } => None,
}
}
#[must_use]
pub fn markdown_image_load_decision(
destination: &MarkdownDestination,
) -> MarkdownImageLoadDecision {
match destination {
MarkdownDestination::Web(url) if matches!(url.scheme(), "http" | "https") => {
MarkdownImageLoadDecision::Remote(url.clone())
}
MarkdownDestination::LocalPath(path) => MarkdownImageLoadDecision::Local(path.clone()),
MarkdownDestination::Fragment(_)
| MarkdownDestination::UnresolvedRelative(_)
| MarkdownDestination::Inert { .. }
| MarkdownDestination::Web(_) => MarkdownImageLoadDecision::Reject,
}
}
#[derive(Debug, Clone)]
pub struct MarkdownImageLoadGuard {
started: Instant,
redirects: usize,
}
impl Default for MarkdownImageLoadGuard {
fn default() -> Self {
Self::new()
}
}
impl MarkdownImageLoadGuard {
#[must_use]
pub fn new() -> Self {
Self {
started: Instant::now(),
redirects: 0,
}
}
pub fn follow_redirect(
&mut self,
destination: &MarkdownDestination,
) -> Result<url::Url, MarkdownImageLoadError> {
self.check_deadline()?;
if self.redirects >= MAX_REDIRECTS {
return Err(MarkdownImageLoadError::TooManyRedirects);
}
let MarkdownImageLoadDecision::Remote(url) = markdown_image_load_decision(destination)
else {
return Err(MarkdownImageLoadError::RejectedDestination);
};
self.redirects = self.redirects.saturating_add(1);
Ok(url)
}
pub fn final_destination(
&self,
destination: &MarkdownDestination,
) -> Result<MarkdownImageLoadDecision, MarkdownImageLoadError> {
self.check_deadline()?;
let decision = markdown_image_load_decision(destination);
if decision == MarkdownImageLoadDecision::Reject {
Err(MarkdownImageLoadError::RejectedDestination)
} else {
Ok(decision)
}
}
fn check_deadline(&self) -> Result<(), MarkdownImageLoadError> {
if self.started.elapsed() >= LOAD_TIMEOUT {
Err(MarkdownImageLoadError::TimedOut)
} else {
Ok(())
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum MarkdownImageLoadError {
#[error("image destination is not loadable")]
RejectedDestination,
#[error("image redirect limit exceeded")]
TooManyRedirects,
#[error("image load timed out")]
TimedOut,
#[error("image load cancelled")]
Cancelled,
#[error("image I/O failed: {0}")]
Io(String),
#[error("image request failed: {0}")]
Network(String),
}
#[derive(Debug, Clone, Default)]
pub struct MarkdownImageCancellationToken(Arc<AtomicBool>);
impl MarkdownImageCancellationToken {
pub fn cancel(&self) {
self.0.store(true, Ordering::Release);
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.0.load(Ordering::Acquire)
}
}
#[derive(Debug, Clone)]
pub struct MarkdownImageLoader {
http: reqwest::Client,
permits: Arc<tokio::sync::Semaphore>,
}
impl MarkdownImageLoader {
pub fn new() -> Result<Self, MarkdownImageLoadError> {
let http = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.timeout(LOAD_TIMEOUT)
.build()
.map_err(|error| MarkdownImageLoadError::Network(error.to_string()))?;
Ok(Self {
http,
permits: Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_LOADS)),
})
}
pub async fn load(
&self,
destination: &MarkdownDestination,
) -> Result<DecodedMarkdownImage, MarkdownImageLoadFailure> {
self.load_cancellable(destination, &MarkdownImageCancellationToken::default())
.await
}
pub async fn load_cancellable(
&self,
destination: &MarkdownDestination,
cancellation: &MarkdownImageCancellationToken,
) -> Result<DecodedMarkdownImage, MarkdownImageLoadFailure> {
ensure_not_cancelled(cancellation)?;
let started = Instant::now();
let permit = self.permits.acquire();
tokio::pin!(permit);
let _permit = loop {
tokio::select! {
result = &mut permit => {
break result.map_err(|_| MarkdownImageLoadError::Network("image loader closed".to_owned()))?;
}
() = tokio::time::sleep(Duration::from_millis(10)) => {
ensure_not_cancelled(cancellation)?;
let _ = remaining_load_time(started)?;
}
}
};
ensure_not_cancelled(cancellation)?;
let encoded = match markdown_image_load_decision(destination) {
MarkdownImageLoadDecision::Remote(url) => {
self.load_remote(url, started, cancellation).await?
}
MarkdownImageLoadDecision::Local(path) => {
let task = tokio::task::spawn_blocking(move || read_local_image(&path));
wait_for_task(task, started, cancellation).await??
}
MarkdownImageLoadDecision::Reject => {
return Err(MarkdownImageLoadError::RejectedDestination.into());
}
};
ensure_not_cancelled(cancellation)?;
let task = tokio::task::spawn_blocking(move || {
decode_markdown_image(std::io::Cursor::new(encoded))
});
wait_for_task(task, started, cancellation)
.await?
.map_err(Into::into)
}
async fn load_remote(
&self,
mut url: url::Url,
started: Instant,
cancellation: &MarkdownImageCancellationToken,
) -> Result<Vec<u8>, MarkdownImageLoadFailure> {
let mut redirects = 0_usize;
loop {
ensure_not_cancelled(cancellation)?;
let request = self.http.get(url.clone()).send();
tokio::pin!(request);
let response = loop {
tokio::select! {
result = &mut request => {
break result.map_err(|error| MarkdownImageLoadError::Network(error.to_string()))?;
}
() = tokio::time::sleep(Duration::from_millis(10)) => {
ensure_not_cancelled(cancellation)?;
let _ = remaining_load_time(started)?;
}
}
};
if response.status().is_redirection() {
if redirects >= MAX_REDIRECTS {
return Err(MarkdownImageLoadError::TooManyRedirects.into());
}
let location = response
.headers()
.get(reqwest::header::LOCATION)
.ok_or(MarkdownImageLoadError::RejectedDestination)?
.to_str()
.map_err(|_| MarkdownImageLoadError::RejectedDestination)?;
let next = url
.join(location)
.map_err(|_| MarkdownImageLoadError::RejectedDestination)?;
let destination = MarkdownDestination::Web(next);
let MarkdownImageLoadDecision::Remote(next) =
markdown_image_load_decision(&destination)
else {
return Err(MarkdownImageLoadError::RejectedDestination.into());
};
url = next;
redirects = redirects.saturating_add(1);
continue;
}
if !response.status().is_success() {
return Err(
MarkdownImageLoadError::Network(format!("HTTP {}", response.status())).into(),
);
}
return read_remote_image(response, started, cancellation).await;
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum MarkdownImageLoadFailure {
#[error(transparent)]
Load(#[from] MarkdownImageLoadError),
#[error(transparent)]
Image(#[from] MarkdownImageError),
}
fn ensure_not_cancelled(
cancellation: &MarkdownImageCancellationToken,
) -> Result<(), MarkdownImageLoadError> {
if cancellation.is_cancelled() {
Err(MarkdownImageLoadError::Cancelled)
} else {
Ok(())
}
}
async fn wait_for_task<T>(
mut task: tokio::task::JoinHandle<T>,
started: Instant,
cancellation: &MarkdownImageCancellationToken,
) -> Result<T, MarkdownImageLoadError> {
loop {
tokio::select! {
result = &mut task => {
return result.map_err(|error| MarkdownImageLoadError::Io(error.to_string()));
}
() = tokio::time::sleep(Duration::from_millis(10)) => {
if let Err(error) = ensure_not_cancelled(cancellation) {
task.abort();
return Err(error);
}
if let Err(error) = remaining_load_time(started) {
task.abort();
return Err(error);
}
}
}
}
}
fn remaining_load_time(started: Instant) -> Result<Duration, MarkdownImageLoadError> {
LOAD_TIMEOUT
.checked_sub(started.elapsed())
.filter(|remaining| !remaining.is_zero())
.ok_or(MarkdownImageLoadError::TimedOut)
}
fn read_local_image(path: &std::path::Path) -> Result<Vec<u8>, MarkdownImageLoadFailure> {
let file =
std::fs::File::open(path).map_err(|error| MarkdownImageLoadError::Io(error.to_string()))?;
read_encoded_bounded(std::io::BufReader::new(file)).map_err(Into::into)
}
async fn read_remote_image(
mut response: reqwest::Response,
started: Instant,
cancellation: &MarkdownImageCancellationToken,
) -> Result<Vec<u8>, MarkdownImageLoadFailure> {
if response
.content_length()
.is_some_and(|length| length > u64::try_from(MAX_ENCODED_BYTES).unwrap_or(u64::MAX))
{
return Err(MarkdownImageError::EncodedTooLarge.into());
}
let mut bytes = Vec::new();
loop {
ensure_not_cancelled(cancellation)?;
let chunk = tokio::time::timeout(remaining_load_time(started)?, response.chunk())
.await
.map_err(|_| MarkdownImageLoadError::TimedOut)?
.map_err(|error| MarkdownImageLoadError::Network(error.to_string()))?;
let Some(chunk) = chunk else {
return Ok(bytes);
};
if bytes.len().saturating_add(chunk.len()) > MAX_ENCODED_BYTES {
return Err(MarkdownImageError::EncodedTooLarge.into());
}
bytes.extend_from_slice(&chunk);
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MarkdownImageCacheKey(String);
impl MarkdownImageCacheKey {
#[must_use]
pub fn new(source: &str, context: &str) -> Self {
Self(format!("markdown-image-v1:{source}:{context}"))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecodedMarkdownImage {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
}
impl DecodedMarkdownImage {
const fn payload_bytes(&self) -> usize {
self.rgba.len()
}
#[must_use]
pub fn bmux_contribution(
&self,
key: impl Into<String>,
destination: Rect,
clip: Rect,
) -> ImageContribution {
ImageContribution::Present(ImagePlacement {
key: ImageKey::new(key),
payload: ImagePayload::Pixels {
bytes: self.rgba.clone(),
width: self.width,
height: self.height,
format: ImagePixelFormat::Rgba8,
},
destination,
clip,
fit: bmux_tui::image::ImageFit::Contain,
allocation: None,
lifecycle: ImageLifecycle::Frame,
})
}
}
#[derive(Debug, Default)]
pub struct MarkdownImageCache {
entries: BTreeMap<MarkdownImageCacheKey, CacheEntry>,
clock: u64,
payload_bytes: usize,
}
#[derive(Debug)]
struct CacheEntry {
image: DecodedMarkdownImage,
last_used: u64,
}
impl MarkdownImageCache {
pub fn get(&mut self, key: &MarkdownImageCacheKey) -> Option<DecodedMarkdownImage> {
self.clock = self.clock.saturating_add(1);
let entry = self.entries.get_mut(key)?;
entry.last_used = self.clock;
Some(entry.image.clone())
}
pub fn insert(&mut self, key: MarkdownImageCacheKey, image: DecodedMarkdownImage) {
if image.payload_bytes() > MAX_CACHE_BYTES {
return;
}
self.clock = self.clock.saturating_add(1);
if let Some(previous) = self.entries.remove(&key) {
self.payload_bytes = self
.payload_bytes
.saturating_sub(previous.image.payload_bytes());
}
self.payload_bytes = self.payload_bytes.saturating_add(image.payload_bytes());
self.entries.insert(
key,
CacheEntry {
image,
last_used: self.clock,
},
);
self.evict_to_limits();
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub const fn payload_bytes(&self) -> usize {
self.payload_bytes
}
fn evict_to_limits(&mut self) {
while self.entries.len() > MAX_CACHE_ENTRIES || self.payload_bytes > MAX_CACHE_BYTES {
let Some(key) = self
.entries
.iter()
.min_by_key(|(key, entry)| (entry.last_used, *key))
.map(|(key, _)| key.clone())
else {
break;
};
if let Some(entry) = self.entries.remove(&key) {
self.payload_bytes = self
.payload_bytes
.saturating_sub(entry.image.payload_bytes());
}
}
}
}
#[derive(Debug, Default)]
pub struct MarkdownImageInflight {
loads: BTreeMap<MarkdownImageCacheKey, MarkdownImageCancellationToken>,
}
impl MarkdownImageInflight {
pub fn start(&mut self, key: MarkdownImageCacheKey) -> bool {
if self.loads.contains_key(&key) {
return false;
}
self.loads
.insert(key, MarkdownImageCancellationToken::default());
true
}
#[must_use]
pub fn cancellation_token(
&self,
key: &MarkdownImageCacheKey,
) -> Option<MarkdownImageCancellationToken> {
self.loads.get(key).cloned()
}
pub fn finish(&mut self, key: &MarkdownImageCacheKey) -> bool {
self.loads.remove(key).is_some()
}
pub fn retain(&mut self, active: &BTreeSet<MarkdownImageCacheKey>) {
self.loads.retain(|key, cancellation| {
if active.contains(key) {
true
} else {
cancellation.cancel();
false
}
});
}
pub fn cancel(&mut self, key: &MarkdownImageCacheKey) {
if let Some(cancellation) = self.loads.remove(key) {
cancellation.cancel();
}
}
pub fn cancel_all(&mut self) {
for cancellation in self.loads.values() {
cancellation.cancel();
}
self.loads.clear();
}
#[must_use]
pub fn len(&self) -> usize {
self.loads.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.loads.is_empty()
}
}
#[derive(Debug, thiserror::Error)]
pub enum MarkdownImageError {
#[error("encoded image exceeds {MAX_ENCODED_BYTES} bytes")]
EncodedTooLarge,
#[error("invalid image: {0}")]
Invalid(#[from] image::ImageError),
#[error("image dimensions {width}x{height} exceed {MAX_DIMENSION}x{MAX_DIMENSION}")]
DimensionsTooLarge {
width: u32,
height: u32,
},
#[error("image pixel count {pixels} exceeds {MAX_DECODED_PIXELS}")]
TooManyPixels {
pixels: u64,
},
#[error("decoded image requires too many bytes")]
DecodedTooLarge,
}
pub fn decode_markdown_image(
reader: impl BufRead + Seek,
) -> Result<DecodedMarkdownImage, MarkdownImageError> {
let encoded = read_encoded_bounded(reader)?;
let cursor = std::io::Cursor::new(&encoded);
let reader = image::ImageReader::new(cursor)
.with_guessed_format()
.map_err(image::ImageError::IoError)?;
let decoder = reader.into_decoder()?;
let (width, height) = decoder.dimensions();
validate_dimensions(width, height)?;
let decoded_bytes = decoder.total_bytes();
let max_decoded_bytes = MAX_DECODED_PIXELS.saturating_mul(4);
if decoded_bytes > max_decoded_bytes {
return Err(MarkdownImageError::DecodedTooLarge);
}
let image = image::DynamicImage::from_decoder(decoder)?;
let rgba = image.into_rgba8().into_raw();
if u64::try_from(rgba.len()).unwrap_or(u64::MAX) > max_decoded_bytes {
return Err(MarkdownImageError::DecodedTooLarge);
}
Ok(DecodedMarkdownImage {
width,
height,
rgba,
})
}
fn read_encoded_bounded(reader: impl BufRead + Seek) -> Result<Vec<u8>, MarkdownImageError> {
let mut bytes = Vec::new();
reader
.take(u64::try_from(MAX_ENCODED_BYTES).unwrap_or(u64::MAX) + 1)
.read_to_end(&mut bytes)
.map_err(image::ImageError::IoError)?;
if bytes.len() > MAX_ENCODED_BYTES {
return Err(MarkdownImageError::EncodedTooLarge);
}
Ok(bytes)
}
fn validate_dimensions(width: u32, height: u32) -> Result<(), MarkdownImageError> {
if width > MAX_DIMENSION || height > MAX_DIMENSION {
return Err(MarkdownImageError::DimensionsTooLarge { width, height });
}
let pixels = u64::from(width).saturating_mul(u64::from(height));
if pixels > MAX_DECODED_PIXELS {
return Err(MarkdownImageError::TooManyPixels { pixels });
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{
DecodedMarkdownImage, MAX_CACHE_BYTES, MAX_CACHE_ENTRIES, MAX_DECODED_PIXELS,
MAX_DIMENSION, MAX_ENCODED_BYTES, MAX_PREFETCH_CONTRIBUTIONS, MAX_REDIRECTS,
MarkdownImageCache, MarkdownImageCacheKey, MarkdownImageCancellationToken,
MarkdownImageError, MarkdownImageInflight, MarkdownImageLoadDecision,
MarkdownImageLoadError, MarkdownImageLoadFailure, MarkdownImageLoadGuard,
MarkdownImageLoader, MarkdownImagePresentationInput, MarkdownImagePresentationPolicy,
MarkdownImagePresentationState, MarkdownImagePresentationStore, MarkdownImageResidency,
MarkdownPresentationRuntime, RESERVED_IMAGE_ROWS, decode_markdown_image,
markdown_image_load_decision, markdown_image_reserved_rows, validate_dimensions,
};
use bcode_markdown_render::{
MarkdownDestination, MarkdownDestinationRejection, resolve_markdown_destination,
};
use image::ImageEncoder;
use std::collections::BTreeSet;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use bmux_tui::geometry::Rect;
use bmux_tui::image::{ImageContribution, ImageLifecycle, ImagePayload, ImagePixelFormat};
fn response(stream: &mut TcpStream, status: &str, headers: &[(&str, &str)], body: &[u8]) {
let mut head = format!("HTTP/1.1 {status}\r\nContent-Length: {}\r\n", body.len());
for (name, value) in headers {
head.push_str(name);
head.push_str(": ");
head.push_str(value);
head.push_str("\r\n");
}
head.push_str("Connection: close\r\n\r\n");
stream.write_all(head.as_bytes()).unwrap();
stream.write_all(body).unwrap();
}
fn serve(
request_count: usize,
handler: impl Fn(usize, &mut TcpStream) + Send + 'static,
) -> String {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let address = listener.local_addr().unwrap();
std::thread::spawn(move || {
for (index, incoming) in listener.incoming().take(request_count).enumerate() {
let mut stream = incoming.unwrap();
let mut request = [0_u8; 2048];
let _ = stream.read(&mut request).unwrap();
handler(index, &mut stream);
}
});
format!("http://{address}/image")
}
fn png(width: u32, height: u32) -> Vec<u8> {
let pixels = vec![0x80; usize::try_from(u64::from(width) * u64::from(height) * 4).unwrap()];
let mut bytes = Vec::new();
image::codecs::png::PngEncoder::new(&mut bytes)
.write_image(&pixels, width, height, image::ExtendedColorType::Rgba8)
.unwrap();
bytes
}
fn image(value: u8, bytes: usize) -> DecodedMarkdownImage {
DecodedMarkdownImage {
width: 1,
height: 1,
rgba: vec![value; bytes],
}
}
fn key(index: usize) -> MarkdownImageCacheKey {
MarkdownImageCacheKey::new(&format!("image-{index}"), "context")
}
#[test]
fn decoded_image_adapts_to_protocol_neutral_bmux_pixels() {
let decoded = DecodedMarkdownImage {
width: 2,
height: 3,
rgba: vec![0x80; 24],
};
let contribution = decoded.bmux_contribution(
"markdown-image:item:1",
Rect::new(2, 4, 20, 6),
Rect::new(0, 2, 80, 20),
);
let ImageContribution::Present(placement) = contribution else {
panic!("expected presented image")
};
assert_eq!(placement.key.as_str(), "markdown-image:item:1");
assert_eq!(placement.destination, Rect::new(2, 4, 20, 6));
assert_eq!(placement.clip, Rect::new(0, 2, 80, 20));
assert_eq!(placement.lifecycle, ImageLifecycle::Frame);
assert!(matches!(
placement.payload,
ImagePayload::Pixels {
bytes,
width: 2,
height: 3,
format: ImagePixelFormat::Rgba8,
} if bytes == vec![0x80; 24]
));
}
#[test]
fn bounded_cache_evicts_deterministic_least_recently_used_entry() {
let mut cache = MarkdownImageCache::default();
for index in 0..MAX_CACHE_ENTRIES {
cache.insert(key(index), image(u8::try_from(index).unwrap_or(0), 4));
}
assert!(cache.get(&key(0)).is_some());
cache.insert(key(MAX_CACHE_ENTRIES), image(255, 4));
assert_eq!(cache.len(), MAX_CACHE_ENTRIES);
assert!(cache.get(&key(0)).is_some());
assert!(cache.get(&key(1)).is_none());
assert!(cache.get(&key(MAX_CACHE_ENTRIES)).is_some());
}
#[test]
fn bounded_cache_enforces_payload_limit_and_skips_oversized_entry() {
let mut cache = MarkdownImageCache::default();
cache.insert(key(0), image(1, MAX_CACHE_BYTES));
cache.insert(key(1), image(2, 4));
assert!(cache.get(&key(0)).is_none());
assert!(cache.get(&key(1)).is_some());
assert!(cache.payload_bytes() <= MAX_CACHE_BYTES);
cache.insert(key(2), image(3, MAX_CACHE_BYTES + 1));
assert!(cache.get(&key(2)).is_none());
}
#[test]
fn inflight_keys_deduplicate_and_cancel_nonresident_work() {
let mut inflight = MarkdownImageInflight::default();
assert!(inflight.start(key(1)));
assert!(!inflight.start(key(1)));
assert!(inflight.start(key(2)));
let removed = inflight.cancellation_token(&key(1)).unwrap();
let retained = inflight.cancellation_token(&key(2)).unwrap();
inflight.retain(&BTreeSet::from([key(2)]));
assert!(removed.is_cancelled());
assert!(!retained.is_cancelled());
assert!(inflight.cancellation_token(&key(1)).is_none());
assert!(inflight.start(key(1)));
assert!(inflight.finish(&key(1)));
assert!(inflight.finish(&key(2)));
assert!(inflight.is_empty());
}
#[test]
fn changed_and_all_inflight_work_are_actively_cancelled() {
let mut inflight = MarkdownImageInflight::default();
assert!(inflight.start(key(1)));
assert!(inflight.start(key(2)));
let changed = inflight.cancellation_token(&key(1)).unwrap();
let remaining = inflight.cancellation_token(&key(2)).unwrap();
inflight.cancel(&key(1));
assert!(changed.is_cancelled());
assert!(!remaining.is_cancelled());
assert_eq!(inflight.len(), 1);
inflight.cancel_all();
assert!(remaining.is_cancelled());
assert!(inflight.is_empty());
}
#[test]
fn capability_loss_cancels_inflight_remote_work() {
let remote = resolve_markdown_destination("https://example.com/image.png", None);
let input = [MarkdownImagePresentationInput {
contribution_id: "image".to_owned(),
cache_key: key(1),
destination: remote,
residency: MarkdownImageResidency::Visible,
}];
let enabled = MarkdownImagePresentationPolicy {
interactive_resident_frame: true,
network_enabled: true,
terminal_supported: true,
};
let disabled = MarkdownImagePresentationPolicy {
network_enabled: false,
..enabled
};
let mut store = MarkdownImagePresentationStore::default();
let mut inflight = MarkdownImageInflight::default();
store.reconcile_with_inflight(&input, enabled, &mut inflight);
let request = store
.schedule_loads(&input, enabled, &mut inflight)
.pop()
.expect("scheduled load");
store.reconcile_with_inflight(&input, disabled, &mut inflight);
assert!(request.cancellation.is_cancelled());
assert!(inflight.is_empty());
assert_eq!(
store.state("image"),
Some(&MarkdownImagePresentationState::NetworkDisabled)
);
}
#[tokio::test]
async fn runtime_reconcile_starts_and_completes_current_local_load() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("image.png");
std::fs::write(&path, png(2, 3)).unwrap();
let input = [MarkdownImagePresentationInput {
contribution_id: "image".to_owned(),
cache_key: MarkdownImageCacheKey::new(&path.to_string_lossy(), "document"),
destination: MarkdownDestination::LocalPath(path),
residency: MarkdownImageResidency::Visible,
}];
let policy = MarkdownImagePresentationPolicy {
interactive_resident_frame: true,
network_enabled: false,
terminal_supported: true,
};
let mut runtime = MarkdownPresentationRuntime::new().unwrap();
let (removed, tasks) = runtime.reconcile_and_start(&input, policy);
assert!(removed.is_empty());
let [task] = tasks.try_into().expect("one image task");
assert!(runtime.complete(task.await.unwrap()));
assert!(matches!(
runtime.images.state("image"),
Some(MarkdownImagePresentationState::Ready(_))
));
let (_removed, tasks) = runtime.reconcile_and_start(&input, policy);
assert!(tasks.is_empty());
}
#[tokio::test]
async fn runtime_discards_completion_after_owner_removal() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("image.png");
std::fs::write(&path, png(2, 3)).unwrap();
let input = [MarkdownImagePresentationInput {
contribution_id: "image".to_owned(),
cache_key: MarkdownImageCacheKey::new(&path.to_string_lossy(), "document"),
destination: MarkdownDestination::LocalPath(path),
residency: MarkdownImageResidency::Visible,
}];
let policy = MarkdownImagePresentationPolicy {
interactive_resident_frame: true,
network_enabled: false,
terminal_supported: true,
};
let mut runtime = MarkdownPresentationRuntime::new().unwrap();
let (_, tasks) = runtime.reconcile_and_start(&input, policy);
let [task] = tasks.try_into().expect("one image task");
let (removed, _) = runtime.reconcile_and_start(&[], policy);
assert_eq!(removed, ["image"]);
assert!(!runtime.complete(task.await.unwrap()));
assert!(runtime.image_cache.is_empty());
}
#[test]
fn decoded_cache_key_is_independent_of_viewport_and_terminal_protocol() {
assert_eq!(
MarkdownImageCacheKey::new("https://example.com/image.png", "document"),
MarkdownImageCacheKey::new("https://example.com/image.png", "document")
);
assert_ne!(
MarkdownImageCacheKey::new("https://example.com/image.png", "document"),
MarkdownImageCacheKey::new("https://example.com/other.png", "document")
);
}
#[test]
fn every_presentation_state_reserves_identical_rows_before_and_after_loading() {
let image = image(1, 4);
let states = [
MarkdownImagePresentationState::Idle,
MarkdownImagePresentationState::Loading,
MarkdownImagePresentationState::Ready(image),
MarkdownImagePresentationState::Failed("failure".to_owned()),
MarkdownImagePresentationState::NetworkDisabled,
MarkdownImagePresentationState::TerminalUnsupported,
];
assert!(
states
.iter()
.all(|state| { markdown_image_reserved_rows(state) == RESERVED_IMAGE_ROWS })
);
}
#[test]
fn load_completion_updates_shared_owners_cache_and_failure_state() {
let remote = resolve_markdown_destination("https://example.com/image.png", None);
let policy = MarkdownImagePresentationPolicy {
interactive_resident_frame: true,
network_enabled: true,
terminal_supported: true,
};
let key = MarkdownImageCacheKey::new("image.png", "document");
let inputs = ["first", "second"].map(|id| MarkdownImagePresentationInput {
contribution_id: id.to_owned(),
cache_key: key.clone(),
destination: remote.clone(),
residency: MarkdownImageResidency::Visible,
});
let mut store = MarkdownImagePresentationStore::default();
let mut inflight = MarkdownImageInflight::default();
let mut cache = MarkdownImageCache::default();
store.reconcile_with_inflight(&inputs, policy, &mut inflight);
let requests = store.schedule_loads(&inputs, policy, &mut inflight);
assert_eq!(requests.len(), 1);
assert!(store.complete_load(&key, Ok(image(7, 4)), &mut cache, &mut inflight));
assert!(matches!(
store.state("first"),
Some(MarkdownImagePresentationState::Ready(_))
));
assert!(matches!(
store.state("second"),
Some(MarkdownImagePresentationState::Ready(_))
));
assert!(cache.get(&key).is_some());
assert!(inflight.cancellation_token(&key).is_none());
let failed_key = MarkdownImageCacheKey::new("failed.png", "document");
let failed = [MarkdownImagePresentationInput {
contribution_id: "failed".to_owned(),
cache_key: failed_key.clone(),
destination: remote,
residency: MarkdownImageResidency::Visible,
}];
store.reconcile_with_inflight(&failed, policy, &mut inflight);
assert_eq!(
store.schedule_loads(&failed, policy, &mut inflight).len(),
1
);
assert!(store.complete_load(
&failed_key,
Err(MarkdownImageLoadFailure::Load(
MarkdownImageLoadError::TimedOut,
)),
&mut cache,
&mut inflight,
));
assert!(matches!(
store.state("failed"),
Some(MarkdownImagePresentationState::Failed(message)) if message.contains("timed out")
));
}
#[test]
fn late_cancelled_load_completion_is_discarded_without_cache_repopulation() {
let remote = resolve_markdown_destination("https://example.com/image.png", None);
let policy = MarkdownImagePresentationPolicy {
interactive_resident_frame: true,
network_enabled: true,
terminal_supported: true,
};
let key = MarkdownImageCacheKey::new("image.png", "document");
let input = [MarkdownImagePresentationInput {
contribution_id: "image".to_owned(),
cache_key: key.clone(),
destination: remote,
residency: MarkdownImageResidency::Visible,
}];
let mut store = MarkdownImagePresentationStore::default();
let mut cache = MarkdownImageCache::default();
let mut inflight = MarkdownImageInflight::default();
store.reconcile_with_inflight(&input, policy, &mut inflight);
assert_eq!(store.schedule_loads(&input, policy, &mut inflight).len(), 1);
store.reconcile_with_inflight(&[], policy, &mut inflight);
assert!(!store.complete_load(&key, Ok(image(2, 2)), &mut cache, &mut inflight));
assert!(cache.get(&key).is_none());
}
#[test]
fn reconstruction_reuses_decoded_cache_without_scheduling_new_work() {
let remote = resolve_markdown_destination("https://example.com/image.png", None);
let policy = MarkdownImagePresentationPolicy {
interactive_resident_frame: true,
network_enabled: true,
terminal_supported: true,
};
let key = MarkdownImageCacheKey::new("image.png", "document");
let input = [MarkdownImagePresentationInput {
contribution_id: "reconstructed".to_owned(),
cache_key: key.clone(),
destination: remote,
residency: MarkdownImageResidency::Visible,
}];
let mut cache = MarkdownImageCache::default();
cache.insert(key, image(9, 4));
let mut store = MarkdownImagePresentationStore::default();
let mut inflight = MarkdownImageInflight::default();
store.reconcile_with_inflight(&input, policy, &mut inflight);
store.hydrate_from_cache(&mut cache);
assert!(matches!(
store.state("reconstructed"),
Some(MarkdownImagePresentationState::Ready(_))
));
assert!(
store
.schedule_loads(&input, policy, &mut inflight)
.is_empty()
);
}
#[test]
fn ready_store_emits_stable_clipped_bmux_frame_contribution() {
let remote = resolve_markdown_destination("https://example.com/image.png", None);
let policy = MarkdownImagePresentationPolicy {
interactive_resident_frame: true,
network_enabled: true,
terminal_supported: true,
};
let input = MarkdownImagePresentationInput {
contribution_id: "owner:image:1".to_owned(),
cache_key: MarkdownImageCacheKey::new("image.png", "document"),
destination: remote,
residency: MarkdownImageResidency::Visible,
};
let mut store = MarkdownImagePresentationStore::default();
store.reconcile(&[input], policy);
store
.state_mut("owner:image:1")
.expect("resident state")
.ready(DecodedMarkdownImage {
width: 2,
height: 2,
rgba: vec![255; 16],
});
let terminal = Rect::new(0, 0, 20, 10);
let destination = Rect::new(4, 3, 8, 4);
let clip = Rect::new(6, 4, 3, 2);
let mut buffer = bmux_tui::buffer::Buffer::empty(terminal);
let mut frame = bmux_tui::frame::Frame::new(&mut buffer);
assert!(store.present_ready(
"owner:image:1",
destination,
clip,
&mut bmux_tui::paint::PaintCx::new(&mut frame)
));
let [ImageContribution::Present(placement)] = frame.images() else {
panic!("expected one presented image");
};
assert_eq!(placement.key.as_str(), "markdown:owner:image:1");
assert_eq!(placement.destination, destination);
assert_eq!(placement.clip, clip);
assert!(!store.present_ready(
"owner:image:1",
Rect::new(15, 8, 2, 2),
Rect::new(0, 0, 1, 1),
&mut bmux_tui::paint::PaintCx::new(&mut frame),
));
MarkdownImagePresentationStore::remove_from_frame(
"owner:image:1",
&mut bmux_tui::paint::PaintCx::new(&mut frame),
);
assert!(matches!(
frame.images().last(),
Some(ImageContribution::Remove(key)) if key.as_str() == "markdown:owner:image:1"
));
}
#[test]
fn large_history_metrics_remain_bounded_and_trimming_cancels_rich_work() {
let local = MarkdownDestination::LocalPath(std::path::PathBuf::from("/trusted/image.png"));
let policy = MarkdownImagePresentationPolicy {
interactive_resident_frame: true,
network_enabled: true,
terminal_supported: true,
};
let inputs = (0..1_000)
.map(|index| MarkdownImagePresentationInput {
contribution_id: format!("image-{index}"),
cache_key: MarkdownImageCacheKey::new(&format!("image-{index}.png"), "document"),
destination: local.clone(),
residency: MarkdownImageResidency::Prefetch,
})
.collect::<Vec<_>>();
let mut store = MarkdownImagePresentationStore::default();
let mut inflight = MarkdownImageInflight::default();
let mut cache = MarkdownImageCache::default();
store.reconcile_with_inflight(&inputs, policy, &mut inflight);
let work = store.schedule_loads(&inputs, policy, &mut inflight);
for (index, request) in work.iter().enumerate() {
cache.insert(
request.cache_key.clone(),
image(u8::try_from(index).unwrap_or(0), 4),
);
}
assert_eq!(inflight.len(), MAX_PREFETCH_CONTRIBUTIONS);
assert_eq!(store.len(), 1_000);
assert!(cache.len() <= MAX_CACHE_ENTRIES);
assert!(cache.payload_bytes() <= MAX_CACHE_BYTES);
let retained = &inputs[900..];
store.reconcile_with_inflight(retained, policy, &mut inflight);
assert!(
work.iter()
.all(|request| request.cancellation.is_cancelled())
);
assert!(inflight.is_empty());
assert_eq!(store.len(), retained.len());
}
#[test]
fn prefetch_scheduling_is_bounded_independently_of_resident_history_size() {
let local = MarkdownDestination::LocalPath(std::path::PathBuf::from("/trusted/image.png"));
let inputs = (0..1_000)
.map(|index| MarkdownImagePresentationInput {
contribution_id: format!("image-{index}"),
cache_key: MarkdownImageCacheKey::new(&format!("image-{index}.png"), "document"),
destination: local.clone(),
residency: MarkdownImageResidency::Prefetch,
})
.collect::<Vec<_>>();
let policy = MarkdownImagePresentationPolicy {
interactive_resident_frame: true,
network_enabled: true,
terminal_supported: true,
};
let mut store = MarkdownImagePresentationStore::default();
let mut inflight = MarkdownImageInflight::default();
store.reconcile_with_inflight(&inputs, policy, &mut inflight);
let requests = store.schedule_loads(&inputs, policy, &mut inflight);
assert_eq!(requests.len(), MAX_PREFETCH_CONTRIBUTIONS);
assert_eq!(store.len(), 1_000);
}
#[test]
fn scheduler_starts_only_visible_or_bounded_prefetch_work_and_honors_network_policy() {
let remote = resolve_markdown_destination("https://example.com/image.png", None);
let local = MarkdownDestination::LocalPath(std::path::PathBuf::from("/trusted/image.png"));
let input =
|id: &str, source: &str, destination, residency| MarkdownImagePresentationInput {
contribution_id: id.to_owned(),
cache_key: MarkdownImageCacheKey::new(source, "document"),
destination,
residency,
};
let inputs = vec![
input(
"hidden",
"hidden.png",
local.clone(),
MarkdownImageResidency::Hidden,
),
input(
"visible",
"visible.png",
local,
MarkdownImageResidency::Visible,
),
input(
"prefetch",
"prefetch.png",
remote.clone(),
MarkdownImageResidency::Prefetch,
),
];
let mut store = MarkdownImagePresentationStore::default();
let mut inflight = MarkdownImageInflight::default();
let enabled = MarkdownImagePresentationPolicy {
interactive_resident_frame: true,
network_enabled: true,
terminal_supported: true,
};
store.reconcile_with_inflight(&inputs, enabled, &mut inflight);
let requests = store.schedule_loads(&inputs, enabled, &mut inflight);
assert_eq!(
requests
.iter()
.map(|request| request.contribution_id.as_str())
.collect::<Vec<_>>(),
["visible", "prefetch"]
);
assert_eq!(
store.state("hidden"),
Some(&MarkdownImagePresentationState::Idle)
);
let remote_only = [input(
"network-disabled",
"remote.png",
remote,
MarkdownImageResidency::Visible,
)];
let disabled = MarkdownImagePresentationPolicy {
interactive_resident_frame: true,
network_enabled: false,
terminal_supported: true,
};
store.reconcile_with_inflight(&remote_only, disabled, &mut inflight);
assert!(
store
.schedule_loads(&remote_only, disabled, &mut inflight)
.is_empty()
);
assert_eq!(
store.state("network-disabled"),
Some(&MarkdownImagePresentationState::NetworkDisabled)
);
assert!(
requests
.iter()
.all(|request| request.cancellation.is_cancelled())
);
assert!(
inflight
.cancellation_token(&requests[0].cache_key)
.is_none()
);
}
#[test]
fn presentation_store_preserves_current_state_and_drops_replaced_or_removed_owners() {
let remote = resolve_markdown_destination("https://example.com/image.png", None);
let policy = MarkdownImagePresentationPolicy {
interactive_resident_frame: true,
network_enabled: true,
terminal_supported: true,
};
let input = |id: &str, source: &str| MarkdownImagePresentationInput {
contribution_id: id.to_owned(),
cache_key: MarkdownImageCacheKey::new(source, "document"),
destination: remote.clone(),
residency: MarkdownImageResidency::Visible,
};
let mut store = MarkdownImagePresentationStore::default();
store.reconcile(&[input("owner:image:1", "a.png")], policy);
assert_eq!(store.len(), 1);
let unchanged = store.reconcile(&[input("owner:image:1", "a.png")], policy);
assert!(unchanged.is_empty());
assert!(
store
.state_mut("owner:image:1")
.expect("resident state")
.start_loading(&remote, policy)
);
store.reconcile(&[input("owner:image:1", "a.png")], policy);
assert_eq!(
store.state("owner:image:1"),
Some(&MarkdownImagePresentationState::Loading)
);
let removed = store.reconcile(&[input("owner:image:1", "changed.png")], policy);
assert_eq!(removed, ["owner:image:1"]);
assert_eq!(
store.state("owner:image:1"),
Some(&MarkdownImagePresentationState::Idle)
);
let removed = store.reconcile(&[input("replacement:image:1", "changed.png")], policy);
assert_eq!(removed, ["owner:image:1"]);
assert!(store.state("owner:image:1").is_none());
assert_eq!(store.len(), 1);
let removed = store.reconcile(&[], policy);
assert_eq!(removed, ["replacement:image:1"]);
assert!(store.is_empty());
}
#[test]
fn unchanged_owner_reconciles_capability_policy_and_recovers_to_idle() {
let remote = resolve_markdown_destination("https://example.com/image.png", None);
let input = [MarkdownImagePresentationInput {
contribution_id: "image".to_owned(),
cache_key: MarkdownImageCacheKey::new("image.png", "document"),
destination: remote,
residency: MarkdownImageResidency::Visible,
}];
let enabled = MarkdownImagePresentationPolicy {
interactive_resident_frame: true,
network_enabled: true,
terminal_supported: true,
};
let unsupported = MarkdownImagePresentationPolicy {
terminal_supported: false,
..enabled
};
let mut store = MarkdownImagePresentationStore::default();
store.reconcile(&input, enabled);
store
.state_mut("image")
.expect("image state")
.ready(image(2, 2));
assert!(store.reconcile(&input, unsupported).is_empty());
assert_eq!(
store.state("image"),
Some(&MarkdownImagePresentationState::TerminalUnsupported)
);
assert!(store.reconcile(&input, enabled).is_empty());
assert_eq!(
store.state("image"),
Some(&MarkdownImagePresentationState::Idle)
);
}
#[test]
fn store_fallback_uses_resident_state_and_compact_link_destination() {
let source = resolve_markdown_destination("https://example.com/image.png", None);
let destination = resolve_markdown_destination("https://example.com", None);
let policy = MarkdownImagePresentationPolicy {
interactive_resident_frame: true,
network_enabled: false,
terminal_supported: true,
};
let input = [MarkdownImagePresentationInput {
contribution_id: "image".to_owned(),
cache_key: MarkdownImageCacheKey::new("image.png", "document"),
destination: source.clone(),
residency: MarkdownImageResidency::Visible,
}];
let mut store = MarkdownImagePresentationStore::default();
store.reconcile(&input, policy);
let fallback = store
.fallback("image", "badge", &source, Some(&destination))
.expect("resident fallback");
assert!(fallback.contains("image network disabled"));
assert!(fallback.contains("https://example.com/"));
}
#[test]
fn presentation_state_is_capability_aware_and_preserves_fallback_context() {
let remote = resolve_markdown_destination("https://example.com/image.png", None);
let local = MarkdownDestination::LocalPath(std::path::PathBuf::from("/trusted/image.png"));
let enabled = MarkdownImagePresentationPolicy {
interactive_resident_frame: true,
network_enabled: true,
terminal_supported: true,
};
let mut state = MarkdownImagePresentationState::initial(&remote, enabled);
assert_eq!(state, MarkdownImagePresentationState::Idle);
let history_policy = MarkdownImagePresentationPolicy {
interactive_resident_frame: false,
network_enabled: true,
terminal_supported: true,
};
let mut history_state = MarkdownImagePresentationState::initial(&remote, history_policy);
assert!(!history_state.start_loading(&remote, history_policy));
assert_eq!(history_state, MarkdownImagePresentationState::Idle);
assert!(state.start_loading(&remote, enabled));
assert_eq!(state, MarkdownImagePresentationState::Loading);
state.ready(DecodedMarkdownImage {
width: 1,
height: 1,
rgba: vec![0, 0, 0, 255],
});
assert!(matches!(state, MarkdownImagePresentationState::Ready(_)));
assert_eq!(
MarkdownImagePresentationState::initial(
&remote,
MarkdownImagePresentationPolicy {
interactive_resident_frame: true,
network_enabled: false,
terminal_supported: true,
},
),
MarkdownImagePresentationState::NetworkDisabled
);
assert_eq!(
MarkdownImagePresentationState::initial(
&local,
MarkdownImagePresentationPolicy {
interactive_resident_frame: true,
network_enabled: true,
terminal_supported: false,
},
),
MarkdownImagePresentationState::TerminalUnsupported
);
let mut failed = MarkdownImagePresentationState::Loading;
failed.failed(&MarkdownImageLoadFailure::Load(
MarkdownImageLoadError::TimedOut,
));
let fallback = failed.fallback("diagram", &remote);
assert!(fallback.contains("diagram"));
assert!(fallback.contains("image load timed out"));
assert!(fallback.contains("https://example.com/image.png"));
let secret = MarkdownDestination::Inert {
reason: MarkdownDestinationRejection::UnsupportedScheme,
};
let inert_fallback = MarkdownImagePresentationState::Idle.fallback("private", &secret);
assert_eq!(inert_fallback, "[private — image idle]");
assert!(!inert_fallback.contains("source:"));
let destination = resolve_markdown_destination("https://example.com/build", None);
let badge = MarkdownImagePresentationState::TerminalUnsupported.linked_badge_fallback(
"Build",
&remote,
Some(&destination),
);
assert!(badge.contains("Build"));
assert!(badge.contains("https://example.com/image.png"));
assert!(badge.ends_with("→ https://example.com/build"));
let unsafe_destination = MarkdownDestination::Inert {
reason: MarkdownDestinationRejection::UnsupportedScheme,
};
assert!(
!MarkdownImagePresentationState::Idle
.linked_badge_fallback("Build", &remote, Some(&unsafe_destination))
.contains('→')
);
}
#[test]
fn rejected_destinations_never_enter_loading_state() {
let destination = MarkdownDestination::UnresolvedRelative("image.png".to_owned());
let policy = MarkdownImagePresentationPolicy {
interactive_resident_frame: true,
network_enabled: true,
terminal_supported: true,
};
let mut state = MarkdownImagePresentationState::initial(&destination, policy);
assert!(!state.start_loading(&destination, policy));
assert_eq!(state, MarkdownImagePresentationState::Idle);
}
#[test]
fn image_loading_policy_allows_only_classified_http_https_and_trusted_local_paths() {
let https = resolve_markdown_destination("https://example.com/image.png", None);
assert!(matches!(
markdown_image_load_decision(&https),
MarkdownImageLoadDecision::Remote(url) if url.scheme() == "https"
));
let local = MarkdownDestination::LocalPath(std::path::PathBuf::from("/trusted/image.png"));
assert_eq!(
markdown_image_load_decision(&local),
MarkdownImageLoadDecision::Local(std::path::PathBuf::from("/trusted/image.png"))
);
for rejected in [
MarkdownDestination::Fragment("image".to_owned()),
MarkdownDestination::UnresolvedRelative("image.png".to_owned()),
MarkdownDestination::Inert {
reason: MarkdownDestinationRejection::UnsupportedScheme,
},
] {
assert_eq!(
markdown_image_load_decision(&rejected),
MarkdownImageLoadDecision::Reject
);
}
}
#[test]
fn every_redirect_and_final_destination_is_revalidated() {
let safe = resolve_markdown_destination("https://example.com/image.png", None);
let unsafe_destination = resolve_markdown_destination("file://remote/image.png", None);
let mut guard = MarkdownImageLoadGuard::new();
for _ in 0..MAX_REDIRECTS {
assert!(guard.follow_redirect(&safe).is_ok());
}
assert_eq!(
guard.follow_redirect(&safe),
Err(MarkdownImageLoadError::TooManyRedirects)
);
let mut guard = MarkdownImageLoadGuard::new();
assert_eq!(
guard.follow_redirect(&unsafe_destination),
Err(MarkdownImageLoadError::RejectedDestination)
);
assert_eq!(
guard.final_destination(&unsafe_destination),
Err(MarkdownImageLoadError::RejectedDestination)
);
assert!(matches!(
guard.final_destination(&safe),
Ok(MarkdownImageLoadDecision::Remote(_))
));
}
#[tokio::test]
async fn cancelled_load_stops_before_network_or_decode_work() {
let loader = MarkdownImageLoader::new().unwrap();
let cancellation = MarkdownImageCancellationToken::default();
cancellation.cancel();
let destination = resolve_markdown_destination("https://example.com/image.png", None);
assert!(matches!(
loader.load_cancellable(&destination, &cancellation).await,
Err(MarkdownImageLoadFailure::Load(
MarkdownImageLoadError::Cancelled
))
));
}
#[tokio::test]
async fn loader_reads_trusted_local_image_and_rejects_unresolved_source() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("image.png");
std::fs::write(&path, png(2, 3)).unwrap();
let loader = MarkdownImageLoader::new().unwrap();
let decoded = loader
.load(&MarkdownDestination::LocalPath(path))
.await
.unwrap();
assert_eq!((decoded.width, decoded.height), (2, 3));
assert!(matches!(
loader
.load(&MarkdownDestination::UnresolvedRelative(
"image.png".to_owned()
))
.await,
Err(MarkdownImageLoadFailure::Load(
MarkdownImageLoadError::RejectedDestination
))
));
}
#[tokio::test]
async fn loader_follows_bounded_http_redirects_and_decodes_final_payload() {
let image = png(2, 3);
let origin = serve(2, move |index, stream| {
if index == 0 {
response(stream, "302 Found", &[("Location", "/final")], &[]);
} else {
response(stream, "200 OK", &[("Content-Type", "image/png")], &image);
}
});
let loader = MarkdownImageLoader::new().unwrap();
let destination = resolve_markdown_destination(&origin, None);
let decoded = loader.load(&destination).await.unwrap();
assert_eq!((decoded.width, decoded.height), (2, 3));
}
#[tokio::test]
async fn loader_revalidates_redirect_scheme_before_following_it() {
let origin = serve(1, |_, stream| {
response(
stream,
"302 Found",
&[("Location", "file:///tmp/secret.png")],
&[],
);
});
let loader = MarkdownImageLoader::new().unwrap();
let destination = resolve_markdown_destination(&origin, None);
assert!(matches!(
loader.load(&destination).await,
Err(MarkdownImageLoadFailure::Load(
MarkdownImageLoadError::RejectedDestination
))
));
}
#[tokio::test]
async fn loader_stops_after_fixed_redirect_limit() {
let origin = serve(MAX_REDIRECTS + 1, |_, stream| {
response(stream, "302 Found", &[("Location", "/again")], &[]);
});
let loader = MarkdownImageLoader::new().unwrap();
let destination = resolve_markdown_destination(&origin, None);
assert!(matches!(
loader.load(&destination).await,
Err(MarkdownImageLoadFailure::Load(
MarkdownImageLoadError::TooManyRedirects
))
));
}
#[tokio::test]
async fn loader_rejects_oversized_remote_payload_before_decode() {
let origin = serve(1, |_, stream| {
response(
stream,
"200 OK",
&[("Content-Type", "image/png")],
&vec![0_u8; MAX_ENCODED_BYTES + 1],
);
});
let loader = MarkdownImageLoader::new().unwrap();
let destination = resolve_markdown_destination(&origin, None);
assert!(matches!(
loader.load(&destination).await,
Err(MarkdownImageLoadFailure::Image(
MarkdownImageError::EncodedTooLarge
))
));
}
#[tokio::test]
async fn loader_rejects_malformed_remote_payload() {
let origin = serve(1, |_, stream| {
response(
stream,
"200 OK",
&[("Content-Type", "image/png")],
b"not an image",
);
});
let loader = MarkdownImageLoader::new().unwrap();
let destination = resolve_markdown_destination(&origin, None);
assert!(matches!(
loader.load(&destination).await,
Err(MarkdownImageLoadFailure::Image(
MarkdownImageError::Invalid(_)
))
));
}
#[tokio::test]
async fn loader_rejects_non_successful_remote_status() {
let origin = serve(1, |_, stream| {
response(stream, "404 Not Found", &[], b"missing");
});
let loader = MarkdownImageLoader::new().unwrap();
let destination = resolve_markdown_destination(&origin, None);
assert!(matches!(
loader.load(&destination).await,
Err(MarkdownImageLoadFailure::Load(
MarkdownImageLoadError::Network(message)
)) if message == "HTTP 404 Not Found"
));
}
#[test]
fn validates_encoded_bytes_before_attempting_decode() {
let oversized = vec![0; MAX_ENCODED_BYTES + 1];
assert!(matches!(
decode_markdown_image(std::io::Cursor::new(oversized)),
Err(MarkdownImageError::EncodedTooLarge)
));
}
#[test]
fn rejects_dimensions_before_pixel_allocation() {
assert!(matches!(
validate_dimensions(MAX_DIMENSION + 1, 1),
Err(MarkdownImageError::DimensionsTooLarge { .. })
));
assert!(matches!(
validate_dimensions(1, MAX_DIMENSION + 1),
Err(MarkdownImageError::DimensionsTooLarge { .. })
));
}
#[test]
fn rejects_pixel_count_before_pixel_allocation() {
assert!(matches!(
validate_dimensions(MAX_DIMENSION, MAX_DIMENSION),
Ok(())
));
assert!(matches!(validate_dimensions(4096, 4096), Ok(())));
assert!(MAX_DECODED_PIXELS >= u64::from(MAX_DIMENSION) * u64::from(MAX_DIMENSION));
}
#[test]
fn decodes_valid_payload_to_rgba() {
let decoded = decode_markdown_image(std::io::Cursor::new(png(2, 3))).unwrap();
assert_eq!((decoded.width, decoded.height), (2, 3));
assert_eq!(decoded.rgba.len(), 24);
}
#[test]
fn malformed_payload_is_rejected() {
assert!(matches!(
decode_markdown_image(std::io::Cursor::new(b"not an image".to_vec())),
Err(MarkdownImageError::Invalid(_))
));
}
}