blob: 0d986fb13335e5d14976a2624a12fc2e197f8262 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use serde::{Deserialize, Serialize};
pub const ATTACHMENT_RESOURCE_PREFIX: &str = "maka://runtime/attachments/";
pub fn parse_resource_ref(value: &str) -> Option<&str> {
value
.strip_prefix(ATTACHMENT_RESOURCE_PREFIX)
.filter(|id| crate::interaction::entity_id(id).is_ok())
}
/// Image signatures are fixed-offset; PDF permits a bounded leading preamble.
pub fn sniff_mime(bytes: &[u8]) -> Option<&'static str> {
if bytes.starts_with(b"\x89PNG\r\n\x1a\n") {
return Some("image/png");
}
if bytes.starts_with(b"\xff\xd8\xff") {
return Some("image/jpeg");
}
if bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a") {
return Some("image/gif");
}
if bytes.starts_with(b"RIFF") && bytes.get(8..12) == Some(b"WEBP") {
return Some("image/webp");
}
bytes[..bytes.len().min(1024)]
.windows(5)
.any(|window| window == b"%PDF-")
.then_some("application/pdf")
}
pub fn sniff_binary_mime(bytes: &[u8]) -> Option<&'static str> {
if let Some(mime) = sniff_mime(bytes) {
return Some(mime);
}
let text = String::from_utf8_lossy(&bytes[..bytes.len().min(512)]);
let text = text.trim_start_matches(js_whitespace).to_ascii_lowercase();
let svg = |text: &str| {
text.strip_prefix("<svg")
.and_then(|rest| rest.chars().next())
.is_some_and(|c| c == '>' || js_whitespace(c))
};
(svg(&text)
|| (text.starts_with("<?xml") && text.match_indices("<svg").any(|(i, _)| svg(&text[i..]))))
.then_some("image/svg+xml")
}
fn js_whitespace(c: char) -> bool {
(c.is_whitespace() && c != '\u{85}') || c == '\u{feff}'
}
pub fn canonical_relative_path(path: &str) -> bool {
!path.is_empty()
&& !path.contains(['\0', '\\'])
&& !path.starts_with('/')
&& !(path.as_bytes().first().is_some_and(u8::is_ascii_alphabetic)
&& path.as_bytes().get(1) == Some(&b':'))
&& path
.split('/')
.all(|s| !s.is_empty() && s != "." && s != "..")
}
impl AttachmentKind {
pub fn from_metadata(mime_type: &str, name: &str) -> Self {
let mime = mime_type.to_lowercase();
if mime.starts_with("image/") {
return Self::Image;
}
if mime == "application/pdf" {
return Self::Pdf;
}
let name = name.to_lowercase();
match name.rsplit_once('.').map(|(_, extension)| extension) {
Some("docx" | "doc" | "xlsx" | "xls" | "pptx" | "ppt") => Self::Doc,
Some(
"c" | "cc" | "cpp" | "cs" | "css" | "go" | "h" | "hpp" | "java" | "js" | "json"
| "jsx" | "kt" | "mjs" | "cjs" | "php" | "py" | "rb" | "rs" | "sh" | "sql"
| "svelte" | "swift" | "ts" | "tsx" | "vue" | "yaml" | "yml" | "zsh",
) => Self::Code,
_ => Self::Other,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AttachmentRef {
pub kind: AttachmentKind,
pub name: String,
pub mime_type: String,
pub bytes: u64,
#[serde(rename = "ref")]
pub storage_ref: StorageRef,
}
impl AttachmentRef {
pub fn text_bytes(&self) -> usize {
let (scope, locator) = match &self.storage_ref {
StorageRef::SessionFile {
session_id,
relative_path,
} => (session_id.as_str(), relative_path.as_str()),
StorageRef::WorkspaceFile { relative_path } => ("", relative_path.as_str()),
StorageRef::ExternalFile { absolute_path } => ("", absolute_path.as_str()),
StorageRef::SessionContext { session_id, ref_id } => {
(session_id.as_str(), ref_id.as_str())
}
};
[self.name.as_str(), self.mime_type.as_str(), scope, locator]
.into_iter()
.fold(0usize, |bytes, text| bytes.saturating_add(text.len()))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AttachmentKind {
Image,
Pdf,
Doc,
Code,
Other,
}
/// Canonical references; client admission must reject host-owned Session context.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(
tag = "kind",
rename_all = "snake_case",
rename_all_fields = "camelCase",
deny_unknown_fields
)]
pub enum StorageRef {
SessionContext {
session_id: String,
ref_id: String,
},
SessionFile {
session_id: String,
relative_path: String,
},
WorkspaceFile {
relative_path: String,
},
ExternalFile {
absolute_path: String,
},
}
impl StorageRef {
/// The invocation supplies Session authority; the opaque URI never does.
pub fn resource_ref(&self) -> Option<String> {
match self {
Self::SessionFile { relative_path, .. }
if crate::interaction::entity_id(relative_path).is_ok() =>
{
Some(format!("{ATTACHMENT_RESOURCE_PREFIX}{relative_path}"))
}
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn modality_precedes_filename_but_mime_whitespace_is_not_reinterpreted() {
for (mime, name, expected) in [
("IMAGE/PNG", "script.rs", AttachmentKind::Image),
("application/pdf", "sheet.xlsx", AttachmentKind::Pdf),
(
"application/octet-stream",
"REPORT.DOCX",
AttachmentKind::Doc,
),
(" image/png", "script.rs", AttachmentKind::Code),
(
"application/pdf; charset=utf-8",
"paper.pdf",
AttachmentKind::Other,
),
] {
assert_eq!(AttachmentKind::from_metadata(mime, name), expected);
}
}
}