neo_solidity/cli/cli_parts/
cli_output.rs

1fn contract_output_prefix(base: &str, contract_name: &str, index: usize, total: usize) -> String {
2    let base_path = std::path::Path::new(base);
3    let base_is_dir = base.ends_with('/') || base.ends_with('\\') || base_path.is_dir();
4
5    let sanitized = standard_json::sanitize_contract_name(contract_name).unwrap_or_else(|| {
6        if total <= 1 {
7            "contract".to_string()
8        } else {
9            format!("contract{index}")
10        }
11    });
12
13    if base_is_dir {
14        return base_path
15            .join(sanitized)
16            .to_string_lossy()
17            .to_string();
18    }
19
20    if total <= 1 {
21        return base.to_string();
22    }
23
24    let (stem, ext) = split_extension(base);
25    if ext.is_empty() {
26        format!("{stem}-{sanitized}")
27    } else {
28        format!("{stem}-{sanitized}{ext}")
29    }
30}
31
32fn split_extension(path: &str) -> (String, String) {
33    use std::path::Path;
34
35    let p = Path::new(path);
36    let Some(file_name) = p.file_name().and_then(|name| name.to_str()) else {
37        return (path.to_string(), String::new());
38    };
39
40    let Some((file_stem, ext)) = file_name.rsplit_once('.') else {
41        return (path.to_string(), String::new());
42    };
43
44    // Treat dotfiles like `.env` as having no extension.
45    if file_stem.is_empty() {
46        return (path.to_string(), String::new());
47    }
48
49    let stem_path = match p.parent().filter(|parent| !parent.as_os_str().is_empty()) {
50        Some(parent) => parent.join(file_stem).to_string_lossy().to_string(),
51        None => file_stem.to_string(),
52    };
53
54    (stem_path, format!(".{ext}"))
55}
56
57fn ensure_output_dir(path: &str) -> Result<(), String> {
58    let Some(parent) = std::path::Path::new(path).parent() else {
59        return Ok(());
60    };
61    if parent.as_os_str().is_empty() {
62        return Ok(());
63    }
64    fs::create_dir_all(parent)
65        .map_err(|err| format!("Failed to create output directory '{}': {err}", parent.display()))
66}
67
68fn write_nef_file(
69    path: &str,
70    script: &[u8],
71    tokens: &[neo_solidity::neo::MethodToken],
72    source: &str,
73    json_warnings: bool,
74) -> Result<u32, String> {
75    // Canonicalize paths when they look like local files; leave URLs/overrides intact.
76    let resolved_source = if source.contains("://") {
77        source.to_string()
78    } else {
79        std::path::Path::new(source)
80            .canonicalize()
81            .ok()
82            .and_then(|p| p.to_str().map(str::to_string))
83            .unwrap_or_else(|| source.to_string())
84    };
85
86    let (clamped, truncated) = clamp_nef_source_with_flag(&resolved_source);
87    if truncated {
88        let msg = format!(
89            "NEF source exceeds {} bytes and was truncated",
90            NEF_SOURCE_MAX_BYTES
91        );
92        emit_warning(&msg, None, json_warnings, Some("NEF_SOURCE_TRUNCATED"));
93    }
94
95    let nef = build_nef_with_tokens(script, COMPILER_ID, clamped.as_ref(), tokens)?;
96    let checksum_offset = nef.len().saturating_sub(4);
97    let checksum = if nef.len() >= 4 {
98        let mut buf = [0u8; 4];
99        buf.copy_from_slice(&nef[checksum_offset..checksum_offset + 4]);
100        u32::from_le_bytes(buf)
101    } else {
102        0
103    };
104    ensure_output_dir(path)?;
105    fs::write(path, nef).map_err(|err| format!("Failed to write NEF file '{path}': {err}"))?;
106    Ok(checksum)
107}
108
109fn write_manifest_file(path: &str, manifest: &serde_json::Value) -> Result<(), String> {
110    let manifest_str = serde_json::to_string_pretty(manifest)
111        .map_err(|err| format!("Manifest serialization failed: {err}"))?;
112    ensure_output_dir(path)?;
113    fs::write(path, manifest_str)
114        .map_err(|err| format!("Failed to write manifest file '{path}': {err}"))?;
115    Ok(())
116}
117
118fn write_json_file(
119    path: &str,
120    script: &[u8],
121    tokens: &[neo_solidity::neo::MethodToken],
122    manifest: &serde_json::Value,
123    metadata: &ContractMetadata,
124    source: &str,
125    json_warnings: bool,
126) -> Result<(), String> {
127    // Canonicalize paths when they look like local files; leave URLs/overrides intact.
128    let resolved_source = if source.contains("://") {
129        source.to_string()
130    } else {
131        std::path::Path::new(source)
132            .canonicalize()
133            .ok()
134            .and_then(|p| p.to_str().map(str::to_string))
135            .unwrap_or_else(|| source.to_string())
136    };
137
138    let (clamped, truncated) = clamp_nef_source_with_flag(&resolved_source);
139    if truncated {
140        let msg = format!(
141            "NEF source exceeds {} bytes and was truncated",
142            NEF_SOURCE_MAX_BYTES
143        );
144        emit_warning(&msg, None, json_warnings, Some("NEF_SOURCE_TRUNCATED"));
145    }
146
147    let tokens_json: Vec<_> = tokens
148        .iter()
149        .map(|token| {
150            let hash_be = token.hash.iter().rev().copied().collect::<Vec<_>>();
151            json!({
152                "hash": format!("0x{}", hex::encode(hash_be)),
153                "method": token.method,
154                "parametersCount": token.parameters_count,
155                "hasReturnValue": token.has_return_value,
156                "callFlags": token.call_flags,
157            })
158        })
159        .collect();
160
161    let nef_bytes = build_nef_with_tokens(script, COMPILER_ID, clamped.as_ref(), tokens)?;
162    let checksum = if nef_bytes.len() >= 4 {
163        hex::encode(&nef_bytes[nef_bytes.len() - 4..])
164    } else {
165        "00000000".to_string()
166    };
167    let nef_image = hex::encode(nef_bytes);
168
169    let json_output = json!({
170        "contract": metadata.name,
171        "compiler": COMPILER_ID,
172        "author": COMPILER_EMAIL,
173        "nef": {
174            "magic": "NEF3",
175            "compiler": COMPILER_ID,
176            "version": compiler_version_string_4(),
177            "source": clamped.as_ref(),
178            "tokens": tokens_json,
179            "script": hex::encode(script),
180            "image": nef_image,
181            "checksum": checksum,
182        },
183        "manifest": manifest,
184    });
185
186    let json_str =
187        serde_json::to_string_pretty(&json_output).map_err(|err| format!("Failed to serialise JSON output: {err}"))?;
188    ensure_output_dir(path)?;
189    fs::write(path, json_str).map_err(|err| format!("Failed to write JSON file '{path}': {err}"))?;
190    Ok(())
191}
192
193fn write_assembly_file(path: &str, script: &[u8]) -> Result<(), String> {
194    let assembly = bytecode::disassemble_neovm_bytecode(script);
195    ensure_output_dir(path)?;
196    fs::write(path, assembly)
197        .map_err(|err| format!("Failed to write assembly file '{path}': {err}"))?;
198    Ok(())
199}