#![allow(improper_ctypes_definitions)]
use anyhow::anyhow;
use cid::Cid;
use itertools::Itertools;
use noosphere_core::data::Did;
use safer_ffi::{char_p::InvalidNulTerminator, prelude::*};
use std::{os::raw::c_void, pin::Pin, str::FromStr, sync::Arc};
use subtext::{Peer, Slashlink};
use tokio::{io::AsyncReadExt, sync::Mutex};
use crate::{
ffi::{NsError, NsHeaders, NsNoosphere, TryOrInitialize},
platform::{PlatformSphereChannel, PlatformStorage},
};
use noosphere_core::context::{
AsyncFileBody, HasMutableSphereContext, HasSphereContext, SphereContentRead,
SphereContentWrite, SphereContext, SphereCursor, SphereFile, SphereReplicaRead, SphereWalker,
};
#[derive_ReprC(rename = "ns_sphere")]
#[repr(opaque)]
pub struct NsSphere {
pub(crate) inner: PlatformSphereChannel,
}
impl NsSphere {
pub(crate) fn inner(
&self,
) -> &SphereCursor<Arc<SphereContext<PlatformStorage>>, PlatformStorage> {
self.inner.immutable()
}
pub(crate) fn inner_mut(&mut self) -> &mut Arc<Mutex<SphereContext<PlatformStorage>>> {
self.inner.mutable()
}
pub(crate) fn to_channel(&self) -> PlatformSphereChannel {
self.inner.clone()
}
}
#[derive_ReprC(rename = "ns_sphere_file")]
#[repr(opaque)]
pub struct NsSphereFile {
inner: SphereFile<Pin<Box<dyn AsyncFileBody>>>,
}
impl NsSphereFile {
pub(crate) fn inner_mut(&mut self) -> &mut SphereFile<Pin<Box<dyn AsyncFileBody>>> {
&mut self.inner
}
}
#[ffi_export]
pub fn ns_sphere_open(
noosphere: &NsNoosphere,
sphere_identity: char_p::Ref<'_>,
error_out: Option<Out<'_, repr_c::Box<NsError>>>,
) -> Option<repr_c::Box<NsSphere>> {
error_out.try_or_initialize(|| {
let fs = noosphere.async_runtime().block_on(async {
let sphere_channel = noosphere
.inner()
.get_sphere_channel(&Did(sphere_identity.to_str().into()))
.await?;
Ok(Box::new(NsSphere {
inner: sphere_channel,
})
.into()) as Result<_, anyhow::Error>
})?;
Ok(fs)
})
}
#[ffi_export]
pub fn ns_sphere_free(sphere: repr_c::Box<NsSphere>) {
drop(sphere)
}
#[ffi_export]
#[allow(clippy::type_complexity)]
pub fn ns_sphere_traverse_by_petname(
noosphere: &NsNoosphere,
sphere: &mut NsSphere,
petname: char_p::Ref<'_>,
context: Option<repr_c::Box<c_void>>,
callback: extern "C" fn(
Option<repr_c::Box<c_void>>,
Option<repr_c::Box<NsError>>,
Option<repr_c::Box<NsSphere>>,
),
) {
let sphere = sphere.inner().clone();
let raw_petnames = format!("@{}", petname.to_str());
noosphere.async_runtime().spawn(async move {
let result = async {
let link = Slashlink::from_str(&raw_petnames)?;
let petnames = match link.peer {
Peer::Name(petnames) => petnames,
_ => Err(anyhow!("No petnames found in {}", raw_petnames))?,
};
let next_sphere_context = sphere.traverse_by_petnames(&petnames).await?;
Ok(next_sphere_context.map(|next_sphere_context| {
Box::new(NsSphere {
inner: next_sphere_context.into(),
})
.into()
})) as Result<Option<_>, anyhow::Error>
}
.await;
match result {
Ok(maybe_sphere) => {
tokio::task::spawn_blocking(move || callback(context, None, maybe_sphere))
}
Err(error) => tokio::task::spawn_blocking(move || {
callback(context, Some(NsError::from(error).into()), None)
}),
};
});
}
#[ffi_export]
pub fn ns_sphere_traverse_by_petname_blocking(
noosphere: &NsNoosphere,
sphere: &mut NsSphere,
petname: char_p::Ref<'_>,
error_out: Option<Out<'_, repr_c::Box<NsError>>>,
) -> Option<repr_c::Box<NsSphere>> {
let closure = || {
let sphere = noosphere.async_runtime().block_on(async {
let raw_petnames = petname.to_str();
let link = Slashlink::from_str(&format!("@{raw_petnames}"))?;
let petnames = match link.peer {
Peer::Name(petnames) => petnames,
_ => return Err(anyhow!("No petnames found in {}", raw_petnames)),
};
let next_sphere_context = sphere.inner().traverse_by_petnames(&petnames).await?;
Ok(next_sphere_context.map(|next_sphere_context| {
Box::new(NsSphere {
inner: next_sphere_context.into(),
})
.into()
})) as Result<Option<_>, anyhow::Error>
})?;
Ok(sphere)
};
match error_out.try_or_initialize(closure) {
Some(maybe_sphere) => maybe_sphere,
None => None,
}
}
#[ffi_export]
#[allow(clippy::type_complexity)]
pub fn ns_sphere_content_read(
noosphere: &NsNoosphere,
sphere: &NsSphere,
slashlink: char_p::Ref<'_>,
context: Option<repr_c::Box<c_void>>,
callback: extern "C" fn(
Option<repr_c::Box<c_void>>,
Option<repr_c::Box<NsError>>,
Option<repr_c::Box<NsSphereFile>>,
),
) {
let sphere = sphere.inner().clone();
let slashlink = slashlink.to_string();
noosphere.async_runtime().spawn(async move {
let result = async {
let slashlink = Slashlink::from_str(&slashlink)?;
let slug = match slashlink.slug {
Some(slug) => slug,
None => return Err(anyhow!("No slug specified in slashlink!")),
};
let cursor = match slashlink.peer {
Peer::Name(petnames) => match sphere.traverse_by_petnames(&petnames).await? {
Some(sphere_context) => sphere_context,
None => return Ok(None),
},
Peer::None => sphere,
Peer::Did(_) => return Err(anyhow!("DID peer in slashlink not yet supported")),
};
info!(
"Reading sphere {} slug {}...",
cursor.identity().await?,
slug
);
let file = cursor.read(&slug).await?;
Ok(file.map(|sphere_file| {
Box::new(NsSphereFile {
inner: sphere_file.boxed(),
})
.into()
}))
}
.await;
match result {
Ok(maybe_file) => {
tokio::task::spawn_blocking(move || callback(context, None, maybe_file))
}
Err(error) => tokio::task::spawn_blocking(move || {
callback(context, Some(NsError::from(error).into()), None)
}),
};
});
}
#[ffi_export]
pub fn ns_sphere_content_read_blocking(
noosphere: &NsNoosphere,
sphere: &NsSphere,
slashlink: char_p::Ref<'_>,
error_out: Option<Out<'_, repr_c::Box<NsError>>>,
) -> Option<repr_c::Box<NsSphereFile>> {
match error_out.try_or_initialize(|| {
noosphere
.async_runtime()
.block_on(async {
let slashlink = Slashlink::from_str(slashlink.to_str())?;
let slug = match slashlink.slug {
Some(slug) => slug,
None => return Err(anyhow!("No slug specified in slashlink!")),
};
let cursor = match slashlink.peer {
Peer::Name(petnames) => {
match sphere.inner().traverse_by_petnames(&petnames).await? {
Some(sphere_context) => sphere_context,
None => return Ok(None),
}
}
Peer::None => sphere.inner().clone(),
Peer::Did(_) => return Err(anyhow!("DID peer in slashlink not yet supported")),
};
info!(
"Reading sphere {} slug {}...",
cursor.identity().await?,
slug
);
let file = cursor.read(&slug).await?;
Ok(file.map(|sphere_file| {
Box::new(NsSphereFile {
inner: sphere_file.boxed(),
})
.into()
}))
})
.map_err(|error| error.into())
}) {
Some(maybe_file) => maybe_file,
None => None,
}
}
#[ffi_export]
pub fn ns_sphere_content_write(
noosphere: &NsNoosphere,
sphere: &mut NsSphere,
slug: char_p::Ref<'_>,
content_type: char_p::Ref<'_>,
bytes: c_slice::Ref<'_, u8>,
additional_headers: Option<&NsHeaders>,
error_out: Option<Out<'_, repr_c::Box<NsError>>>,
) {
error_out.try_or_initialize(|| {
noosphere.async_runtime().block_on(async {
let slug = slug.to_str();
let mut cursor = SphereCursor::latest(sphere.inner_mut().clone());
info!(
"Writing sphere {} slug {}...",
cursor.identity().await?,
slug
);
cursor
.write(
slug,
content_type.to_str(),
bytes.as_ref(),
additional_headers.map(|headers| headers.inner().clone()),
)
.await?;
println!("Updated {slug:?}...");
Ok(())
})
});
}
#[ffi_export]
pub fn ns_sphere_content_remove(
noosphere: &NsNoosphere,
sphere: &mut NsSphere,
slug: char_p::Ref<'_>,
error_out: Option<Out<'_, repr_c::Box<NsError>>>,
) {
error_out.try_or_initialize(|| {
noosphere
.async_runtime()
.block_on(async { sphere.inner_mut().remove(slug.to_str()).await })?;
Ok(())
});
}
#[ffi_export]
pub fn ns_sphere_save(
noosphere: &NsNoosphere,
sphere: &mut NsSphere,
additional_headers: Option<&NsHeaders>,
error_out: Option<Out<'_, repr_c::Box<NsError>>>,
) {
error_out.try_or_initialize(|| {
let cid = noosphere.async_runtime().block_on(
sphere
.inner_mut()
.save(additional_headers.map(|headers| headers.inner().clone())),
)?;
println!("Saved sphere; new revision is {cid}");
Ok(())
});
}
#[ffi_export]
pub fn ns_sphere_content_list(
noosphere: &NsNoosphere,
sphere: &NsSphere,
error_out: Option<Out<'_, repr_c::Box<NsError>>>,
) -> c_slice::Box<char_p::Box> {
let possible_output = error_out.try_or_initialize(|| {
noosphere.async_runtime().block_on(async {
let slug_set = SphereWalker::from(sphere.inner()).list_slugs().await?;
let mut all_slugs: Vec<char_p::Box> = Vec::new();
for slug in slug_set.into_iter() {
all_slugs.push(
slug.try_into()
.map_err(|error: InvalidNulTerminator<String>| anyhow!(error))?,
);
}
Ok(all_slugs)
})
});
match possible_output {
Some(slugs) => slugs,
None => Vec::new(),
}
.into_boxed_slice()
.into()
}
#[ffi_export]
pub fn ns_sphere_content_changes(
noosphere: &NsNoosphere,
sphere: &NsSphere,
since_cid: Option<char_p::Ref<'_>>,
error_out: Option<Out<'_, repr_c::Box<NsError>>>,
) -> c_slice::Box<char_p::Box> {
let possible_output = error_out.try_or_initialize(|| {
noosphere.async_runtime().block_on(async {
let since = match since_cid {
Some(cid_string) => Some(
Cid::from_str(cid_string.to_str())
.map_err(|error| anyhow!(error))?
.into(),
),
None => None,
};
let changed_slug_set = SphereWalker::from(sphere.inner())
.content_changes(since.as_ref())
.await?;
let mut changed_slugs: Vec<char_p::Box> = Vec::new();
for slug in changed_slug_set.into_iter() {
changed_slugs.push(
slug.try_into()
.map_err(|error: InvalidNulTerminator<String>| anyhow!(error))?,
);
}
Ok(changed_slugs)
})
});
match possible_output {
Some(slugs) => slugs,
None => Vec::new(),
}
.into_boxed_slice()
.into()
}
#[ffi_export]
pub fn ns_sphere_file_free(sphere_file: repr_c::Box<NsSphereFile>) {
drop(sphere_file)
}
#[ffi_export]
#[allow(clippy::type_complexity)]
pub fn ns_sphere_file_contents_read(
noosphere: &NsNoosphere,
mut sphere_file: repr_c::Box<NsSphereFile>,
context: Option<repr_c::Box<c_void>>,
callback: extern "C" fn(
Option<repr_c::Box<c_void>>,
Option<repr_c::Box<NsError>>,
Option<c_slice::Box<u8>>,
),
) {
noosphere.async_runtime().spawn(async move {
let result = async {
let mut buffer = Vec::new();
sphere_file
.inner_mut()
.contents
.read_to_end(&mut buffer)
.await
.map_err(|error| anyhow!(error))?;
Ok(buffer.into_boxed_slice().into()) as Result<_, anyhow::Error>
}
.await;
match result {
Ok(maybe_bytes) => {
tokio::task::spawn_blocking(move || callback(context, None, Some(maybe_bytes)))
}
Err(error) => tokio::task::spawn_blocking(move || {
callback(context, Some(NsError::from(error).into()), None)
}),
};
});
}
#[ffi_export]
pub fn ns_sphere_file_contents_read_blocking(
noosphere: &NsNoosphere,
sphere_file: &mut NsSphereFile,
error_out: Option<Out<'_, repr_c::Box<NsError>>>,
) -> Option<c_slice::Box<u8>> {
error_out.try_or_initialize(|| {
noosphere.async_runtime().block_on(async {
let mut buffer = Vec::new();
sphere_file
.inner_mut()
.contents
.read_to_end(&mut buffer)
.await
.map_err(|error| anyhow!(error))?;
Ok(buffer.into_boxed_slice().into())
})
})
}
#[ffi_export]
pub fn ns_sphere_file_header_values_read(
sphere_file: &NsSphereFile,
name: char_p::Ref<'_>,
) -> c_slice::Box<char_p::Box> {
sphere_file
.inner
.memo
.get_header(name.to_str())
.into_iter()
.filter_map(|header| header.try_into().ok())
.collect::<Vec<char_p::Box>>()
.into_boxed_slice()
.into()
}
#[ffi_export]
pub fn ns_sphere_file_header_value_first(
sphere_file: &NsSphereFile,
name: char_p::Ref<'_>,
) -> Option<char_p::Box> {
sphere_file
.inner
.memo
.get_first_header(name.to_str())
.into_iter()
.filter_map(|value| value.try_into().ok())
.next()
}
#[ffi_export]
pub fn ns_sphere_file_header_names_read(sphere_file: &NsSphereFile) -> c_slice::Box<char_p::Box> {
sphere_file
.inner
.memo
.headers
.iter()
.map(|(name, _)| name)
.unique()
.filter_map(|name| name.to_owned().try_into().ok())
.collect::<Vec<char_p::Box>>()
.into_boxed_slice()
.into()
}
#[ffi_export]
pub fn ns_sphere_file_version_get(
sphere_file: &NsSphereFile,
error_out: Option<Out<'_, repr_c::Box<NsError>>>,
) -> Option<char_p::Box> {
error_out.try_or_initialize(|| {
sphere_file
.inner
.memo_version
.to_string()
.try_into()
.map_err(|error: InvalidNulTerminator<String>| anyhow!(error).into())
})
}
#[ffi_export]
pub fn ns_sphere_identity(
noosphere: &NsNoosphere,
sphere: &NsSphere,
error_out: Option<Out<'_, repr_c::Box<NsError>>>,
) -> Option<char_p::Box> {
error_out.try_or_initialize(|| {
match noosphere
.async_runtime()
.block_on(async { sphere.inner().identity().await })
{
Ok(identity) => identity
.to_string()
.try_into()
.map_err(|error: InvalidNulTerminator<String>| anyhow!(error).into()),
Err(error) => Err(anyhow!(error).into()),
}
})
}
#[ffi_export]
pub fn ns_sphere_author_identity(
noosphere: &NsNoosphere,
sphere: &NsSphere,
error_out: Option<Out<'_, repr_c::Box<NsError>>>,
) -> Option<char_p::Box> {
error_out.try_or_initialize(|| {
match noosphere
.async_runtime()
.block_on(async { sphere.inner().sphere_context().await?.author().did().await })
{
Ok(identity) => identity
.to_string()
.try_into()
.map_err(|error: InvalidNulTerminator<String>| anyhow!(error).into()),
Err(error) => Err(anyhow!(error).into()),
}
})
}
#[ffi_export]
pub fn ns_sphere_version(
noosphere: &NsNoosphere,
sphere: &NsSphere,
error_out: Option<Out<'_, repr_c::Box<NsError>>>,
) -> Option<char_p::Box> {
error_out.try_or_initialize(|| {
match noosphere
.async_runtime()
.block_on(async { sphere.inner().version().await })
{
Ok(version) => version
.to_string()
.try_into()
.map_err(|error: InvalidNulTerminator<String>| anyhow!(error).into()),
Err(error) => Err(anyhow!(error).into()),
}
})
}