Skip to main content

p2panda_rs/schema/validate/
blob.rs

1// SPDX-License-Identifier: AGPL-&3.0-or-later
2
3use once_cell::sync::Lazy;
4use regex::Regex;
5
6use crate::hash::Hash;
7use crate::operation::plain::{PlainFields, PlainValue};
8use crate::schema::validate::error::BlobError;
9
10/// Checks "mime_type" field of operations with "blob_v1" schema id.
11///
12/// 1. It matches expected mime type format
13pub fn validate_mime_type(value: &str) -> bool {
14    static NAME_REGEX: Lazy<Regex> = Lazy::new(|| {
15        // Unwrap as we checked the regular expression for correctness
16        Regex::new("^[a-z-]{1,12}\\/(([a-z0-9]{1,18})[\\.|+|-]){0,6}[a-z0-9]{1,16}$").unwrap()
17    });
18
19    NAME_REGEX.is_match(value)
20}
21
22/// Checks "pieces" field of operations with "blob_v1" schema id.
23///
24/// 1. It is not empty
25pub fn validate_pieces(value: &[Vec<Hash>]) -> bool {
26    !value.is_empty()
27}
28
29/// Validate formatting for operations following `blob_v1` system schemas.
30///
31/// These operations contain a "length", "mime_type" and "pieces" field some of which have special
32/// limitations defined by the p2panda specification.
33///
34/// Please note that this does not check type field type or the operation fields in general, as
35/// this should be handled by other validation methods. This method is only checking the special
36/// requirements of this particular system schema.
37pub fn validate_blob_v1_fields(fields: &PlainFields) -> Result<(), BlobError> {
38    // `length` field doesn't have any special requirements.
39
40    // Check "mime_type" field
41    match fields.get("mime_type") {
42        Some(PlainValue::String(value)) => {
43            if validate_mime_type(value) {
44                Ok(())
45            } else {
46                Err(BlobError::MimeTypeInvalid)
47            }
48        }
49        _ => Ok(()),
50    }?;
51
52    // Check "pieces" field
53    match fields.get("pieces") {
54        Some(PlainValue::PinnedRelationList(value)) => {
55            if validate_pieces(value) {
56                Ok(())
57            } else {
58                Err(BlobError::PiecesEmpty)
59            }
60        }
61        _ => Ok(()),
62    }?;
63
64    Ok(())
65}
66
67#[cfg(test)]
68mod test {
69    use rstest::rstest;
70
71    use crate::document::DocumentViewId;
72    use crate::operation::plain::PlainFields;
73    use crate::test_utils::fixtures::random_document_view_id;
74
75    use super::{validate_blob_v1_fields, validate_mime_type};
76
77    #[rstest]
78    #[case(vec![
79       ("length", 1.into()),
80       ("mime_type", "image/png".into()),
81       ("pieces", vec![random_document_view_id()].into()),
82    ].into())]
83    #[case(vec![
84        ("length", 1000.into()),
85        ("mime_type", "application/x-zip-compressed".into()),
86        ("pieces", vec![random_document_view_id()].into()),
87     ].into())]
88    #[should_panic]
89    #[case::invalid_mime_type(vec![
90        ("length", 100.into()),
91        ("mime_type", "not a mime type".into()),
92        ("pieces", vec![random_document_view_id()].into()),
93     ].into())]
94    #[should_panic]
95    #[case::empty_pieces(vec![
96       ("length", 1.into()),
97       ("mime_type", "image/png".into()),
98       ("pieces", Vec::<DocumentViewId>::new().into()),
99    ].into())]
100    fn check_fields(#[case] fields: PlainFields) {
101        assert!(validate_blob_v1_fields(&fields).is_ok());
102    }
103
104    #[rstest]
105    #[case("video/webm")]
106    #[case("image/webp")]
107    #[case("x-conference/x-cooltalk")]
108    #[case("application/vnd.cluetrust.cartomobile-config-pkg")]
109    #[case("application/emma+xml")]
110    #[case("my/made.up.mime.type")] // This still passes....
111    #[should_panic]
112    #[case("wrong format")]
113    #[should_panic]
114    #[case("wrong_format")]
115    #[should_panic]
116    #[case("wrong/f o r m a t")]
117    #[should_panic]
118    #[case("wrong/!format!")]
119    #[should_panic]
120    #[case("wrong/for..mat")]
121    #[should_panic]
122    #[case("wro.ng/for..mat")]
123    #[should_panic]
124    #[case("this/mime.type.has.one.too.many.elements.yes")]
125    #[should_panic]
126    #[case("this/mime.type.hasoneelementwhichiswaytoolong")]
127    #[should_panic]
128    #[case("thismimetypealsohasonelongelement/one.element.too.long")]
129    fn check_mime_type_field(#[case] name_str: &str) {
130        assert!(validate_mime_type(name_str));
131    }
132}