1use prikk_error::{PrikkError, Result};
10
11use crate::canonical::{is_contiguous_op_seq, is_strictly_sorted};
12use crate::payload::common::{Intent, OperationCondition, OperationConditionEntry};
13use crate::{CanonicalEncode, CanonicalWriter, ObjectId, WireType};
14
15mod operations;
16
17pub use operations::{
18 ChangePerm, CreateFile, CreateSymlink, DeleteNode, DeleteNodePreimage, EditText, RenamePath,
19 ReplaceBinary,
20};
21
22pub const TEXT_SPAN_HASH_BYTES: usize = 32;
24
25#[must_use]
27pub fn text_span_hash(bytes: &[u8]) -> [u8; TEXT_SPAN_HASH_BYTES] {
28 prikk_hash::sha256(bytes)
29}
30
31pub fn validate_text_anchor_id(value: &str) -> Result<()> {
33 if value.is_empty() {
34 return Err(PrikkError::CanonicalEncoding(
35 "text anchor id must not be empty".to_string(),
36 ));
37 }
38 if !value.is_ascii() {
39 return Err(PrikkError::CanonicalEncoding(
40 "text anchor id must be ASCII in v1".to_string(),
41 ));
42 }
43 if value.bytes().any(|byte| byte < 0x21 || byte == 0x7f) {
44 return Err(PrikkError::CanonicalEncoding(
45 "text anchor id must not contain whitespace or control characters".to_string(),
46 ));
47 }
48 Ok(())
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct PatchPayload {
54 pub operations: Vec<Operation>,
56 pub parent_patch_ids: Vec<ObjectId>,
58 pub intent: Option<Intent>,
60 pub preconditions: Vec<OperationConditionEntry>,
62 pub purpose: PatchPurpose,
64}
65
66impl PatchPayload {
67 pub fn validate(&self) -> Result<()> {
69 if self.operations.is_empty() {
70 return Err(PrikkError::CanonicalEncoding(
71 "patch operations must contain at least one operation".to_string(),
72 ));
73 }
74 let op_seq: Vec<u32> = self.operations.iter().map(|op| op.op_seq).collect();
75 if !is_contiguous_op_seq(&op_seq) {
76 return Err(PrikkError::CanonicalEncoding(
77 "patch operations must have contiguous op_seq values starting at 1".to_string(),
78 ));
79 }
80 if !is_strictly_sorted(&self.parent_patch_ids) {
81 return Err(PrikkError::CanonicalEncoding(
82 "parent_patch_ids must be sorted and unique".to_string(),
83 ));
84 }
85 if !is_strictly_sorted(&self.preconditions) {
86 return Err(PrikkError::CanonicalEncoding(
87 "patch preconditions must be sorted and unique".to_string(),
88 ));
89 }
90 Ok(())
91 }
92}
93
94impl CanonicalEncode for PatchPayload {
95 fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
96 self.validate()?;
97 writer.repeated_record_list(1, &self.operations)?;
98 writer.repeated_object_id(2, &self.parent_patch_ids)?;
99 if let Some(intent) = self.intent {
100 writer.field_enum_u16(3, intent.code())?;
101 }
102 writer.repeated_record(4, &self.preconditions)?;
103 if self.purpose != PatchPurpose::Normal {
104 writer.field_enum_u16(5, self.purpose.code())?;
105 }
106 Ok(())
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
112#[repr(u16)]
113pub enum PatchPurpose {
114 Normal = 1,
116 RollbackDraft = 2,
118}
119
120impl PatchPurpose {
121 #[must_use]
123 pub const fn code(self) -> u16 {
124 self as u16
125 }
126
127 pub fn from_present_code(code: u16) -> Result<Self> {
129 match code {
130 1 => Err(PrikkError::CanonicalEncoding(
131 "PatchPurpose::Normal must be omitted, not encoded explicitly".to_string(),
132 )),
133 2 => Ok(Self::RollbackDraft),
134 other => Err(PrikkError::CanonicalEncoding(format!(
135 "unknown patch purpose code: {other}"
136 ))),
137 }
138 }
139
140 pub fn decode_from_patch_payload(bytes: &[u8]) -> Result<Self> {
143 let mut cursor = PatchPayloadFieldCursor::new(bytes);
144 let mut purpose = Self::Normal;
145 let mut seen_purpose = false;
146 while let Some(field) = cursor.next_field()? {
147 match field.tag {
148 1..=4 => {}
149 5 => {
150 if seen_purpose {
151 return Err(PrikkError::CanonicalEncoding(
152 "duplicate PatchPurpose field".to_string(),
153 ));
154 }
155 seen_purpose = true;
156 field.require_wire(WireType::EnumU16)?;
157 purpose = Self::from_present_code(field.read_u16()?)?;
158 }
159 other => {
160 return Err(PrikkError::CanonicalEncoding(format!(
161 "unknown PatchPayload field tag: {other}"
162 )));
163 }
164 }
165 }
166 Ok(purpose)
167 }
168}
169
170struct PatchPayloadFieldCursor<'a> {
171 bytes: &'a [u8],
172 pos: usize,
173 last_tag: Option<u16>,
174}
175
176impl<'a> PatchPayloadFieldCursor<'a> {
177 const fn new(bytes: &'a [u8]) -> Self {
178 Self {
179 bytes,
180 pos: 0,
181 last_tag: None,
182 }
183 }
184
185 fn next_field(&mut self) -> Result<Option<PatchPayloadField<'a>>> {
186 if self.pos == self.bytes.len() {
187 return Ok(None);
188 }
189 let tag = u16::from_be_bytes(self.read_array::<2>()?);
190 if tag == 0 {
191 return Err(PrikkError::CanonicalEncoding(
192 "field tag 0 is reserved".to_string(),
193 ));
194 }
195 if let Some(last) = self.last_tag {
196 if tag < last {
197 return Err(PrikkError::CanonicalEncoding(format!(
198 "field tag order violation: {tag} after {last}"
199 )));
200 }
201 }
202 self.last_tag = Some(tag);
203 let wire_type = self.read_u8()?;
204 let len = usize::try_from(u64::from_be_bytes(self.read_array::<8>()?)).map_err(|_| {
205 PrikkError::CanonicalEncoding("canonical field length does not fit usize".to_string())
206 })?;
207 let value = self.read_exact(len)?;
208 Ok(Some(PatchPayloadField {
209 tag,
210 wire_type,
211 value,
212 }))
213 }
214
215 fn read_u8(&mut self) -> Result<u8> {
216 let bytes = self.read_exact(1)?;
217 let Some(byte) = bytes.first() else {
218 return Err(PrikkError::CanonicalEncoding(
219 "unexpected empty byte".to_string(),
220 ));
221 };
222 Ok(*byte)
223 }
224
225 fn read_array<const N: usize>(&mut self) -> Result<[u8; N]> {
226 let bytes = self.read_exact(N)?;
227 let mut out = [0_u8; N];
228 out.copy_from_slice(bytes);
229 Ok(out)
230 }
231
232 fn read_exact(&mut self, len: usize) -> Result<&'a [u8]> {
233 let end = self
234 .pos
235 .checked_add(len)
236 .ok_or_else(|| PrikkError::CanonicalEncoding("canonical range overflow".to_string()))?;
237 let Some(slice) = self.bytes.get(self.pos..end) else {
238 return Err(PrikkError::CanonicalEncoding(
239 "unexpected end of canonical payload".to_string(),
240 ));
241 };
242 self.pos = end;
243 Ok(slice)
244 }
245}
246
247struct PatchPayloadField<'a> {
248 tag: u16,
249 wire_type: u8,
250 value: &'a [u8],
251}
252
253impl PatchPayloadField<'_> {
254 fn require_wire(&self, expected: WireType) -> Result<()> {
255 if self.wire_type == expected as u8 {
256 return Ok(());
257 }
258 Err(PrikkError::CanonicalEncoding(format!(
259 "field {} has wrong wire type: expected {}, got {}",
260 self.tag, expected as u8, self.wire_type
261 )))
262 }
263
264 fn read_u16(&self) -> Result<u16> {
265 if self.value.len() != 2 {
266 return Err(PrikkError::CanonicalEncoding(format!(
267 "field {} expected 2 bytes, got {}",
268 self.tag,
269 self.value.len()
270 )));
271 }
272 let mut out = [0_u8; 2];
273 out.copy_from_slice(self.value);
274 Ok(u16::from_be_bytes(out))
275 }
276}
277
278#[derive(Debug, Clone, PartialEq, Eq)]
280pub struct Operation {
281 pub op_seq: u32,
283 pub op_id: Option<String>,
285 pub preconditions: Vec<OperationCondition>,
287 pub kind: OperationKind,
289}
290
291impl CanonicalEncode for Operation {
292 fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
293 writer.field_u32(1, self.op_seq)?;
294 writer.field_string_opt(2, self.op_id.as_deref())?;
295 writer.repeated_record(3, &self.preconditions)?;
296 match &self.kind {
297 OperationKind::CreateFile(value) => writer.field_record(10, value)?,
298 OperationKind::DeleteNode(value) => writer.field_record(11, value)?,
299 OperationKind::EditText(value) => writer.field_record(12, value)?,
300 OperationKind::RenamePath(value) => writer.field_record(13, value)?,
301 OperationKind::ChangePerm(value) => writer.field_record(14, value)?,
302 OperationKind::CreateSymlink(value) => writer.field_record(15, value)?,
303 OperationKind::ReplaceBinary(value) => writer.field_record(16, value)?,
304 }
305 Ok(())
306 }
307}
308
309#[derive(Debug, Clone, PartialEq, Eq)]
311pub enum OperationKind {
312 CreateFile(CreateFile),
314 DeleteNode(DeleteNode),
316 EditText(EditText),
318 RenamePath(RenamePath),
320 ChangePerm(ChangePerm),
322 CreateSymlink(CreateSymlink),
324 ReplaceBinary(ReplaceBinary),
326}