1use crate::data_contract::config::moderation::ContractModerationReason;
2use crate::data_contract::document_type::accessors::{
3 DocumentTypeV0Getters, DocumentTypeV2Getters,
4};
5use crate::data_contract::document_type::property_constraints::SystemProperty;
6use crate::data_contract::document_type::{
7 property_at_path, DocumentPropertyType, DocumentTypeRef,
8};
9use crate::data_contract::errors::DataContractError;
10use crate::document::{Document, DocumentV0Getters};
11use crate::identity::TimestampMillis;
12#[cfg(feature = "serde-conversion")]
13use crate::serialization::serde_bytes_var;
14use crate::ProtocolError;
15use bincode::{Decode, DecodeUntrusted, Encode};
16use byteorder::{BigEndian, ReadBytesExt};
17use platform_value::{Identifier, Value};
18use serde::{Deserialize, Serialize};
19use std::collections::{BTreeMap, BTreeSet};
20use std::io::{BufReader, Read};
21
22#[derive(
34 Debug, Clone, PartialEq, Eq, Default, Encode, Decode, DecodeUntrusted, Serialize, Deserialize,
35)]
36#[serde(rename_all = "camelCase")]
37pub struct ContractDocumentRemoval {
38 pub document_owner_id: Identifier,
40 pub moderator_id: Identifier,
42 pub reason: ContractModerationReason,
45 pub removed_at: TimestampMillis,
47 pub document_hash: [u8; 32],
51 pub restoration: Option<ContractDocumentRestoration>,
53 #[serde(default, skip_serializing_if = "Vec::is_empty")]
59 #[cfg_attr(feature = "serde-conversion", serde(with = "serde_bytes_var"))]
60 pub kept_fields: Vec<u8>,
61}
62
63#[derive(
65 Debug, Clone, PartialEq, Eq, Default, Encode, Decode, DecodeUntrusted, Serialize, Deserialize,
66)]
67#[serde(rename_all = "camelCase")]
68pub struct ContractDocumentRestoration {
69 pub moderator_id: Identifier,
71 pub restored_at: TimestampMillis,
73}
74
75const KEPT_VALUE_ABSENT: u8 = 0;
77const KEPT_VALUE_PRESENT: u8 = 1;
79
80pub fn encode_kept_fields(
89 document: &Document,
90 document_type: DocumentTypeRef,
91) -> Result<Vec<u8>, ProtocolError> {
92 let mut encoded = Vec::new();
93 for path in document_type.moderator_deletion_kept_fields() {
94 let value = match SystemProperty::from_name(path) {
95 Some(property) => system_property_bytes(document, property),
96 None => {
97 let property = kept_property_type(&document_type, path)?;
98 match document.get(path).filter(|value| !value.is_null()) {
99 Some(value) => Some(property.encode_value_ref_with_size(value, false)?),
100 None => None,
101 }
102 }
103 };
104 match value {
105 Some(value) => {
106 encoded.push(KEPT_VALUE_PRESENT);
107 encoded.extend(value);
108 }
109 None => encoded.push(KEPT_VALUE_ABSENT),
110 }
111 }
112 Ok(encoded)
113}
114
115pub fn decode_kept_fields(
119 encoded: &[u8],
120 document_type: DocumentTypeRef,
121) -> Result<BTreeMap<String, Value>, ProtocolError> {
122 let corrupted = |message: String| {
123 ProtocolError::DataContractError(DataContractError::CorruptedSerialization(message))
124 };
125 let mut buf = BufReader::new(encoded);
126 let mut values = BTreeMap::new();
127 for path in document_type.moderator_deletion_kept_fields() {
128 let value = match SystemProperty::from_name(path) {
129 Some(property) => {
130 let marker = buf
131 .read_u8()
132 .map_err(|_| corrupted(format!("kept fields end before \"{path}\"")))?;
133 match marker {
134 KEPT_VALUE_ABSENT => None,
135 KEPT_VALUE_PRESENT => Some(
136 read_system_property(&mut buf, property)
137 .map_err(|_| corrupted(format!("kept fields end inside \"{path}\"")))?,
138 ),
139 marker => {
140 return Err(corrupted(format!(
141 "kept field \"{path}\" has unknown marker {marker}"
142 )))
143 }
144 }
145 }
146 None => {
147 let (value, finished) = kept_property_type(&document_type, path)?
148 .read_optionally_from(&mut buf, false)
149 .map_err(ProtocolError::DataContractError)?;
150 if finished {
151 return Err(corrupted(format!("kept fields end before \"{path}\"")));
152 }
153 value
154 }
155 };
156 if let Some(value) = value {
157 values.insert(path.clone(), value);
158 }
159 }
160 let mut trailing = [0u8; 1];
161 if buf
162 .read(&mut trailing)
163 .map_err(|error| corrupted(error.to_string()))?
164 > 0
165 {
166 return Err(corrupted(
167 "kept fields run past the last path their type keeps".to_string(),
168 ));
169 }
170 Ok(values)
171}
172
173fn kept_property_type<'a>(
175 document_type: &'a DocumentTypeRef,
176 path: &str,
177) -> Result<&'a DocumentPropertyType, ProtocolError> {
178 property_at_path(document_type.properties(), path)
179 .map(|property| &property.property_type)
180 .ok_or_else(|| {
181 ProtocolError::CorruptedCodeExecution(format!(
182 "document type {} keeps \"{path}\", which it does not declare",
183 document_type.name()
184 ))
185 })
186}
187
188fn system_property_bytes(document: &Document, property: SystemProperty) -> Option<Vec<u8>> {
190 let long = |value: Option<u64>| value.map(|value| value.to_be_bytes().to_vec());
191 let short = |value: Option<u32>| value.map(|value| value.to_be_bytes().to_vec());
192 match property {
193 SystemProperty::CreatedAt => long(document.created_at()),
194 SystemProperty::UpdatedAt => long(document.updated_at()),
195 SystemProperty::TransferredAt => long(document.transferred_at()),
196 SystemProperty::CreatedAtBlockHeight => long(document.created_at_block_height()),
197 SystemProperty::UpdatedAtBlockHeight => long(document.updated_at_block_height()),
198 SystemProperty::TransferredAtBlockHeight => long(document.transferred_at_block_height()),
199 SystemProperty::CreatedAtCoreBlockHeight => short(document.created_at_core_block_height()),
200 SystemProperty::UpdatedAtCoreBlockHeight => short(document.updated_at_core_block_height()),
201 SystemProperty::TransferredAtCoreBlockHeight => {
202 short(document.transferred_at_core_block_height())
203 }
204 }
205}
206
207fn read_system_property(
210 buf: &mut BufReader<&[u8]>,
211 property: SystemProperty,
212) -> std::io::Result<Value> {
213 match property {
214 SystemProperty::CreatedAtCoreBlockHeight
215 | SystemProperty::UpdatedAtCoreBlockHeight
216 | SystemProperty::TransferredAtCoreBlockHeight => {
217 buf.read_u32::<BigEndian>().map(Value::U32)
218 }
219 SystemProperty::CreatedAt
220 | SystemProperty::UpdatedAt
221 | SystemProperty::TransferredAt
222 | SystemProperty::CreatedAtBlockHeight
223 | SystemProperty::UpdatedAtBlockHeight
224 | SystemProperty::TransferredAtBlockHeight => buf.read_u64::<BigEndian>().map(Value::U64),
225 }
226}
227
228pub fn kept_value_at(
234 kept_paths: &BTreeSet<String>,
235 kept_values: &BTreeMap<String, Value>,
236 property: &str,
237) -> Option<Option<Value>> {
238 if kept_paths.contains(property) {
239 return Some(kept_values.get(property).cloned());
240 }
241 let (kept, inner) = kept_paths.iter().find_map(|kept| {
242 let inner = property.strip_prefix(kept.as_str())?.strip_prefix('.')?;
243 Some((kept, inner))
244 })?;
245 Some(
248 kept_values
249 .get(kept)
250 .and_then(|object| object.get_optional_value_at_path(inner).ok().flatten())
251 .filter(|value| !value.is_null())
252 .cloned(),
253 )
254}
255
256impl ContractDocumentRemoval {
257 pub fn is_restored(&self) -> bool {
260 self.restoration.is_some()
261 }
262
263 pub fn kept_values(
267 &self,
268 document_type: DocumentTypeRef,
269 ) -> Result<BTreeMap<String, Value>, ProtocolError> {
270 if self.kept_fields.is_empty() {
271 return Ok(BTreeMap::new());
272 }
273 decode_kept_fields(&self.kept_fields, document_type)
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use crate::data_contract::config::moderation::{ContractModerationConfig, ContractModerators};
281 use crate::data_contract::config::DataContractConfig;
282 use crate::data_contract::document_type::{is_path_listed, DocumentType};
283 use crate::document::DocumentV0;
284 use platform_value::platform_value;
285 use platform_version::version::PlatformVersion;
286
287 fn post_type() -> DocumentType {
290 let platform_version = PlatformVersion::latest();
291 let config = DataContractConfig::default_for_version(platform_version)
292 .expect("expected a default config")
293 .with_moderation(Some(ContractModerationConfig {
294 banlist: false,
295 suspensions: false,
296 moderators: ContractModerators::ContractOwner,
297 warnings: false,
298 }));
299 DocumentType::try_from_schema(
300 Identifier::new([1; 32]),
301 1,
302 config.version(),
303 "post",
304 platform_value!({
305 "type": "object",
306 "properties": {
307 "text": { "type": "string", "maxLength": 50, "position": 0 },
308 "hashtag": { "type": "string", "maxLength": 61, "position": 1 },
309 "score": { "type": "integer", "position": 2 },
310 "author": {
311 "type": "array",
312 "byteArray": true,
313 "minItems": 32,
314 "maxItems": 32,
315 "contentMediaType": "application/x.dash.dpp.identifier",
316 "position": 3,
317 },
318 "meta": {
319 "type": "object",
320 "position": 4,
321 "properties": {
322 "tags": {
323 "type": "array",
324 "items": { "type": "string", "maxLength": 20 },
325 "maxItems": 5,
326 "position": 0,
327 },
328 "note": { "type": "string", "maxLength": 20, "position": 1 },
329 },
330 "additionalProperties": false,
331 },
332 "extra": {
333 "type": "object",
334 "position": 5,
335 "properties": {
336 "count": { "type": "integer", "position": 0 },
337 "label": { "type": "string", "maxLength": 20, "position": 1 },
338 },
339 "additionalProperties": false,
340 },
341 },
342 "required": ["$createdAt", "$createdAtCoreBlockHeight"],
343 "additionalProperties": false,
344 "moderatorAbilities": {
345 "delete": true,
346 "deleteKeepsFields": [
347 "$createdAt",
348 "$createdAtCoreBlockHeight",
349 "author",
350 "extra.count",
351 "hashtag",
352 "meta",
353 "score",
354 "text",
355 ],
356 },
357 }),
358 None,
359 &BTreeMap::new(),
360 &config,
361 true,
362 &mut vec![],
363 platform_version,
364 )
365 .expect("expected the post type")
366 }
367
368 fn post(properties: BTreeMap<String, Value>) -> Document {
369 Document::V0(DocumentV0 {
370 id: Identifier::new([2; 32]),
371 owner_id: Identifier::new([3; 32]),
372 properties,
373 created_at: Some(1_700_000_000_000),
374 created_at_core_block_height: Some(55),
375 ..Default::default()
376 })
377 }
378
379 #[test]
380 fn should_keep_values_as_the_document_holds_them() {
381 let document_type = post_type();
382 let meta = Value::Map(vec![
383 (
384 Value::Text("tags".to_string()),
385 Value::Array(vec![
386 Value::Text("privacy".to_string()),
387 Value::Text("payments".to_string()),
388 ]),
389 ),
390 (
391 Value::Text("note".to_string()),
392 Value::Text("n".to_string()),
393 ),
394 ]);
395 let document = post(BTreeMap::from([
396 ("hashtag".to_string(), Value::Text("dash".to_string())),
397 ("score".to_string(), Value::I64(-3)),
398 ("author".to_string(), Value::Identifier([4; 32])),
399 ("meta".to_string(), meta.clone()),
400 (
401 "extra".to_string(),
402 Value::Map(vec![
403 (Value::Text("count".to_string()), Value::I64(7)),
404 (
405 Value::Text("label".to_string()),
406 Value::Text("not kept".to_string()),
407 ),
408 ]),
409 ),
410 ]));
411
412 let encoded =
413 encode_kept_fields(&document, document_type.as_ref()).expect("expected to encode");
414 assert_eq!(encoded[0], KEPT_VALUE_PRESENT);
416 assert_eq!(&encoded[1..9], &1_700_000_000_000u64.to_be_bytes());
417 assert_eq!(encoded.last(), Some(&KEPT_VALUE_ABSENT));
419
420 let removal = ContractDocumentRemoval {
421 kept_fields: encoded,
422 ..Default::default()
423 };
424 assert_eq!(
425 removal
426 .kept_values(document_type.as_ref())
427 .expect("expected to decode"),
428 BTreeMap::from([
429 ("$createdAt".to_string(), Value::U64(1_700_000_000_000)),
430 ("$createdAtCoreBlockHeight".to_string(), Value::U32(55)),
431 ("author".to_string(), Value::Identifier([4; 32])),
432 ("extra.count".to_string(), Value::I64(7)),
433 ("hashtag".to_string(), Value::Text("dash".to_string())),
434 ("meta".to_string(), meta),
435 ("score".to_string(), Value::I64(-3)),
436 ])
437 );
438 }
439
440 #[test]
441 fn should_read_a_kept_path_or_a_path_inside_a_kept_object() {
442 let document_type = post_type();
443 let kept_paths = document_type.moderator_deletion_kept_fields();
444 let document = post(BTreeMap::from([
445 ("hashtag".to_string(), Value::Text("dash".to_string())),
446 (
447 "meta".to_string(),
448 Value::Map(vec![(
449 Value::Text("tags".to_string()),
450 Value::Array(vec![Value::Text("privacy".to_string())]),
451 )]),
452 ),
453 (
454 "extra".to_string(),
455 Value::Map(vec![(Value::Text("count".to_string()), Value::I64(7))]),
456 ),
457 ]));
458 let removal = ContractDocumentRemoval {
459 kept_fields: encode_kept_fields(&document, document_type.as_ref())
460 .expect("expected to encode"),
461 ..Default::default()
462 };
463 let kept_values = removal
464 .kept_values(document_type.as_ref())
465 .expect("expected to decode");
466 for path in ["hashtag", "meta.tags", "extra.count"] {
468 let value = document.get(path).cloned();
469 assert!(value.is_some(), "{path}");
470 assert_eq!(
471 kept_value_at(kept_paths, &kept_values, path),
472 Some(value),
473 "{path}"
474 );
475 }
476 for path in ["meta.note", "text"] {
478 assert_eq!(
479 kept_value_at(kept_paths, &kept_values, path),
480 Some(None),
481 "{path}"
482 );
483 }
484 for path in ["extra.label", "extra", "hashtagged", "$ownerId"] {
487 assert!(!is_path_listed(kept_paths, path), "{path}");
488 assert_eq!(
489 kept_value_at(kept_paths, &kept_values, path),
490 None,
491 "{path}"
492 );
493 }
494 }
495
496 #[test]
497 fn should_keep_nothing_of_a_document_that_holds_none_of_the_paths() {
498 let document_type = post_type();
499 let document = Document::V0(DocumentV0 {
500 id: Identifier::new([2; 32]),
501 owner_id: Identifier::new([3; 32]),
502 ..Default::default()
503 });
504 let encoded =
505 encode_kept_fields(&document, document_type.as_ref()).expect("expected to encode");
506 assert_eq!(encoded, vec![KEPT_VALUE_ABSENT; 8]);
507 assert_eq!(
508 decode_kept_fields(&encoded, document_type.as_ref()).expect("expected to decode"),
509 BTreeMap::new()
510 );
511 }
512
513 #[test]
514 fn should_refuse_kept_fields_cut_short_or_running_past_the_last_path() {
515 let document_type = post_type();
516 let document = post(BTreeMap::from([(
517 "hashtag".to_string(),
518 Value::Text("dash".to_string()),
519 )]));
520 let encoded =
521 encode_kept_fields(&document, document_type.as_ref()).expect("expected to encode");
522 decode_kept_fields(&encoded[..encoded.len() - 1], document_type.as_ref())
523 .expect_err("the last path's marker is missing");
524 decode_kept_fields(&encoded[..3], document_type.as_ref())
525 .expect_err("cut short inside a time");
526 let mut trailing = encoded.clone();
527 trailing.push(0);
528 decode_kept_fields(&trailing, document_type.as_ref()).expect_err("a byte past the end");
529 let mut unknown_marker = encoded;
530 unknown_marker[0] = 7;
531 decode_kept_fields(&unknown_marker, document_type.as_ref()).expect_err("an unknown marker");
532 }
533}