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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
//! Received (imported) ref bookkeeping — DC-78 §D4's distinct `remotes/` namespace.
//!
//! A received ref is deliberately **not** a `refs/by-id/` pointer. `refs/by-id/`'s own consistency
//! check (a RefState object's embedded `ref_name` must equal its pointer's declared name — enforced
//! by `refs/verify/scan.rs`) cannot be satisfied by a renamed local identifier: the RefState object
//! keeps the *origin's own* embedded ref name (its content-addressed identity and signature would be
//! invalidated by editing it), so a pointer declaring `remotes/heads/main` could never agree with a
//! payload that says `heads/main`. Storing received refs under their own key, in their own small
//! index format, sidesteps that conflict entirely rather than patching the check to allow it — the
//! latter would be new verification-path surface, which §D6 rules out.
//!
//! This index is never read by `verify_repository`. Every object a received pointer leads to
//! (RefState, Block, Patch, Blob, Attestation) is an ordinary object-store entry, checked exactly
//! like any other by the existing type-based object scan — §D6's "no new verification path" holds
//! because there genuinely is none: this module only makes received tips *discoverable* by name.
//!
//! RFC 102 Stage 5, design-v1.md §14/Step 0 item 2: backed by `received_index.rs`'s shared, append-
//! only, last-entry-wins container (the refs container+pointer-index pattern, applied here because a
//! received ref's own name doesn't exist at `init` either — the same architecturally-forced shape
//! Stage 4 hit with refs). Replaced the old one-file-per-ref directory entirely, not layered on top of
//! it.
use prikk_error::{PrikkError, Result};
use prikk_object::ObjectId;
use crate::layout::{RepositoryLayout, ref_name_key_bytes};
use crate::received_index::{
ReceivedIndexEntry, append_received_index_entry, list_resolved_received_entries,
lookup_received_index_entry,
};
/// One received ref pointer.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReceivedPointer {
/// Local logical name, always `remotes/<origin ref name>`.
pub ref_name: String,
/// The received tip's RefState object id.
pub ref_state_id: ObjectId,
}
/// Validate a received ref name: the reserved `remotes/` namespace, required rather than rejected
/// (the mirror image of `validate_local_branch_ref`/`validate_local_tag_ref`, which reject it).
pub fn validate_received_ref(ref_name: &str) -> Result<()> {
if ref_name.is_empty() {
return Err(PrikkError::InvalidName(
"ref name must not be empty".to_string(),
));
}
if !ref_name.starts_with("remotes/") {
return Err(PrikkError::InvalidName(format!(
"ref {ref_name} is not a received ref; expected remotes/<name>"
)));
}
let rest = &ref_name["remotes/".len()..];
if rest.is_empty() {
return Err(PrikkError::InvalidName(
"received ref must include a name after remotes/".to_string(),
));
}
if ref_name.chars().any(|ch| ch == '\0' || ch.is_control()) {
return Err(PrikkError::InvalidName(format!(
"ref {ref_name} contains a forbidden control character"
)));
}
if rest.starts_with('/') || rest.ends_with('/') || rest.contains("//") {
return Err(PrikkError::InvalidName(format!(
"received ref {ref_name} contains an empty path component"
)));
}
if rest
.split('/')
.any(|component| component == "." || component == "..")
{
return Err(PrikkError::InvalidName(format!(
"received ref {ref_name} contains a traversal component"
)));
}
Ok(())
}
/// Write (or overwrite) a received ref's pointer. Each import replaces the prior received state for
/// that name outright — there is no CAS and no merge between two received histories under one name;
/// D4 explicitly leaves remote-tracking-ref semantics out of scope, so a re-import is simply "this is
/// what I have now," and turning that into real local history remains the operator's own deliberate
/// merge, using machinery that already exists. Never checks for an existing entry first — matching
/// `append_received_index_entry`'s own reasoning, "last entry wins" already makes a duplicate
/// harmless and there is no CAS to enforce here.
pub(crate) fn write_received_pointer(
layout: &RepositoryLayout,
ref_name: &str,
ref_state_id: ObjectId,
) -> Result<()> {
validate_received_ref(ref_name)?;
append_received_index_entry(
layout,
&ReceivedIndexEntry {
ref_name_key: ref_name_key_bytes(ref_name),
ref_name: ref_name.to_string(),
ref_state_id,
},
)
}
/// Read a received ref's current pointer, if one has been imported.
pub fn read_received_pointer(
layout: &RepositoryLayout,
ref_name: &str,
) -> Result<Option<ReceivedPointer>> {
validate_received_ref(ref_name)?;
let key = ref_name_key_bytes(ref_name);
Ok(lookup_received_index_entry(layout, key)?.map(ReceivedPointer::from))
}
/// Enumerate every received ref pointer, sorted by name — the received-namespace counterpart of
/// `RefStore::list_ref_pointers`.
pub fn list_received_pointers(layout: &RepositoryLayout) -> Result<Vec<ReceivedPointer>> {
let mut pointers: Vec<ReceivedPointer> = list_resolved_received_entries(layout)?
.into_iter()
.map(ReceivedPointer::from)
.collect();
pointers.sort_by(|left, right| left.ref_name.cmp(&right.ref_name));
Ok(pointers)
}
impl From<ReceivedIndexEntry> for ReceivedPointer {
fn from(entry: ReceivedIndexEntry) -> Self {
Self {
ref_name: entry.ref_name,
ref_state_id: entry.ref_state_id,
}
}
}
#[cfg(all(test, target_os = "linux"))]
mod tests;