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, 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
51pub const PATCH_PARENT_IDS_RETIRED_SCHEMA: u32 = 2;
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct PatchPayload {
64 pub operations: Vec<Operation>,
66 pub intent: Option<Intent>,
68 pub preconditions: Vec<OperationConditionEntry>,
70 pub purpose: PatchPurpose,
72}
73
74impl PatchPayload {
75 pub fn validate(&self) -> Result<()> {
77 if self.operations.is_empty() {
78 return Err(PrikkError::CanonicalEncoding(
79 "patch operations must contain at least one operation".to_string(),
80 ));
81 }
82 let op_seq: Vec<u32> = self.operations.iter().map(|op| op.op_seq).collect();
83 if !is_contiguous_op_seq(&op_seq) {
84 return Err(PrikkError::CanonicalEncoding(
85 "patch operations must have contiguous op_seq values starting at 1".to_string(),
86 ));
87 }
88 if !is_strictly_sorted(&self.preconditions) {
89 return Err(PrikkError::CanonicalEncoding(
90 "patch preconditions must be sorted and unique".to_string(),
91 ));
92 }
93 Ok(())
94 }
95}
96
97impl CanonicalEncode for PatchPayload {
98 fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
99 self.validate()?;
100 writer.repeated_record_list(1, &self.operations)?;
101 if let Some(intent) = self.intent {
105 writer.field_enum_u16(3, intent.code())?;
106 }
107 writer.repeated_record(4, &self.preconditions)?;
108 if self.purpose != PatchPurpose::Normal {
109 writer.field_enum_u16(5, self.purpose.code())?;
110 }
111 Ok(())
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
117#[repr(u16)]
118pub enum PatchPurpose {
119 Normal = 1,
121 RollbackDraft = 2,
123}
124
125impl PatchPurpose {
126 #[must_use]
128 pub const fn code(self) -> u16 {
129 self as u16
130 }
131
132 pub fn from_present_code(code: u16) -> Result<Self> {
134 match code {
135 1 => Err(PrikkError::CanonicalEncoding(
136 "PatchPurpose::Normal must be omitted, not encoded explicitly".to_string(),
137 )),
138 2 => Ok(Self::RollbackDraft),
139 other => Err(PrikkError::CanonicalEncoding(format!(
140 "unknown patch purpose code: {other}"
141 ))),
142 }
143 }
144
145 pub fn decode_from_patch_payload(bytes: &[u8]) -> Result<Self> {
148 let mut cursor = PatchPayloadFieldCursor::new(bytes);
149 let mut purpose = Self::Normal;
150 let mut seen_purpose = false;
151 while let Some(field) = cursor.next_field()? {
152 match field.tag {
153 1..=4 => {}
154 5 => {
155 if seen_purpose {
156 return Err(PrikkError::CanonicalEncoding(
157 "duplicate PatchPurpose field".to_string(),
158 ));
159 }
160 seen_purpose = true;
161 field.require_wire(WireType::EnumU16)?;
162 purpose = Self::from_present_code(field.read_u16()?)?;
163 }
164 other => {
165 return Err(PrikkError::CanonicalEncoding(format!(
166 "unknown PatchPayload field tag: {other}"
167 )));
168 }
169 }
170 }
171 Ok(purpose)
172 }
173}
174
175struct PatchPayloadFieldCursor<'a> {
176 bytes: &'a [u8],
177 pos: usize,
178 last_tag: Option<u16>,
179}
180
181impl<'a> PatchPayloadFieldCursor<'a> {
182 const fn new(bytes: &'a [u8]) -> Self {
183 Self {
184 bytes,
185 pos: 0,
186 last_tag: None,
187 }
188 }
189
190 fn next_field(&mut self) -> Result<Option<PatchPayloadField<'a>>> {
191 if self.pos == self.bytes.len() {
192 return Ok(None);
193 }
194 let tag = u16::from_be_bytes(self.read_array::<2>()?);
195 if tag == 0 {
196 return Err(PrikkError::CanonicalEncoding(
197 "field tag 0 is reserved".to_string(),
198 ));
199 }
200 if let Some(last) = self.last_tag {
201 if tag < last {
202 return Err(PrikkError::CanonicalEncoding(format!(
203 "field tag order violation: {tag} after {last}"
204 )));
205 }
206 }
207 self.last_tag = Some(tag);
208 let wire_type = self.read_u8()?;
209 let len = usize::try_from(u64::from_be_bytes(self.read_array::<8>()?)).map_err(|_| {
210 PrikkError::CanonicalEncoding("canonical field length does not fit usize".to_string())
211 })?;
212 let value = self.read_exact(len)?;
213 Ok(Some(PatchPayloadField {
214 tag,
215 wire_type,
216 value,
217 }))
218 }
219
220 fn read_u8(&mut self) -> Result<u8> {
221 let bytes = self.read_exact(1)?;
222 let Some(byte) = bytes.first() else {
223 return Err(PrikkError::CanonicalEncoding(
224 "unexpected empty byte".to_string(),
225 ));
226 };
227 Ok(*byte)
228 }
229
230 fn read_array<const N: usize>(&mut self) -> Result<[u8; N]> {
231 let bytes = self.read_exact(N)?;
232 let mut out = [0_u8; N];
233 out.copy_from_slice(bytes);
234 Ok(out)
235 }
236
237 fn read_exact(&mut self, len: usize) -> Result<&'a [u8]> {
238 let end = self
239 .pos
240 .checked_add(len)
241 .ok_or_else(|| PrikkError::CanonicalEncoding("canonical range overflow".to_string()))?;
242 let Some(slice) = self.bytes.get(self.pos..end) else {
243 return Err(PrikkError::CanonicalEncoding(
244 "unexpected end of canonical payload".to_string(),
245 ));
246 };
247 self.pos = end;
248 Ok(slice)
249 }
250}
251
252struct PatchPayloadField<'a> {
253 tag: u16,
254 wire_type: u8,
255 value: &'a [u8],
256}
257
258impl PatchPayloadField<'_> {
259 fn require_wire(&self, expected: WireType) -> Result<()> {
260 if self.wire_type == expected as u8 {
261 return Ok(());
262 }
263 Err(PrikkError::CanonicalEncoding(format!(
264 "field {} has wrong wire type: expected {}, got {}",
265 self.tag, expected as u8, self.wire_type
266 )))
267 }
268
269 fn read_u16(&self) -> Result<u16> {
270 if self.value.len() != 2 {
271 return Err(PrikkError::CanonicalEncoding(format!(
272 "field {} expected 2 bytes, got {}",
273 self.tag,
274 self.value.len()
275 )));
276 }
277 let mut out = [0_u8; 2];
278 out.copy_from_slice(self.value);
279 Ok(u16::from_be_bytes(out))
280 }
281}
282
283#[derive(Debug, Clone, PartialEq, Eq)]
285pub struct Operation {
286 pub op_seq: u32,
288 pub op_id: Option<String>,
290 pub preconditions: Vec<OperationCondition>,
292 pub kind: OperationKind,
294}
295
296impl CanonicalEncode for Operation {
297 fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
298 writer.field_u32(1, self.op_seq)?;
299 writer.field_string_opt(2, self.op_id.as_deref())?;
300 writer.repeated_record(3, &self.preconditions)?;
301 match &self.kind {
302 OperationKind::CreateFile(value) => writer.field_record(10, value)?,
303 OperationKind::DeleteNode(value) => writer.field_record(11, value)?,
304 OperationKind::EditText(value) => writer.field_record(12, value)?,
305 OperationKind::RenamePath(value) => writer.field_record(13, value)?,
306 OperationKind::ChangePerm(value) => writer.field_record(14, value)?,
307 OperationKind::CreateSymlink(value) => writer.field_record(15, value)?,
308 OperationKind::ReplaceBinary(value) => writer.field_record(16, value)?,
309 }
310 Ok(())
311 }
312}
313
314#[derive(Debug, Clone, PartialEq, Eq)]
316pub enum OperationKind {
317 CreateFile(CreateFile),
319 DeleteNode(DeleteNode),
321 EditText(EditText),
323 RenamePath(RenamePath),
325 ChangePerm(ChangePerm),
327 CreateSymlink(CreateSymlink),
329 ReplaceBinary(ReplaceBinary),
331}