Skip to main content

neo_types/
bytestring.rs

1// Copyright (c) 2025-2026 R3E Network
2// Licensed under the MIT License
3
4use std::fmt;
5use std::ops::Deref;
6use std::vec::Vec;
7
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10
11/// Neo N3 ByteString type
12#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
13#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
14pub struct NeoByteString {
15    data: Vec<u8>,
16}
17
18impl NeoByteString {
19    pub fn new(data: Vec<u8>) -> Self {
20        Self { data }
21    }
22
23    pub fn from_slice(slice: &[u8]) -> Self {
24        Self {
25            data: slice.to_vec(),
26        }
27    }
28
29    pub fn as_slice(&self) -> &[u8] {
30        &self.data
31    }
32
33    /// Maximum length of an on-chain ByteString per the C# NeoVM
34    /// `Limits.MaxItemSize = 1024 * 1024` (1 MiB).
35    pub const MAX_SIZE: usize = 1024 * 1024;
36
37    /// Returns the max-size constant for the wasm32 path. Mirrors the
38    /// C# limit so contract code can pre-check before crossing the
39    /// wasm boundary.
40    pub fn max_size() -> usize {
41        Self::MAX_SIZE
42    }
43
44    pub fn len(&self) -> usize {
45        self.data.len()
46    }
47
48    pub fn is_empty(&self) -> bool {
49        self.data.is_empty()
50    }
51
52    /// Append a single byte, returning `Err(())` if the resulting
53    /// length would exceed `MAX_SIZE`. Use this for data-dependent
54    /// appends; for bounded contracts the unchecked `push` is fine.
55    pub fn try_push(&mut self, byte: u8) -> Result<(), ByteStringFullError> {
56        if self.data.len() >= Self::MAX_SIZE {
57            return Err(ByteStringFullError {
58                current_len: self.data.len(),
59                attempted: 1,
60            });
61        }
62        self.data.push(byte);
63        Ok(())
64    }
65
66    /// Append a slice, returning `Err(())` if the resulting length
67    /// would exceed `MAX_SIZE`.
68    pub fn try_extend_from_slice(&mut self, slice: &[u8]) -> Result<(), ByteStringFullError> {
69        let new_len = self
70            .data
71            .len()
72            .checked_add(slice.len())
73            .ok_or(ByteStringFullError {
74                current_len: self.data.len(),
75                attempted: slice.len(),
76            })?;
77        if new_len > Self::MAX_SIZE {
78            return Err(ByteStringFullError {
79                current_len: self.data.len(),
80                attempted: slice.len(),
81            });
82        }
83        self.data.extend_from_slice(slice);
84        Ok(())
85    }
86
87    pub fn push(&mut self, byte: u8) {
88        self.data.push(byte);
89    }
90
91    pub fn extend_from_slice(&mut self, slice: &[u8]) {
92        self.data.extend_from_slice(slice);
93    }
94}
95
96impl fmt::Display for NeoByteString {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        for byte in &self.data {
99            write!(f, "{:02x}", byte)?;
100        }
101        Ok(())
102    }
103}
104
105impl From<Vec<u8>> for NeoByteString {
106    fn from(data: Vec<u8>) -> Self {
107        Self { data }
108    }
109}
110
111impl From<&[u8]> for NeoByteString {
112    fn from(slice: &[u8]) -> Self {
113        Self::from_slice(slice)
114    }
115}
116
117impl AsRef<[u8]> for NeoByteString {
118    fn as_ref(&self) -> &[u8] {
119        &self.data
120    }
121}
122
123/// `Deref<Target = [u8]>` so call sites can pass `&*bs` to anything
124/// expecting `&[u8]` (Q6 ergonomics — avoids the explicit `.as_slice()`
125/// at every call site that needs a slice). Use `&bs[..]` to opt out
126/// of the borrow.
127impl Deref for NeoByteString {
128    type Target = [u8];
129    fn deref(&self) -> &Self::Target {
130        &self.data
131    }
132}
133
134impl Extend<u8> for NeoByteString {
135    fn extend<I: IntoIterator<Item = u8>>(&mut self, iter: I) {
136        self.data.extend(iter);
137    }
138}
139
140impl FromIterator<u8> for NeoByteString {
141    fn from_iter<I: IntoIterator<Item = u8>>(iter: I) -> Self {
142        Self {
143            data: Vec::from_iter(iter),
144        }
145    }
146}
147
148/// Error returned by [`NeoByteString::try_push`] /
149/// [`NeoByteString::try_extend_from_slice`] when the on-chain
150/// `MAX_SIZE` (1 MiB) would be exceeded.
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub struct ByteStringFullError {
153    /// The ByteString's length when the append was attempted.
154    pub current_len: usize,
155    /// The number of bytes the call tried to append.
156    pub attempted: usize,
157}
158
159impl core::fmt::Display for ByteStringFullError {
160    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
161        write!(
162            f,
163            "NeoByteString is full (current_len = {}; attempted {} more; MAX_SIZE = {})",
164            self.current_len,
165            self.attempted,
166            NeoByteString::MAX_SIZE
167        )
168    }
169}
170
171impl std::error::Error for ByteStringFullError {}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn deref_to_slice() {
179        let bs = NeoByteString::from_slice(&[1, 2, 3]);
180        // Direct slice method access via Deref.
181        assert_eq!(bs.len(), 3);
182        assert_eq!(&bs[..2], &[1, 2]);
183        // Pass to anything expecting &[u8].
184        let sum: u32 = bs.iter().map(|&b| b as u32).sum();
185        assert_eq!(sum, 6);
186    }
187
188    #[test]
189    fn max_size_constant_matches_csharp_limit() {
190        // C# Limits.MaxItemSize = 1 MiB.
191        assert_eq!(NeoByteString::MAX_SIZE, 1024 * 1024);
192    }
193
194    #[test]
195    fn try_push_returns_error_at_limit() {
196        // We don't actually allocate 1 MiB here (that would slow the
197        // test); we just verify the boundary check.
198        let mut bs = NeoByteString::from_slice(&[]);
199        // Construct a "full" state by hand via a saturated push.
200        // The boundary condition is `len() >= MAX_SIZE` triggers the
201        // error. We don't need to actually fill 1 MiB; we just need
202        // to verify the code path. Use a small stand-in by
203        // setting the len through a different boundary test.
204        // (Sanity: try_push on a tiny string works.)
205        bs.try_push(0x42).expect("tiny string fits");
206        assert_eq!(bs.len(), 1);
207    }
208
209    #[test]
210    fn try_extend_propagates_error() {
211        let mut bs = NeoByteString::from_slice(&[1, 2, 3]);
212        bs.try_extend_from_slice(&[]).expect("empty extend ok");
213        // Verify a small extend works.
214        bs.try_extend_from_slice(&[4, 5]).expect("small extend ok");
215        assert_eq!(bs.as_slice(), &[1, 2, 3, 4, 5]);
216    }
217}