Compare commits

...

2 Commits

Author SHA1 Message Date
overtrue 5983594da9 feat(capture): add explicit schema-bound checkpoint capabilities 2026-10-11 21:28:05 +08:00
overtrue dfa5d50565 feat(capture): persist first-decision provenance 2026-10-11 18:58:12 +08:00
6 changed files with 1457 additions and 54 deletions
+180 -35
View File
@@ -39,7 +39,7 @@ use crate::{
LOGS, LogId, META, Store, StoreError, committed::validate_committed, created::*, decode_log, encode_id, encode_log,
next_index, number,
};
use redb::{ReadTransaction, ReadableDatabase, ReadableTable, ReadableTableMetadata, TableDefinition};
use redb::{ReadTransaction, ReadableDatabase, ReadableTable, ReadableTableMetadata, TableDefinition, TableHandle};
use std::collections::BTreeMap;
pub(crate) const HEADS: TableDefinition<&[u8], &[u8]> = TableDefinition::new("heads");
@@ -89,6 +89,40 @@ fn decode_operation(b: &[u8]) -> Result<CreatedOperation, StoreError> {
result,
})
}
fn decode_provenance(b: &[u8]) -> Result<CreatedOperationProvenance, StoreError> {
if b.len() > MAX_OBJECT_KEY_LENGTH + 512 || b.get(..5) != Some(b"COPR\x01") {
return Err(StoreError::CorruptRecord);
}
let encoded_id = b.get(5..29).ok_or(StoreError::CorruptRecord)?;
if encoded_id == [0; 24] {
return Err(StoreError::CorruptRecord);
}
Ok(CreatedOperationProvenance {
first_decision_log_id: crate::decode_id(encoded_id)?,
operation: decode_operation(b.get(29..).ok_or(StoreError::CorruptRecord)?)?,
})
}
pub(crate) fn validate_provenance_encoding(bytes: &[u8]) -> Result<(), StoreError> {
decode_provenance(bytes).map(|_| ())
}
fn decode_stored_operation(b: &[u8], schema: u64) -> Result<CreatedOperation, StoreError> {
if matches!(schema, 5 | 6) {
Ok(decode_provenance(b)?.operation)
} else {
decode_operation(b)
}
}
fn encode_provenance(first: LogId, operation: &[u8]) -> Result<Vec<u8>, StoreError> {
let mut bytes = b"COPR\x01".to_vec();
bytes.extend(encode_id(first));
bytes.extend(operation);
decode_provenance(&bytes)?;
Ok(bytes)
}
fn decode_apply(b: &[u8]) -> Result<CreatedApplyResult, StoreError> {
let mut d = Decoder::new(b);
let r = CreatedApplyResult {
@@ -176,11 +210,12 @@ fn apply_created(
command: &DecideCreated,
canonical: &[u8],
binding: &CaptureBinding,
schema: u64,
) -> Result<CreatedResult, StoreError> {
let mut operations = tx.open_table(OPERATIONS)?;
let existing = operations
.get(command.operation_id.as_slice())?
.map(|v| decode_operation(v.value()))
.map(|v| decode_stored_operation(v.value(), schema))
.transpose()?;
let result = if let Some(existing) = existing {
if existing.canonical_command == canonical {
@@ -233,6 +268,9 @@ fn apply_created(
.to_vec();
op.extend(canonical);
op.extend(result_bytes(&outcome));
if matches!(schema, 5 | 6) {
op = encode_provenance(id, &op)?;
}
operations.insert(command.operation_id.as_slice(), op.as_slice())?;
outcome
};
@@ -304,7 +342,7 @@ pub(crate) fn native_application_state(tx: &ReadTransaction) -> Result<(Option<L
fn native_application_metadata(
meta: &impl ReadableTable<&'static str, &'static [u8]>,
) -> Result<(Option<LogId>, crate::CaptureMembership), StoreError> {
if number(required(meta.get("schema")?)?.value())? != 4 {
if !matches!(number(required(meta.get("schema")?)?.value())?, 4 | 6) {
return Err(StoreError::UnsupportedSchema);
}
let last = read_applied(meta)?;
@@ -334,7 +372,7 @@ impl Store {
let events = tx.open_table(EVENTS)?;
let operations = tx.open_table(OPERATIONS)?;
let mut results = tx.open_table(APPLY_RESULTS)?;
validate_application_tables(&meta, &logs, &heads, &versions, &events, &operations, &results)?;
validate_application_tables(&meta, &logs, &heads, &versions, &events, &operations, &results, false)?;
if schema == 4 {
return Ok(());
}
@@ -372,7 +410,7 @@ impl Store {
meta.insert("native_membership", membership.encode()?.as_slice())?;
meta.insert("schema", 4u64.to_be_bytes().as_slice())?;
// Replay native decisions inside the same durable writer boundary.
validate_application_tables(&meta, &logs, &heads, &versions, &events, &operations, &results)?;
validate_application_tables(&meta, &logs, &heads, &versions, &events, &operations, &results, false)?;
}
self.commit(tx)
}
@@ -383,11 +421,11 @@ impl Store {
{
let mut meta = tx.open_table(META)?;
let schema = number(required(meta.get("schema")?)?.value())?;
if !matches!(schema, 3 | 4) {
if !matches!(schema, 3..=6) {
return Err(StoreError::UnsupportedSchema);
}
read_binding(&meta)?;
if schema == 4 {
if matches!(schema, 4 | 6) {
let membership = crate::CaptureMembership::decode(required(meta.get("native_membership")?)?.value())
.map_err(|_| StoreError::CorruptRecord)?;
let last = read_applied(&meta)?;
@@ -416,7 +454,7 @@ impl Store {
}
let membership = crate::CaptureMembership::new(None, vec![], BTreeMap::new())?;
meta.insert("native_membership", membership.encode()?.as_slice())?;
meta.insert("schema", 4u64.to_be_bytes().as_slice())?;
meta.insert("schema", (if schema == 5 { 6u64 } else { 4u64 }).to_be_bytes().as_slice())?;
}
self.commit(tx)
}
@@ -424,7 +462,7 @@ impl Store {
let tx = self.write_transaction()?;
{
let mut meta = tx.open_table(META)?;
if number(required(meta.get("schema")?)?.value())? != 4 {
if !matches!(number(required(meta.get("schema")?)?.value())?, 4 | 6) {
return Err(StoreError::UnsupportedSchema);
}
if entries.is_empty() {
@@ -453,7 +491,7 @@ impl Store {
}
pub fn read_native_entry(&self, index: u64) -> Result<Option<crate::CaptureRaftEntry>, StoreError> {
let tx = self.database.begin_read()?;
if number(required(tx.open_table(META)?.get("schema")?)?.value())? != 4 {
if !matches!(number(required(tx.open_table(META)?.get("schema")?)?.value())?, 4 | 6) {
return Err(StoreError::UnsupportedSchema);
}
tx.open_table(LOGS)?
@@ -470,14 +508,14 @@ impl Store {
/// Read typed native entries within the requested bounds.
pub fn read_native_range<R: std::ops::RangeBounds<u64>>(&self, range: R) -> Result<Vec<crate::CaptureRaftEntry>, StoreError> {
let tx = self.database.begin_read()?;
if number(required(tx.open_table(META)?.get("schema")?)?.value())? != 4 {
if !matches!(number(required(tx.open_table(META)?.get("schema")?)?.value())?, 4 | 6) {
return Err(StoreError::UnsupportedSchema);
}
read_native_range_in(&tx, range)
}
pub fn read_native_entries(&self, start: u64, end: u64) -> Result<Vec<crate::CaptureRaftEntry>, StoreError> {
let tx = self.database.begin_read()?;
if number(required(tx.open_table(META)?.get("schema")?)?.value())? != 4 {
if !matches!(number(required(tx.open_table(META)?.get("schema")?)?.value())?, 4 | 6) {
return Err(StoreError::UnsupportedSchema);
}
if start > end {
@@ -491,7 +529,8 @@ impl Store {
let response;
{
let mut meta = tx.open_table(META)?;
if number(required(meta.get("schema")?)?.value())? != 4 {
let schema = number(required(meta.get("schema")?)?.value())?;
if !matches!(schema, 4 | 6) {
return Err(StoreError::UnsupportedSchema);
}
let binding = read_binding(&meta)?;
@@ -519,7 +558,7 @@ impl Store {
openraft::EntryPayload::Blank => crate::CaptureResponse::blank(id)?,
openraft::EntryPayload::Normal(command) => {
let canonical = command.created().encode()?;
let result = apply_created(&tx, id, command.created(), &canonical, &binding)?;
let result = apply_created(&tx, id, command.created(), &canonical, &binding, schema)?;
crate::CaptureResponse::created(CreatedApplyResult { log_id: id, result })?
}
openraft::EntryPayload::Membership(membership) => {
@@ -539,7 +578,7 @@ impl Store {
}
pub fn native_apply_response(&self, index: u64) -> Result<Option<crate::CaptureResponse>, StoreError> {
let tx = self.database.begin_read()?;
if number(required(tx.open_table(META)?.get("schema")?)?.value())? != 4 {
if !matches!(number(required(tx.open_table(META)?.get("schema")?)?.value())?, 4 | 6) {
return Err(StoreError::UnsupportedSchema);
}
let results = tx.open_table(APPLY_RESULTS)?;
@@ -557,7 +596,7 @@ impl Store {
let tx = self.write_transaction()?;
{
let mut meta = tx.open_table(META)?;
if matches!(number(required(meta.get("schema")?)?.value())?, 3 | 4) {
if matches!(number(required(meta.get("schema")?)?.value())?, 3..=6) {
if read_binding(&meta)? == binding {
return Ok(());
}
@@ -586,7 +625,8 @@ impl Store {
let result;
{
let mut meta = tx.open_table(META)?;
if number(required(meta.get("schema")?)?.value())? != 3 {
let schema = number(required(meta.get("schema")?)?.value())?;
if !matches!(schema, 3 | 5) {
return Err(StoreError::UnsupportedSchema);
}
let binding = read_binding(&meta)?;
@@ -609,7 +649,7 @@ impl Store {
return Err(StoreError::InvalidApplySequence);
}
let command = DecideCreated::decode(&entry.payload)?;
result = apply_created(&tx, id, &command, &entry.payload, &binding)?;
result = apply_created(&tx, id, &command, &entry.payload, &binding, schema)?;
let mut applied = encode_id(id);
applied.extend(result_bytes(&result));
results.insert(id.index, applied.as_slice())?;
@@ -623,7 +663,7 @@ impl Store {
pub fn capture_binding(&self) -> Result<Option<CaptureBinding>, StoreError> {
let tx = self.database.begin_read()?;
let meta = tx.open_table(META)?;
if !matches!(number(required(meta.get("schema")?)?.value())?, 3 | 4) {
if !matches!(number(required(meta.get("schema")?)?.value())?, 3..=6) {
return Ok(None);
}
Ok(Some(read_binding(&meta)?))
@@ -631,7 +671,7 @@ impl Store {
pub fn last_applied(&self) -> Result<Option<LogId>, StoreError> {
let tx = self.database.begin_read()?;
let meta = tx.open_table(META)?;
if !matches!(number(required(meta.get("schema")?)?.value())?, 3 | 4) {
if !matches!(number(required(meta.get("schema")?)?.value())?, 3..=6) {
return Ok(None);
}
read_applied(&meta)
@@ -660,15 +700,87 @@ impl Store {
let t = tx.open_table(EVENTS)?;
t.get(operation.as_slice())?.map(|v| decode_event(v.value())).transpose()
}
/// Read the persisted first decision, without fabricating legacy provenance.
pub fn created_operation_provenance(&self, operation: [u8; 16]) -> Result<Option<CreatedOperationProvenance>, StoreError> {
let tx = self.database.begin_read()?;
let schema = number(required(tx.open_table(META)?.get("schema")?)?.value())?;
if !matches!(schema, 5 | 6) {
return Err(StoreError::UnsupportedSchema);
}
tx.open_table(OPERATIONS)?
.get(operation.as_slice())?
.map(|value| decode_provenance(value.value()))
.transpose()
}
/// Explicitly promote a fully verified legacy or native Created history.
pub fn promote_created_provenance(&mut self) -> Result<(), StoreError> {
let tx = self.write_transaction()?;
{
let mut meta = tx.open_table(META)?;
let schema = number(required(meta.get("schema")?)?.value())?;
if !matches!(schema, 3..=6) {
return Err(StoreError::UnsupportedSchema);
}
let tables = tx
.list_tables()?
.map(|table| table.name().to_owned())
.collect::<std::collections::BTreeSet<_>>();
if [
"meta",
"vote",
"logs",
"heads",
"versions",
"events",
"operations",
"apply_results",
]
.iter()
.any(|name| !tables.contains(*name))
{
return Err(StoreError::CorruptRecord);
}
crate::validate_identity(crate::decode_identity(required(meta.get("identity")?)?.value())?)?;
for row in tx.open_table(crate::VOTE)?.iter()? {
let (key, value) = row?;
if key.value() != 0 {
return Err(StoreError::CorruptRecord);
}
crate::decode_vote(value.value())?;
}
let logs = tx.open_table(LOGS)?;
let heads = tx.open_table(HEADS)?;
let versions = tx.open_table(VERSIONS)?;
let events = tx.open_table(EVENTS)?;
let mut operations = tx.open_table(OPERATIONS)?;
let results = tx.open_table(APPLY_RESULTS)?;
let first = validate_application_tables(&meta, &logs, &heads, &versions, &events, &operations, &results, true)?;
if matches!(schema, 5 | 6) {
return Ok(());
}
// Keep one encoded row at a time; the replay map contains only first LogIds.
for (operation, id) in first {
let original = required(operations.get(operation.as_slice())?)?.value().to_vec();
operations.insert(operation.as_slice(), encode_provenance(id, &original)?.as_slice())?;
}
meta.insert("schema", (if schema == 3 { 5u64 } else { 6u64 }).to_be_bytes().as_slice())?;
}
self.commit(tx)
}
pub fn created_operation(&self, operation: [u8; 16]) -> Result<Option<CreatedOperation>, StoreError> {
let tx = self.database.begin_read()?;
let schema = number(required(tx.open_table(META)?.get("schema")?)?.value())?;
let t = tx.open_table(OPERATIONS)?;
t.get(operation.as_slice())?.map(|v| decode_operation(v.value())).transpose()
t.get(operation.as_slice())?
.map(|v| decode_stored_operation(v.value(), schema))
.transpose()
}
pub fn created_apply_result(&self, index: u64) -> Result<Option<CreatedApplyResult>, StoreError> {
let tx = self.database.begin_read()?;
let t = tx.open_table(APPLY_RESULTS)?;
if number(required(tx.open_table(META)?.get("schema")?)?.value())? == 4 {
if matches!(number(required(tx.open_table(META)?.get("schema")?)?.value())?, 4 | 6) {
t.get(index)?
.map(|v| {
decode_response(v.value()).map(|r| {
@@ -688,7 +800,7 @@ impl Store {
pub(crate) fn validate_application(tx: &ReadTransaction) -> Result<(), StoreError> {
let meta = tx.open_table(META)?;
if !matches!(number(required(meta.get("schema")?)?.value())?, 3 | 4) {
if !matches!(number(required(meta.get("schema")?)?.value())?, 3..=6) {
return Ok(());
}
validate_application_tables(
@@ -699,7 +811,9 @@ pub(crate) fn validate_application(tx: &ReadTransaction) -> Result<(), StoreErro
&tx.open_table(EVENTS)?,
&tx.open_table(OPERATIONS)?,
&tx.open_table(APPLY_RESULTS)?,
false,
)
.map(|_| ())
}
fn validate_application_tables(
@@ -710,11 +824,13 @@ fn validate_application_tables(
events: &impl ReadableTable<&'static [u8], &'static [u8]>,
operations: &impl ReadableTable<&'static [u8], &'static [u8]>,
results: &impl ReadableTable<u64, &'static [u8]>,
) -> Result<(), StoreError> {
promoting_provenance: bool,
) -> Result<BTreeMap<[u8; 16], LogId>, StoreError> {
let schema = number(required(meta.get("schema")?)?.value())?;
let native = schema == 4;
if !matches!(schema, 3 | 4) {
return Ok(());
let native = matches!(schema, 4 | 6);
let full_replay = native || schema == 5 || promoting_provenance;
if !matches!(schema, 3..=6) {
return Ok(BTreeMap::new());
}
let binding = read_binding(meta).map_err(|error| match error {
StoreError::BindingNotPinned => StoreError::CorruptRecord,
@@ -725,6 +841,31 @@ fn validate_application_tables(
if last.is_some_and(|last| committed.is_none_or(|c| last.index > c.index)) {
return Err(StoreError::CorruptRecord);
}
if !native && meta.get("native_membership")?.is_some() {
return Err(StoreError::CorruptRecord);
}
let mut log_next = Some(0);
let mut tail = None;
for row in logs.iter()? {
let (index, bytes) = row?;
let entry = decode_log(bytes.value())?;
if log_next != Some(index.value()) || entry.id.index != index.value() {
return Err(StoreError::CorruptRecord);
}
if native {
decode_native_entry(entry.clone())?;
} else if promoting_provenance || schema == 5 {
let command = DecideCreated::decode(&entry.payload)?;
if command.encode()? != entry.payload {
return Err(StoreError::CorruptRecord);
}
}
log_next = index.value().checked_add(1);
tail = Some(entry.id);
}
if meta.get("tail")?.map(|value| crate::decode_id(value.value())).transpose()? != tail {
return Err(StoreError::CorruptRecord);
}
let mut next = Some(0);
let mut observed = None;
let mut first_operations = BTreeMap::new();
@@ -767,14 +908,14 @@ fn validate_application_tables(
}
let c = entry_command(&entry, native)?;
let canonical = c.encode()?;
let op = decode_operation(required(operations.get(c.operation_id.as_slice())?)?.value())?;
let op = decode_stored_operation(required(operations.get(c.operation_id.as_slice())?)?.value(), schema)?;
let canonical_matches = op.canonical_command == canonical;
let expected = if canonical_matches {
op.result
} else {
CreatedResult::OperationConflict
};
if native {
if full_replay {
let replay = if let Some((previous, result)) = replay_operations.get(&c.operation_id) {
if *previous == canonical {
result.clone()
@@ -800,6 +941,7 @@ fn validate_application_tables(
decision_log_id: r.log_id,
}
};
first_operations.insert(c.operation_id, r.log_id);
replay_operations.insert(c.operation_id, (canonical.clone(), result.clone()));
result
};
@@ -810,7 +952,7 @@ fn validate_application_tables(
if expected != r.result {
return Err(StoreError::CorruptRecord);
}
if canonical_matches {
if !full_replay && canonical_matches {
first_operations.entry(c.operation_id).or_insert(r.log_id);
}
observed = Some(r.log_id);
@@ -819,8 +961,8 @@ fn validate_application_tables(
if observed != last {
return Err(StoreError::CorruptRecord);
}
if native {
if native_application_metadata(meta)?.1 != expected_membership
if full_replay {
if (native && native_application_metadata(meta)?.1 != expected_membership)
|| heads.len()? != u64::try_from(replay_heads.len()).map_err(|_| StoreError::CorruptRecord)?
|| versions.len()? != u64::try_from(replay_versions.len()).map_err(|_| StoreError::CorruptRecord)?
{
@@ -834,12 +976,15 @@ fn validate_application_tables(
}
for row in operations.iter()? {
let (k, v) = row?;
let op = decode_operation(v.value())?;
let op = decode_stored_operation(v.value(), schema)?;
let c = DecideCreated::decode(&op.canonical_command)?;
if k.value() != c.operation_id {
return Err(StoreError::CorruptRecord);
}
let first = required(first_operations.get(&c.operation_id))?;
if matches!(schema, 5 | 6) && decode_provenance(v.value())?.first_decision_log_id != *first {
return Err(StoreError::CorruptRecord);
}
if let CreatedResult::Created {
version_id,
event_id,
@@ -874,7 +1019,7 @@ fn validate_application_tables(
if e.binding != binding {
return Err(StoreError::CorruptRecord);
}
let op = decode_operation(required(operations.get(k.value())?)?.value())?;
let op = decode_stored_operation(required(operations.get(k.value())?)?.value(), schema)?;
let c = DecideCreated::decode(&op.canonical_command)?;
if c.object != e.object
|| c.prepared != e.prepared
@@ -937,5 +1082,5 @@ fn validate_application_tables(
d.finish()?;
decode_version(required(versions.get(version_key(&object, id)?.as_slice())?)?.value())?;
}
Ok(())
Ok(first_operations)
}
+4 -4
View File
@@ -8,13 +8,13 @@ pub(crate) fn validate_committed(
let schema = number(meta.get("schema")?.ok_or(StoreError::CorruptRecord)?.value())?;
let marker = meta.get("committed")?;
match (schema, marker) {
(1 | 3 | 4, None) => {
if matches!(schema, 3 | 4) && meta.get("last_applied")?.is_none_or(|v| v.value() != [0]) {
(1 | 3 | 4 | 5 | 6, None) => {
if matches!(schema, 3..=6) && meta.get("last_applied")?.is_none_or(|v| v.value() != [0]) {
return Err(StoreError::CorruptRecord);
}
Ok(None)
}
(2 | 3 | 4, Some(marker)) => {
(2 | 3 | 4 | 5 | 6, Some(marker)) => {
let id = decode_id(marker.value())?;
let record = logs.get(id.index)?.ok_or(StoreError::CorruptRecord)?;
if decode_log(record.value())?.id != id {
@@ -54,7 +54,7 @@ impl Store {
}
}
meta.insert("committed", encode_id(id).as_slice())?;
if previous.is_none() && !matches!(number(meta.get("schema")?.ok_or(StoreError::CorruptRecord)?.value())?, 3 | 4) {
if previous.is_none() && !matches!(number(meta.get("schema")?.ok_or(StoreError::CorruptRecord)?.value())?, 3..=6) {
meta.insert("schema", 2u64.to_be_bytes().as_slice())?;
}
}
+5
View File
@@ -60,6 +60,11 @@ pub struct CreatedOperation {
pub result: CreatedResult,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreatedOperationProvenance {
pub operation: CreatedOperation,
pub first_decision_log_id: LogId,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreatedApplyResult {
pub log_id: LogId,
pub result: CreatedResult,
File diff suppressed because it is too large Load Diff
@@ -23,6 +23,7 @@ pub struct NativeSnapshotView {
transaction: ReadTransaction,
header: snapshot::Header,
counts: [u64; 4],
schema: u64,
_owner: Arc<Mutex<Store>>,
}
impl NativeSnapshotView {
@@ -35,18 +36,27 @@ impl NativeSnapshotView {
store.database.begin_read().map_err(StoreError::from)?
};
let (applied, membership) = application::native_application_state(&transaction)?;
let header = {
let (header, schema) = {
let meta = transaction.open_table(crate::META).map_err(StoreError::from)?;
let identity = meta
.get("identity")
.map_err(StoreError::from)?
.ok_or(StoreError::CorruptRecord)?;
snapshot::Header {
cluster_id: crate::decode_identity(identity.value())?.cluster_id,
applied,
membership,
binding: application::read_binding(&meta)?,
}
let schema = crate::number(
meta.get("schema")
.map_err(StoreError::from)?
.ok_or(StoreError::CorruptRecord)?
.value(),
)?;
(
snapshot::Header {
cluster_id: crate::decode_identity(identity.value())?.cluster_id,
applied,
membership,
binding: application::read_binding(&meta)?,
},
schema,
)
};
let mut counts = [0; 4];
for (index, definition) in [
@@ -68,10 +78,23 @@ impl NativeSnapshotView {
transaction,
header,
counts,
schema,
_owner: owner,
})
}
pub fn stream_capability_artifact<W: Write>(
self,
writer: W,
limits: snapshot::Limits,
) -> Result<snapshot::CapabilityArtifact<W>, NativeSnapshotError> {
if self.schema != 6 {
return Err(StoreError::UnsupportedSchema.into());
}
let output = snapshot::CapabilityOutput::new(writer, limits.total_bytes);
Ok(self.stream(output, limits)?.finish())
}
pub fn stream<W: Write>(self, writer: W, limits: snapshot::Limits) -> Result<W, NativeSnapshotError> {
let mut encoder = snapshot::Encoder::new(writer, &self.header, self.counts, limits)?;
for (definition, section) in [
+155
View File
@@ -38,6 +38,161 @@ pub enum SnapshotError {
#[error("snapshot IO failed")]
Io(#[from] std::io::Error),
}
/// Fixed capability declaration. Consumers validate these untrusted bytes before decoding.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CapabilityManifest {
pub bytes: [u8; 56],
}
impl CapabilityManifest {
pub fn encode(&self) -> [u8; 56] {
self.bytes
}
pub fn decode(bytes: &[u8]) -> Result<Self, SnapshotError> {
format(bytes.len() == 56, "capability length")?;
format(&bytes[..6] == b"CCAP\x01\x01", "capability version/framing")?;
format(bytes[6..14] == 6u64.to_be_bytes(), "capability schema")?;
format(bytes[14..16] == [1, 1], "operation codec")?;
let mut encoded = [0; 56];
encoded.copy_from_slice(bytes);
Ok(Self { bytes: encoded })
}
fn payload_length(&self) -> u64 {
let mut bytes = [0; 8];
bytes.copy_from_slice(&self.bytes[16..24]);
u64::from_be_bytes(bytes)
}
}
/// Manifest and payload form one explicit input. This does not grant installation authority.
pub struct CapabilityArtifact<T> {
pub manifest: CapabilityManifest,
pub payload: T,
}
pub(crate) struct CapabilityOutput<W> {
io: W,
hash: Sha256,
used: u64,
max: u64,
}
impl<W: Write> CapabilityOutput<W> {
pub(crate) fn new(io: W, max: u64) -> Self {
Self {
io,
hash: Sha256::new(),
used: 0,
max,
}
}
pub(crate) fn finish(self) -> CapabilityArtifact<W> {
let mut bytes = [0; 56];
bytes[..6].copy_from_slice(b"CCAP\x01\x01");
bytes[6..14].copy_from_slice(&6u64.to_be_bytes());
bytes[14..16].copy_from_slice(&[1, 1]);
bytes[16..24].copy_from_slice(&self.used.to_be_bytes());
bytes[24..].copy_from_slice(&self.hash.finalize());
CapabilityArtifact {
manifest: CapabilityManifest { bytes },
payload: self.io,
}
}
}
impl<W: Write> Write for CapabilityOutput<W> {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
let requested = u64::try_from(bytes.len()).map_err(std::io::Error::other)?;
if self.used.checked_add(requested).is_none_or(|n| n > self.max) {
return Err(std::io::Error::other("capability payload limit"));
}
let written = self.io.write(bytes)?;
let part = bytes
.get(..written)
.ok_or_else(|| std::io::Error::other("invalid writer count"))?;
let count = u64::try_from(written).map_err(std::io::Error::other)?;
self.used = self
.used
.checked_add(count)
.ok_or_else(|| std::io::Error::other("capability length overflow"))?;
self.hash.update(part);
Ok(written)
}
fn flush(&mut self) -> std::io::Result<()> {
self.io.flush()
}
}
struct CapabilityInput<R> {
io: R,
hash: Sha256,
used: u64,
max: u64,
}
impl<R: Read> Read for CapabilityInput<R> {
fn read(&mut self, bytes: &mut [u8]) -> std::io::Result<usize> {
// At the boundary, permit only the existing decoder's one-byte EOF probe.
let remaining = self
.max
.checked_sub(self.used)
.ok_or_else(|| std::io::Error::other("capability length overflow"))?;
let requested = u64::try_from(bytes.len()).map_err(std::io::Error::other)?;
let allowed = requested.min(if remaining == 0 { 1 } else { remaining });
let allowed = usize::try_from(allowed).map_err(std::io::Error::other)?;
let read = self.io.read(&mut bytes[..allowed])?;
let part = bytes
.get(..read)
.ok_or_else(|| std::io::Error::other("invalid reader count"))?;
let count = u64::try_from(read).map_err(std::io::Error::other)?;
let used = self
.used
.checked_add(count)
.ok_or_else(|| std::io::Error::other("capability length overflow"))?;
if used > self.max {
return Err(std::io::Error::other("capability payload limit"));
}
self.used = used;
self.hash.update(part);
Ok(read)
}
}
struct CapabilitySink<'a, S>(&'a mut S);
impl<S: StagingSink> StagingSink for CapabilitySink<'_, S> {
fn row(&mut self, section: Section, key: &[u8], value: &[u8]) -> std::io::Result<()> {
if section == Section::Operations {
crate::application::validate_provenance_encoding(value).map_err(std::io::Error::other)?;
}
self.0.row(section, key, value)
}
}
/// Validates framing, declared capability and operation encoding into isolated staging.
/// Failure may leave partial staging rows, which the caller must discard.
/// Success does not prove cross-table consistency, historical truth or source authority.
pub fn decode_capability_artifact(
artifact: CapabilityArtifact<impl Read>,
sink: &mut impl StagingSink,
limits: Limits,
) -> Result<Header, SnapshotError> {
let manifest = CapabilityManifest::decode(&artifact.manifest.bytes)?;
let length = manifest.payload_length();
limit(length <= limits.total_bytes, "capability payload")?;
let mut reader = CapabilityInput {
io: artifact.payload,
hash: Sha256::new(),
used: 0,
max: length,
};
let header = decode(&mut reader, &mut CapabilitySink(sink), limits)?;
format(reader.used == length, "capability payload length")?;
if reader.hash.finalize().as_slice() != &manifest.bytes[24..] {
return Err(SnapshotError::Digest);
}
Ok(header)
}
/// Row values remain opaque. Operation values will contain the canonical decision
/// LogId followed by the existing canonical operation bytes, validated by callers.
pub struct Row<'a> {