use super::artifact::{self, ArtifactId, ArtifactIdentity, Block, ContainerKind};
use crate::property_graph::{GraphGeneration, StoreInstanceId};
use crate::vfs::Vfs;
use std::path::{Path, PathBuf};
pub trait EntropyProvider {
fn fill_nonce(&mut self, output: &mut [u8; 16]) -> std::io::Result<()>;
}
#[derive(Default)]
pub struct OsEntropy;
impl EntropyProvider for OsEntropy {
fn fill_nonce(&mut self, output: &mut [u8; 16]) -> std::io::Result<()> {
#[cfg(feature = "test-seams")]
if let Some(value) = qualification_nonce() {
*output = value.to_le_bytes();
return Ok(());
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
{
let result = unsafe { libc::getentropy(output.as_mut_ptr().cast(), output.len()) };
if result == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
#[cfg(windows)]
{
crate::sys::windows::fill_entropy(output)
}
#[cfg(not(any(target_os = "macos", target_os = "linux", windows)))]
{
let _ = output;
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"native graph OS entropy is unsupported on this platform",
))
}
}
}
pub fn fresh_store_identity(entropy: &mut dyn EntropyProvider) -> std::io::Result<StoreInstanceId> {
StoreInstanceId::new(nonce(entropy)?)
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))
}
fn nonce(entropy: &mut dyn EntropyProvider) -> std::io::Result<u128> {
let mut bytes = [0_u8; 16];
entropy.fill_nonce(&mut bytes)?;
let value = u128::from_le_bytes(bytes);
if value == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"entropy returned reserved zero identity",
));
}
Ok(value)
}
#[derive(Debug)]
pub enum AllocationError {
Format(crate::format::frame::FormatError),
Entropy(std::io::Error),
Collision {
artifact: ArtifactId,
source: std::io::Error,
},
CreateFailed {
artifact: ArtifactId,
source: std::io::Error,
},
}
impl AllocationError {
pub const fn attempted_artifact(&self) -> Option<ArtifactId> {
match self {
Self::Collision { artifact, .. } | Self::CreateFailed { artifact, .. } => {
Some(*artifact)
}
Self::Format(_) | Self::Entropy(_) => None,
}
}
}
impl std::fmt::Display for AllocationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Format(e) => e.fmt(f),
Self::Entropy(e) => write!(f, "graph entropy: {e}"),
Self::Collision { artifact, source } => write!(
f,
"graph object {:032x} collision; not owned: {source}",
artifact.get()
),
Self::CreateFailed { artifact, source } => write!(
f,
"graph object {:032x} creation failed: {source}",
artifact.get()
),
}
}
}
impl std::error::Error for AllocationError {}
pub struct ArtifactAllocator<'a> {
pub filesystem: &'a dyn Vfs,
pub directory: &'a Path,
pub store: StoreInstanceId,
pub entropy: &'a mut dyn EntropyProvider,
}
impl ArtifactAllocator<'_> {
pub fn create(
&mut self,
generation: GraphGeneration,
creation_serial: u64,
blocks: &[Block<'_>],
output: &mut [u8],
) -> Result<ArtifactIdentity, AllocationError> {
let length = artifact::encoded_len(ContainerKind::Object, blocks)
.map_err(AllocationError::Format)?;
if output.len() < length {
return Err(AllocationError::Format(
crate::format::frame::FormatError::new(
"native graph allocation",
crate::format::frame::FormatCheck::Length,
"output reservation too small",
),
));
}
let artifact = ArtifactId::new(nonce(self.entropy).map_err(AllocationError::Entropy)?)
.map_err(AllocationError::Format)?;
let identity = ArtifactIdentity {
store: self.store,
artifact,
generation,
creation_serial,
};
let used = artifact::encode_into(ContainerKind::Object, identity, blocks, output)
.map_err(AllocationError::Format)?;
let bytes = output.get(..used).ok_or_else(|| {
AllocationError::Format(crate::format::frame::FormatError::new(
"native graph allocation",
crate::format::frame::FormatCheck::Length,
"invalid encoded object extent",
))
})?;
artifact::decode(ContainerKind::Object, Some((self.store, artifact)), bytes)
.map_err(AllocationError::Format)?;
self.filesystem
.create_new(&artifact_path(self.directory, artifact), bytes)
.map_err(|source| {
if source.kind() == std::io::ErrorKind::AlreadyExists {
AllocationError::Collision { artifact, source }
} else {
AllocationError::CreateFailed { artifact, source }
}
})?;
Ok(identity)
}
}
pub fn artifact_path(directory: &Path, artifact: ArtifactId) -> PathBuf {
directory.join(format!("graph-{:032x}.zgraph", artifact.get()))
}
#[cfg(feature = "test-seams")]
thread_local! {
static QUALIFICATION_NONCES: std::cell::Cell<Option<(u64, u64)>> = const { std::cell::Cell::new(None) };
}
#[cfg(feature = "test-seams")]
fn qualification_nonce() -> Option<u128> {
QUALIFICATION_NONCES.with(|cell| {
let (seed, ordinal) = cell.get()?;
cell.set(Some((seed, ordinal.wrapping_add(1))));
Some(xxhash_rust::xxh3::xxh3_128_with_seed(&ordinal.to_le_bytes(), seed) | 1)
})
}
#[cfg(feature = "test-seams")]
pub(crate) fn with_qualification_nonces<T>(seed: u64, run: impl FnOnce() -> T) -> T {
struct Restore(Option<(u64, u64)>);
impl Drop for Restore {
fn drop(&mut self) {
QUALIFICATION_NONCES.with(|cell| cell.set(self.0));
}
}
let restore = Restore(QUALIFICATION_NONCES.with(|cell| cell.replace(Some((seed, 0)))));
let result = run();
drop(restore);
result
}