Skip to main content

neo_devpack_solidity/neo/
build.rs

1use super::constants::{
2    MAX_CALL_FLAGS, MAX_METHOD_TOKENS, MAX_TOKEN_METHOD_LENGTH, NEF_MAGIC, NEF_SOURCE_MAX_BYTES,
3};
4use super::encoding::{
5    calculate_checksum, write_fixed_string, write_varbytes, write_varint, write_varstring,
6};
7use super::method_token::MethodToken;
8use super::source::clamp_nef_source;
9
10/// Build a NEF (Neo Executable Format) file from raw NeoVM bytecode.
11///
12/// The implementation follows the Neo N3 specification:
13///
14/// - Magic header `NEF3`
15/// - Compiler identifier (fixed 64 bytes, UTF-8, padded with zeros)
16/// - Source URL (varstring, max 256 bytes)
17/// - Reserved byte (must be 0)
18/// - Method token table (varint count + entries, max 128 entries)
19/// - Reserved 2 bytes (must be 0)
20/// - Script payload (varbytes)
21/// - Checksum (first four bytes of double SHA256 over all previous bytes)
22///
23/// # Arguments
24/// * `script` - The NeoVM bytecode
25/// * `compiler` - Compiler identifier string (max 64 bytes)
26///
27/// # Returns
28/// Complete NEF file as byte vector
29pub fn build_nef(script: &[u8], compiler: &str) -> Result<Vec<u8>, String> {
30    build_nef_with_tokens(script, compiler, "", &[])
31}
32
33/// Build a NEF file with method tokens for cross-contract calls.
34///
35/// # Arguments
36/// * `script` - The NeoVM bytecode
37/// * `compiler` - Compiler identifier string (max 64 bytes)
38/// * `source` - Optional source URL (max 256 bytes)
39/// * `tokens` - Array of method tokens for cross-contract calls
40///
41/// # Returns
42/// Complete NEF file as byte vector
43pub fn build_nef_with_tokens(
44    script: &[u8],
45    compiler: &str,
46    source: &str,
47    tokens: &[MethodToken],
48) -> Result<Vec<u8>, String> {
49    if script.is_empty() {
50        return Err("NEF script payload cannot be empty".to_string());
51    }
52    let source = clamp_nef_source(source);
53    if tokens.len() > MAX_METHOD_TOKENS {
54        return Err(format!(
55            "NEF method token table exceeds {MAX_METHOD_TOKENS} entries"
56        ));
57    }
58
59    for token in tokens {
60        let method_len = token.method.len();
61        if method_len > MAX_TOKEN_METHOD_LENGTH {
62            return Err(format!(
63                "method token '{}' exceeds {MAX_TOKEN_METHOD_LENGTH} bytes",
64                token.method
65            ));
66        }
67        if token.method.starts_with('_') {
68            return Err(format!(
69                "method token '{}' must not start with '_'",
70                token.method
71            ));
72        }
73        if token.call_flags & !MAX_CALL_FLAGS != 0 {
74            return Err(format!(
75                "method token '{}' has invalid call flags {:#x}",
76                token.method, token.call_flags
77            ));
78        }
79    }
80
81    // Rough capacity hint: header (magic + compiler + empty source + reserves) plus script and tokens.
82    let token_size: usize = tokens.iter().map(|t| 20 + t.method.len() + 10).sum();
83    let mut buffer = Vec::with_capacity(80 + token_size + script.len());
84
85    // Magic (4 bytes)
86    buffer.extend_from_slice(b"NEF3");
87
88    // Compiler identifier (64 bytes, zero padded)
89    write_fixed_string(&mut buffer, compiler, 64);
90
91    // Source URL (varstring, max 256 bytes)
92    write_varstring(&mut buffer, &source);
93
94    // Reserved byte must be zero
95    buffer.push(0u8);
96
97    // Method token table
98    write_varint(&mut buffer, tokens.len() as u64);
99    for token in tokens {
100        token.serialize(&mut buffer)?;
101    }
102
103    // Reserved bytes (2 bytes, must be 0)
104    buffer.extend_from_slice(&[0u8; 2]);
105
106    // Script payload (length-prefixed)
107    write_varbytes(&mut buffer, script);
108
109    // Checksum (first 4 bytes of double SHA-256)
110    let checksum = calculate_checksum(&buffer);
111    buffer.extend_from_slice(&checksum.to_le_bytes());
112
113    Ok(buffer)
114}
115
116/// Result of parsing a NEF file via [`parse_nef`].
117///
118/// Fields mirror the inputs to [`build_nef_with_tokens`], so a `ParsedNef`
119/// can be fed straight back into the builder for round-trip tests.
120#[derive(Debug, Clone)]
121pub struct ParsedNef {
122    /// Compiler identifier (the fixed 64-byte field, trimmed of trailing NULs).
123    pub compiler: String,
124    /// Source URL / identifier (varstring, UTF-8).
125    pub source: String,
126    /// Method token table.
127    pub tokens: Vec<MethodToken>,
128    /// NeoVM script payload.
129    pub script: Vec<u8>,
130}
131
132/// Parse a Neo N3 NEF3 file produced by [`build_nef_with_tokens`].
133///
134/// Validates, in order:
135/// * the 4-byte `NEF3` magic,
136/// * the trailing `sha256(sha256(prefix))[..4]` checksum,
137/// * that every varint length prefix (source, token count, method name, script)
138///   is consistent with the remaining buffer,
139/// * the single reserved byte (post-source) and two reserved bytes (pre-script)
140///   are all zero,
141/// * method-token invariants matching those enforced by the builder
142///   (`method.len() <= MAX_TOKEN_METHOD_LENGTH`, `!method.starts_with('_')`,
143///   `call_flags & !MAX_CALL_FLAGS == 0`, `tokens.len() <= MAX_METHOD_TOKENS`).
144///
145/// Returns [`Err`] with a human-readable diagnostic on any violation.
146pub fn parse_nef(bytes: &[u8]) -> Result<ParsedNef, String> {
147    // Minimum size: 4 (magic) + 64 (compiler) + 1 (varint=0 source) + 1 (reserved)
148    // + 1 (varint=0 tokens) + 2 (reserved) + 1 (varint=0 script) + 4 (checksum).
149    const MIN_NEF_SIZE: usize = 4 + 64 + 1 + 1 + 1 + 2 + 1 + 4;
150    if bytes.len() < MIN_NEF_SIZE {
151        return Err(format!(
152            "NEF buffer too small: got {} bytes, need at least {}",
153            bytes.len(),
154            MIN_NEF_SIZE
155        ));
156    }
157
158    // --- checksum first (over everything except the trailing 4 bytes) -------
159    let (prefix, trailer) = bytes.split_at(bytes.len() - 4);
160    let expected = calculate_checksum(prefix).to_le_bytes();
161    if trailer != expected {
162        return Err(format!(
163            "NEF checksum mismatch: stored={trailer:02x?} expected={expected:02x?}"
164        ));
165    }
166
167    let mut cursor = Cursor::new(prefix);
168
169    // --- magic --------------------------------------------------------------
170    let magic = cursor.take(4)?;
171    if magic != NEF_MAGIC {
172        return Err(format!(
173            "NEF magic mismatch: got {magic:02x?}, expected {NEF_MAGIC:02x?}"
174        ));
175    }
176
177    // --- compiler (64 byte fixed field, zero padded) ------------------------
178    let compiler_bytes = cursor.take(64)?;
179    // Strip trailing NULs written by `write_fixed_string`.
180    let trimmed_end = compiler_bytes
181        .iter()
182        .rposition(|&b| b != 0)
183        .map(|i| i + 1)
184        .unwrap_or(0);
185    let compiler = std::str::from_utf8(&compiler_bytes[..trimmed_end])
186        .map_err(|e| format!("NEF compiler field is not valid UTF-8: {e}"))?
187        .to_string();
188
189    // --- source (varstring) -------------------------------------------------
190    let source_len = cursor.read_varint()? as usize;
191    if source_len > NEF_SOURCE_MAX_BYTES {
192        return Err(format!(
193            "NEF source length {source_len} exceeds maximum {NEF_SOURCE_MAX_BYTES}"
194        ));
195    }
196    let source_bytes = cursor.take(source_len)?;
197    let source = std::str::from_utf8(source_bytes)
198        .map_err(|e| format!("NEF source is not valid UTF-8: {e}"))?
199        .to_string();
200
201    // --- reserved byte ------------------------------------------------------
202    let reserved1 = cursor.take(1)?[0];
203    if reserved1 != 0 {
204        return Err(format!(
205            "NEF reserved byte after source must be 0, got {reserved1:#x}"
206        ));
207    }
208
209    // --- method tokens ------------------------------------------------------
210    let token_count = cursor.read_varint()? as usize;
211    if token_count > MAX_METHOD_TOKENS {
212        return Err(format!(
213            "NEF method token count {token_count} exceeds maximum {MAX_METHOD_TOKENS}"
214        ));
215    }
216    let mut tokens = Vec::with_capacity(token_count);
217    for i in 0..token_count {
218        // 20-byte hash
219        let hash_bytes = cursor.take(20)?;
220        let mut hash = [0u8; 20];
221        hash.copy_from_slice(hash_bytes);
222
223        // method name (varbytes -> UTF-8 string, max 32 bytes)
224        let method_len = cursor.read_varint()? as usize;
225        if method_len > MAX_TOKEN_METHOD_LENGTH {
226            return Err(format!(
227                "NEF token[{i}] method length {method_len} exceeds maximum {MAX_TOKEN_METHOD_LENGTH}"
228            ));
229        }
230        let method_bytes = cursor.take(method_len)?;
231        let method = std::str::from_utf8(method_bytes)
232            .map_err(|e| format!("NEF token[{i}] method name is not valid UTF-8: {e}"))?
233            .to_string();
234        if method.starts_with('_') {
235            return Err(format!(
236                "NEF token[{i}] method name '{method}' must not start with '_'"
237            ));
238        }
239
240        // parameters_count (u16 LE)
241        let params_bytes = cursor.take(2)?;
242        let parameters_count = u16::from_le_bytes([params_bytes[0], params_bytes[1]]);
243
244        // has_return_value (u8 bool)
245        let has_return_value = cursor.take(1)?[0] != 0;
246
247        // call_flags (u8)
248        let call_flags = cursor.take(1)?[0];
249        if call_flags & !MAX_CALL_FLAGS != 0 {
250            return Err(format!(
251                "NEF token[{i}] has invalid call flags {call_flags:#x}"
252            ));
253        }
254
255        tokens.push(MethodToken {
256            hash,
257            method,
258            parameters_count,
259            has_return_value,
260            call_flags,
261        });
262    }
263
264    // --- reserved 2 bytes ---------------------------------------------------
265    let reserved2 = cursor.take(2)?;
266    if reserved2 != [0u8, 0u8] {
267        return Err(format!(
268            "NEF reserved bytes before script must be [0,0], got {reserved2:02x?}"
269        ));
270    }
271
272    // --- script (varbytes) --------------------------------------------------
273    let script_len = cursor.read_varint()? as usize;
274    let script = cursor.take(script_len)?.to_vec();
275    if script.is_empty() {
276        return Err("NEF script payload cannot be empty".to_string());
277    }
278
279    // Must have consumed exactly the prefix (no trailing garbage before the checksum).
280    if !cursor.is_empty() {
281        return Err(format!(
282            "NEF has {} unexpected trailing bytes before checksum",
283            cursor.remaining()
284        ));
285    }
286
287    Ok(ParsedNef {
288        compiler,
289        source,
290        tokens,
291        script,
292    })
293}
294
295/// Byte-oriented cursor used by [`parse_nef`].
296///
297/// Intentionally tiny (no dependency on `std::io::Cursor`) so every error path
298/// returns our `String` diagnostics rather than `io::Error`.
299struct Cursor<'a> {
300    buf: &'a [u8],
301    pos: usize,
302}
303
304impl<'a> Cursor<'a> {
305    fn new(buf: &'a [u8]) -> Self {
306        Self { buf, pos: 0 }
307    }
308
309    fn remaining(&self) -> usize {
310        self.buf.len() - self.pos
311    }
312
313    fn is_empty(&self) -> bool {
314        self.pos >= self.buf.len()
315    }
316
317    fn take(&mut self, n: usize) -> Result<&'a [u8], String> {
318        if self.pos + n > self.buf.len() {
319            return Err(format!(
320                "NEF truncated: tried to read {} bytes at offset {}, only {} available",
321                n,
322                self.pos,
323                self.buf.len().saturating_sub(self.pos)
324            ));
325        }
326        let slice = &self.buf[self.pos..self.pos + n];
327        self.pos += n;
328        Ok(slice)
329    }
330
331    /// Read a Neo-style varint (matching [`super::encoding::write_varint`]).
332    fn read_varint(&mut self) -> Result<u64, String> {
333        let prefix = self.take(1)?[0];
334        match prefix {
335            0xFD => {
336                let b = self.take(2)?;
337                Ok(u16::from_le_bytes([b[0], b[1]]) as u64)
338            }
339            0xFE => {
340                let b = self.take(4)?;
341                Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]) as u64)
342            }
343            0xFF => {
344                let b = self.take(8)?;
345                Ok(u64::from_le_bytes([
346                    b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
347                ]))
348            }
349            n => Ok(n as u64),
350        }
351    }
352}