mavryk_smart_rollup_encoding/
entrypoint.rs1use 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#[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 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#[derive(Debug, PartialEq, Eq)]
46pub enum EntrypointError {
47 TooLarge(String),
49 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 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 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}