• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tari-project / tari / 30437398376

29 Jul 2026 08:54AM UTC coverage: 62.188% (+0.1%) from 62.053%
30437398376

push

github

web-flow
fix: bound the number of signatures in a sidechain quorum certificate (#7944)

Description
---
Checks max signature length

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

39 of 39 new or added lines in 2 files covered. (100.0%)

269 existing lines in 25 files now uncovered.

73171 of 117661 relevant lines covered (62.19%)

222229.11 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.89
/infrastructure/max_size/src/string.rs
1
//  Copyright 2022. The Tari Project
2
//
3
//  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
4
//  following conditions are met:
5
//
6
//  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
7
//  disclaimer.
8
//
9
//  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
10
//  following disclaimer in the documentation and/or other materials provided with the distribution.
11
//
12
//  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
13
//  products derived from this software without specific prior written permission.
14
//
15
//  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
16
//  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
//  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
18
//  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19
//  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
20
//  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
21
//  USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
22

23
use std::{convert::TryFrom, fmt::Display};
24

25
use borsh::{
26
    BorshDeserialize,
27
    BorshSerialize,
28
    io::{Error, ErrorKind},
29
};
30
use serde::{Deserialize, Deserializer, Serialize};
31

32
use crate::checked_de::{read_bytes, read_checked_len};
33

34
/// A string that can only be a up to MAX length long
35
///
36
/// The bound is enforced by every constructor *and* by deserialization (see the hand written
37
/// `BorshDeserialize`/`Deserialize` implementations below), so `len() <= MAX` is a true invariant
38
/// even for values decoded from untrusted input.
39
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, BorshSerialize)]
40
pub struct MaxSizeString<const MAX: usize> {
41
    string: String,
42
}
43

44
/// Mirror of [`MaxSizeString`] used only to decode the wire format before the bound is checked.
45
/// It must keep the exact same (serde) shape as `MaxSizeString` so that the serialized
46
/// representation is unchanged.
47
#[derive(Deserialize)]
48
#[serde(rename = "MaxSizeString")]
49
struct MaxSizeStringShadow {
50
    string: String,
51
}
52

53
impl<'de, const MAX: usize> Deserialize<'de> for MaxSizeString<MAX> {
54
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
6✔
55
        let shadow = MaxSizeStringShadow::deserialize(deserializer)?;
6✔
56
        Self::try_from(shadow.string).map_err(serde::de::Error::custom)
6✔
57
    }
6✔
58
}
59

60
impl<const MAX: usize> BorshDeserialize for MaxSizeString<MAX> {
61
    fn deserialize_reader<R: borsh::io::Read>(reader: &mut R) -> borsh::io::Result<Self> {
6✔
62
        // The length is validated before any data is read, so an oversized payload is rejected up
63
        // front instead of being decoded and silently accepted.
64
        let len = read_checked_len(reader, MAX, "MaxSizeString")?;
6✔
65
        let bytes = read_bytes(reader, len)?;
4✔
66
        let string = String::from_utf8(bytes).map_err(|e| Error::new(ErrorKind::InvalidData, e.to_string()))?;
3✔
67
        Ok(Self { string })
2✔
68
    }
6✔
69
}
70

71
impl<const MAX: usize> MaxSizeString<MAX> {
72
    pub fn from_str_checked(s: &str) -> Option<Self> {
5✔
73
        if s.len() > MAX {
5✔
74
            return None;
1✔
75
        }
4✔
76
        Some(Self { string: s.to_string() })
4✔
77
    }
5✔
78

79
    pub fn from_utf8_bytes_checked<T: AsRef<[u8]>>(bytes: T) -> Option<Self> {
3✔
80
        let b = bytes.as_ref();
3✔
81
        if b.len() > MAX {
3✔
82
            return None;
1✔
83
        }
2✔
84

85
        let s = String::from_utf8(b.to_vec()).ok()?;
2✔
86
        Some(Self { string: s })
1✔
87
    }
3✔
88

89
    pub fn len(&self) -> usize {
8✔
90
        self.string.len()
8✔
91
    }
8✔
92

93
    pub fn is_empty(&self) -> bool {
×
UNCOV
94
        self.string.is_empty()
×
UNCOV
95
    }
×
96

97
    pub fn as_str(&self) -> &str {
5✔
98
        &self.string
5✔
99
    }
5✔
100

UNCOV
101
    pub fn into_string(self) -> String {
×
102
        self.string
×
103
    }
×
104
}
105

106
impl<const MAX: usize> TryFrom<String> for MaxSizeString<MAX> {
107
    type Error = MaxSizeStringLengthError;
108

109
    fn try_from(value: String) -> Result<Self, Self::Error> {
10✔
110
        if value.len() > MAX {
10✔
111
            return Err(MaxSizeStringLengthError {
2✔
112
                actual: value.len(),
2✔
113
                expected: MAX,
2✔
114
            });
2✔
115
        }
8✔
116
        Ok(Self { string: value })
8✔
117
    }
10✔
118
}
119

120
impl<const MAX: usize> TryFrom<&str> for MaxSizeString<MAX> {
121
    type Error = MaxSizeStringLengthError;
122

123
    fn try_from(value: &str) -> Result<Self, Self::Error> {
11✔
124
        if value.len() > MAX {
11✔
UNCOV
125
            return Err(MaxSizeStringLengthError {
×
UNCOV
126
                actual: value.len(),
×
UNCOV
127
                expected: MAX,
×
UNCOV
128
            });
×
129
        }
11✔
130
        Ok(Self {
11✔
131
            string: value.to_string(),
11✔
132
        })
11✔
133
    }
11✔
134
}
135

136
impl<const MAX: usize> AsRef<[u8]> for MaxSizeString<MAX> {
UNCOV
137
    fn as_ref(&self) -> &[u8] {
×
UNCOV
138
        self.string.as_ref()
×
UNCOV
139
    }
×
140
}
141

142
impl<const MAX: usize> Display for MaxSizeString<MAX> {
UNCOV
143
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
UNCOV
144
        write!(f, "{}", self.string)
×
UNCOV
145
    }
×
146
}
147

148
#[derive(Debug, thiserror::Error)]
149
#[error("Invalid String length: expected {expected}, got {actual}")]
150
pub struct MaxSizeStringLengthError {
151
    expected: usize,
152
    actual: usize,
153
}
154

155
#[cfg(test)]
156
mod tests {
157
    mod from_str_checked {
158
        use crate::MaxSizeString;
159
        #[test]
160
        fn it_returns_none_if_size_exceeded() {
1✔
161
            let s = MaxSizeString::<10>::from_str_checked("12345678901234567890");
1✔
162
            assert_eq!(s, None);
1✔
163
        }
1✔
164

165
        #[test]
166
        fn it_returns_some_if_size_in_bounds() {
1✔
167
            let s = MaxSizeString::<0>::from_str_checked("").unwrap();
1✔
168
            assert_eq!(s.as_str(), "");
1✔
169
            assert_eq!(s.len(), 0);
1✔
170

171
            let s = MaxSizeString::<10>::from_str_checked("1234567890").unwrap();
1✔
172
            assert_eq!(s.as_str(), "1234567890");
1✔
173
            assert_eq!(s.len(), 10);
1✔
174

175
            let s = MaxSizeString::<10>::from_str_checked("1234").unwrap();
1✔
176
            assert_eq!(s.as_str(), "1234");
1✔
177
            assert_eq!(s.len(), 4);
1✔
178

179
            let s = MaxSizeString::<8>::from_str_checked("🚀🚀").unwrap();
1✔
180
            assert_eq!(s.as_str(), "🚀🚀");
1✔
181
            // 8 here because an emoji char take 4 bytes each
182
            assert_eq!(s.len(), 8);
1✔
183
        }
1✔
184
    }
185

186
    mod from_utf8_bytes_checked {
187
        use crate::MaxSizeString;
188
        #[test]
189
        fn it_returns_none_if_size_exceeded() {
1✔
190
            let s = MaxSizeString::<10>::from_utf8_bytes_checked([0u8; 11]);
1✔
191
            assert_eq!(s, None);
1✔
192
        }
1✔
193

194
        #[test]
195
        fn it_returns_some_if_size_in_bounds() {
1✔
196
            let s = MaxSizeString::<12>::from_utf8_bytes_checked("💡🧭🛖".as_bytes()).unwrap();
1✔
197
            assert_eq!(s.as_str(), "💡🧭🛖");
1✔
198
            assert_eq!(s.len(), 12);
1✔
199
        }
1✔
200

201
        #[test]
202
        fn it_returns_none_if_invalid_utf8() {
1✔
203
            let s = MaxSizeString::<10>::from_utf8_bytes_checked([255u8; 10]);
1✔
204
            assert_eq!(s, None);
1✔
205
        }
1✔
206
    }
207

208
    mod deserialization {
209
        use borsh::BorshDeserialize;
210

211
        use crate::MaxSizeString;
212

213
        const MAX: usize = 10;
214
        type Str = MaxSizeString<MAX>;
215

216
        #[test]
217
        fn borsh_round_trips_a_valid_value() {
1✔
218
            let s = Str::try_from("abc").unwrap();
1✔
219
            let encoded = borsh::to_vec(&s).unwrap();
1✔
220
            assert_eq!(Str::try_from_slice(&encoded).unwrap(), s);
1✔
221

222
            // The encoding is unchanged from the derived implementation, i.e. it is the plain
223
            // borsh encoding of the inner `String`
224
            assert_eq!(encoded, borsh::to_vec(&"abc".to_string()).unwrap());
1✔
225
        }
1✔
226

227
        #[test]
228
        fn borsh_accepts_exactly_max_and_rejects_max_plus_one() {
1✔
229
            let at_max = borsh::to_vec(&"a".repeat(MAX)).unwrap();
1✔
230
            assert_eq!(Str::try_from_slice(&at_max).unwrap().len(), MAX);
1✔
231

232
            let over_max = borsh::to_vec(&"a".repeat(MAX + 1)).unwrap();
1✔
233
            let err = Str::try_from_slice(&over_max).unwrap_err();
1✔
234
            assert!(err.to_string().contains("exceeds the maximum size"), "{}", err);
1✔
235
        }
1✔
236

237
        #[test]
238
        fn borsh_rejects_an_oversized_length_prefix_without_reading_the_body() {
1✔
239
            // A length prefix of 4 GiB and no data at all: this must fail on the length check
240
            // alone
241
            let payload = u32::MAX.to_le_bytes();
1✔
242
            let err = Str::try_from_slice(&payload).unwrap_err();
1✔
243
            assert!(err.to_string().contains("exceeds the maximum size"), "{}", err);
1✔
244
        }
1✔
245

246
        #[test]
247
        fn borsh_rejects_invalid_utf8() {
1✔
248
            let payload = borsh::to_vec(&vec![255u8; MAX]).unwrap();
1✔
249
            assert!(Str::try_from_slice(&payload).is_err());
1✔
250
        }
1✔
251

252
        #[test]
253
        fn borsh_rejects_a_truncated_body() {
1✔
254
            let mut payload = borsh::to_vec(&"a".repeat(MAX)).unwrap();
1✔
255
            payload.pop();
1✔
256
            assert!(Str::try_from_slice(&payload).is_err());
1✔
257
        }
1✔
258

259
        #[test]
260
        fn serde_round_trips_a_valid_value_without_changing_the_representation() {
1✔
261
            let s = Str::try_from("abc").unwrap();
1✔
262
            let json = serde_json::to_string(&s).unwrap();
1✔
263
            assert_eq!(json, r#"{"string":"abc"}"#);
1✔
264
            assert_eq!(serde_json::from_str::<Str>(&json).unwrap(), s);
1✔
265
        }
1✔
266

267
        #[test]
268
        fn bincode_round_trips_a_valid_value_and_rejects_max_plus_one() {
1✔
269
            // bincode is the compact (non human readable) serde format used for the on-disk chain
270
            // storage, so the encoding must be unchanged
271
            let s = Str::try_from("abc").unwrap();
1✔
272
            let encoded = bincode::serialize(&s).unwrap();
1✔
273
            assert_eq!(encoded, bincode::serialize(&"abc".to_string()).unwrap());
1✔
274
            assert_eq!(bincode::deserialize::<Str>(&encoded).unwrap(), s);
1✔
275

276
            let at_max = bincode::serialize(&"a".repeat(MAX)).unwrap();
1✔
277
            assert_eq!(bincode::deserialize::<Str>(&at_max).unwrap().len(), MAX);
1✔
278

279
            let over_max = bincode::serialize(&"a".repeat(MAX + 1)).unwrap();
1✔
280
            let err = bincode::deserialize::<Str>(&over_max).unwrap_err();
1✔
281
            assert!(err.to_string().contains("Invalid String length"), "{}", err);
1✔
282
        }
1✔
283

284
        #[test]
285
        fn serde_accepts_exactly_max_and_rejects_max_plus_one() {
1✔
286
            let at_max = format!(r#"{{"string":"{}"}}"#, "a".repeat(MAX));
1✔
287
            assert_eq!(serde_json::from_str::<Str>(&at_max).unwrap().len(), MAX);
1✔
288

289
            let over_max = format!(r#"{{"string":"{}"}}"#, "a".repeat(MAX + 1));
1✔
290
            let err = serde_json::from_str::<Str>(&over_max).unwrap_err();
1✔
291
            assert!(err.to_string().contains("Invalid String length"), "{}", err);
1✔
292
        }
1✔
293
    }
294
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc