solidity_metadata/
lib.rs

1use minicbor::{data::Type, Decode, Decoder};
2use semver::Version;
3use std::str::FromStr;
4use thiserror::Error;
5
6/// Parsed metadata hash
7/// (https://docs.soliditylang.org/en/v0.8.14/metadata.html#encoding-of-the-metadata-hash-in-the-bytecode).
8///
9/// Currently we are interested only in `solc` value.
10#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
11pub struct MetadataHash {
12    pub solc: Option<Version>,
13}
14
15impl MetadataHash {
16    pub fn from_cbor(encoded: &[u8]) -> Result<(Self, usize), minicbor::decode::Error> {
17        let mut context = DecodeContext::default();
18        let result = minicbor::decode_with(encoded, &mut context)?;
19
20        Ok((result, context.used_size))
21    }
22}
23
24#[derive(Clone, Debug, Error, PartialEq, Eq, Hash)]
25enum ParseMetadataHashError {
26    #[error("invalid solc type. Expected \"string\" or \"bytes\", found \"{0}\"")]
27    InvalidSolcType(Type),
28    #[error("solc is not a valid version: {0}")]
29    InvalidSolcVersion(String),
30    #[error("\"solc\" key met more than once")]
31    DuplicateKeys,
32}
33
34#[derive(Default, Debug, Clone, PartialEq, Eq)]
35struct DecodeContext {
36    used_size: usize,
37}
38
39impl<'b> Decode<'b, DecodeContext> for MetadataHash {
40    fn decode(
41        d: &mut Decoder<'b>,
42        ctx: &mut DecodeContext,
43    ) -> Result<Self, minicbor::decode::Error> {
44        use minicbor::decode::Error;
45
46        let number_of_elements = d.map()?.unwrap_or(u64::MAX);
47
48        let mut solc = None;
49        for _ in 0..number_of_elements {
50            // try to parse the key
51            match d.str() {
52                Ok("solc") => {
53                    if solc.is_some() {
54                        // duplicate keys are not allowed in CBOR (RFC 8949)
55                        return Err(Error::custom(ParseMetadataHashError::DuplicateKeys));
56                    }
57                    solc = match d.datatype()? {
58                        // Appeared in 0.5.9.
59                        // https://docs.soliditylang.org/en/v0.8.17/metadata.html#encoding-of-the-metadata-hash-in-the-bytecode
60                        Type::Bytes => {
61                            // Release builds of solc use a 3 byte encoding of the version
62                            // (one byte each for major, minor and patch version number)
63                            let bytes = d.bytes()?;
64                            if bytes.len() != 3 {
65                                // Something went wrong
66                                return Err(Error::custom(
67                                    ParseMetadataHashError::InvalidSolcVersion(
68                                        "release build should be encoded as exactly 3 bytes".into(),
69                                    ),
70                                ));
71                            }
72                            let (major, minor, patch) = (bytes[0], bytes[1], bytes[2]);
73                            Some(Version::new(major as u64, minor as u64, patch as u64))
74                        }
75                        Type::String => {
76                            // Prerelease builds use a complete version string including commit hash and build date
77                            let s = d.str()?;
78                            let version = Version::from_str(s).map_err(|err| {
79                                Error::custom(ParseMetadataHashError::InvalidSolcVersion(
80                                    err.to_string(),
81                                ))
82                            })?;
83                            Some(version)
84                        }
85                        type_ => {
86                            // value of "solc" key must be either String or Bytes
87                            return Err(Error::custom(ParseMetadataHashError::InvalidSolcType(
88                                type_,
89                            )));
90                        }
91                    }
92                }
93                Ok(_) => {
94                    // if key is not "solc" str we may skip the corresponding value
95                    d.skip()?;
96                }
97                Err(err) => return Err(err),
98            }
99        }
100
101        // Update context and set the number of bytes that have been used during decoding.
102        // That mechanism allows us to pass number of used bytes into the caller of `decode`
103        // function.
104        ctx.used_size = d.position();
105
106        Ok(MetadataHash { solc })
107    }
108
109    fn nil() -> Option<Self> {
110        Some(Self { solc: None })
111    }
112}
113
114#[cfg(test)]
115mod metadata_hash_deserialization_tests {
116    use super::*;
117    use blockscout_display_bytes::decode_hex;
118    use std::str::FromStr;
119
120    fn is_valid_custom_error(
121        error: minicbor::decode::Error,
122        expected: ParseMetadataHashError,
123    ) -> bool {
124        if !error.is_custom() {
125            return false;
126        }
127
128        // Unfortunately, current `minicbor::decode::Error` implementation
129        // does not allow to retrieve insides out of custom error,
130        // so the only way to ensure the valid error occurred is by string comparison.
131        let parse_metadata_hash_error_to_string = |err: ParseMetadataHashError| match err {
132            ParseMetadataHashError::InvalidSolcType(_) => "InvalidSolcType",
133            ParseMetadataHashError::InvalidSolcVersion(_) => "InvalidSolcVersion",
134            ParseMetadataHashError::DuplicateKeys => "DuplicateKeys",
135        };
136        format!("{error:?}").contains(parse_metadata_hash_error_to_string(expected))
137    }
138
139    #[test]
140    fn deserialization_metadata_hash_without_solc_tag() {
141        // given
142        // { "bzzr0": b"d4fba422541feba2d648f6657d9354ec14ea9f5919b520abe0feb60981d7b17c" }
143        let hex =
144            "a165627a7a72305820d4fba422541feba2d648f6657d9354ec14ea9f5919b520abe0feb60981d7b17c";
145        let encoded = decode_hex(hex).unwrap();
146        let expected = MetadataHash { solc: None };
147        let expected_size = encoded.len();
148
149        // when
150        let (decoded, decoded_size) = MetadataHash::from_cbor(encoded.as_ref())
151            .expect("Error when decoding valid metadata hash");
152
153        // then
154        assert_eq!(expected, decoded, "Incorrectly decoded");
155        assert_eq!(expected_size, decoded_size, "Incorrect decoded size")
156    }
157
158    #[test]
159    fn deserialization_metadata_hash_with_solc_as_version() {
160        // given
161        // { "ipfs": b"1220BCC988B1311237F2C00CCD0BFBD8B01D24DC18F720603B0DE93FE6327DF53625", "solc": b'00080e' }
162        let hex = "a2646970667358221220bcc988b1311237f2c00ccd0bfbd8b01d24dc18f720603b0de93fe6327df5362564736f6c634300080e";
163        let encoded = decode_hex(hex).unwrap();
164        let expected = MetadataHash {
165            solc: Some(Version::new(0, 8, 14)),
166        };
167        let expected_size = encoded.len();
168
169        // when
170        let (decoded, decoded_size) = MetadataHash::from_cbor(encoded.as_ref())
171            .expect("Error when decoding valid metadata hash");
172
173        // then
174        assert_eq!(expected, decoded, "Incorrectly decoded");
175        assert_eq!(expected_size, decoded_size, "Incorrect decoded size")
176    }
177
178    #[test]
179    fn deserialization_metadata_hash_with_solc_as_string() {
180        // given
181        // {"ipfs": b'1220BA5AF27FE13BC83E671BD6981216D35DF49AB3AC923741B8948B277F93FBF732', "solc": "0.8.15-ci.2022.5.23+commit.21591531"}
182        let hex = "a2646970667358221220ba5af27fe13bc83e671bd6981216d35df49ab3ac923741b8948b277f93fbf73264736f6c637823302e382e31352d63692e323032322e352e32332b636f6d6d69742e3231353931353331";
183        let encoded = decode_hex(hex).unwrap();
184        let expected = MetadataHash {
185            solc: Some(
186                Version::from_str("0.8.15-ci.2022.5.23+commit.21591531")
187                    .expect("solc version parsing"),
188            ),
189        };
190        let expected_size = encoded.len();
191
192        // when
193        let (decoded, decoded_size) = MetadataHash::from_cbor(encoded.as_ref())
194            .expect("Error when decoding valid metadata hash");
195
196        // then
197        assert_eq!(expected, decoded, "Incorrectly decoded");
198        assert_eq!(expected_size, decoded_size, "Incorrect decoded size")
199    }
200
201    #[test]
202    fn deserialization_of_non_exhausted_string() {
203        // given
204        // { "ipfs": b"1220BCC988B1311237F2C00CCD0BFBD8B01D24DC18F720603B0DE93FE6327DF53625", "solc": b'00080e' } \
205        // { "bzzr0": b"d4fba422541feba2d648f6657d9354ec14ea9f5919b520abe0feb60981d7b17c" }
206        let first = "a2646970667358221220bcc988b1311237f2c00ccd0bfbd8b01d24dc18f720603b0de93fe6327df5362564736f6c634300080e";
207        let second =
208            "a165627a7a72305820d4fba422541feba2d648f6657d9354ec14ea9f5919b520abe0feb60981d7b17c";
209        let hex = format!("{first}{second}");
210        let encoded = decode_hex(&hex).unwrap();
211        let expected = MetadataHash {
212            solc: Some(Version::new(0, 8, 14)),
213        };
214        let expected_size = decode_hex(first).unwrap().len();
215
216        // when
217        let (decoded, decoded_size) = MetadataHash::from_cbor(encoded.as_ref())
218            .expect("Error when decoding valid metadata hash");
219
220        // then
221        assert_eq!(expected, decoded, "Incorrectly decoded");
222        assert_eq!(expected_size, decoded_size, "Incorrect decoded size")
223    }
224
225    #[test]
226    fn deserialization_of_non_cbor_hex_should_fail() {
227        // given
228        let hex = "1234567890";
229        let encoded = decode_hex(hex).unwrap();
230
231        // when
232        let decoded = MetadataHash::from_cbor(encoded.as_ref());
233
234        // then
235        assert!(decoded.is_err(), "Deserialization should fail");
236        assert!(
237            decoded.unwrap_err().is_type_mismatch(),
238            "Should fail with type mismatch"
239        )
240    }
241
242    #[test]
243    fn deserialization_of_non_map_should_fail() {
244        // given
245        // "solc"
246        let hex = "64736f6c63";
247        let encoded = decode_hex(hex).unwrap();
248
249        // when
250        let decoded = MetadataHash::from_cbor(encoded.as_ref());
251
252        // then
253        assert!(decoded.is_err(), "Deserialization should fail");
254        assert!(
255            decoded.unwrap_err().is_type_mismatch(),
256            "Should fail with type mismatch"
257        )
258    }
259
260    #[test]
261    fn deserialization_with_duplicated_solc_should_fail() {
262        // given
263        // { "solc": b'000400', "ipfs": b"1220BCC988B1311237F2C00CCD0BFBD8B01D24DC18F720603B0DE93FE6327DF53625", "solc": b'00080e' }
264        let hex = "a364736f6c6343000400646970667358221220bcc988b1311237f2c00ccd0bfbd8b01d24dc18f720603b0de93fe6327df5362564736f6c634300080e";
265        let encoded = decode_hex(hex).unwrap();
266
267        // when
268        let decoded = MetadataHash::from_cbor(encoded.as_ref());
269
270        // then
271        assert!(decoded.is_err(), "Deserialization should fail");
272        assert!(
273            is_valid_custom_error(decoded.unwrap_err(), ParseMetadataHashError::DuplicateKeys),
274            "Should fail with custom (DuplicateKey) error"
275        );
276    }
277
278    #[test]
279    fn deserialization_with_not_enough_elements_should_fail() {
280        // given
281        // 3 elements expected in the map but got only 2:
282        // { "ipfs": b"1220BCC988B1311237F2C00CCD0BFBD8B01D24DC18F720603B0DE93FE6327DF53625", "solc": b'00080e' }
283        let hex = "a3646970667358221220bcc988b1311237f2c00ccd0bfbd8b01d24dc18f720603b0de93fe6327df5362564736f6c634300080e";
284        let encoded = decode_hex(hex).unwrap();
285
286        // when
287        let decoded = MetadataHash::from_cbor(encoded.as_ref());
288
289        // then
290        assert!(decoded.is_err(), "Deserialization should fail");
291        assert!(
292            decoded.unwrap_err().is_end_of_input(),
293            "Should fail with end of input error"
294        );
295    }
296
297    #[test]
298    fn deserialization_with_solc_neither_bytes_nor_string_should_fail() {
299        // given
300        // { "ipfs": b"1220BCC988B1311237F2C00CCD0BFBD8B01D24DC18F720603B0DE93FE6327DF53625", "solc": 123 } \
301        let hex= "a2646970667358221220bcc988b1311237f2c00ccd0bfbd8b01d24dc18f720603b0de93fe6327df5362564736f6c63187B";
302        let encoded = decode_hex(hex).unwrap();
303
304        // when
305        let decoded = MetadataHash::from_cbor(encoded.as_ref());
306
307        // then
308        assert!(decoded.is_err(), "Deserialization should fail");
309        assert!(
310            is_valid_custom_error(
311                decoded.unwrap_err(),
312                ParseMetadataHashError::InvalidSolcType(minicbor::data::Type::Int)
313            ),
314            "Should fail with custom (InvalidSolcType) error"
315        );
316    }
317}