use std::{fmt::Debug, num::NonZeroU32, path::Path, sync::Arc};
use anyhow::{ensure, Context, Result};
use neoengram_core::{Chunk, Commit};
use serde::{Deserialize, Serialize};
use crate::storage::STORAGE_TEMP_FILE_PREFIX;
mod json;
use json::JsonMetadataStore;
const MAX_PAGE_SIZE: u32 = 4_096;
const MANIFEST_HASH_DOMAIN: &[u8] = b"neoengram-file-manifest-v2";
const TREE_HASH_DOMAIN: &[u8] = b"neoengram-tree-v2";
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PageRequest {
pub(crate) after: Option<String>,
pub(crate) limit: NonZeroU32,
}
impl PageRequest {
pub(crate) fn new(after: Option<String>, limit: u32) -> Result<Self> {
let limit = NonZeroU32::new(limit).context("分页大小必须大于零")?;
let request = Self { after, limit };
request.validate()?;
Ok(request)
}
pub(crate) fn first(limit: u32) -> Result<Self> {
Self::new(None, limit)
}
pub(crate) fn validate(&self) -> Result<()> {
ensure!(
self.limit.get() <= MAX_PAGE_SIZE,
"分页大小不能超过 {MAX_PAGE_SIZE}: {}",
self.limit
);
Ok(())
}
pub(crate) fn limit_usize(&self) -> Result<usize> {
self.validate()?;
usize::try_from(self.limit.get()).context("当前平台无法表示分页大小")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Page<T> {
pub(crate) items: Vec<T>,
pub(crate) next: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct IndexVersion {
revision: u64,
digest: [u8; 32],
}
impl IndexVersion {
const fn new(revision: u64, digest: [u8; 32]) -> Self {
Self { revision, digest }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ManifestRef {
pub(crate) id: String,
pub(crate) chunk_count: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct FileRecord {
pub(crate) path: String,
pub(crate) total_size: u64,
pub(crate) chunk_count: u64,
pub(crate) manifest_id: String,
}
impl FileRecord {
pub(crate) fn from_manifest(path: String, total_size: u64, manifest: &ManifestRef) -> Self {
Self {
path,
total_size,
chunk_count: manifest.chunk_count,
manifest_id: manifest.id.clone(),
}
}
}
pub(crate) trait ManifestReader: Debug {
fn total_size(&self) -> u64;
fn chunk_count(&self) -> u64;
fn scan_chunks(&self, request: &PageRequest) -> Result<Page<Chunk>>;
}
pub(crate) trait FileSetReader: Debug {
fn get_file(&self, path: &str) -> Result<Option<FileRecord>>;
fn scan_files(&self, prefix: Option<&str>, request: &PageRequest) -> Result<Page<FileRecord>>;
}
pub(crate) trait IndexReader: FileSetReader {
fn format_version(&self) -> u32;
fn version(&self) -> &IndexVersion;
}
pub(crate) trait IndexTxn: IndexReader {
fn upsert_file(&mut self, file: &FileRecord) -> Result<()>;
fn delete_prefix(&mut self, prefix: &str) -> Result<u64>;
fn commit(self: Box<Self>) -> Result<IndexVersion>;
}
pub(crate) trait TreeReader: FileSetReader {
fn file_count(&self) -> u64;
}
pub(crate) trait TreeWriter: Debug {
fn append_file(&mut self, file: &FileRecord) -> Result<()>;
fn finish(self: Box<Self>) -> Result<String>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum MetadataStoreKind {
Json,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct StoredReference {
pub(crate) name: String,
pub(crate) target: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ReferenceCas {
Updated,
Mismatch { actual: Option<String> },
}
pub(crate) trait MetadataReader: Debug {
fn read_head_reference(&self) -> Result<String>;
fn get_reference(&self, name: &str) -> Result<Option<String>>;
fn open_manifest(&self, id: &str) -> Result<Option<Box<dyn ManifestReader + '_>>>;
fn open_tree(&self, id: &str) -> Result<Option<Box<dyn TreeReader + '_>>>;
fn get_commit(&self, id: &str) -> Result<Option<Commit>>;
}
pub(crate) trait MetadataSnapshot: MetadataReader {
fn scan_tree_ids(&self, request: &PageRequest) -> Result<Page<String>>;
fn scan_manifest_ids(&self, request: &PageRequest) -> Result<Page<String>>;
fn scan_commit_ids(&self, request: &PageRequest) -> Result<Page<String>>;
fn scan_references(&self, prefix: &str, request: &PageRequest)
-> Result<Page<StoredReference>>;
}
pub(crate) trait MetadataStore: MetadataReader + Send + Sync {
fn kind(&self) -> MetadataStoreKind;
fn initialize(&self, head_reference: &str) -> Result<()>;
fn validate_layout(&self) -> Result<()>;
fn verify_integrity(&self) -> Result<()>;
fn read_index(&self) -> Result<Box<dyn IndexReader + '_>>;
fn begin_index_transaction(
&self,
expected: Option<&IndexVersion>,
) -> Result<Box<dyn IndexTxn + '_>>;
fn snapshot(&self) -> Result<Box<dyn MetadataSnapshot + '_>>;
fn put_manifest(
&self,
total_size: u64,
chunks: &mut dyn Iterator<Item = Result<Chunk>>,
) -> Result<ManifestRef>;
fn begin_tree_write(&self) -> Result<Box<dyn TreeWriter + '_>>;
fn put_commit(&self, id: &str, commit: &Commit) -> Result<()>;
fn compare_exchange_reference(
&self,
name: &str,
expected: Option<&str>,
new_target: Option<&str>,
) -> Result<ReferenceCas>;
}
pub(crate) fn open_metadata_store(
kind: MetadataStoreKind,
metadata_root: &Path,
) -> Arc<dyn MetadataStore> {
match kind {
MetadataStoreKind::Json => Arc::new(JsonMetadataStore::new(metadata_root.to_path_buf())),
}
}
fn validate_metadata_object_id(id: &str) -> Result<()> {
ensure!(
id.len() == 64
&& id
.bytes()
.all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()),
"元数据对象 ID 不是 64 位小写十六进制: {id}"
);
Ok(())
}
fn file_manifest_id(total_size: u64, chunks: &[Chunk]) -> Result<String> {
Ok(describe_manifest(total_size, chunks)?.id)
}
pub(crate) fn describe_manifest(total_size: u64, chunks: &[Chunk]) -> Result<ManifestRef> {
let mut hasher = ManifestHasher::new(total_size);
for chunk in chunks {
hasher.push(chunk)?;
}
hasher.finish()
}
struct ManifestHasher {
hasher: blake3::Hasher,
total_size: u64,
next_offset: u64,
chunk_count: u64,
}
impl ManifestHasher {
fn new(total_size: u64) -> Self {
let mut hasher = blake3::Hasher::new();
hasher.update(MANIFEST_HASH_DOMAIN);
hasher.update(&[0]);
hasher.update(&total_size.to_le_bytes());
Self {
hasher,
total_size,
next_offset: 0,
chunk_count: 0,
}
}
fn push(&mut self, chunk: &Chunk) -> Result<()> {
validate_metadata_object_id(&chunk.hash)?;
ensure!(chunk.size > 0, "Manifest Chunk 大小必须大于零");
ensure!(
chunk.offset == self.next_offset,
"Manifest Chunk 偏移不连续: {}",
chunk.hash
);
let next_offset = self
.next_offset
.checked_add(chunk.size)
.context("Manifest Chunk 总大小溢出")?;
let chunk_count = self
.chunk_count
.checked_add(1)
.context("Manifest Chunk 数量溢出")?;
self.hasher.update(chunk.hash.as_bytes());
self.hasher.update(&chunk.offset.to_le_bytes());
self.hasher.update(&chunk.size.to_le_bytes());
self.next_offset = next_offset;
self.chunk_count = chunk_count;
Ok(())
}
fn finish(mut self) -> Result<ManifestRef> {
ensure!(
self.next_offset == self.total_size,
"Manifest Chunk 总大小与文件大小不一致"
);
self.hasher.update(&self.chunk_count.to_le_bytes());
Ok(ManifestRef {
id: self.hasher.finalize().to_hex().to_string(),
chunk_count: self.chunk_count,
})
}
}
pub(crate) fn tree_records_id(files: &[FileRecord]) -> Result<String> {
let canonical = serde_json::to_vec(files).context("无法序列化 Tree 文件记录")?;
let mut hasher = blake3::Hasher::new();
hasher.update(TREE_HASH_DOMAIN);
hasher.update(&[0]);
hasher.update(&canonical);
Ok(hasher.finalize().to_hex().to_string())
}
fn validate_reference_name(name: &str) -> Result<()> {
ensure!(name.starts_with("refs/"), "引用名必须位于 refs/ 下: {name}");
ensure!(!name.contains('\\'), "引用名必须使用 `/` 分隔: {name}");
for component in name.split('/') {
ensure!(
!component.is_empty() && component != "." && component != "..",
"引用名未规范化: {name}"
);
ensure!(
!component.starts_with(STORAGE_TEMP_FILE_PREFIX),
"引用名使用了保留的临时文件前缀: {name}"
);
ensure!(
component
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-')),
"引用名包含不支持的字符: {name}"
);
}
Ok(())
}
fn validate_reference_prefix(prefix: &str) -> Result<()> {
if prefix == "refs" {
return Ok(());
}
validate_reference_name(prefix)
}
#[cfg(test)]
mod tests;