scale_value/string_impls/custom_parsers/
hex.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
// Copyright (C) 2022-2023 Parity Technologies (UK) Ltd. (admin@parity.io)
// This file is a part of the scale-value crate.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//         http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::prelude::*;
use crate::{
    stringify::{ParseError, ParseErrorKind},
    Value,
};

/// Attempt to parse a hex string into a [`Value<()>`] (or more specifically,
/// an unnamed composite).
///
/// - Returns an error if we see a leading `0x` and then see invalid hex
///   characters after this.
/// - Returns `None` if no `0x` is seen first.
/// - Returns `Some(value)` if parsing was successful. In this case, the string
///   reference given is wound forwards to consume what was parsed.
pub fn parse_hex(s: &mut &str) -> Option<Result<Value<()>, ParseError>> {
    if !s.starts_with("0x") {
        return None;
    }

    let bytes = s.as_bytes();
    let mut composite_values = vec![];

    // find all valid hex chars after 0x:
    let mut idx = 2;
    let mut last_nibble = None;
    loop {
        // Break if we hit end of string.
        let Some(b) = bytes.get(idx) else {
            break;
        };

        // Break as soon as we hit some non-alphanumeric char.
        if !b.is_ascii_alphanumeric() {
            break;
        }

        // Turn 4-bit hex char into nibble:
        let hex_nibble = match *b {
            b'A'..=b'F' => b - b'A' + 10,
            b'a'..=b'f' => b - b'a' + 10,
            b'0'..=b'9' => b - b'0',
            b => {
                return Some(Err(
                    ParseErrorKind::custom(ParseHexError::InvalidChar(b as char)).at(idx)
                ))
            }
        };

        match last_nibble {
            None => {
                // The first of 2 chars that make a single byte; keep hold of:
                last_nibble = Some(hex_nibble)
            }
            Some(n) => {
                // The second; combine and push byte to output:
                let byte = n * 16 + hex_nibble;
                composite_values.push(Value::u128(byte as u128));
                last_nibble = None;
            }
        }

        idx += 1;
    }

    // We have leftovers; wrong length!
    if last_nibble.is_some() {
        return Some(Err(ParseErrorKind::custom(ParseHexError::WrongLength).between(0, idx)));
    }

    // Consume the "used" up bytes and return our Value.
    //
    // # Safety
    //
    // We have consumed only ASCII chars to get this far, so
    // we know the bytes following them make up a valid str.
    *s = unsafe { core::str::from_utf8_unchecked(&bytes[idx..]) };
    Some(Ok(Value::unnamed_composite(composite_values)))
}

#[derive(Debug, PartialEq, Clone, derive_more::Display)]
#[allow(missing_docs)]
pub enum ParseHexError {
    #[display("Invalid hex character: {_0}")]
    InvalidChar(char),
    #[display("Hex string is the wrong length; should be an even length")]
    WrongLength,
}

#[cfg(feature = "std")]
impl std::error::Error for ParseHexError {}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn parses_same_as_hex_crate() {
        let expects = ["0x", "0x00", "0x000102030405060708090A0B", "0xDEADBEEF", "0x00BAB10C"];

        for input in expects {
            let expected_hex = hex::decode(input.trim_start_matches("0x")).expect("valid hex");
            let cursor = &mut &*input;
            let hex = parse_hex(cursor).expect("valid hex expected").expect("no error expected");

            assert_eq!(hex, Value::from_bytes(expected_hex), "values should match");
        }
    }

    #[test]
    fn consumes_parsed_hex() {
        let expects =
            [("0x foo", " foo"), ("0x00,bar", ",bar"), ("0x123456-2", "-2"), ("0xDEADBEEF ", " ")];

        for (input, expected_remaining) in expects {
            let cursor = &mut &*input;
            let _ = parse_hex(cursor).expect("valid hex expected").expect("no error expected");

            assert_eq!(*cursor, expected_remaining);
        }
    }

    #[test]
    fn err_wrong_length() {
        let expects = ["0x1", "0x123"];

        for input in expects {
            let cursor = &mut &*input;
            let err =
                parse_hex(cursor).expect("some result expected").expect_err("an error is expected");

            assert_eq!(err.start_loc, 0);
            assert_eq!(err.end_loc, Some(input.len()));

            let ParseErrorKind::Custom(err) = err.err else { panic!("expected custom error") };

            assert_eq!(err, ParseHexError::WrongLength.to_string());
            assert_eq!(input, *cursor);
        }
    }

    #[test]
    fn err_invalid_char() {
        let expects = [("0x12345x", 'x', 7), ("0x123h4", 'h', 5), ("0xG23h4", 'G', 2)];

        for (input, bad_char, pos) in expects {
            let cursor = &mut &*input;
            let err =
                parse_hex(cursor).expect("some result expected").expect_err("an error is expected");

            assert_eq!(err.start_loc, pos);
            assert!(err.end_loc.is_none());

            let ParseErrorKind::Custom(err) = err.err else { panic!("expected custom error") };

            assert_eq!(err, ParseHexError::InvalidChar(bad_char).to_string());
            assert_eq!(input, *cursor);
        }
    }

    #[test]
    fn empty_string_doesnt_panic() {
        assert!(parse_hex(&mut "").is_none());
        assert!(parse_hex(&mut "0").is_none());
    }
}