Files
memona-plugin-aindrive/src/address.rs
T
RaymondCho 8fea308580
CI / checks (push) Successful in 38s
Tests use the README's example drive id instead of a real drive's (#5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 14:28:47 +00:00

143 lines
4.4 KiB
Rust

//! Where a drive answers, and how a Memona path names something in it.
use crate::sdk::ErrorCode;
/// The drive's MCP endpoint, from whichever drive address was pasted: the
/// server URL aindrive's MCP panel shows (`…/mcp/d/<id>`) or the drive's own
/// page (`…/d/<id>`, with or without a folder after it). Anything before the
/// drive segments is kept, so a server hosted under a path still works.
pub fn endpoint(input: &str) -> Result<String, ErrorCode> {
let input = input.trim();
let rest = input
.strip_prefix("https://")
.or_else(|| input.strip_prefix("http://"))
.ok_or(ErrorCode::InvalidInput)?;
let origin_end =
input.len() - rest.len() + rest.find(['/', '?', '#']).unwrap_or(rest.len());
let (origin, tail) = input.split_at(origin_end);
if origin_end == input.len() - rest.len() {
return Err(ErrorCode::InvalidInput);
}
let path = tail.split(['?', '#']).next().unwrap_or_default();
let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
let marker = segments
.iter()
.position(|segment| *segment == "d")
.ok_or(ErrorCode::InvalidInput)?;
let id = segments.get(marker + 1).ok_or(ErrorCode::InvalidInput)?;
if id.is_empty()
|| id.len() > 64
|| !id
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
{
return Err(ErrorCode::InvalidInput);
}
let prefix_end = match marker.checked_sub(1) {
Some(before) if segments[before] == "mcp" => before,
_ => marker,
};
let mut url = origin.to_owned();
for segment in &segments[..prefix_end] {
url.push('/');
url.push_str(segment);
}
url.push_str("/mcp/d/");
url.push_str(id);
Ok(url)
}
/// A drive path is relative to the drive root, and the root itself is empty.
pub fn drive_path(path: &str) -> &str {
path.trim_matches('/')
}
/// A name Memona can show as one path segment. The drive's device may hold
/// names the host refuses, and one of those would fail the whole listing.
pub fn showable(name: &str) -> bool {
!name.is_empty()
&& name != "."
&& name != ".."
&& !name.contains(['/', '\\'])
&& !name.chars().any(char::is_control)
}
#[cfg(test)]
mod tests {
use super::{drive_path, endpoint, showable};
use crate::sdk::ErrorCode;
#[tokio::test]
async fn the_mcp_panel_url_is_used_as_given() {
assert_eq!(
endpoint("https://aindrive.ainetwork.ai/mcp/d/AbCdEf123456"),
Ok("https://aindrive.ainetwork.ai/mcp/d/AbCdEf123456".to_owned())
);
assert_eq!(
endpoint(" https://aindrive.ainetwork.ai/mcp/d/AbCdEf123456/ \n"),
Ok("https://aindrive.ainetwork.ai/mcp/d/AbCdEf123456".to_owned())
);
}
#[tokio::test]
async fn a_drive_page_address_finds_the_same_endpoint() {
for page in [
"https://aindrive.ainetwork.ai/d/AbCdEf123456",
"https://aindrive.ainetwork.ai/d/AbCdEf123456?path=notes",
"https://aindrive.ainetwork.ai/d/AbCdEf123456/notes/today.md",
"https://aindrive.ainetwork.ai/d/AbCdEf123456/manage#members",
] {
assert_eq!(
endpoint(page),
Ok("https://aindrive.ainetwork.ai/mcp/d/AbCdEf123456".to_owned()),
"{page}"
);
}
}
#[tokio::test]
async fn a_self_hosted_server_keeps_its_port_and_base_path() {
assert_eq!(
endpoint("http://localhost:3737/d/a_b-C"),
Ok("http://localhost:3737/mcp/d/a_b-C".to_owned())
);
assert_eq!(
endpoint("https://files.example/aindrive/mcp/d/abc"),
Ok("https://files.example/aindrive/mcp/d/abc".to_owned())
);
}
#[tokio::test]
async fn an_address_without_a_drive_is_refused() {
for input in [
"",
"aindrive.ainetwork.ai/d/abc",
"ftp://aindrive.ainetwork.ai/d/abc",
"https://",
"https://aindrive.ainetwork.ai",
"https://aindrive.ainetwork.ai/d/",
"https://aindrive.ainetwork.ai/d/a%2Fb",
"https://aindrive.ainetwork.ai/drives/abc",
] {
assert_eq!(endpoint(input), Err(ErrorCode::InvalidInput), "{input}");
}
}
#[tokio::test]
async fn memona_paths_become_drive_relative() {
assert_eq!(drive_path("/"), "");
assert_eq!(drive_path("/notes/today.md"), "notes/today.md");
assert_eq!(drive_path("/notes/"), "notes");
}
#[tokio::test]
async fn names_the_host_would_refuse_are_not_shown() {
assert!(showable("작가노트_2026.md"));
assert!(showable(".env"));
assert!(!showable(""));
assert!(!showable(".."));
assert!(!showable("a\\b"));
assert!(!showable("line\nbreak"));
}
}