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::{CanonicalEncode, CanonicalWriter, ObjectId};
8
9pub const TEXT_SPAN_HASH_BYTES: usize = 32;
11
12#[must_use]
14pub fn text_span_hash(bytes: &[u8]) -> [u8; TEXT_SPAN_HASH_BYTES] {
15 prikk_hash::sha256(bytes)
16}
17
18pub fn validate_text_anchor_id(value: &str) -> Result<()> {
20 if value.is_empty() {
21 return Err(PrikkError::CanonicalEncoding(
22 "text anchor id must not be empty".to_string(),
23 ));
24 }
25 if !value.is_ascii() {
26 return Err(PrikkError::CanonicalEncoding(
27 "text anchor id must be ASCII in v1".to_string(),
28 ));
29 }
30 if value.bytes().any(|byte| byte < 0x21 || byte == 0x7f) {
31 return Err(PrikkError::CanonicalEncoding(
32 "text anchor id must not contain whitespace or control characters".to_string(),
33 ));
34 }
35 Ok(())
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct PatchPayload {
41 pub operations: Vec<Operation>,
43 pub parent_patch_ids: Vec<ObjectId>,
45 pub intent: Option<Intent>,
47 pub preconditions: Vec<OperationConditionEntry>,
49}
50
51impl PatchPayload {
52 pub fn validate(&self) -> Result<()> {
54 let op_seq: Vec<u32> = self.operations.iter().map(|op| op.op_seq).collect();
55 if !is_contiguous_op_seq(&op_seq) {
56 return Err(PrikkError::CanonicalEncoding(
57 "patch operations must have contiguous op_seq values starting at 1".to_string(),
58 ));
59 }
60 if !is_strictly_sorted(&self.parent_patch_ids) {
61 return Err(PrikkError::CanonicalEncoding(
62 "parent_patch_ids must be sorted and unique".to_string(),
63 ));
64 }
65 if !is_strictly_sorted(&self.preconditions) {
66 return Err(PrikkError::CanonicalEncoding(
67 "patch preconditions must be sorted and unique".to_string(),
68 ));
69 }
70 Ok(())
71 }
72}
73
74impl CanonicalEncode for PatchPayload {
75 fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
76 self.validate()?;
77 writer.repeated_record(1, &self.operations)?;
78 writer.repeated_object_id(2, &self.parent_patch_ids)?;
79 if let Some(intent) = self.intent {
80 writer.field_u32(3, u32::from(intent.code()))?;
81 }
82 writer.repeated_record(4, &self.preconditions)?;
83 Ok(())
84 }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct Operation {
90 pub op_seq: u32,
92 pub op_id: Option<String>,
94 pub preconditions: Vec<OperationCondition>,
96 pub kind: OperationKind,
98}
99
100impl CanonicalEncode for Operation {
101 fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
102 writer.field_u32(1, self.op_seq)?;
103 writer.field_string_opt(2, self.op_id.as_deref())?;
104 writer.repeated_record(3, &self.preconditions)?;
105 match &self.kind {
106 OperationKind::CreateFile(value) => writer.field_record(10, value)?,
107 OperationKind::DeleteFile(value) => writer.field_record(11, value)?,
108 OperationKind::EditText(value) => writer.field_record(12, value)?,
109 OperationKind::RenamePath(value) => writer.field_record(13, value)?,
110 OperationKind::ChangePerm(value) => writer.field_record(14, value)?,
111 OperationKind::CreateSymlink(value) => writer.field_record(15, value)?,
112 OperationKind::ReplaceBinary(value) => writer.field_record(16, value)?,
113 }
114 Ok(())
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
120pub enum OperationKind {
121 CreateFile(CreateFile),
123 DeleteFile(DeleteFile),
125 EditText(EditText),
127 RenamePath(RenamePath),
129 ChangePerm(ChangePerm),
131 CreateSymlink(CreateSymlink),
133 ReplaceBinary(ReplaceBinary),
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct CreateFile {
140 pub path: String,
142 pub blob_id: ObjectId,
144 pub mode: u32,
146}
147
148impl CanonicalEncode for CreateFile {
149 fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
150 writer.field_string(1, &self.path)?;
151 writer.field_bytes(2, self.blob_id.as_bytes())?;
152 writer.field_u32(3, self.mode)?;
153 Ok(())
154 }
155}
156
157#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct DeleteFile {
160 pub path: String,
162 pub old_blob_id: ObjectId,
164}
165
166impl CanonicalEncode for DeleteFile {
167 fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
168 writer.field_string(1, &self.path)?;
169 writer.field_bytes(2, self.old_blob_id.as_bytes())?;
170 Ok(())
171 }
172}
173
174#[derive(Debug, Clone, PartialEq, Eq)]
176pub struct EditText {
177 pub path: String,
179 pub anchor_id: String,
184 pub old_span_hash: [u8; TEXT_SPAN_HASH_BYTES],
186 pub replacement: String,
188}
189
190impl EditText {
191 pub fn validate(&self) -> Result<()> {
193 if self.path.is_empty() {
194 return Err(PrikkError::CanonicalEncoding(
195 "EditText path must not be empty".to_string(),
196 ));
197 }
198 validate_text_anchor_id(&self.anchor_id)?;
199 Ok(())
200 }
201}
202
203impl CanonicalEncode for EditText {
204 fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
205 self.validate()?;
206 writer.field_string(1, &self.path)?;
207 writer.field_string(2, &self.anchor_id)?;
208 writer.field_bytes(3, &self.old_span_hash)?;
209 writer.field_string(4, &self.replacement)?;
210 Ok(())
211 }
212}
213
214#[derive(Debug, Clone, PartialEq, Eq)]
216pub struct RenamePath {
217 pub src: String,
219 pub dst: String,
221}
222
223impl CanonicalEncode for RenamePath {
224 fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
225 writer.field_string(1, &self.src)?;
226 writer.field_string(2, &self.dst)?;
227 Ok(())
228 }
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct ChangePerm {
234 pub path: String,
236 pub old_mode: u32,
238 pub new_mode: u32,
240}
241
242impl CanonicalEncode for ChangePerm {
243 fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
244 writer.field_string(1, &self.path)?;
245 writer.field_u32(2, self.old_mode)?;
246 writer.field_u32(3, self.new_mode)?;
247 Ok(())
248 }
249}
250
251#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct CreateSymlink {
254 pub path: String,
256 pub target: String,
258}
259
260impl CanonicalEncode for CreateSymlink {
261 fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
262 writer.field_string(1, &self.path)?;
263 writer.field_string(2, &self.target)?;
264 Ok(())
265 }
266}
267
268#[derive(Debug, Clone, PartialEq, Eq)]
270pub struct ReplaceBinary {
271 pub path: String,
273 pub old_blob_id: ObjectId,
275 pub new_blob_id: ObjectId,
277}
278
279impl CanonicalEncode for ReplaceBinary {
280 fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
281 writer.field_string(1, &self.path)?;
282 writer.field_bytes(2, self.old_blob_id.as_bytes())?;
283 writer.field_bytes(3, self.new_blob_id.as_bytes())?;
284 Ok(())
285 }
286}