8fea308580
CI / checks (push) Successful in 38s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
143 lines
4.4 KiB
Rust
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"));
|
|
}
|
|
}
|