use std::fmt;
use serde::{
Deserialize,
Deserializer,
Serialize,
Serializer,
de::Error as DeserializerError,
};
use thiserror::Error;
use crate::{
url::{
ap_uri::{is_ap_uri, ApUri},
common::Origin,
http_uri::HttpUri,
},
};
pub const GATEWAY_PATH_PREFIX: &str = "/.well-known/apgateway/";
#[derive(Debug, Error)]
#[error("{0}")]
pub struct CanonicalUriError(pub &'static str);
#[derive(Clone, Debug, PartialEq)]
pub enum CanonicalUri {
Http(HttpUri),
Ap(ApUri),
}
pub fn with_gateway(ap_uri: &ApUri, gateway_base: &str) -> String {
format!("{}{}{}", gateway_base, GATEWAY_PATH_PREFIX, ap_uri.to_did_url())
}
impl CanonicalUri {
pub fn parse(value: &str) -> Result<Self, CanonicalUriError> {
let canonical_uri = NonCanonicalUri::parse(value)?
.into_canonical();
Ok(canonical_uri)
}
pub fn parse_canonical(value: &str) -> Result<Self, CanonicalUriError> {
let canonical_uri = Self::parse(value)?;
if canonical_uri.to_string() != value {
return Err(CanonicalUriError("URI is not canonical"));
};
Ok(canonical_uri)
}
pub fn authority(&self) -> String {
match self {
Self::Http(http_uri) => http_uri.authority().to_string(),
Self::Ap(ap_uri) => ap_uri.authority().to_string(),
}
}
pub fn origin(&self) -> Origin {
match self {
Self::Http(http_uri) => http_uri.origin(),
Self::Ap(ap_uri) => ap_uri.origin(),
}
}
pub fn to_http_uri(&self, maybe_gateway: Option<&str>) -> Option<String> {
let http_uri = match self {
Self::Http(http_uri) => http_uri.to_string(),
Self::Ap(ap_uri) => {
if let Some(gateway) = maybe_gateway {
with_gateway(ap_uri, gateway)
} else {
return None;
}
},
};
Some(http_uri)
}
}
impl fmt::Display for CanonicalUri {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Http(http_uri) => write!(formatter, "{}", http_uri),
Self::Ap(ap_uri) => write!(formatter, "{}", ap_uri),
}
}
}
fn parse_compatible_ap_uri(
http_uri: HttpUri,
) -> Result<(ApUri, HttpUri), CanonicalUriError> {
let relative_http_uri = http_uri.to_relative();
let did_url = relative_http_uri
.strip_prefix(GATEWAY_PATH_PREFIX)
.ok_or(CanonicalUriError("invalid gateway URI"))?;
let ap_uri = ApUri::from_did_url(did_url)
.map_err(CanonicalUriError)?;
let gateway_string = http_uri.base();
let gateway = HttpUri::parse(&gateway_string)
.map_err(CanonicalUriError)?;
Ok((ap_uri, gateway))
}
#[derive(Clone, Debug, PartialEq)]
pub enum NonCanonicalUri {
Http(HttpUri),
Ap((Option<HttpUri>, ApUri)),
}
impl NonCanonicalUri {
pub fn parse(value: &str) -> Result<Self, CanonicalUriError> {
let uri = if is_ap_uri(value) {
let ap_uri = ApUri::parse(value)
.map_err(CanonicalUriError)?;
NonCanonicalUri::Ap((None, ap_uri))
} else {
let http_uri = HttpUri::parse(value).map_err(CanonicalUriError)?;
if http_uri.path().starts_with(GATEWAY_PATH_PREFIX) {
let (ap_uri, gateway) = parse_compatible_ap_uri(http_uri)?;
NonCanonicalUri::Ap((Some(gateway), ap_uri))
} else {
NonCanonicalUri::Http(http_uri)
}
};
Ok(uri)
}
pub fn into_canonical(self) -> CanonicalUri {
match self {
Self::Http(http_uri) => CanonicalUri::Http(http_uri),
Self::Ap((_maybe_gateway, ap_uri)) => {
let canonical_ap_uri = ap_uri.without_query();
CanonicalUri::Ap(canonical_ap_uri)
},
}
}
}
impl fmt::Display for NonCanonicalUri {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Http(http_uri) => write!(formatter, "{}", http_uri),
Self::Ap((maybe_gateway, ap_uri)) => {
if let Some(gateway) = maybe_gateway {
let compatible_ap_uri =
with_gateway(ap_uri, &gateway.to_string());
write!(formatter, "{}", compatible_ap_uri)
} else {
write!(formatter, "{}", ap_uri)
}
}
}
}
}
#[deprecated]
pub fn parse_url(
value: &str,
) -> Result<(CanonicalUri, Option<String>), CanonicalUriError> {
let uri = NonCanonicalUri::parse(value)?;
let maybe_gateway = if let NonCanonicalUri::Ap((maybe_gateway, _)) = &uri {
maybe_gateway.as_ref().map(|http_uri| http_uri.to_string())
} else {
None
};
let canonical_uri = uri.into_canonical();
Ok((canonical_uri, maybe_gateway))
}
impl<'de> Deserialize<'de> for CanonicalUri {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: Deserializer<'de>
{
let value: String = Deserialize::deserialize(deserializer)?;
Self::parse(&value).map_err(DeserializerError::custom)
}
}
impl Serialize for NonCanonicalUri {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer
{
let uri_string = self.to_string();
serializer.serialize_str(&uri_string)
}
}
pub fn is_same_uri(url_1: &str, url_2: &str) -> Result<bool, CanonicalUriError> {
let canonical_uri_1 = CanonicalUri::parse(url_1)?;
let canonical_uri_2 = CanonicalUri::parse(url_2)?;
let is_same = canonical_uri_1 == canonical_uri_2;
Ok(is_same)
}
pub fn is_same_origin(id_1: &str, id_2: &str) -> Result<bool, CanonicalUriError> {
let id_1 = CanonicalUri::parse(id_1)?;
let id_2 = CanonicalUri::parse(id_2)?;
let is_same = id_1.origin() == id_2.origin();
Ok(is_same)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_http_uri_from_http_uri() {
let input = "https://social.example/users/test";
let canonical_uri = CanonicalUri::parse_canonical(input).unwrap();
let output = canonical_uri.to_http_uri(None).unwrap();
assert_eq!(output, input);
}
#[test]
fn test_http_uri_from_ap_uri() {
let input = "ap://did:key:z6MkvUie7gDQugJmyDQQPhMCCBfKJo7aGvzQYF2BqvFvdwx6/actor";
let canonical_uri = CanonicalUri::parse_canonical(input).unwrap();
let gateway = "https://gateway.example";
let output = canonical_uri.to_http_uri(Some(gateway)).unwrap();
let expected_output = "https://gateway.example/.well-known/apgateway/did:key:z6MkvUie7gDQugJmyDQQPhMCCBfKJo7aGvzQYF2BqvFvdwx6/actor";
assert_eq!(output, expected_output);
}
#[test]
fn test_http_uri_from_ap_uri_no_gateway() {
let input = "ap://did:key:z6MkvUie7gDQugJmyDQQPhMCCBfKJo7aGvzQYF2BqvFvdwx6/actor";
let canonical_uri = CanonicalUri::parse_canonical(input).unwrap();
let maybe_output = canonical_uri.to_http_uri(None);
assert!(maybe_output.is_none());
}
#[test]
fn test_parse_uri_http() {
let url = "https://social.example/users/test";
let uri = NonCanonicalUri::parse(url).unwrap();
assert!(matches!(uri, NonCanonicalUri::Http(_)));
assert_eq!(uri.to_string(), url);
}
#[test]
fn test_parse_uri_ap() {
let url = "ap://did:key:z6MkvUie7gDQugJmyDQQPhMCCBfKJo7aGvzQYF2BqvFvdwx6/actor";
let uri = NonCanonicalUri::parse(url).unwrap();
assert!(matches!(uri, NonCanonicalUri::Ap((None, _))));
assert_eq!(uri.to_string(), url);
}
#[test]
fn test_parse_uri_ap_with_gateway() {
let url = "https://social.example/.well-known/apgateway/did:key:z6MkvUie7gDQugJmyDQQPhMCCBfKJo7aGvzQYF2BqvFvdwx6/actor";
let uri = NonCanonicalUri::parse(url).unwrap();
assert!(matches!(uri, NonCanonicalUri::Ap((Some(_), _))));
assert_eq!(uri.to_string(), url);
}
#[test]
fn test_parse_uri_ap_with_gateway_and_query_params() {
let url = "https://social.example/.well-known/apgateway/did:key:z6MkvUie7gDQugJmyDQQPhMCCBfKJo7aGvzQYF2BqvFvdwx6/collection?page=1&test=true";
let uri = NonCanonicalUri::parse(url).unwrap();
assert_eq!(uri.to_string(), url);
}
#[test]
#[expect(deprecated)]
fn test_parse_url_https() {
let url = "https://social.example/users/test";
let (canonical_uri, maybe_gateway) = parse_url(url).unwrap();
assert!(matches!(canonical_uri, CanonicalUri::Http(_)));
assert_eq!(maybe_gateway, None);
assert_eq!(canonical_uri.to_string(), url);
}
#[test]
#[expect(deprecated)]
fn test_parse_url_https_with_fragment() {
let url = "https://www.w3.org/ns/activitystreams#Public";
let (canonical_uri, maybe_gateway) = parse_url(url).unwrap();
assert!(matches!(canonical_uri, CanonicalUri::Http(_)));
assert_eq!(maybe_gateway, None);
assert_eq!(canonical_uri.to_string(), url);
}
#[test]
#[expect(deprecated)]
fn test_parse_url_i2p() {
let url = "http://social.example.i2p/users/test";
let (canonical_uri, maybe_gateway) = parse_url(url).unwrap();
assert!(matches!(canonical_uri, CanonicalUri::Http(_)));
assert_eq!(maybe_gateway, None);
assert_eq!(canonical_uri.to_string(), url);
}
#[test]
#[expect(deprecated)]
fn test_parse_url_localhost() {
let url = "http://127.0.0.1:8380/users/test";
let (canonical_uri, maybe_gateway) = parse_url(url).unwrap();
assert!(matches!(canonical_uri, CanonicalUri::Http(_)));
assert_eq!(maybe_gateway, None);
assert_eq!(canonical_uri.to_string(), url);
}
#[test]
#[expect(deprecated)]
fn test_parse_url_ap() {
let url = "ap://did:key:z6MkvUie7gDQugJmyDQQPhMCCBfKJo7aGvzQYF2BqvFvdwx6/actor";
let (canonical_uri, maybe_gateway) = parse_url(url).unwrap();
assert!(matches!(canonical_uri, CanonicalUri::Ap(_)));
assert_eq!(maybe_gateway, None);
assert_eq!(canonical_uri.to_string(), url);
}
#[test]
#[expect(deprecated)]
fn test_parse_url_ap_with_gateway() {
let url = "https://social.example/.well-known/apgateway/did:key:z6MkvUie7gDQugJmyDQQPhMCCBfKJo7aGvzQYF2BqvFvdwx6/actor";
let (canonical_uri, maybe_gateway) = parse_url(url).unwrap();
assert!(matches!(canonical_uri, CanonicalUri::Ap(_)));
assert_eq!(maybe_gateway.as_deref(), Some("https://social.example"));
let expected_canonical_uri = "ap://did:key:z6MkvUie7gDQugJmyDQQPhMCCBfKJo7aGvzQYF2BqvFvdwx6/actor";
assert_eq!(canonical_uri.to_string(), expected_canonical_uri);
}
#[test]
#[expect(deprecated)]
fn test_parse_url_ap_with_gateway_unsupported_did() {
let url = "https://social.example/.well-known/apgateway/did:example:123456";
let error = parse_url(url).err().unwrap();
assert_eq!(error.to_string(), "invalid 'ap' URI");
}
#[test]
fn test_parse_ap_uri_with_query_params() {
let url = "ap://did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2/actor?gateways=https%3A%2F%2Fserver1.example,https%3A%2F%2Fserver2.example";
let canonical_uri = CanonicalUri::parse(url).unwrap();
assert!(matches!(canonical_uri, CanonicalUri::Ap(_)));
assert_eq!(canonical_uri.to_string(), "ap://did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2/actor");
}
#[test]
fn test_parse_canonical_ap_uri() {
let url = "ap://did:key:z6MkvUie7gDQugJmyDQQPhMCCBfKJo7aGvzQYF2BqvFvdwx6/actor";
let canonical_uri = CanonicalUri::parse_canonical(url).unwrap();
assert_eq!(canonical_uri.to_string(), url);
}
#[test]
fn test_parse_canonical_ap_uri_compatible() {
let url = "https://social.example/.well-known/apgateway/did:key:z6MkvUie7gDQugJmyDQQPhMCCBfKJo7aGvzQYF2BqvFvdwx6/actor";
let error = CanonicalUri::parse_canonical(url).err().unwrap();
assert_eq!(error.0, "URI is not canonical");
}
#[test]
fn test_parse_canonical_ap_uri_with_query_params() {
let url = "ap://did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2/actor?gateways=https%3A%2F%2Fserver1.example,https%3A%2F%2Fserver2.example";
let error = CanonicalUri::parse_canonical(url).err().unwrap();
assert_eq!(error.0, "URI is not canonical");
}
#[test]
fn test_parse_canonical_ap_uri_percent_encoded_did() {
let url = "ap://did%3Akey%3Az6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2/actor";
let error = CanonicalUri::parse_canonical(url).err().unwrap();
assert_eq!(error.0, "URI is not canonical");
}
#[test]
fn test_is_same_uri() {
let url_1 = "ap://did:key:z6MkvUie7gDQugJmyDQQPhMCCBfKJo7aGvzQYF2BqvFvdwx6/actor";
let url_2 = "https://social.example/.well-known/apgateway/did:key:z6MkvUie7gDQugJmyDQQPhMCCBfKJo7aGvzQYF2BqvFvdwx6/actor";
let url_3 = "ap://did%3Akey%3Az6MkvUie7gDQugJmyDQQPhMCCBfKJo7aGvzQYF2BqvFvdwx6/actor";
let url_4 = "ap://did:key:z6MkvUie7gDQugJmyDQQPhMCCBfKJo7aGvzQYF2BqvFvdwx6/actor?gateways=https%3A%2F%2Fserver1.example,https%3A%2F%2Fserver2.example";
let url_5 = "ap://did:key:z6MkvUie7gDQugJmyDQQPhMCCBfKJo7aGvzQYF2BqvFvdwx6/actor#main-key";
assert_eq!(is_same_uri(url_1, url_2).unwrap(), true);
assert_eq!(is_same_uri(url_1, url_3).unwrap(), true);
assert_eq!(is_same_uri(url_1, url_4).unwrap(), true);
assert_eq!(is_same_uri(url_1, url_5).unwrap(), false);
}
#[test]
fn test_is_same_origin() {
let id_1 = "https://one.example/1";
let id_2 = "https://one.example/2";
let id_3 = "https://two.example/3";
assert_eq!(is_same_origin(id_1, id_2).unwrap(), true);
assert_eq!(is_same_origin(id_1, id_3).unwrap(), false);
let id_4 = "ap://did:key:z6MkvUie7gDQugJmyDQQPhMCCBfKJo7aGvzQYF2BqvFvdwx6/one";
let id_5 = "ap://did:key:z6MkvUie7gDQugJmyDQQPhMCCBfKJo7aGvzQYF2BqvFvdwx6/two";
let id_6 = "ap://did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK/one";
assert_eq!(is_same_origin(id_4, id_5).unwrap(), true);
assert_eq!(is_same_origin(id_4, id_6).unwrap(), false);
assert_eq!(is_same_origin(id_4, id_1).unwrap(), false);
let id_7 = "https://one.example/.well-known/apgateway/did:key:z6MkvUie7gDQugJmyDQQPhMCCBfKJo7aGvzQYF2BqvFvdwx6/actor";
assert_eq!(is_same_origin(id_7, id_4).unwrap(), true);
assert_eq!(is_same_origin(id_7, id_1).unwrap(), false);
}
}