diff --git a/Cargo.lock b/Cargo.lock index c91a8cc2122..235fb80f453 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1932,7 +1932,7 @@ dependencies = [ "iroha_config", "iroha_crypto", "iroha_data_model", - "serde_json", + "json5", "vergen", ] @@ -1947,6 +1947,7 @@ dependencies = [ "iroha_crypto", "iroha_data_model", "iroha_primitives", + "json5", "proptest", "serde", "serde_json", @@ -1962,6 +1963,7 @@ version = "2.0.0-pre-rc.11" dependencies = [ "crossbeam", "iroha_config_derive", + "json5", "serde", "serde_json", "thiserror", @@ -2371,6 +2373,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "k256" version = "0.9.6" @@ -2839,6 +2852,50 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "pest" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc7bc69c062e492337d74d59b120c274fd3d261b6bf6d3207d499b4b379c41a" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b75706b9642ebcb34dab3bc7750f811609a0eb1dd8b88c2d15bf628c1c65b2" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f9272122f5979a6511a749af9db9bfc810393f63119970d7085fed1c4ea0db" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8717927f9b79515e565a64fe46c38b8cd0427e64c40680b14a7365ab09ac8d" +dependencies = [ + "once_cell", + "pest", + "sha1", +] + [[package]] name = "petgraph" version = "0.6.2" @@ -3610,6 +3667,17 @@ dependencies = [ "digest 0.10.3", ] +[[package]] +name = "sha1" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006769ba83e921b3085caa8334186b00cf92b4cb1a6cf4632fbccc8eff5c7549" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.3", +] + [[package]] name = "sha2" version = "0.9.9" @@ -4319,6 +4387,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +[[package]] +name = "ucd-trie" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" + [[package]] name = "unicase" version = "2.6.0" diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 570e5195573..c6dd57aea29 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -9,13 +9,15 @@ clippy::std_instead_of_core, clippy::std_instead_of_alloc )] -use std::{panic, path::PathBuf, sync::Arc}; +use std::{panic, sync::Arc}; use color_eyre::eyre::{eyre, Result, WrapErr}; +use eyre::ContextCompat; use iroha_actor::{broker::*, prelude::*}; use iroha_config::{ base::proxy::{LoadFromDisk, LoadFromEnv, Override}, iroha::{Configuration, ConfigurationProxy}, + path::Path as ConfigPath, }; use iroha_core::{ block_sync::BlockSynchronizer, @@ -50,25 +52,33 @@ pub struct Arguments { /// Set this flag on the peer that should submit genesis on the network initial start. pub submit_genesis: bool, /// Set custom genesis file path. `None` if `submit_genesis` set to `false`. - pub genesis_path: Option, + pub genesis_path: Option, /// Set custom config file path. - pub config_path: PathBuf, + pub config_path: ConfigPath, } -const CONFIGURATION_PATH: &str = "config.json"; -const GENESIS_PATH: &str = "genesis.json"; +const CONFIGURATION_PATH: &str = "config"; +const GENESIS_PATH: &str = "genesis"; const SUBMIT_GENESIS: bool = false; impl Default for Arguments { fn default() -> Self { Self { submit_genesis: SUBMIT_GENESIS, - genesis_path: Some(GENESIS_PATH.into()), - config_path: CONFIGURATION_PATH.into(), + genesis_path: Some(ConfigPath::default(GENESIS_PATH).expect(&format!( + "Default genesis path `{}` has extension, but it should not have one.", + GENESIS_PATH + ))), + config_path: ConfigPath::default(CONFIGURATION_PATH).expect(&format!( + "Default config path `{}` has extension, but it should not have one.", + GENESIS_PATH + )), } } } +pub mod config {} + /// Iroha is an [Orchestrator](https://en.wikipedia.org/wiki/Orchestration_%28computing%29) of the /// system. It configures, coordinates and manages transactions and queries processing, work of consensus and storage. pub struct Iroha { @@ -154,7 +164,13 @@ impl Iroha { query_judge: QueryJudgeBoxed, ) -> Result { let broker = Broker::new(); - let file_proxy = ConfigurationProxy::from_path(&args.config_path); + let file_proxy = ConfigurationProxy::from_path( + &args + .config_path + .first_existing_path() + .wrap_err("Configuration file does not exist")? + .as_ref(), + ); let env_proxy = ConfigurationProxy::from_env(); let config = file_proxy.override_with(env_proxy).build()?; @@ -170,7 +186,12 @@ impl Iroha { let genesis = if let Some(genesis_path) = &args.genesis_path { GenesisNetwork::from_configuration( args.submit_genesis, - RawGenesisBlock::from_path(genesis_path)?, + RawGenesisBlock::from_path( + genesis_path + .first_existing_path() + .wrap_err("Genesis block file doesn't exist")? + .as_ref(), + )?, Some(&config.genesis), &config.sumeragi.transaction_limits, ) diff --git a/cli/src/main.rs b/cli/src/main.rs index 0a3567fab6d..93635b097d4 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,15 +1,13 @@ //! Iroha peer command-line interface. -use core::str::FromStr; - use eyre::WrapErr as _; -use iroha::Arguments; +use iroha_config::path::Path as ConfigPath; use iroha_core::prelude::AllowAll; use iroha_permissions_validators::public_blockchain::default_permissions; #[tokio::main] async fn main() -> Result<(), color_eyre::Report> { - let mut args = Arguments::default(); + let mut args = iroha::Arguments::default(); if std::env::args().any(|a| is_help(&a)) { print_help(); return Ok(()); @@ -23,7 +21,7 @@ async fn main() -> Result<(), color_eyre::Report> { if std::env::args().any(|a| is_submit(&a)) { args.submit_genesis = true; if let Ok(genesis_path) = std::env::var("IROHA2_GENESIS_PATH") { - args.genesis_path = Some(std::path::PathBuf::from_str(&genesis_path)?); + args.genesis_path = Some(ConfigPath::user_provided(&genesis_path)?); } } else { args.genesis_path = None; @@ -37,7 +35,7 @@ async fn main() -> Result<(), color_eyre::Report> { } if let Ok(config_path) = std::env::var("IROHA2_CONFIG_PATH") { - args.config_path = std::path::PathBuf::from_str(&config_path)?; + args.config_path = ConfigPath::user_provided(&config_path)?; } if !args.config_path.exists() { // Require all the fields defined in default `config.json` @@ -85,12 +83,12 @@ fn print_help() { println!("pass `--version` or `-V` to print version information"); println!(); println!("Iroha 2 is configured via environment variables:"); - println!(" IROHA2_CONFIG_PATH is the location of your `config.json`"); - println!(" IROHA2_GENESIS_PATH is the location of `genesis.json`"); + println!(" IROHA2_CONFIG_PATH is the location of your `config.json` or `config.json5`"); + println!(" IROHA2_GENESIS_PATH is the location of `genesis.json` or `genesis.json5`"); println!("If either of these is not provided, Iroha checks the current directory."); println!( - "Additionally, in case of absence of both IROHA2_CONFIG_PATH and `config.json` -in the current directory, all the variables from `config.json` should be set via the environment + "Additionally, in case of absence of both IROHA2_CONFIG_PATH and `config.json`/`config.json5` +in the current directory, all the variables from `config.json`/`config.json5` should be set via the environment as follows:" ); println!(" IROHA_TORII is the torii gateway config"); diff --git a/client_cli/Cargo.toml b/client_cli/Cargo.toml index 4c192d7995f..3a0f38e0b0a 100644 --- a/client_cli/Cargo.toml +++ b/client_cli/Cargo.toml @@ -25,7 +25,7 @@ iroha_config = { version = "=2.0.0-pre-rc.11", path = "../config" } color-eyre = "0.6.2" clap = { version = "3.2.16", features = ["derive"] } dialoguer = { version = "0.10.2", default-features = false } -serde_json = "1.0.83" +json5 = "0.4.1" [build-dependencies] anyhow = "1.0.60" diff --git a/client_cli/src/main.rs b/client_cli/src/main.rs index 5ea2c2a70e5..4d09c3c2bfe 100644 --- a/client_cli/src/main.rs +++ b/client_cli/src/main.rs @@ -13,7 +13,7 @@ use std::{ fmt, - fs::{read as read_file, File}, + fs::{self, read as read_file}, io::stdin, str::FromStr, time::Duration, @@ -21,12 +21,12 @@ use std::{ use clap::StructOpt; use color_eyre::{ - eyre::{Error, WrapErr}, + eyre::{ContextCompat as _, Error, WrapErr}, Result, }; use dialoguer::Confirm; use iroha_client::client::Client; -use iroha_config::client::Configuration as ClientConfiguration; +use iroha_config::{client::Configuration as ClientConfiguration, path::Path as ConfigPath}; use iroha_crypto::prelude::*; use iroha_data_model::prelude::*; @@ -48,8 +48,8 @@ impl FromStr for Metadata { } let err_msg = format!("Failed to open the metadata file {}.", &file); let deser_err_msg = format!("Failed to deserialize metadata from file: {}", &file); - let file = File::open(file).wrap_err(err_msg)?; - let metadata: UnlimitedMetadata = serde_json::from_reader(file).wrap_err(deser_err_msg)?; + let content = fs::read_to_string(file).wrap_err(err_msg)?; + let metadata: UnlimitedMetadata = json5::from_str(&content).wrap_err(deser_err_msg)?; Ok(Self(metadata)) } } @@ -63,8 +63,8 @@ impl FromStr for Configuration { fn from_str(file: &str) -> Result { let deser_err_msg = format!("Failed to decode config file {} ", &file); let err_msg = format!("Failed to open config file {}", &file); - let file = File::open(file).wrap_err(err_msg)?; - let cfg = serde_json::from_reader(file).wrap_err(deser_err_msg)?; + let content = fs::read_to_string(file).wrap_err(err_msg)?; + let cfg = json5::from_str(&content).wrap_err(deser_err_msg)?; Ok(Self(cfg)) } } @@ -106,7 +106,7 @@ pub enum Subcommand { Wasm(wasm::Args), /// The subcommand related to block streaming Blocks(blocks::Args), - /// The subcommand related to multi-instructions as Json + /// The subcommand related to multi-instructions as Json or Json5 Json(json::Args), } @@ -147,7 +147,17 @@ fn main() -> Result<()> { let config = if let Some(config) = config_opt { config } else { - Configuration::from_str("config.json")? + let config_path = ConfigPath::default("config") + .expect("Never fails, because default `config` path has no extensions"); + #[allow(clippy::expect_used)] + Configuration::from_str( + config_path + .first_existing_path() + .wrap_err("Configuration file does not exist")? + .as_ref() + .to_string_lossy() + .as_ref(), + )? }; let Configuration(config) = config; println!( @@ -157,7 +167,7 @@ fn main() -> Result<()> { #[cfg(debug_assertions)] eprintln!( "{}", - &serde_json::to_string(&config).wrap_err("Failed to serialize configuration.")? + &json5::to_string(&config).wrap_err("Failed to serialize configuration.")? ); #[cfg(not(debug_assertions))] eprintln!("This is a release build, debug information omitted from messages"); @@ -317,7 +327,7 @@ mod domain { /// Domain name as double-quoted string #[structopt(short, long)] pub id: DomainId, - /// The JSON file with key-value metadata pairs + /// The JSON/JSON5 file with key-value metadata pairs #[structopt(short, long, default_value = "")] pub metadata: super::Metadata, } @@ -356,7 +366,7 @@ mod domain { } mod account { - use std::{fmt::Debug, fs::File}; + use std::fmt::Debug; use iroha_client::client; @@ -440,9 +450,8 @@ mod account { let err_msg = format!("Failed to open the signature condition file {}", &s); let deser_err_msg = format!("Failed to deserialize signature condition from file {}", &s); - let file = File::open(s).wrap_err(err_msg)?; - let condition: Box = - serde_json::from_reader(file).wrap_err(deser_err_msg)?; + let content = fs::read_to_string(s).wrap_err(err_msg)?; + let condition: Box = json5::from_str(&content).wrap_err(deser_err_msg)?; Ok(Self(SignatureCheckCondition(EvaluatesTo::new_unchecked( condition, )))) @@ -454,7 +463,7 @@ mod account { pub struct SignatureCondition { /// Signature condition file pub condition: Signature, - /// The JSON file with key-value metadata pairs + /// The JSON/JSON5 file with key-value metadata pairs #[structopt(short, long, default_value = "")] pub metadata: super::Metadata, } @@ -498,10 +507,10 @@ mod account { /// Account id #[structopt(short, long)] pub id: ::Id, - /// The JSON file with a permission token + /// The JSON/JSON5 file with a permission token #[structopt(short, long)] pub permission: Permission, - /// The JSON file with key-value metadata pairs + /// The JSON/JSON5 file with key-value metadata pairs #[structopt(short, long, default_value = "")] pub metadata: super::Metadata, } @@ -514,13 +523,12 @@ mod account { type Err = Error; fn from_str(s: &str) -> Result { - let file = File::open(s) - .wrap_err(format!("Failed to open the permission token file {}", &s))?; - let permission_token: PermissionToken = - serde_json::from_reader(file).wrap_err(format!( - "Failed to deserialize the permission token from file {}", - &s - ))?; + let content = fs::read_to_string(s) + .wrap_err(format!("Failed to read the permission token file {}", &s))?; + let permission_token: PermissionToken = json5::from_str(&content).wrap_err(format!( + "Failed to deserialize the permission token from file {}", + &s + ))?; Ok(Self(permission_token)) } } @@ -600,7 +608,7 @@ mod asset { /// Value type stored in asset #[structopt(short, long)] pub value_type: AssetValueType, - /// /// The JSON file with key-value metadata pairs + /// The JSON/JSON5 file with key-value metadata pairs #[structopt(short, long, default_value = "")] pub metadata: super::Metadata, } @@ -639,7 +647,7 @@ mod asset { /// Quantity to mint #[structopt(short, long)] pub quantity: u32, - /// /// The JSON file with key-value metadata pairs + /// The JSON/JSON5 file with key-value metadata pairs #[structopt(short, long, default_value = "")] pub metadata: super::Metadata, } @@ -677,7 +685,7 @@ mod asset { /// Quantity of asset as number #[structopt(short, long)] pub quantity: u32, - /// /// The JSON file with key-value metadata pairs + /// The JSON/JSON5 file with key-value metadata pairs #[structopt(short, long, default_value = "")] pub metadata: super::Metadata, } @@ -777,7 +785,7 @@ mod peer { /// Public key of the peer #[structopt(short, long)] pub key: PublicKey, - /// /// The JSON file with key-value metadata pairs + /// The JSON/JSON5 file with key-value metadata pairs #[structopt(short, long, default_value = "")] pub metadata: super::Metadata, } @@ -803,7 +811,7 @@ mod peer { /// Public key of the peer #[structopt(short, long)] pub key: PublicKey, - /// /// The JSON file with key-value metadata pairs + /// The JSON/JSON5 file with key-value metadata pairs #[structopt(short, long, default_value = "")] pub metadata: super::Metadata, } @@ -858,7 +866,7 @@ mod wasm { } mod json { - use std::io::BufReader; + use std::io::{BufReader, Read as _}; use super::*; @@ -868,8 +876,12 @@ mod json { impl RunArgs for Args { fn run(self, cfg: &ClientConfiguration) -> Result<()> { - let reader = BufReader::new(stdin()); - let instructions: Vec = serde_json::from_reader(reader)?; + let mut reader = BufReader::new(stdin()); + let mut raw_content = Vec::new(); + reader.read_to_end(&mut raw_content)?; + + let content = String::from_utf8(raw_content)?; + let instructions: Vec = json5::from_str(&content)?; submit(instructions, cfg, UnlimitedMetadata::new()) .wrap_err("Failed to submit parsed instructions") } diff --git a/config/Cargo.toml b/config/Cargo.toml index d1af9dd1ec1..b2c08062d4e 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -16,6 +16,7 @@ url = { version = "2.2.2", features = ["serde"] } serde = { version = "1.0.142", default-features = false, features = ["derive"] } serde_json = "1.0.83" +json5 = "0.4.1" thiserror = "1.0.32" crossbeam = "0.8.2" derive_more = "0.99.17" diff --git a/config/base/Cargo.toml b/config/base/Cargo.toml index e3811016594..66ebc49acca 100644 --- a/config/base/Cargo.toml +++ b/config/base/Cargo.toml @@ -9,5 +9,6 @@ iroha_config_derive = { path = "derive" } serde = { version = "1.0.142", default-features = false, features = ["derive"] } serde_json = "1.0.83" +json5 = "0.4.1" thiserror = "1.0.32" crossbeam = "0.8.2" diff --git a/config/base/derive/src/proxy.rs b/config/base/derive/src/proxy.rs index 18569e34e83..9b710e25a68 100644 --- a/config/base/derive/src/proxy.rs +++ b/config/base/derive/src/proxy.rs @@ -96,9 +96,12 @@ pub fn impl_load_from_env(ast: &StructWithFields) -> TokenStream { let inner = if is_string { quote! { Ok(var) } } else if as_str_attr { - quote! { serde_json::from_value(var.into()).map_err(#err_variant) } + quote! {{ + let value: ::serde_json::Value = var.into(); + ::json5::from_str(&value.to_string()).map_err(#err_variant) + }} } else { - quote! { serde_json::from_str(&var).map_err(#err_variant) } + quote! { ::json5::from_str(&var).map_err(#err_variant) } }; let mut set_field = quote! { let #ident = std::env::var(#field_env) @@ -165,7 +168,7 @@ pub fn impl_load_from_disk(ast: &StructWithFields) -> TokenStream { }) .and_then( |s| -> ::core::result::Result { - serde_json::from_str(&s).map_err(#serde_err_variant) + json5::from_str(&s).map_err(#serde_err_variant) }, ) .map_or(#none_proxy, ::std::convert::identity); diff --git a/config/base/src/lib.rs b/config/base/src/lib.rs index aeb72208dcf..f1649a61fa1 100644 --- a/config/base/src/lib.rs +++ b/config/base/src/lib.rs @@ -334,7 +334,7 @@ pub mod derive { /// Used in [`LoadFromDisk`](`crate::proxy::LoadFromDisk`) trait for deserialization errors #[error("Deserializing JSON failed: {0}")] #[serde(skip)] - SerdeError(#[from] serde_json::Error), + SerdeError(#[from] json5::Error), } impl Error { diff --git a/config/src/lib.rs b/config/src/lib.rs index 6810dfba65a..99b164bb7b4 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -10,6 +10,7 @@ pub mod iroha; pub mod kura; pub mod logger; pub mod network; +pub mod path; pub mod queue; pub mod sumeragi; pub mod telemetry; diff --git a/config/src/path.rs b/config/src/path.rs new file mode 100644 index 00000000000..22e34112f7c --- /dev/null +++ b/config/src/path.rs @@ -0,0 +1,95 @@ +//! Module with configuration path related structures. + +use std::{borrow::Cow, path::PathBuf}; + +use InnerPath::*; + +pub const ALLOWED_CONFIG_EXTENSIONS: [&str; 2] = ["json", "json5"]; + +/// Error type for [`Path`]. +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error { + #[error( + "Provided config file has no extension, allowed extensions are: {:?}.", + ALLOWED_CONFIG_EXTENSIONS + )] + UserProvidedConfigFileHasNoExtension, + #[error( + "Provided config file has invalid extension `{0}`, \ + allowed extensions are: {:?}.", + ALLOWED_CONFIG_EXTENSIONS + )] + InvalidExtensionError(String), + #[error("Provided by default config file has extension when it should not have one.")] + DefaultConfigFileHasExtension, +} + +pub type Result = std::result::Result; + +/// Inner helper struct. +/// +/// With this struct, we force to use [`Path`]'s constructors instead of constructing it directly. +#[derive(Debug, Clone)] +enum InnerPath { + Default(PathBuf), + UserProvided(PathBuf), +} + +/// Wrapper around path to config file (i.e. config.json, genesis.json). +/// +/// Provides abstraction above user-provided config and default ones. +#[derive(Debug, Clone)] +pub struct Path(InnerPath); + +impl Path { + /// Construct new [`Path`] from the default `path`. + /// + /// `path` should not contain any extension. + pub fn default(path: impl Into) -> Result { + let path = path.into(); + + if path.extension().is_some() { + return Err(Error::DefaultConfigFileHasExtension); + } + + Ok(Self(Default(path))) + } + + /// Construct new [`Path`] from user-provided `path`. + /// + /// `path` should contain one of the allowed extensions. + pub fn user_provided(path: impl Into) -> Result { + let path = path.into(); + + let extension = path + .extension() + .ok_or_else(|| Error::UserProvidedConfigFileHasNoExtension)? + .to_string_lossy(); + if !ALLOWED_CONFIG_EXTENSIONS.contains(&extension.as_ref()) { + return Err(Error::InvalidExtensionError(extension.into_owned())); + } + + Ok(Self(UserProvided(path))) + } + + /// Try to get first existing path by applying possible extensions if there are any. + pub fn first_existing_path(&self) -> Option> { + match &self.0 { + Default(path) => ALLOWED_CONFIG_EXTENSIONS.iter().find_map(|extension| { + let path_ext = path.with_extension(extension); + path_ext.exists().then_some(Cow::Owned(path_ext)) + }), + UserProvided(path) => path.exists().then_some(Cow::Borrowed(&path)), + } + } + + /// Check if config path exists by applying allowed extensions if there are any. + pub fn exists(&self) -> bool { + match &self.0 { + Default(path) => ALLOWED_CONFIG_EXTENSIONS + .iter() + .any(|extension| path.with_extension(extension).exists()), + UserProvided(path) => path.exists(), + } + } +}