use alloc::{borrow::Cow, string::String};
use crate::types::{ToolOutput, TruncationPolicy};
pub const TOOL_OUTPUT_QUARANTINE_TAG: &str = md_tmpl_core::TOOL_OUTPUT_QUARANTINE_TAG;
pub const TOOL_OUTPUT_QUARANTINE_OPEN: &str = md_tmpl_core::TOOL_OUTPUT_QUARANTINE_OPEN_TAG;
pub const TOOL_OUTPUT_QUARANTINE_CLOSE: &str = md_tmpl_core::TOOL_OUTPUT_QUARANTINE_CLOSE_TAG;
pub(crate) const OPEN_FRAME: &str = "<tool_output_quarantine>\n";
pub(crate) const CLOSE_FRAME: &str = "\n</tool_output_quarantine>";
pub(crate) const FRAME_OVERHEAD: usize = OPEN_FRAME.len() + CLOSE_FRAME.len();
pub const DELIMITER_RULES: &[(&str, &str)] = md_tmpl_core::TOKEN_DELIMITERS;
pub const DEFAULT_SANITIZE_TAG: &str = md_tmpl_core::DEFAULT_SANITIZE_TAG;
pub const DEFAULT_SANITIZE_NOTICE: &str = md_tmpl_core::DEFAULT_SANITIZE_NOTICE;
pub const SANITIZE_NOTICE_TAG_PLACEHOLDER: &str = md_tmpl_core::SANITIZE_NOTICE_TAG_PLACEHOLDER;
pub use md_tmpl_core::{
format_sanitize_notice, is_sanitized_block_str, sanitize_block_idempotent_str,
sanitize_block_str, sanitize_str, unsanitize_block_str,
};
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum OutputSanitization {
None,
#[default]
SanitizeTokens,
Quarantine,
SanitizeTagged(&'static str),
SanitizeWithNotice {
tag: &'static str,
notice: &'static str,
},
}
impl<'de> serde::Deserialize<'de> for OutputSanitization {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
#[serde(rename_all = "snake_case")]
enum Helper {
None,
SanitizeTokens,
Quarantine,
SanitizeTagged(String),
SanitizeWithNotice { tag: String, notice: String },
}
match Helper::deserialize(deserializer)? {
Helper::None => Ok(Self::None),
Helper::SanitizeTokens => Ok(Self::SanitizeTokens),
Helper::Quarantine => Ok(Self::Quarantine),
Helper::SanitizeTagged(tag) => Ok(Self::SanitizeTagged(alloc::boxed::Box::leak(
tag.into_boxed_str(),
))),
Helper::SanitizeWithNotice { tag, notice } => Ok(Self::SanitizeWithNotice {
tag: alloc::boxed::Box::leak(tag.into_boxed_str()),
notice: alloc::boxed::Box::leak(notice.into_boxed_str()),
}),
}
}
}
impl OutputSanitization {
const fn protection_level(self) -> u8 {
match self {
Self::None => 0,
Self::SanitizeTokens => 1,
Self::Quarantine | Self::SanitizeTagged(_) | Self::SanitizeWithNotice { .. } => 2,
}
}
#[must_use]
pub const fn at_least(self, floor: Self) -> Self {
if floor.protection_level() > self.protection_level() {
floor
} else {
self
}
}
#[must_use]
pub const fn is_none(&self) -> bool {
matches!(self, Self::None)
}
#[must_use]
pub const fn sanitizes_tokens(self) -> bool {
!matches!(self, Self::None)
}
#[must_use]
pub const fn is_quarantined(self) -> bool {
matches!(
self,
Self::Quarantine | Self::SanitizeTagged(_) | Self::SanitizeWithNotice { .. }
)
}
#[must_use]
pub fn apply_str(self, raw: &str) -> Cow<'_, str> {
match self {
Self::None => Cow::Borrowed(raw),
Self::SanitizeTokens => {
if is_quarantined_output(raw) {
Cow::Borrowed(raw)
} else {
md_tmpl_core::sanitize_untrusted_str(raw, TOOL_OUTPUT_QUARANTINE_TAG)
}
}
Self::Quarantine => {
if is_quarantined_output(raw) {
Cow::Borrowed(raw)
} else {
Cow::Owned(quarantine_tool_output(raw))
}
}
Self::SanitizeTagged(tag) => {
if is_sanitized_block_str(raw, Some(tag), None) {
Cow::Borrowed(raw)
} else {
Cow::Owned(
sanitize_block_str(raw, Some(tag), None).expect("tag is a valid XML tag"),
)
}
}
Self::SanitizeWithNotice { tag, notice } => {
if is_sanitized_block_str(raw, Some(tag), Some(notice)) {
Cow::Borrowed(raw)
} else {
Cow::Owned(
sanitize_block_str(raw, Some(tag), Some(notice))
.expect("tag is a valid XML tag"),
)
}
}
}
}
#[must_use]
pub fn apply(self, output: ToolOutput) -> ToolOutput {
match self {
Self::None => output,
Self::SanitizeTokens => output.sanitized(),
Self::Quarantine => output.quarantined(),
Self::SanitizeTagged(tag) => output.sanitized_block(tag),
Self::SanitizeWithNotice { tag, notice } => output.sanitized_with(tag, notice),
}
}
}
#[must_use]
pub fn neutralize_delimiters(raw: &str) -> String {
md_tmpl_core::sanitize_untrusted_str(raw, TOOL_OUTPUT_QUARANTINE_TAG).into_owned()
}
#[must_use]
pub fn is_sanitized_output(raw: &str) -> bool {
is_quarantined_output(raw)
|| !md_tmpl_core::has_untrusted_breakout(raw, TOOL_OUTPUT_QUARANTINE_TAG)
}
#[must_use]
pub fn is_quarantined_output(raw: &str) -> bool {
md_tmpl_core::is_quarantined_str(raw, Some(TOOL_OUTPUT_QUARANTINE_TAG))
}
#[must_use]
pub fn unquarantine_tool_output(raw: &str) -> Option<&str> {
md_tmpl_core::unquarantine_str(raw, Some(TOOL_OUTPUT_QUARANTINE_TAG))
}
#[must_use]
pub fn quarantine_tool_output(raw: &str) -> String {
md_tmpl_core::quarantine_untrusted_idempotent_str(raw, Some(TOOL_OUTPUT_QUARANTINE_TAG))
.expect("TOOL_OUTPUT_QUARANTINE_TAG is a valid XML tag")
}
impl ToolOutput {
#[must_use]
pub fn sanitized(mut self) -> Self {
if !is_quarantined_output(&self.content)
&& let Cow::Owned(clean) =
md_tmpl_core::sanitize_untrusted_str(&self.content, TOOL_OUTPUT_QUARANTINE_TAG)
{
self.content = clean;
}
self
}
#[must_use]
pub fn is_sanitized(&self) -> bool {
is_sanitized_output(&self.content)
}
#[must_use]
pub fn quarantined(mut self) -> Self {
self.content = quarantine_tool_output(&self.content);
self
}
#[must_use]
pub fn is_quarantined(&self) -> bool {
is_quarantined_output(&self.content)
}
#[must_use]
pub fn sanitized_block(mut self, tag: &str) -> Self {
if !is_sanitized_block_str(&self.content, Some(tag), None) {
self.content =
sanitize_block_str(&self.content, Some(tag), None).expect("tag is a valid XML tag");
}
self
}
#[must_use]
pub fn sanitized_with(mut self, tag: &str, notice: &str) -> Self {
if !is_sanitized_block_str(&self.content, Some(tag), Some(notice)) {
self.content = sanitize_block_str(&self.content, Some(tag), Some(notice))
.expect("tag is a valid XML tag");
}
self
}
#[must_use]
pub fn is_sanitized_with(&self, tag: &str) -> bool {
if is_sanitized_block_str(&self.content, Some(tag), None) {
return true;
}
let Some(rest) = self
.content
.strip_prefix('<')
.and_then(|r| r.strip_prefix(tag))
.and_then(|r| r.strip_prefix('>'))
else {
return false;
};
let Some(inner) = rest
.strip_suffix('>')
.and_then(|r| r.strip_suffix(tag))
.and_then(|r| r.strip_suffix("</"))
else {
return false;
};
!md_tmpl_core::has_untrusted_breakout(inner, tag)
}
#[must_use]
pub fn unsanitized_with<'a>(&'a self, tag: &str) -> Option<&'a str> {
if let Some(payload) = unsanitize_block_str(&self.content, Some(tag), None) {
return Some(payload);
}
let rest = self
.content
.strip_prefix('<')
.and_then(|r| r.strip_prefix(tag))
.and_then(|r| r.strip_prefix('>'))?;
let body = rest
.strip_suffix('>')
.and_then(|r| r.strip_suffix(tag))
.and_then(|r| r.strip_suffix("</"))?;
let inner = body.strip_prefix('\n').unwrap_or(body);
let inner = inner.strip_suffix('\n').unwrap_or(inner);
let payload = if let Some((_notice_line, after)) = inner.split_once('\n') {
after.strip_prefix('\n').unwrap_or(after)
} else {
inner
};
if md_tmpl_core::has_untrusted_breakout(payload, tag) {
return None;
}
Some(payload)
}
#[must_use]
pub fn is_sanitized_with_notice(&self, tag: &str, notice: &str) -> bool {
is_sanitized_block_str(&self.content, Some(tag), Some(notice))
}
#[must_use]
pub fn unsanitized_with_notice<'a>(&'a self, tag: &str, notice: &str) -> Option<&'a str> {
unsanitize_block_str(&self.content, Some(tag), Some(notice))
}
#[must_use]
pub fn sanitized_truncated(mut self, policy: &TruncationPolicy) -> Self {
self.content = policy.truncate_sanitized(&self.content).into_owned();
self
}
#[must_use]
pub fn quarantined_truncated(mut self, policy: &TruncationPolicy) -> Self {
self.content = policy.truncate_quarantined(&self.content);
self
}
}