neo_solidity/cli/cli_parts/
cli_output.rs1fn 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 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 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 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}