use std::borrow::Cow;
use std::io;
use std::sync::Arc;
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use rig_core::completion::Message;
use rig_core::conversation::Change;
use rig_core::message::DocumentSourceKind::{Base64, Raw, Unknown, Url};
use rig_core::message::{Image, ImageMediaType};
use sha2::{Digest, Sha256};
use crate::Files;
const BLOB: &str = "blob:";
pub const BLOBS: &str = "blobs/";
pub(crate) fn put_blob(files: &(impl Files + ?Sized), name: &str, bytes: &[u8]) -> io::Result<()> {
files.write_file(&format!("{BLOBS}{name}"), bytes)
}
pub(crate) fn read_blob(files: &(impl Files + ?Sized), name: &str) -> io::Result<Vec<u8>> {
files.read_file(&format!("{BLOBS}{name}"))
}
pub(crate) fn blob_names(files: &(impl Files + ?Sized)) -> io::Result<Vec<String>> {
let names = files.files()?.into_iter();
Ok(names
.filter_map(|name| Some(name.strip_prefix(BLOBS)?.to_owned()))
.collect())
}
fn inline(image: &Image) -> bool {
matches!(image.data, Base64(_) | Raw(_))
}
pub fn store_images<'m>(message: &'m Message, store: &dyn Files) -> io::Result<Cow<'m, Message>> {
store_images_with(message, &mut |name, bytes| put_blob(store, &name, &bytes))
}
pub(crate) type PutBlob<'a> = dyn FnMut(String, Vec<u8>) -> io::Result<()> + 'a;
pub(crate) fn store_images_with<'m>(
message: &'m Message,
put: &mut PutBlob<'_>,
) -> io::Result<Cow<'m, Message>> {
if !message.images().any(inline) {
return Ok(Cow::Borrowed(message));
}
let mut message = message.clone();
for image in message.images_mut() {
let bytes = match &image.data {
Base64(data) => STANDARD.decode(data).ok(),
Raw(bytes) => Some(bytes.clone()),
_ => None,
};
if let Some(bytes) = bytes {
let extension = image.media_type.as_ref();
let extension = extension.map_or("bin", ImageMediaType::extension);
let name = format!("{:x}.{extension}", Sha256::digest(&bytes));
image.data = Url(format!("{BLOB}{name}"));
put(name, bytes)?;
}
}
Ok(Cow::Owned(message))
}
pub fn load_images(message: &mut Message, store: &dyn Files) -> io::Result<()> {
load_images_with(message, &|name| read_blob(store, name))
}
pub(crate) fn load_images_with(
message: &mut Message,
blob: &dyn Fn(&str) -> io::Result<Vec<u8>>,
) -> io::Result<()> {
let mut gone = Ok(());
for image in message.images_mut() {
if let Url(url) = &image.data
&& let Some(name) = url.strip_prefix(BLOB)
{
image.data = match blob(name) {
Ok(bytes) => Base64(STANDARD.encode(bytes)),
Err(failure) => {
gone = gone.and(Err(failure));
Unknown
}
};
}
}
gone
}
pub(crate) fn named_blobs(bytes: &[u8], named: &mut std::collections::BTreeSet<String>) {
let name_byte = |byte: &u8| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-');
let mut rest = bytes;
while let Some(at) = memchr::memmem::find(rest, BLOB.as_bytes()) {
let after = rest.get(at + BLOB.len()..).unwrap_or_default();
let len = after.iter().take_while(|byte| name_byte(byte)).count();
if let Some(name) = after
.get(..len)
.and_then(|name| std::str::from_utf8(name).ok())
&& !name.is_empty()
{
named.insert(name.to_owned());
}
rest = after.get(len..).unwrap_or_default();
}
}
pub(crate) fn messages_mut(
change: &mut Change,
f: &mut dyn FnMut(&mut Message) -> io::Result<()>,
) -> io::Result<()> {
match change {
Change::Push { entries } | Change::Splice { with: entries, .. } => entries
.iter_mut()
.try_for_each(|entry| f(Arc::make_mut(&mut entry.message))),
_ => Ok(()),
}
}
pub(crate) fn store_change(change: &Change, put: &mut PutBlob<'_>) -> io::Result<Option<Change>> {
let has_inline = match change {
Change::Push { entries } | Change::Splice { with: entries, .. } => entries
.iter()
.any(|entry| entry.message.images().any(inline)),
_ => false,
};
if !has_inline {
return Ok(None);
}
let mut change = change.clone();
messages_mut(&mut change, &mut |message| {
if let Cow::Owned(stored) = store_images_with(message, put)? {
*message = stored;
}
Ok(())
})?;
Ok(Some(change))
}