1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
//! Blob role validation (FDD-03 §9.3.0).
//!
//! Now that `BlobPayload.blob_kind` exists, store readers validate a blob's role at
//! decode time rather than trusting the reference: snapshot references must resolve
//! to a `SNAPSHOT` blob, and file-content references must not. This fails closed
//! before `blob_kind` becomes the semantic source for node-kind behavior in the
//! Phase 4 node model.
use prikk_error::{PrikkError, Result};
use prikk_object::{BlobKind, BlobPayload, CanonicalEncode, NodeKind, ObjectId, ObjectType};
/// Decode a Blob that must be a `SNAPSHOT` blob, returning its content.
pub(crate) fn decode_snapshot_blob(canonical_payload: &[u8]) -> Result<Vec<u8>> {
let blob = BlobPayload::decode_canonical(canonical_payload)?;
if blob.blob_kind != BlobKind::Snapshot {
return Err(PrikkError::Integrity(
"snapshot reference points to a non-SNAPSHOT blob".to_string(),
));
}
Ok(blob.content)
}
/// Decode a Blob used as file content, returning its content. A file node MUST NOT
/// reference a `SNAPSHOT` blob (§9.3.0). `TEXT` and `BINARY` are both accepted in
/// the pre-node interim; Phase 4 node-kind derivation tightens this per operation.
pub(crate) fn decode_file_content_blob(canonical_payload: &[u8]) -> Result<Vec<u8>> {
let blob = BlobPayload::decode_canonical(canonical_payload)?;
if blob.blob_kind == BlobKind::Snapshot {
return Err(PrikkError::Integrity(
"file content reference points to a SNAPSHOT blob".to_string(),
));
}
Ok(blob.content)
}
/// Decode a file-content Blob, returning the derived node kind and content. A
/// file node MUST NOT reference a `SNAPSHOT` blob (§9.3.0); `TEXT` derives
/// `TextFile` and `BINARY` derives `BinaryFile`. Used by the inverse path to fill
/// `DeleteNode.old_node_kind` from a created file's blob.
pub(crate) fn decode_file_content_blob_with_kind(
canonical_payload: &[u8],
) -> Result<(prikk_object::NodeKind, Vec<u8>)> {
let blob = BlobPayload::decode_canonical(canonical_payload)?;
if blob.blob_kind == BlobKind::Snapshot {
return Err(PrikkError::Integrity(
"file content reference points to a SNAPSHOT blob".to_string(),
));
}
let kind = prikk_object::NodeKind::from_file_blob_kind(blob.blob_kind)?;
Ok((kind, blob.content))
}
/// Resolve a blob's `blob_kind` from its canonical payload and require `BINARY`
/// (FDD-03 §9.3 `ReplaceBinary`). `ReplaceBinary` operates on binary file nodes
/// only: v1 rejects text<->binary transitions and SNAPSHOT references on this path,
/// so applying it requires *both* the old and the new blob to resolve to `BINARY`.
/// This is the binary-only enforcement primitive; node-addressed `ReplaceBinary`
/// application (which calls it on both blob ids) is wired in at the node model
/// (increment 4.4).
#[cfg(test)]
pub(crate) fn ensure_blob_kind_is_binary(canonical_payload: &[u8]) -> Result<()> {
let blob = BlobPayload::decode_canonical(canonical_payload)?;
if blob.blob_kind == BlobKind::Binary {
return Ok(());
}
Err(PrikkError::Integrity(format!(
"ReplaceBinary requires BINARY blobs; found blob_kind {:?}",
blob.blob_kind
)))
}
/// Verify that `bytes` identify as the expected file-content Blob *under the blob
/// kind derived from `old_node_kind`* (FDD-03 §9.3): `TEXT_FILE` -> `Text`,
/// `BINARY_FILE` -> `Binary`. This both confirms the `old_blob_id` precondition
/// and enforces the `old_node_kind` <-> `blob_kind` agreement, so a binary-file
/// deletion is not rejected by a Text-only recompute and a kind/blob mismatch
/// (including a snapshot-blob id) fails closed. Symlink nodes have no file-content
/// blob and are rejected.
pub(crate) fn ensure_blob_matches_node_kind(
bytes: &[u8],
expected: ObjectId,
old_node_kind: NodeKind,
) -> Result<()> {
let blob_kind = match old_node_kind {
NodeKind::TextFile => BlobKind::Text,
NodeKind::BinaryFile => BlobKind::Binary,
NodeKind::Symlink => {
return Err(PrikkError::Integrity(
"DeleteNode symlink node has no file-content blob".to_string(),
));
}
};
let payload = BlobPayload::new(blob_kind, bytes.to_vec());
let id = ObjectId::from_canonical_payload(ObjectType::Blob, 1, &payload.to_canonical_bytes()?);
if id == expected {
return Ok(());
}
Err(PrikkError::Integrity(format!(
"DeleteNode old_blob_id/old_node_kind mismatch: expected {expected}, got {id}"
)))
}
#[cfg(test)]
mod tests;