1use crate::{
4 Cheatcode, CheatcodesCore, Result,
5 Vm::*,
6 json::{
7 check_json_key_exists, parse_json, parse_json_coerce, parse_json_coerce_default,
8 parse_json_keys, resolve_type, split_value_key,
9 },
10};
11use alloy_dyn_abi::DynSolType;
12use alloy_sol_types::SolValue;
13use foundry_common::{fmt::StructDefinitions, fs};
14use foundry_config::fs_permissions::FsAccessKind;
15use serde_json::Value as JsonValue;
16use toml::Value as TomlValue;
17use toml_edit::{DocumentMut, Item, Table, TableLike};
18
19impl Cheatcode for keyExistsTomlCall {
20 fn apply_context_free(&self) -> Result {
21 let Self { toml, key } = self;
22 check_json_key_exists(&toml_to_json_string(toml)?, key)
23 }
24}
25
26impl Cheatcode for parseToml_0Call {
27 fn apply_context(&self, state: &mut CheatcodesCore) -> Result {
28 let Self { toml } = self;
29 parse_toml(
30 toml,
31 "$",
32 state.analysis.as_ref().and_then(|analysis| analysis.struct_defs().ok()),
33 )
34 }
35}
36
37impl Cheatcode for parseToml_1Call {
38 fn apply_context(&self, state: &mut CheatcodesCore) -> Result {
39 let Self { toml, key } = self;
40 parse_toml(
41 toml,
42 key,
43 state.analysis.as_ref().and_then(|analysis| analysis.struct_defs().ok()),
44 )
45 }
46}
47
48macro_rules! impl_parse_toml {
49 ($call:ident, $call_with_default:ident, $ty:expr) => {
50 impl Cheatcode for $call {
51 fn apply_context_free(&self) -> Result {
52 let Self { toml, key } = self;
53 parse_toml_coerce(toml, key, &$ty)
54 }
55 }
56
57 impl Cheatcode for $call_with_default {
58 fn apply_context_free(&self) -> Result {
59 let Self { toml, key, defaultValue } = self;
60 parse_toml_coerce_default(toml, key, &$ty, defaultValue)
61 }
62 }
63 };
64}
65
66impl_parse_toml!(parseTomlUint_0Call, parseTomlUint_1Call, DynSolType::Uint(256));
67impl_parse_toml!(
68 parseTomlUintArray_0Call,
69 parseTomlUintArray_1Call,
70 DynSolType::Array(Box::new(DynSolType::Uint(256)))
71);
72impl_parse_toml!(parseTomlInt_0Call, parseTomlInt_1Call, DynSolType::Int(256));
73impl_parse_toml!(
74 parseTomlIntArray_0Call,
75 parseTomlIntArray_1Call,
76 DynSolType::Array(Box::new(DynSolType::Int(256)))
77);
78impl_parse_toml!(parseTomlBool_0Call, parseTomlBool_1Call, DynSolType::Bool);
79impl_parse_toml!(
80 parseTomlBoolArray_0Call,
81 parseTomlBoolArray_1Call,
82 DynSolType::Array(Box::new(DynSolType::Bool))
83);
84impl_parse_toml!(parseTomlAddress_0Call, parseTomlAddress_1Call, DynSolType::Address);
85impl_parse_toml!(
86 parseTomlAddressArray_0Call,
87 parseTomlAddressArray_1Call,
88 DynSolType::Array(Box::new(DynSolType::Address))
89);
90impl_parse_toml!(parseTomlString_0Call, parseTomlString_1Call, DynSolType::String);
91impl_parse_toml!(
92 parseTomlStringArray_0Call,
93 parseTomlStringArray_1Call,
94 DynSolType::Array(Box::new(DynSolType::String))
95);
96impl_parse_toml!(parseTomlBytes_0Call, parseTomlBytes_1Call, DynSolType::Bytes);
97impl_parse_toml!(
98 parseTomlBytesArray_0Call,
99 parseTomlBytesArray_1Call,
100 DynSolType::Array(Box::new(DynSolType::Bytes))
101);
102impl_parse_toml!(parseTomlBytes32_0Call, parseTomlBytes32_1Call, DynSolType::FixedBytes(32));
103impl_parse_toml!(
104 parseTomlBytes32Array_0Call,
105 parseTomlBytes32Array_1Call,
106 DynSolType::Array(Box::new(DynSolType::FixedBytes(32)))
107);
108
109impl Cheatcode for parseTomlType_0Call {
110 fn apply_context(&self, state: &mut CheatcodesCore) -> Result {
111 let Self { toml, typeDescription } = self;
112 parse_toml_coerce(
113 toml,
114 "$",
115 &resolve_type(
116 typeDescription,
117 state.analysis.as_ref().and_then(|analysis| analysis.struct_defs().ok()),
118 )?,
119 )
120 .map(|v| v.abi_encode())
121 }
122}
123
124impl Cheatcode for parseTomlType_1Call {
125 fn apply_context(&self, state: &mut CheatcodesCore) -> Result {
126 let Self { toml, key, typeDescription } = self;
127 parse_toml_coerce(
128 toml,
129 key,
130 &resolve_type(
131 typeDescription,
132 state.analysis.as_ref().and_then(|analysis| analysis.struct_defs().ok()),
133 )?,
134 )
135 .map(|v| v.abi_encode())
136 }
137}
138
139impl Cheatcode for parseTomlTypeArrayCall {
140 fn apply_context(&self, state: &mut CheatcodesCore) -> Result {
141 let Self { toml, key, typeDescription } = self;
142 let ty = resolve_type(
143 typeDescription,
144 state.analysis.as_ref().and_then(|analysis| analysis.struct_defs().ok()),
145 )?;
146 parse_toml_coerce(toml, key, &DynSolType::Array(Box::new(ty))).map(|v| v.abi_encode())
147 }
148}
149
150impl Cheatcode for parseTomlKeysCall {
151 fn apply_context_free(&self) -> Result {
152 let Self { toml, key } = self;
153 parse_toml_keys(toml, key)
154 }
155}
156
157impl Cheatcode for writeToml_0Call {
158 fn apply_context(&self, state: &mut CheatcodesCore) -> Result {
159 let Self { json, path } = self;
160 let value =
161 serde_json::from_str(json).unwrap_or_else(|_| JsonValue::String(json.to_owned()));
162
163 let toml_string = format_json_to_toml(value)?;
164 super::fs::write_file(state, path.as_ref(), toml_string.as_bytes())
165 }
166}
167
168impl Cheatcode for writeToml_1Call {
169 fn apply_context(&self, state: &mut CheatcodesCore) -> Result {
170 let Self { json: value, path, valueKey } = self;
171
172 let data_path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
175 let mut document = if data_path.exists() {
176 parse_toml_document(&fs::locked_read_to_string(&data_path)?)?
177 } else {
178 DocumentMut::new()
179 };
180 upsert_toml_value(&mut document, value, valueKey)?;
181
182 super::fs::write_file(state, path.as_ref(), document.to_string().as_bytes())
183 }
184}
185
186fn parse_toml_str(toml: &str) -> Result<TomlValue> {
188 toml::from_str(toml).map_err(|e| fmt_err!("failed parsing TOML: {e}"))
189}
190
191fn parse_toml(toml: &str, key: &str, struct_defs: Option<&StructDefinitions>) -> Result {
193 parse_json(&toml_to_json_string(toml)?, key, struct_defs)
194}
195
196fn parse_toml_coerce(toml: &str, key: &str, ty: &DynSolType) -> Result {
198 parse_json_coerce(&toml_to_json_string(toml)?, key, ty)
199}
200
201fn parse_toml_coerce_default<T: SolValue>(
204 toml: &str,
205 key: &str,
206 ty: &DynSolType,
207 default: &T,
208) -> Result {
209 parse_json_coerce_default(&toml_to_json_string(toml)?, key, ty, default)
210}
211
212fn parse_toml_keys(toml: &str, key: &str) -> Result {
214 parse_json_keys(&toml_to_json_string(toml)?, key)
215}
216
217fn toml_to_json_string(toml: &str) -> Result<String> {
219 let toml = parse_toml_str(toml)?;
220 let json = toml_to_json_value(toml);
221 serde_json::to_string(&json).map_err(|e| fmt_err!("failed to serialize JSON: {e}"))
222}
223
224fn format_json_to_toml(json: JsonValue) -> Result<String> {
226 let toml = json_to_toml_value(json);
227 toml::to_string_pretty(&toml).map_err(|e| fmt_err!("failed to serialize TOML: {e}"))
228}
229
230pub(super) fn toml_to_json_value(toml: TomlValue) -> JsonValue {
232 match toml {
233 TomlValue::String(s) => match s.as_str() {
234 "null" => JsonValue::Null,
235 _ => JsonValue::String(s),
236 },
237 TomlValue::Integer(i) => JsonValue::Number(i.into()),
238 TomlValue::Float(f) => match serde_json::Number::from_f64(f) {
239 Some(n) => JsonValue::Number(n),
240 None => JsonValue::String(f.to_string()),
241 },
242 TomlValue::Boolean(b) => JsonValue::Bool(b),
243 TomlValue::Array(a) => JsonValue::Array(a.into_iter().map(toml_to_json_value).collect()),
244 TomlValue::Table(t) => {
245 JsonValue::Object(t.into_iter().map(|(k, v)| (k, toml_to_json_value(v))).collect())
246 }
247 TomlValue::Datetime(d) => JsonValue::String(d.to_string()),
248 }
249}
250
251fn json_to_toml_value(json: JsonValue) -> TomlValue {
253 match json {
254 JsonValue::String(s) => TomlValue::String(s),
255 JsonValue::Number(n) => match n.as_i64() {
256 Some(i) => TomlValue::Integer(i),
257 None => match n.as_f64() {
258 Some(f) => TomlValue::Float(f),
259 None => TomlValue::String(n.to_string()),
260 },
261 },
262 JsonValue::Bool(b) => TomlValue::Boolean(b),
263 JsonValue::Array(a) => TomlValue::Array(a.into_iter().map(json_to_toml_value).collect()),
264 JsonValue::Object(o) => {
265 TomlValue::Table(o.into_iter().map(|(k, v)| (k, json_to_toml_value(v))).collect())
266 }
267 JsonValue::Null => TomlValue::String("null".to_string()),
268 }
269}
270
271fn parse_toml_document(toml: &str) -> Result<DocumentMut> {
273 toml.parse().map_err(|e| fmt_err!("failed parsing TOML: {e}"))
274}
275
276fn upsert_toml_value(document: &mut DocumentMut, value: &str, key: &str) -> Result<()> {
282 let parts = split_value_key(key)?;
283
284 if let Some((key_to_insert, path_to_parent)) = parts.split_last() {
287 let mut current_level = document.as_item_mut();
288
289 for segment in path_to_parent {
290 let is_inline = current_level.is_inline_table();
291 let Some(table) = current_level.as_table_like_mut() else {
292 return Err(fmt_err!("path segment '{segment}' does not resolve to an object."));
293 };
294 if !table.contains_key(segment) {
295 let mut intermediary = Table::new();
296 intermediary.set_implicit(true);
297 insert_toml_item(table, is_inline, segment, Item::Table(intermediary));
298 }
299 current_level = table.get_mut(segment).unwrap();
300 }
301
302 let is_inline = current_level.is_inline_table();
303 let Some(parent) = current_level.as_table_like_mut() else {
304 return Err(fmt_err!("final destination is not an object, cannot insert key."));
305 };
306
307 let value =
308 serde_json::from_str(value).unwrap_or_else(|_| JsonValue::String(value.to_owned()));
309 let mut item = json_to_toml_item(value)?;
310
311 match parent.get_mut(key_to_insert) {
314 Some(existing) if !existing.is_none() => {
315 if existing.is_value() {
316 item = item.into_value().map_or_else(|item| item, Item::Value);
318 }
319 let shape_changed =
320 std::mem::discriminant(existing) != std::mem::discriminant(&item);
321 let header_prefix =
323 tables(existing).first().and_then(|table| table.decor().prefix()).cloned();
324 match (&*existing, &mut item) {
325 (Item::Value(old), Item::Value(new)) => {
326 *new.decor_mut() = old.decor().clone();
327 }
328 (old, new) => {
329 for (old, new) in tables(old).into_iter().zip(tables_mut(new)) {
330 *new.decor_mut() = old.decor().clone();
331 new.set_position(old.position());
332 if !old.is_implicit() {
333 new.set_implicit(false);
334 }
335 }
336 }
337 }
338 let new_is_value = item.is_value();
339 *existing = item;
340 if shape_changed {
341 let mut key = parent.key_mut(key_to_insert).expect("replaced key must exist");
342 let decor = key.leaf_decor_mut();
343 decor.clear();
344 if new_is_value {
345 decor.set_suffix(" ");
346 if let Some(prefix) = header_prefix {
347 decor.set_prefix(prefix);
348 }
349 }
350 }
351 }
352 _ => insert_toml_item(parent, is_inline, key_to_insert, item),
353 }
354 }
355
356 Ok(())
357}
358
359fn insert_toml_item(parent: &mut dyn TableLike, is_inline: bool, key: &str, mut item: Item) {
364 if is_inline {
365 let trailing =
366 parent.iter_mut().filter_map(|(_, item)| item.as_value_mut()).last().and_then(|last| {
367 let suffix = last.decor().suffix().cloned();
368 last.decor_mut().set_suffix("");
369 suffix
370 });
371 item = item.into_value().map_or_else(|item| item, Item::Value);
372 if let Some(value) = item.as_value_mut() {
373 value.decor_mut().clear();
374 if let Some(trailing) = trailing {
375 value.decor_mut().set_suffix(trailing);
376 }
377 }
378 }
379 parent.insert(key, item);
380}
381
382fn json_to_toml_item(value: JsonValue) -> Result<Item> {
384 const KEY: &str = "value";
385
386 let wrapper = JsonValue::Object([(KEY.to_string(), value)].into_iter().collect());
387 let mut item = parse_toml_document(&format_json_to_toml(wrapper)?)?
388 .remove(KEY)
389 .ok_or_else(|| fmt_err!("failed to serialize TOML value"))?;
390 reset_table_layout(&mut item);
392 Ok(item)
393}
394
395fn reset_table_layout(item: &mut Item) {
397 for table in tables_mut(item) {
398 table.set_position(None);
399 table.decor_mut().clear();
400 for (_, item) in table.iter_mut() {
401 reset_table_layout(item);
402 }
403 }
404}
405
406fn tables(item: &Item) -> Vec<&Table> {
408 match item {
409 Item::Table(table) => vec![table],
410 Item::ArrayOfTables(array) => array.iter().collect(),
411 _ => Vec::new(),
412 }
413}
414
415fn tables_mut(item: &mut Item) -> Vec<&mut Table> {
417 match item {
418 Item::Table(table) => vec![table],
419 Item::ArrayOfTables(array) => array.iter_mut().collect(),
420 _ => Vec::new(),
421 }
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427
428 const CONFIG: &str = r#"# Deployment config.
429
430owner = "0x000000000000000000000000000000000000dEaD" # multisig
431deployed_at = 2024-04-27T11:57:21Z
432max_supply = 0xffff_ffff
433salt = 'literal-string'
434
435# Mainnet settings.
436[mainnet]
437token = "0x0000000000000000000000000000000000000000" # filled by script
438limits = { daily = 1_000, weekly = 5_000 }
439
440# Base settings.
441[base]
442chain_id = 8453
443"#;
444
445 fn upsert(toml: &str, value: &str, key: &str) -> Result<String> {
446 let mut document = parse_toml_document(toml)?;
447 upsert_toml_value(&mut document, value, key)?;
448 Ok(document.to_string())
449 }
450
451 #[test]
452 fn upsert_toml_keeps_comment_above_key() {
453 let toml = "key1 = \"1\"\n\n# this is key2\nkey2 = \"2\"\n";
454 assert_eq!(
455 upsert(toml, "abcd", ".key2").unwrap(),
456 "key1 = \"1\"\n\n# this is key2\nkey2 = \"abcd\"\n"
457 );
458 }
459
460 #[test]
461 fn upsert_toml_replaces_only_the_value() {
462 let address = "0x000000000000000000000000000000000000bEEF";
463 assert_eq!(
464 upsert(CONFIG, address, ".mainnet.token").unwrap(),
465 CONFIG.replace(
466 r#"token = "0x0000000000000000000000000000000000000000""#,
467 &format!(r#"token = "{address}""#)
468 )
469 );
470 assert_eq!(
471 upsert(CONFIG, "2000", ".mainnet.limits.daily").unwrap(),
472 CONFIG.replace("daily = 1_000", "daily = 2000")
473 );
474 assert_eq!(
475 upsert(CONFIG, "{\"chain_id\": 10}", "base").unwrap(),
476 CONFIG.replace("chain_id = 8453", "chain_id = 10")
477 );
478 assert_eq!(
479 upsert(CONFIG, "{\"daily\": 2000}", ".mainnet.limits").unwrap(),
480 CONFIG.replace("daily = 1_000, weekly = 5_000", "daily = 2000")
481 );
482 }
483
484 #[test]
485 fn upsert_toml_adds_keys() {
486 assert_eq!(
487 upsert(CONFIG, "30000000", ".base.gas_limit").unwrap(),
488 format!("{CONFIG}gas_limit = 30000000\n")
489 );
490 assert_eq!(
491 upsert(CONFIG, "{\"block\": 123}", ".optimism.contracts").unwrap(),
492 format!("{CONFIG}\n[optimism.contracts]\nblock = 123\n")
493 );
494 assert_eq!(
495 upsert(CONFIG, "{\"monthly\": 9}", ".mainnet.limits.extra").unwrap(),
496 CONFIG.replace("weekly = 5_000 }", "weekly = 5_000, extra = { monthly = 9 } }")
497 );
498 }
499
500 #[test]
501 fn upsert_toml_preserves_inline_table_formatting_when_adding_keys() {
502 let toml = "limits = { daily = 1_000, weekly = 5_000 }\n";
503 assert_eq!(
504 upsert(toml, "9000", ".limits.monthly").unwrap(),
505 "limits = { daily = 1_000, weekly = 5_000, monthly = 9000 }\n"
506 );
507 assert_eq!(
508 upsert(toml, "9000", ".limits.extra.monthly").unwrap(),
509 "limits = { daily = 1_000, weekly = 5_000, extra = { monthly = 9000 } }\n"
510 );
511
512 let toml = "limits={daily=1_000,weekly = 5_000}\n";
513 assert_eq!(
514 upsert(toml, "9000", ".limits.monthly").unwrap(),
515 "limits={daily=1_000,weekly = 5_000, monthly = 9000}\n"
516 );
517
518 let toml = "limits = {\n # Keep this cap conservative.\n daily = 1_000,\n weekly = 5_000,\n}\n";
519 assert_eq!(
520 upsert(toml, "9000", ".limits.monthly").unwrap(),
521 "limits = {\n # Keep this cap conservative.\n daily = 1_000,\n weekly = 5_000, monthly = 9000,\n}\n"
522 );
523 }
524
525 #[test]
526 fn upsert_toml_replaces_arrays_of_tables_in_place() {
527 let toml = "# Production RPC endpoints.\n[[rpc]]\nurl = \"old\"\n";
528 assert_eq!(
529 upsert(toml, r#"[{"url":"new"}]"#, ".rpc").unwrap(),
530 "# Production RPC endpoints.\n[[rpc]]\nurl = \"new\"\n"
531 );
532 }
533
534 #[test]
535 fn upsert_toml_keeps_comments_when_the_item_shape_changes() {
536 let toml = "# Deployment settings.\n[deployment] # maintained by the deploy script\nchain_id = 1\n";
537 assert_eq!(
538 upsert(toml, "disabled", ".deployment").unwrap(),
539 "# Deployment settings.\ndeployment = \"disabled\"\n"
540 );
541
542 let toml = "# Production RPC endpoint.\n[rpc]\nurl = \"a\"\n";
543 assert_eq!(
544 upsert(toml, r#"[{"url":"a"},{"url":"b"}]"#, ".rpc").unwrap(),
545 "# Production RPC endpoint.\n[[rpc]]\nurl = \"a\"\n\n[[rpc]]\nurl = \"b\"\n"
546 );
547
548 let toml = "# Production RPC endpoints.\nrpc = []\n";
549 assert_eq!(
550 upsert(toml, r#"[{"url":"a"},{"url":"b"}]"#, ".rpc").unwrap(),
551 "# Production RPC endpoints.\nrpc = [{ url = \"a\" }, { url = \"b\" }]\n"
552 );
553 }
554
555 #[test]
556 fn upsert_toml_formats_new_values_like_write_toml() {
557 let value = r#"{"list": ["0x01", "0x02"], "empty": {}, "nested": {"a": {"b": 1}}}"#;
558 let expected = format_json_to_toml(
559 serde_json::json!({ "new": serde_json::from_str::<JsonValue>(value).unwrap() }),
560 )
561 .unwrap();
562 assert_eq!(upsert("", value, ".new").unwrap(), expected);
563 }
564
565 #[test]
566 fn upsert_toml_errors() {
567 assert_eq!(
568 upsert(CONFIG, "1", ".owner.x").unwrap_err().to_string(),
569 "final destination is not an object, cannot insert key."
570 );
571 assert_eq!(
572 upsert(CONFIG, "1", ".owner.x.y").unwrap_err().to_string(),
573 "path segment 'x' does not resolve to an object."
574 );
575 assert_eq!(
576 upsert(CONFIG, "1", "$.").unwrap_err().to_string(),
577 "'valueKey' cannot be empty or just '$'"
578 );
579 }
580}