mavryk_smart_rollup_encoding/
entrypoint.rs

1// SPDX-FileCopyrightText: 2022 TriliTech <contact@trili.tech>
2//
3// SPDX-License-Identifier: MIT
4
5//! Smart contract entrypoint.
6//!
7//! Port of the *simple encoding* scheme from [entrypoint_repr.ml].
8//!
9//! [entrypoint_repr.ml]: <https://gitlab.com/tezos/tezos/-/blob/80b2cccb9c663dde2d86a6c94806fc149b7d1ef3/src/proto_alpha/lib_protocol/entrypoint_repr.ml>
10
11use mavryk_data_encoding::enc::{self, BinWriter};
12use mavryk_data_encoding::encoding::{Encoding, HasEncoding};
13use mavryk_data_encoding::has_encoding;
14use mavryk_data_encoding::nom::{bounded_dynamic, NomReader};
15use nom::branch;
16use nom::bytes::complete::{take_while, take_while1};
17use nom::combinator::{self, map, map_res, recognize};
18use nom::sequence::pair;
19
20/// The entrypoint of a smart contract.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct Entrypoint {
23    name: String,
24}
25
26impl Entrypoint {
27    const MAX_LEN: usize = 31;
28    const DEFAULT: &'static str = "default";
29
30    /// Entrypoint name, 1-31 bytes long.
31    pub fn name(&self) -> &str {
32        self.name.as_str()
33    }
34}
35
36impl Default for Entrypoint {
37    fn default() -> Self {
38        Self {
39            name: String::from(Self::DEFAULT),
40        }
41    }
42}
43
44/// Possible errors when creating entrypoints.
45#[derive(Debug, PartialEq, Eq)]
46pub enum EntrypointError {
47    /// The maximum size of an entrpoint is 31 bytes.
48    TooLarge(String),
49    /// Entrypoint must match with `([A-Za-z0-9_][A-Za-z0-9_.%@]*)?`.
50    InvalidChars(String),
51}
52
53impl TryFrom<String> for Entrypoint {
54    type Error = EntrypointError;
55
56    fn try_from(name: String) -> Result<Self, Self::Error> {
57        if name.is_empty() {
58            return Ok(Self::default());
59        } else if name.len() > Self::MAX_LEN {
60            return Err(EntrypointError::TooLarge(name));
61        };
62
63        let first_char_valid = match name.as_bytes()[0] {
64            b'_' => true,
65            c => c.is_ascii_alphanumeric(),
66        };
67
68        if first_char_valid
69            && name[1..].bytes().all(|c: u8| {
70                c.is_ascii_alphanumeric() || matches!(c, b'_' | b'.' | b'%' | b'@')
71            })
72        {
73            Ok(Entrypoint { name })
74        } else {
75            Err(EntrypointError::InvalidChars(name))
76        }
77    }
78}
79
80has_encoding!(Entrypoint, ENTRYPOINT_SIMPLE_ENCODING, { Encoding::Custom });
81
82impl NomReader for Entrypoint {
83    fn nom_read(input: &[u8]) -> mavryk_data_encoding::nom::NomResult<Self> {
84        map(
85            // Can't use mavryk_data_encoding 'bounded_string' here, as we have an
86            // additional constraint on the string, that need to match entrypoint encoding
87            map_res(
88                bounded_dynamic(
89                    Self::MAX_LEN,
90                    branch::alt((
91                        combinator::eof,
92                        recognize(pair(
93                            take_while1(|byte: u8| {
94                                byte.is_ascii_alphanumeric() || byte == b'_'
95                            }),
96                            take_while(|byte: u8| match byte {
97                                b'_' | b'.' | b'@' | b'%' => true,
98                                b => b.is_ascii_alphanumeric(),
99                            }),
100                        )),
101                    )),
102                ),
103                |bytes| alloc::str::from_utf8(bytes).map(str::to_string),
104            ),
105            |name| {
106                if name.is_empty() {
107                    Self::default()
108                } else {
109                    Self { name }
110                }
111            },
112        )(input)
113    }
114}
115
116impl BinWriter for Entrypoint {
117    fn bin_write(&self, output: &mut Vec<u8>) -> mavryk_data_encoding::enc::BinResult {
118        enc::bounded_string(Self::MAX_LEN)(&self.name, output)
119    }
120}
121
122#[cfg(feature = "testing")]
123mod testing {
124    use super::*;
125    use proptest::prelude::*;
126    use proptest::string::string_regex;
127
128    impl Entrypoint {
129        /// Generate an arbitrary entrypoint
130        pub fn arb() -> BoxedStrategy<Entrypoint> {
131            string_regex("([A-Za-z0-9_][A-Za-z0-9._%@]*)?")
132                .unwrap()
133                .prop_map(|mut s| {
134                    s.truncate(Entrypoint::MAX_LEN);
135                    Entrypoint::try_from(s).unwrap()
136                })
137                .boxed()
138        }
139    }
140}
141
142#[cfg(test)]
143mod test {
144    use super::*;
145    use proptest::prelude::*;
146
147    #[test]
148    fn default_entrypoint() {
149        let default = Entrypoint::default();
150        assert_eq!("default", default.name());
151        assert_eq!(
152            default,
153            Entrypoint {
154                name: "default".into()
155            }
156        );
157        assert_eq!(default, Entrypoint::try_from("".to_string()).unwrap());
158        assert_eq!(
159            default,
160            Entrypoint::try_from("default".to_string()).unwrap()
161        );
162
163        let mut bin = Vec::new();
164        default.bin_write(&mut bin).unwrap();
165
166        assert_eq!(
167            vec![0, 0, 0, 7, b'd', b'e', b'f', b'a', b'u', b'l', b't'],
168            bin
169        );
170
171        let parsed = Ok(([].as_slice(), default));
172        assert_eq!(parsed, Entrypoint::nom_read(bin.as_slice()));
173
174        assert_eq!(
175            parsed,
176            Entrypoint::nom_read(
177                [0, 0, 0, 7, b'd', b'e', b'f', b'a', b'u', b'l', b't'].as_slice()
178            )
179        );
180    }
181
182    #[test]
183    fn encode_decode_non_default() {
184        let entrypoint = Entrypoint::try_from("an_entrypoint".to_string()).unwrap();
185
186        let mut bin = Vec::new();
187        entrypoint
188            .bin_write(&mut bin)
189            .expect("serialization should work");
190
191        let (remaining, deserde) =
192            Entrypoint::nom_read(bin.as_slice()).expect("deserialization should work");
193
194        assert!(remaining.is_empty());
195
196        assert_eq!(entrypoint, deserde);
197    }
198
199    #[test]
200    fn too_large_entrypoint() {
201        let is_ok = vec![b'E'; 31];
202        let large = vec![b'E'; 32];
203
204        let ok_name = String::from_utf8(is_ok).unwrap();
205        let large_name = String::from_utf8(large).unwrap();
206
207        assert!(Entrypoint::try_from(ok_name).is_ok());
208        assert_eq!(
209            Err(EntrypointError::TooLarge(large_name.clone())),
210            Entrypoint::try_from(large_name)
211        );
212
213        let mut is_ok = vec![0, 0, 0, 31];
214        let mut large = vec![0, 0, 0, 32];
215
216        is_ok.append(vec![b'A'; 31].as_mut());
217        large.append(vec![b'A'; 32].as_mut());
218
219        assert!(Entrypoint::nom_read(is_ok.as_slice()).is_ok());
220        assert!(Entrypoint::nom_read(large.as_slice()).is_err());
221    }
222
223    #[test]
224    fn non_valid_entrypoint() {
225        let invalid_name = String::from("a-");
226
227        assert_eq!(
228            Err(EntrypointError::InvalidChars(invalid_name.clone())),
229            Entrypoint::try_from(invalid_name)
230        );
231
232        let invalid = vec![0, 0, 0, 4, 0xe2, 0x8d, 0xa8, b'a'];
233
234        assert!(Entrypoint::nom_read(invalid.as_slice()).is_err());
235    }
236
237    proptest! {
238        #[test]
239        fn encode_decode_valid_entrypoint(entrypoint in Entrypoint::arb(),
240                                          remaining_input in any::<Vec<u8>>()) {
241            let mut encoded = Vec::new();
242            entrypoint.bin_write(&mut encoded).expect("encoding entrypoint should work");
243            encoded.extend_from_slice(remaining_input.as_slice());
244
245            let (remaining, decoded) = Entrypoint::nom_read(encoded.as_slice())
246                .expect("decoding entrypoint should work");
247
248            assert_eq!(remaining, remaining_input.as_slice());
249
250            assert_eq!(entrypoint, decoded);
251        }
252    }
253}