1use prikk_error::{PrikkError, Result};
4
5use crate::canonical::{is_contiguous_op_seq, is_strictly_sorted};
6use crate::payload::common::{Intent, OperationCondition, OperationConditionEntry};
7use crate::payload::node::{NodeId, NodeKind};
8use crate::{CanonicalEncode, CanonicalWriter, ObjectId};
9
10pub const TEXT_SPAN_HASH_BYTES: usize = 32;
12
13#[must_use]
15pub fn text_span_hash(bytes: &[u8]) -> [u8; TEXT_SPAN_HASH_BYTES] {
16 prikk_hash::sha256(bytes)
17}
18
19pub fn validate_text_anchor_id(value: &str) -> Result<()> {
21 if value.is_empty() {
22 return Err(PrikkError::CanonicalEncoding(
23 "text anchor id must not be empty".to_string(),
24 ));
25 }
26 if !value.is_ascii() {
27 return Err(PrikkError::CanonicalEncoding(
28 "text anchor id must be ASCII in v1".to_string(),
29 ));
30 }
31 if value.bytes().any(|byte| byte < 0x21 || byte == 0x7f) {
32 return Err(PrikkError::CanonicalEncoding(
33 "text anchor id must not contain whitespace or control characters".to_string(),
34 ));
35 }
36 Ok(())
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct PatchPayload {
42 pub operations: Vec<Operation>,
44 pub parent_patch_ids: Vec<ObjectId>,
46 pub intent: Option<Intent>,
48 pub preconditions: Vec<OperationConditionEntry>,
50}
51
52impl PatchPayload {
53 pub fn validate(&self) -> Result<()> {
55 if self.operations.is_empty() {
56 return Err(PrikkError::CanonicalEncoding(
57 "patch operations must contain at least one operation".to_string(),
58 ));
59 }
60 let op_seq: Vec<u32> = self.operations.iter().map(|op| op.op_seq).collect();
61 if !is_contiguous_op_seq(&op_seq) {
62 return Err(PrikkError::CanonicalEncoding(
63 "patch operations must have contiguous op_seq values starting at 1".to_string(),
64 ));
65 }
66 if !is_strictly_sorted(&self.parent_patch_ids) {
67 return Err(PrikkError::CanonicalEncoding(
68 "parent_patch_ids must be sorted and unique".to_string(),
69 ));
70 }
71 if !is_strictly_sorted(&self.preconditions) {
72 return Err(PrikkError::CanonicalEncoding(
73 "patch preconditions must be sorted and unique".to_string(),
74 ));
75 }
76 Ok(())
77 }
78}
79
80impl CanonicalEncode for PatchPayload {
81 fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
82 self.validate()?;
83 writer.repeated_record_list(1, &self.operations)?;
84 writer.repeated_object_id(2, &self.parent_patch_ids)?;
85 if let Some(intent) = self.intent {
86 writer.field_enum_u16(3, intent.code())?;
87 }
88 writer.repeated_record(4, &self.preconditions)?;
89 Ok(())
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct Operation {
96 pub op_seq: u32,
98 pub op_id: Option<String>,
100 pub preconditions: Vec<OperationCondition>,
102 pub kind: OperationKind,
104}
105
106impl CanonicalEncode for Operation {
107 fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
108 writer.field_u32(1, self.op_seq)?;
109 writer.field_string_opt(2, self.op_id.as_deref())?;
110 writer.repeated_record(3, &self.preconditions)?;
111 match &self.kind {
112 OperationKind::CreateFile(value) => writer.field_record(10, value)?,
113 OperationKind::DeleteNode(value) => writer.field_record(11, value)?,
114 OperationKind::EditText(value) => writer.field_record(12, value)?,
115 OperationKind::RenamePath(value) => writer.field_record(13, value)?,
116 OperationKind::ChangePerm(value) => writer.field_record(14, value)?,
117 OperationKind::CreateSymlink(value) => writer.field_record(15, value)?,
118 OperationKind::ReplaceBinary(value) => writer.field_record(16, value)?,
119 }
120 Ok(())
121 }
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
126pub enum OperationKind {
127 CreateFile(CreateFile),
129 DeleteNode(DeleteNode),
131 EditText(EditText),
133 RenamePath(RenamePath),
135 ChangePerm(ChangePerm),
137 CreateSymlink(CreateSymlink),
139 ReplaceBinary(ReplaceBinary),
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct CreateFile {
146 pub path: String,
148 pub node_id: NodeId,
150 pub blob_id: ObjectId,
152 pub mode: u32,
154}
155
156impl CreateFile {
157 pub fn validate(&self) -> Result<()> {
160 if self.node_id.is_zero() {
161 return Err(PrikkError::CanonicalEncoding(
162 "CreateFile node_id must be nonzero".to_string(),
163 ));
164 }
165 Ok(())
166 }
167}
168
169impl CanonicalEncode for CreateFile {
170 fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
171 self.validate()?;
172 writer.field_repo_path(1, &self.path)?;
173 writer.field_bytes(2, self.node_id.as_bytes())?;
174 writer.field_object_id(3, &self.blob_id)?;
175 writer.field_u32(4, self.mode)?;
176 Ok(())
177 }
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
182pub enum DeleteNodePreimage {
183 File {
185 old_blob_id: ObjectId,
187 old_mode: u32,
189 },
190 Symlink {
192 old_target: String,
194 },
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
201pub struct DeleteNode {
202 pub path: String,
204 pub node_id: NodeId,
206 pub old_node_kind: NodeKind,
208 pub preimage: DeleteNodePreimage,
210}
211
212impl DeleteNode {
213 pub fn validate(&self) -> Result<()> {
216 if self.node_id.is_zero() {
217 return Err(PrikkError::CanonicalEncoding(
218 "DeleteNode node_id must be nonzero".to_string(),
219 ));
220 }
221 let consistent = matches!(
222 (self.old_node_kind, &self.preimage),
223 (
224 NodeKind::TextFile | NodeKind::BinaryFile,
225 DeleteNodePreimage::File { .. }
226 ) | (NodeKind::Symlink, DeleteNodePreimage::Symlink { .. })
227 );
228 if !consistent {
229 return Err(PrikkError::CanonicalEncoding(
230 "DeleteNode old_node_kind does not match preimage discriminator".to_string(),
231 ));
232 }
233 Ok(())
234 }
235}
236
237impl CanonicalEncode for DeleteNode {
238 fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
239 self.validate()?;
240 writer.field_repo_path(1, &self.path)?;
241 writer.field_bytes(2, self.node_id.as_bytes())?;
242 writer.field_enum_u16(3, self.old_node_kind.code())?;
243 match &self.preimage {
244 DeleteNodePreimage::File {
245 old_blob_id,
246 old_mode,
247 } => {
248 writer.field_object_id(4, old_blob_id)?;
249 writer.field_u32(6, *old_mode)?;
250 }
251 DeleteNodePreimage::Symlink { old_target } => {
252 writer.field_string(5, old_target)?;
253 }
254 }
255 Ok(())
256 }
257}
258
259#[derive(Debug, Clone, PartialEq, Eq)]
261pub struct EditText {
262 pub node_id: NodeId,
264 pub span_id: [u8; TEXT_SPAN_HASH_BYTES],
266 pub old_span_hash: [u8; TEXT_SPAN_HASH_BYTES],
268 pub left_anchor_hash: [u8; TEXT_SPAN_HASH_BYTES],
270 pub right_anchor_hash: [u8; TEXT_SPAN_HASH_BYTES],
272 pub replacement_text: Vec<u8>,
274 pub presentation_hint_line: Option<u32>,
276 pub presentation_hint_column: Option<u32>,
278 pub old_span_text: Vec<u8>,
280}
281
282impl EditText {
283 pub fn validate(&self) -> Result<()> {
287 if self.node_id.is_zero() {
288 return Err(PrikkError::CanonicalEncoding(
289 "EditText node_id must be nonzero".to_string(),
290 ));
291 }
292 if self.old_span_hash != text_span_hash(&self.old_span_text) {
293 return Err(PrikkError::CanonicalEncoding(
294 "EditText old_span_hash must equal SHA-256(old_span_text)".to_string(),
295 ));
296 }
297 if core::str::from_utf8(&self.old_span_text).is_err() {
298 return Err(PrikkError::CanonicalEncoding(
299 "EditText old_span_text must be well-formed UTF-8".to_string(),
300 ));
301 }
302 if core::str::from_utf8(&self.replacement_text).is_err() {
303 return Err(PrikkError::CanonicalEncoding(
304 "EditText replacement_text must be well-formed UTF-8".to_string(),
305 ));
306 }
307 Ok(())
308 }
309}
310
311impl CanonicalEncode for EditText {
312 fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
313 self.validate()?;
314 writer.field_bytes(1, self.node_id.as_bytes())?;
315 writer.field_bytes(2, &self.span_id)?;
316 writer.field_bytes(3, &self.old_span_hash)?;
317 writer.field_bytes(4, &self.left_anchor_hash)?;
318 writer.field_bytes(5, &self.right_anchor_hash)?;
319 writer.field_bytes(6, &self.replacement_text)?;
320 if let Some(line) = self.presentation_hint_line {
321 writer.field_u32(7, line)?;
322 }
323 if let Some(column) = self.presentation_hint_column {
324 writer.field_u32(8, column)?;
325 }
326 writer.field_bytes(9, &self.old_span_text)?;
327 Ok(())
328 }
329}
330
331#[derive(Debug, Clone, PartialEq, Eq)]
333pub struct RenamePath {
334 pub node_id: NodeId,
336 pub old_path: String,
338 pub new_path: String,
340}
341
342impl RenamePath {
343 pub fn validate(&self) -> Result<()> {
346 if self.node_id.is_zero() {
347 return Err(PrikkError::CanonicalEncoding(
348 "RenamePath node_id must be nonzero".to_string(),
349 ));
350 }
351 Ok(())
352 }
353}
354
355impl CanonicalEncode for RenamePath {
356 fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
357 self.validate()?;
358 writer.field_bytes(1, self.node_id.as_bytes())?;
359 writer.field_repo_path(2, &self.old_path)?;
360 writer.field_repo_path(3, &self.new_path)?;
361 Ok(())
362 }
363}
364
365#[derive(Debug, Clone, PartialEq, Eq)]
367pub struct ChangePerm {
368 pub node_id: NodeId,
370 pub old_mode: u32,
372 pub new_mode: u32,
374}
375
376impl ChangePerm {
377 pub fn validate(&self) -> Result<()> {
379 if self.node_id.is_zero() {
380 return Err(PrikkError::CanonicalEncoding(
381 "ChangePerm node_id must be nonzero".to_string(),
382 ));
383 }
384 Ok(())
385 }
386}
387
388impl CanonicalEncode for ChangePerm {
389 fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
390 self.validate()?;
391 writer.field_bytes(1, self.node_id.as_bytes())?;
392 writer.field_u32(2, self.old_mode)?;
393 writer.field_u32(3, self.new_mode)?;
394 Ok(())
395 }
396}
397
398#[derive(Debug, Clone, PartialEq, Eq)]
401pub struct CreateSymlink {
402 pub path: String,
404 pub node_id: NodeId,
406 pub target: String,
409}
410
411impl CreateSymlink {
412 pub fn validate(&self) -> Result<()> {
414 if self.node_id.is_zero() {
415 return Err(PrikkError::CanonicalEncoding(
416 "CreateSymlink node_id must be nonzero".to_string(),
417 ));
418 }
419 Ok(())
420 }
421}
422
423impl CanonicalEncode for CreateSymlink {
424 fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
425 self.validate()?;
426 writer.field_repo_path(1, &self.path)?;
427 writer.field_bytes(2, self.node_id.as_bytes())?;
428 writer.field_string(3, &self.target)?;
429 Ok(())
430 }
431}
432
433#[derive(Debug, Clone, PartialEq, Eq)]
435pub struct ReplaceBinary {
436 pub node_id: NodeId,
438 pub old_blob_id: ObjectId,
440 pub new_blob_id: ObjectId,
442}
443
444impl ReplaceBinary {
445 pub fn validate(&self) -> Result<()> {
448 if self.node_id.is_zero() {
449 return Err(PrikkError::CanonicalEncoding(
450 "ReplaceBinary node_id must be nonzero".to_string(),
451 ));
452 }
453 Ok(())
454 }
455}
456
457impl CanonicalEncode for ReplaceBinary {
458 fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
459 self.validate()?;
460 writer.field_bytes(1, self.node_id.as_bytes())?;
461 writer.field_object_id(2, &self.old_blob_id)?;
462 writer.field_object_id(3, &self.new_blob_id)?;
463 Ok(())
464 }
465}