From f749a47804cd296a3059cd3f8079c62dbaa5fdc0 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Fri, 7 Aug 2026 07:26:48 +0900 Subject: refactor: merge split inherent impl blocks into one per type Enable clippy::multiple_inherent_impl and fix the 21 sites it reports. Types whose inherent methods were spread across two or three impl blocks now keep them in a single block; only the impl headers move, no method bodies change. Co-Authored-By: Claude Opus 5 (1M context) --- crates/shirabe/src/command/config_command.rs | 2188 +++++++++++++------------- 1 file changed, 1092 insertions(+), 1096 deletions(-) (limited to 'crates/shirabe/src/command/config_command.rs') diff --git a/crates/shirabe/src/command/config_command.rs b/crates/shirabe/src/command/config_command.rs index c4409f20..2727f36b 100644 --- a/crates/shirabe/src/command/config_command.rs +++ b/crates/shirabe/src/command/config_command.rs @@ -61,15 +61,7 @@ impl ConfigCommand { "suggest", "extra", ]; -} - -impl Default for ConfigCommand { - fn default() -> Self { - Self::new() - } -} -impl ConfigCommand { pub fn new() -> Self { let command = ConfigCommand { base_command_data: BaseCommandData::new(None), @@ -84,652 +76,1018 @@ impl ConfigCommand { .expect("ConfigCommand::configure uses static, valid metadata"); command } -} - -impl Command for ConfigCommand { - fn configure(&self) -> anyhow::Result<()> { - self.set_name("config")?; - self.set_description("Sets config options"); - self.set_definition(&[ - InputOption::new("global", Some(PhpMixed::String("g".to_string())), Some(InputOption::VALUE_NONE), "Apply command to the global config file", None).unwrap().into(), - InputOption::new("editor", Some(PhpMixed::String("e".to_string())), Some(InputOption::VALUE_NONE), "Open editor", None).unwrap().into(), - InputOption::new("auth", Some(PhpMixed::String("a".to_string())), Some(InputOption::VALUE_NONE), "Affect auth config file (only used for --editor)", None).unwrap().into(), - InputOption::new("unset", None, Some(InputOption::VALUE_NONE), "Unset the given setting-key", None).unwrap().into(), - InputOption::new("list", Some(PhpMixed::String("l".to_string())), Some(InputOption::VALUE_NONE), "List configuration settings", None).unwrap().into(), - InputOption::new("file", Some(PhpMixed::String("f".to_string())), Some(InputOption::VALUE_REQUIRED), "If you want to choose a different composer.json or config.json", None).unwrap().into(), - InputOption::new("absolute", None, Some(InputOption::VALUE_NONE), "Returns absolute paths when fetching *-dir config values instead of relative", None).unwrap().into(), - InputOption::new("json", Some(PhpMixed::String("j".to_string())), Some(InputOption::VALUE_NONE), "JSON decode the setting value, to be used with extra.* keys", None).unwrap().into(), - InputOption::new("merge", Some(PhpMixed::String("m".to_string())), Some(InputOption::VALUE_NONE), "Merge the setting value with the current value, to be used with extra.* or audit.ignore[-abandoned] keys in combination with --json", None).unwrap().into(), - InputOption::new("append", None, Some(InputOption::VALUE_NONE), "When adding a repository, append it (lowest priority) to the existing ones instead of prepending it (highest priority)", None).unwrap().into(), - InputOption::new("source", None, Some(InputOption::VALUE_NONE), "Display where the config value is loaded from", None).unwrap().into(), - InputArgument::new5("setting-key", None, "Setting key", None, self.suggest_setting_keys()).unwrap().into(), - InputArgument::new("setting-value", Some(InputArgument::IS_ARRAY), "Setting value", None).unwrap().into(), - ]); - self.set_help( - "This command allows you to edit composer config settings and repositories\n\ - in either the local composer.json file or the global config.json file.\n\n\ - Additionally it lets you edit most properties in the local composer.json.\n\n\ - To set a config setting:\n\n\ - \t%command.full_name% bin-dir bin/\n\n\ - To read a config setting:\n\n\ - \t%command.full_name% bin-dir\n\ - \tOutputs: bin\n\n\ - To edit the global config.json file:\n\n\ - \t%command.full_name% --global\n\n\ - To add a repository:\n\n\ - \t%command.full_name% repositories.foo vcs https://bar.com\n\n\ - To remove a repository (repo is a short alias for repositories):\n\n\ - \t%command.full_name% --unset repo.foo\n\n\ - To disable packagist.org:\n\n\ - \t%command.full_name% repo.packagist.org false\n\n\ - You can alter repositories in the global config.json file by passing in the\n\ - --global option.\n\n\ - To add or edit suggested packages you can use:\n\n\ - \t%command.full_name% suggest.package reason for the suggestion\n\n\ - To add or edit extra properties you can use:\n\n\ - \t%command.full_name% extra.property value\n\n\ - Or to add a complex value you can use json with:\n\n\ - \t%command.full_name% extra.property --json '{\"foo\":true, \"bar\": []}'\n\n\ - To edit the file in an external editor:\n\n\ - \t%command.full_name% --editor\n\n\ - To choose your editor you can set the \"EDITOR\" env variable.\n\n\ - To get a list of configuration values in the file:\n\n\ - \t%command.full_name% --list\n\n\ - You can always pass more than one option. As an example, if you want to edit the\n\ - global config.json file.\n\n\ - \t%command.full_name% --editor --global\n\n\ - Read more at https://getcomposer.org/doc/03-cli.md#config", - ); - Ok(()) - } - fn initialize( + pub(crate) fn handle_single_value( &self, - input: std::rc::Rc>, - output: std::rc::Rc>, + key: &str, + callbacks: &(ValidatorFn, NormalizerFn), + values: &[String], + method: &str, ) -> anyhow::Result<()> { - ::initialize( - self, - input.clone(), - output, - )?; + let (validator, normalizer) = callbacks; + if 1 != values.len() { + return Err(RuntimeException { + message: "You can only pass one value. Example: shirabe config process-timeout 300" + .to_string(), + code: 0, + } + .into()); + } - let config = self.config.borrow().as_ref().unwrap().clone(); - let auth_config_file = self.get_auth_config_file(input.clone(), &config.borrow())?; + let validation = validator(&PhpMixed::String(values[0].clone())); + if validation.as_bool() != Some(true) { + let suffix = if !validation.is_null() && validation.as_bool() != Some(false) { + format!(" ({})", validation.as_string().unwrap_or("")) + } else { + String::new() + }; + return Err(RuntimeException { + message: format!("\"{}\" is an invalid value{}", values[0].clone(), suffix), + code: 0, + } + .into()); + } - let auth_config_file_jf = std::rc::Rc::new(std::cell::RefCell::new(JsonFile::new( - auth_config_file, - None, - Some(self.get_io().clone()), - )?)); - *self.auth_config_file.borrow_mut() = Some(auth_config_file_jf.clone()); - *self.auth_config_source.borrow_mut() = - Some(JsonConfigSource::new(auth_config_file_jf, true)); + let normalized_value = normalizer(&PhpMixed::String(values[0].clone())); - // Initialize the global file if it's not there, ignoring any warnings or notices - let auth_config_file = self.auth_config_file.borrow().as_ref().unwrap().clone(); - if input.borrow().get_option("global")?.as_bool() == Some(true) - && !auth_config_file.borrow().exists() - { - touch(auth_config_file.borrow().get_path()); - let mut empty_objs: IndexMap = IndexMap::new(); - for k in &[ - "bitbucket-oauth", - "github-oauth", - "gitlab-oauth", - "gitlab-token", - "http-basic", - "bearer", - "forgejo-token", - ] { - empty_objs.insert(k.to_string(), PhpMixed::Object(IndexMap::new())); + if key == "disable-tls" { + let config = self.config.borrow().as_ref().unwrap().clone(); + if !normalized_value.as_bool().unwrap_or(false) + && config + .borrow() + .get("disable-tls") + .as_bool() + .unwrap_or(false) + { + self.get_io().write_error( + "You are now running Composer with SSL/TLS protection enabled.", + ); + } else if normalized_value.as_bool().unwrap_or(false) + && !config + .borrow() + .get("disable-tls") + .as_bool() + .unwrap_or(false) + { + self.get_io().write_error("You are now running Composer with SSL/TLS protection disabled."); } - auth_config_file - .borrow() - .write(PhpMixed::Array(empty_objs))?; - let path_clone = auth_config_file.borrow().get_path().to_string(); - Silencer::call(|| { - shirabe_php_shim::chmod(&path_clone, 0o600); - Ok(()) - }); + } + + let mut config_source = self.config_source.borrow_mut(); + let config_source = config_source.as_mut().unwrap(); + match method { + "addConfigSetting" => config_source.add_config_setting(key, normalized_value)?, + "addProperty" => config_source.add_property(key, normalized_value)?, + _ => unreachable!(), } Ok(()) } - fn execute( + pub(crate) fn handle_multi_value( &self, - input: std::rc::Rc>, - output: std::rc::Rc>, - ) -> anyhow::Result { - // Open file in editor - if input.borrow().get_option("editor")?.as_bool() == Some(true) { - let mut editor = Platform::get_env("EDITOR"); - if editor.is_none() || editor.as_deref() == Some("") { - if Platform::is_windows() { - editor = Some("notepad".to_string()); - } else { - for candidate in &["editor", "vim", "vi", "nano", "pico", "ed"] { - if !exec(&format!("which {}", candidate), None, None) - .unwrap_or_default() - .is_empty() - { - editor = Some(candidate.to_string()); - break; - } - } - } - } else { - editor = Some(escapeshellcmd(&editor.unwrap())); - } - - let file = if input.borrow().get_option("auth")?.as_bool() == Some(true) { - self.auth_config_file - .borrow() - .as_ref() - .unwrap() - .borrow() - .get_path() - .to_string() + key: &str, + callbacks: &(ValidatorFn, NormalizerFn), + values: &[String], + method: &str, + ) -> anyhow::Result<()> { + let (validator, normalizer) = callbacks; + let values_mixed = + PhpMixed::List(values.iter().map(|s| PhpMixed::String(s.clone())).collect()); + let validation = validator(&values_mixed); + if validation.as_bool() != Some(true) { + let suffix = if !validation.is_null() && validation.as_bool() != Some(false) { + format!(" ({})", validation.as_string().unwrap_or("")) } else { - self.config_file - .borrow() - .as_ref() - .unwrap() - .borrow() - .get_path() - .to_string() + String::new() }; - system( - &format!( - "{} {}{}", - editor.unwrap_or_default(), - file, - if Platform::is_windows() { - "" - } else { - " > `tty`" - } + return Err(RuntimeException { + message: format!( + "{} is an invalid value{}", + PhpMixed::from(json_encode(&values_mixed).ok()), + suffix ), - None, - ); + code: 0, + } + .into()); + } - return Ok(0); + let mut config_source = self.config_source.borrow_mut(); + let config_source = config_source.as_mut().unwrap(); + match method { + "addConfigSetting" => { + config_source.add_config_setting(key, normalizer(&values_mixed))? + } + "addProperty" => config_source.add_property(key, normalizer(&values_mixed))?, + _ => unreachable!(), } + Ok(()) + } - let config = self.config.borrow().as_ref().unwrap().clone(); - let config_file = self.config_file.borrow().as_ref().unwrap().clone(); - let auth_config_file = self.auth_config_file.borrow().as_ref().unwrap().clone(); - if input.borrow().get_option("global")?.as_bool() != Some(true) { - let config_read = config_file.borrow_mut().read()?; - let config_map = match config_read { - PhpMixed::Array(m) => m, - _ => IndexMap::new(), + /// Display the contents of the file in a pretty formatted way + pub(crate) fn list_configuration( + &self, + contents: PhpMixed, + raw_contents: PhpMixed, + output: std::rc::Rc>, + k: Option, + show_source: bool, + ) { + let orig_k = k.clone(); + let contents_arr = contents.as_array().cloned().unwrap_or_default(); + let raw_contents_arr = raw_contents.as_array().cloned().unwrap_or_default(); + let mut k = k; + for (key, value) in &contents_arr { + if k.is_none() && !matches!(key.as_str(), "config" | "repositories") { + continue; + } + + let raw_val = raw_contents_arr.get(key).cloned().unwrap_or(PhpMixed::Null); + + let value_inner = value.clone(); + + if is_array(&value_inner) + && (!is_numeric(&key_first_key(&value_inner).unwrap_or_default().into()) + || (key == "repositories" && k.is_none())) + { + let mut new_k = k.clone().unwrap_or_default(); + new_k.push_str(&Preg::replace( + php_regex!("{^config\\.}"), + "", + &format!("{}.", key), + )); + k = Some(new_k); + self.list_configuration( + value_inner, + raw_val, + output.clone(), + k.clone(), + show_source, + ); + k = orig_k.clone(); + + continue; + } + + let value_display: String = if is_array(&value_inner) { + let arr_strs: Vec = value_inner + .as_list() + .map(|l| { + l.iter() + .map(|val| { + if is_array(val) { + json_encode(val).unwrap_or_default() + } else { + val.as_string().unwrap_or("").to_string() + } + }) + .collect::>() + }) + .unwrap_or_default(); + format!("[{}]", implode(", ", &arr_strs)) + } else if is_bool(&value_inner) { + var_export(&value_inner, true) + } else { + value_inner.as_string().unwrap_or("").to_string() }; - let config_file_path = config_file.borrow().get_path().to_string(); - config.borrow_mut().merge(&config_map, &config_file_path); - let auth_data: PhpMixed = if auth_config_file.borrow().exists() { - auth_config_file.borrow_mut().read()? + + let source = if show_source { + format!( + " ({})", + self.config + .borrow() + .as_ref() + .unwrap() + .borrow_mut() + .get_source_of_value(&format!("{}{}", k.clone().unwrap_or_default(), key)) + ) } else { - PhpMixed::Array(IndexMap::new()) + String::new() }; - let mut wrap: IndexMap = IndexMap::new(); - wrap.insert("config".to_string(), auth_data); - let auth_config_file_path = auth_config_file.borrow().get_path().to_string(); - config.borrow_mut().merge(&wrap, &auth_config_file_path); - } - { - let config_rc = config.clone(); - self.get_io() - .borrow_mut() - .load_configuration(&mut config_rc.borrow_mut())?; + let link: String = + if k.is_some() && strpos(k.as_ref().unwrap(), "repositories") == Some(0) { + "https://getcomposer.org/doc/05-repositories.md".to_string() + } else { + let id_source = if k.as_deref() == Some("") || k.is_none() { + key.clone() + } else { + k.clone().unwrap() + }; + let id = Preg::replace(php_regex!("{\\..*$}"), "", &id_source); + let id = Preg::replace( + php_regex!("{[^a-z0-9]}i"), + "-", + &strtolower(&shirabe_php_shim::trim(&id, Some(" \t\n\r\0\u{0B}"))), + ); + let id = Preg::replace(php_regex!("{-+}"), "-", &id); + format!("https://getcomposer.org/doc/06-config.md#{}", id) + }; + if is_string(&raw_val) + && raw_val + .as_string() + .map(|s| s.to_string()) + .unwrap_or_default() + != value_display + { + self.get_io().write3( + &format!( + "[{}{}] {} ({}){}", + link, + k.clone().unwrap_or_default(), + key, + raw_val.as_string().unwrap_or(""), + value_display, + source + ), + true, + io_interface::QUIET, + ); + } else { + self.get_io().write3( + &format!( + "[{}{}] {}{}", + link, + k.clone().unwrap_or_default(), + key, + value_display, + source + ), + true, + io_interface::QUIET, + ); + } } + } - // List the configuration of the file settings - if input.borrow().get_option("list")?.as_bool() == Some(true) { - let all_map = config.borrow_mut().all(0)?; - let raw_map = config.borrow().raw(); - let to_mixed = |m: IndexMap| -> PhpMixed { - PhpMixed::Array(m.into_iter().collect()) - }; - self.list_configuration( - to_mixed(all_map), - to_mixed(raw_map), - output, - None, - input.borrow().get_option("source")?.as_bool() == Some(true), - ); + /// Suggest setting-keys, while taking given options in account. + fn suggest_setting_keys(&self) -> crate::console::input::SuggestedValues { + crate::console::input::SuggestedValues::Closure(Box::new(|this, input, _suggestions| { + if input.get_option("list")?.to_bool() + || input.get_option("editor")?.to_bool() + || input.get_option("auth")?.to_bool() + { + return Ok(vec![]); + } - return Ok(0); - } + let this = this + .as_any() + .downcast_ref::() + .expect("suggestSettingKeys is bound to ConfigCommand"); + // PHP passes the CompletionInput itself; the accessors only read from it, so a + // clone behind a fresh handle is equivalent. + let input_handle: std::rc::Rc< + std::cell::RefCell< + dyn shirabe_external_packages::symfony::console::input::InputInterface, + >, + > = std::rc::Rc::new(std::cell::RefCell::new(input.clone())); - let setting_key_arg = input.borrow().get_argument("setting-key")?; - let setting_key = match setting_key_arg.as_string() { - Some(s) => s.to_string(), - None => return Ok(0), - }; + // initialize configuration + let mut config = Factory::create_config(None, None)?; - // If the user enters in a config variable, parse it and save to file - let setting_values_raw = input.borrow().get_argument("setting-value")?; - let setting_values: Vec = setting_values_raw - .as_list() - .map(|l| { - l.iter() - .filter_map(|v| v.as_string().map(|s| s.to_string())) - .collect() - }) - .unwrap_or_default(); - if !setting_values.is_empty() && input.borrow().get_option("unset")?.as_bool() == Some(true) - { - return Err(RuntimeException { - message: "You can not combine a setting value with --unset".to_string(), - code: 0, + // load configuration + let config_file = JsonFile::new( + this.get_composer_config_file(input_handle.clone(), &config)?, + None, + None, + )?; + if config_file.exists() { + let path = config_file.get_path().to_string(); + let data = config_file.read()?.as_array().cloned().unwrap_or_default(); + config.merge(&data, &path); } - .into()); - } - // show the value if no value is provided - if setting_values.is_empty() && input.borrow().get_option("unset")?.as_bool() != Some(true) - { - let properties: Vec<&'static str> = Self::CONFIGURABLE_PACKAGE_PROPERTIES.to_vec(); - let mut properties_defaults: IndexMap = IndexMap::new(); - properties_defaults.insert("type".to_string(), PhpMixed::String("library".to_string())); - properties_defaults.insert("description".to_string(), PhpMixed::String(String::new())); - properties_defaults.insert("homepage".to_string(), PhpMixed::String(String::new())); - properties_defaults.insert( - "minimum-stability".to_string(), - PhpMixed::String("stable".to_string()), - ); - properties_defaults.insert("prefer-stable".to_string(), PhpMixed::Bool(false)); - properties_defaults.insert("keywords".to_string(), PhpMixed::List(vec![])); - properties_defaults.insert("license".to_string(), PhpMixed::List(vec![])); - properties_defaults.insert("suggest".to_string(), PhpMixed::List(vec![])); - properties_defaults.insert("extra".to_string(), PhpMixed::List(vec![])); - let raw_data = config_file.borrow_mut().read()?; - let data = config.borrow_mut().all(0)?; - let mut source = config.borrow_mut().get_source_of_value(&setting_key); + // load auth-configuration + let auth_config_file = JsonFile::new( + this.get_auth_config_file(input_handle.clone(), &config)?, + None, + None, + )?; + if auth_config_file.exists() { + let path = auth_config_file.get_path().to_string(); + let mut data = IndexMap::new(); + data.insert("config".to_string(), auth_config_file.read()?); + config.merge(&data, &path); + } - let mut value: PhpMixed; - let mut matches: IndexMap = IndexMap::new(); - if Preg::is_match3( - php_regex!("/^repos?(?:itories)?(?:\\.(.+))?/"), - &setting_key, - Some(&mut matches), - ) { - if matches.get(&CaptureKey::ByIndex(1)).is_none() { - value = data - .get("repositories") - .cloned() - .unwrap_or_else(|| PhpMixed::Array(IndexMap::new())); - } else { - let repo_key = matches - .get(&CaptureKey::ByIndex(1)) - .cloned() - .unwrap_or_default(); - let repos = data.get("repositories").cloned(); - value = match repos - .as_ref() - .and_then(|r| r.as_array().and_then(|a| a.get(&repo_key))) - { - Some(v) => v.clone(), - None => { - return Err(InvalidArgumentException { - message: format!("There is no {} repository defined", repo_key), - code: 0, - } - .into()); - } - }; - } - } else if strpos(&setting_key, ".").is_some() { - let bits = explode(".", &setting_key); - // PHP: $data here is the mixed dot-segment cursor; the rest of the loop walks it. - let mut cursor: PhpMixed = if bits[0] == "extra" || bits[0] == "suggest" { - PhpMixed::Array(raw_data.as_array().cloned().unwrap_or_else(IndexMap::new)) - } else { - data.get("config").cloned().unwrap_or(PhpMixed::Null) - }; - let mut r#match = false; - let mut key_acc: Option = None; - for bit in &bits { - let new_key = match &key_acc { - Some(k) => format!("{}.{}", k, bit), - None => bit.clone(), - }; - key_acc = Some(new_key.clone()); - r#match = false; - if let Some(arr) = cursor.as_array() - && let Some(v) = arr.get(&new_key) - { - r#match = true; - cursor = v.clone(); - key_acc = None; - } - } - - if !r#match { - return Err(RuntimeException { - message: format!("{} is not defined.", setting_key), - code: 0, - } - .into()); - } + // collect all configuration setting-keys + let raw_config = config.raw(); + let mut keys = flatten_setting_keys( + raw_config.get("config").cloned().unwrap_or(PhpMixed::Null), + "", + ); + keys.extend(flatten_setting_keys( + raw_config + .get("repositories") + .cloned() + .unwrap_or(PhpMixed::Null), + "repositories.", + )); - value = cursor; - } else if data - .get("config") - .and_then(|c| c.as_array()) - .map(|c| c.contains_key(&setting_key)) - .unwrap_or(false) - { - value = config.borrow_mut().get_with_flags( - &setting_key, - if input.borrow().get_option("absolute")?.as_bool() == Some(true) { - 0 - } else { - Config::RELATIVE_PATHS - }, - )?; - // ensure we get {} output for properties which are objects - if value.as_array().map(|a| a.is_empty()).unwrap_or(false) { - let schema = JsonFile::parse_json( - Some(JsonFile::COMPOSER_SCHEMA_JSON), - Some("composer.schema.json"), - )?; - let type_value = schema - .as_array() - .and_then(|a| a.get("properties")) - .and_then(|v| v.as_array()) - .and_then(|a| a.get("config")) - .and_then(|v| v.as_array()) - .and_then(|a| a.get("properties")) - .and_then(|v| v.as_array()) - .and_then(|a| a.get(&setting_key)) - .and_then(|v| v.as_array()) - .and_then(|a| a.get("type")) - .cloned(); - if let Some(tv) = type_value { - let type_array = match &tv { - PhpMixed::List(_) | PhpMixed::Array(_) => tv, - other => PhpMixed::List(vec![other.clone()]), - }; - let type_strings: Vec = type_array - .as_list() - .map(|l| { - l.iter() - .filter_map(|v| v.as_string().map(|s| s.to_string())) - .collect::>() - }) - .unwrap_or_default(); - if type_strings.iter().any(|s| s == "object") { - value = PhpMixed::Object(IndexMap::new()); - } - } - } - } else if raw_data - .as_array() - .and_then(|a| a.get(&setting_key)) - .is_some() - && in_array_strict( - setting_key.as_str(), - &properties - .iter() - .map(|s| PhpMixed::String(s.to_string())) - .collect::>(), - ) - { - value = raw_data - .as_array() - .unwrap() - .get(&setting_key) - .unwrap() - .clone(); - source = config_file.borrow().get_path().to_string(); - } else if let Some(v) = properties_defaults.get(&setting_key) { - value = v.clone(); - source = "defaults".to_string(); - } else { - return Err(RuntimeException { - message: format!("{} is not defined", setting_key), - code: 0, - } - .into()); - } + // if unsetting … + if input.get_option("unset")?.to_bool() { + // … keep only the currently customized setting-keys … + let sources = [ + config_file.get_path().to_string(), + auth_config_file.get_path().to_string(), + ]; + keys.retain(|key| sources.contains(&config.get_source_of_value(key))); - let value_str = if is_array(&value) || is_object(&value) || is_bool(&value) { - JsonFile::encode_with_options( - &value, - JsonEncodeOptions { - pretty_print: false, - ..Default::default() - }, - )? + // … else if showing or setting a value … } else { - value.as_string().unwrap_or("").to_string() - }; + // … add all configurable package-properties, no matter if it exist + keys.extend( + Self::CONFIGURABLE_PACKAGE_PROPERTIES + .iter() + .map(|property| property.to_string()), + ); - let mut source_of_config_value = String::new(); - if input.borrow().get_option("source")?.as_bool() == Some(true) { - source_of_config_value = format!(" ({})", source); + // it would be nice to distinguish between showing and setting + // a value, but that makes the implementation much more complex + // and partially impossible because symfony's implementation + // does not complete arguments followed by other arguments } - self.get_io().write3( - &format!("{}{}", value_str, source_of_config_value), - true, - io_interface::QUIET, - ); + // add all existing configurable package-properties + if config_file.exists() { + let properties: IndexMap = config_file + .read()? + .as_array() + .cloned() + .unwrap_or_default() + .into_iter() + .filter(|(key, _)| { + Self::CONFIGURABLE_PACKAGE_PROPERTIES.contains(&key.as_str()) + }) + .collect(); - return Ok(0); - } + keys.extend(flatten_setting_keys(PhpMixed::Array(properties), "")); + } - let values: Vec = setting_values; // what the user is trying to add/change + // filter settings-keys by completion value + let completion_value = input.get_completion_value(); - let boolean_validator = |val: &PhpMixed| -> bool { - matches!(val.as_string().unwrap_or(""), "true" | "false" | "1" | "0") - }; - let boolean_normalizer = |val: &PhpMixed| -> PhpMixed { - let s = val.as_string().unwrap_or(""); - PhpMixed::Bool(s != "false" && !s.is_empty() && s != "0") - }; + if !completion_value.is_empty() { + keys.retain(|key| key.starts_with(&completion_value)); + } - // handle config values - let unique_config_values = build_unique_config_values(); - let multi_config_values = build_multi_config_values(); + keys.sort(); - // allow unsetting audit config entirely - if input.borrow().get_option("unset")?.as_bool() == Some(true) && setting_key == "audit" { - self.config_source - .borrow_mut() - .as_mut() - .unwrap() - .remove_config_setting(&setting_key); + keys.dedup(); + Ok(keys) + })) + } +} - return Ok(0); - } +impl Default for ConfigCommand { + fn default() -> Self { + Self::new() + } +} - if input.borrow().get_option("unset")?.as_bool() == Some(true) - && (unique_config_values.contains_key(&setting_key) - || multi_config_values.contains_key(&setting_key)) - { - if setting_key == "disable-tls" - && config - .borrow() - .get("disable-tls") - .as_bool() - .unwrap_or(false) - { - self.get_io().write_error( - "You are now running Composer with SSL/TLS protection enabled.", - ); - } - - self.config_source - .borrow_mut() - .as_mut() - .unwrap() - .remove_config_setting(&setting_key); +impl Command for ConfigCommand { + fn configure(&self) -> anyhow::Result<()> { + self.set_name("config")?; + self.set_description("Sets config options"); + self.set_definition(&[ + InputOption::new("global", Some(PhpMixed::String("g".to_string())), Some(InputOption::VALUE_NONE), "Apply command to the global config file", None).unwrap().into(), + InputOption::new("editor", Some(PhpMixed::String("e".to_string())), Some(InputOption::VALUE_NONE), "Open editor", None).unwrap().into(), + InputOption::new("auth", Some(PhpMixed::String("a".to_string())), Some(InputOption::VALUE_NONE), "Affect auth config file (only used for --editor)", None).unwrap().into(), + InputOption::new("unset", None, Some(InputOption::VALUE_NONE), "Unset the given setting-key", None).unwrap().into(), + InputOption::new("list", Some(PhpMixed::String("l".to_string())), Some(InputOption::VALUE_NONE), "List configuration settings", None).unwrap().into(), + InputOption::new("file", Some(PhpMixed::String("f".to_string())), Some(InputOption::VALUE_REQUIRED), "If you want to choose a different composer.json or config.json", None).unwrap().into(), + InputOption::new("absolute", None, Some(InputOption::VALUE_NONE), "Returns absolute paths when fetching *-dir config values instead of relative", None).unwrap().into(), + InputOption::new("json", Some(PhpMixed::String("j".to_string())), Some(InputOption::VALUE_NONE), "JSON decode the setting value, to be used with extra.* keys", None).unwrap().into(), + InputOption::new("merge", Some(PhpMixed::String("m".to_string())), Some(InputOption::VALUE_NONE), "Merge the setting value with the current value, to be used with extra.* or audit.ignore[-abandoned] keys in combination with --json", None).unwrap().into(), + InputOption::new("append", None, Some(InputOption::VALUE_NONE), "When adding a repository, append it (lowest priority) to the existing ones instead of prepending it (highest priority)", None).unwrap().into(), + InputOption::new("source", None, Some(InputOption::VALUE_NONE), "Display where the config value is loaded from", None).unwrap().into(), + InputArgument::new5("setting-key", None, "Setting key", None, self.suggest_setting_keys()).unwrap().into(), + InputArgument::new("setting-value", Some(InputArgument::IS_ARRAY), "Setting value", None).unwrap().into(), + ]); + self.set_help( + "This command allows you to edit composer config settings and repositories\n\ + in either the local composer.json file or the global config.json file.\n\n\ + Additionally it lets you edit most properties in the local composer.json.\n\n\ + To set a config setting:\n\n\ + \t%command.full_name% bin-dir bin/\n\n\ + To read a config setting:\n\n\ + \t%command.full_name% bin-dir\n\ + \tOutputs: bin\n\n\ + To edit the global config.json file:\n\n\ + \t%command.full_name% --global\n\n\ + To add a repository:\n\n\ + \t%command.full_name% repositories.foo vcs https://bar.com\n\n\ + To remove a repository (repo is a short alias for repositories):\n\n\ + \t%command.full_name% --unset repo.foo\n\n\ + To disable packagist.org:\n\n\ + \t%command.full_name% repo.packagist.org false\n\n\ + You can alter repositories in the global config.json file by passing in the\n\ + --global option.\n\n\ + To add or edit suggested packages you can use:\n\n\ + \t%command.full_name% suggest.package reason for the suggestion\n\n\ + To add or edit extra properties you can use:\n\n\ + \t%command.full_name% extra.property value\n\n\ + Or to add a complex value you can use json with:\n\n\ + \t%command.full_name% extra.property --json '{\"foo\":true, \"bar\": []}'\n\n\ + To edit the file in an external editor:\n\n\ + \t%command.full_name% --editor\n\n\ + To choose your editor you can set the \"EDITOR\" env variable.\n\n\ + To get a list of configuration values in the file:\n\n\ + \t%command.full_name% --list\n\n\ + You can always pass more than one option. As an example, if you want to edit the\n\ + global config.json file.\n\n\ + \t%command.full_name% --editor --global\n\n\ + Read more at https://getcomposer.org/doc/03-cli.md#config", + ); + Ok(()) + } - return Ok(0); - } - if let Some(callbacks) = unique_config_values.get(&setting_key) { - self.handle_single_value(&setting_key, callbacks, &values, "addConfigSetting")?; + fn initialize( + &self, + input: std::rc::Rc>, + output: std::rc::Rc>, + ) -> anyhow::Result<()> { + ::initialize( + self, + input.clone(), + output, + )?; - return Ok(0); - } - if let Some(callbacks) = multi_config_values.get(&setting_key) { - self.handle_multi_value(&setting_key, callbacks, &values, "addConfigSetting")?; + let config = self.config.borrow().as_ref().unwrap().clone(); + let auth_config_file = self.get_auth_config_file(input.clone(), &config.borrow())?; - return Ok(0); - } - // handle preferred-install per-package config - let mut matches: IndexMap = IndexMap::new(); - if Preg::is_match3( - php_regex!("/^preferred-install\\.(.+)/"), - &setting_key, - Some(&mut matches), - ) { - if input.borrow().get_option("unset")?.as_bool() == Some(true) { - self.config_source - .borrow_mut() - .as_mut() - .unwrap() - .remove_config_setting(&setting_key); + let auth_config_file_jf = std::rc::Rc::new(std::cell::RefCell::new(JsonFile::new( + auth_config_file, + None, + Some(self.get_io().clone()), + )?)); + *self.auth_config_file.borrow_mut() = Some(auth_config_file_jf.clone()); + *self.auth_config_source.borrow_mut() = + Some(JsonConfigSource::new(auth_config_file_jf, true)); - return Ok(0); + // Initialize the global file if it's not there, ignoring any warnings or notices + let auth_config_file = self.auth_config_file.borrow().as_ref().unwrap().clone(); + if input.borrow().get_option("global")?.as_bool() == Some(true) + && !auth_config_file.borrow().exists() + { + touch(auth_config_file.borrow().get_path()); + let mut empty_objs: IndexMap = IndexMap::new(); + for k in &[ + "bitbucket-oauth", + "github-oauth", + "gitlab-oauth", + "gitlab-token", + "http-basic", + "bearer", + "forgejo-token", + ] { + empty_objs.insert(k.to_string(), PhpMixed::Object(IndexMap::new())); } + auth_config_file + .borrow() + .write(PhpMixed::Array(empty_objs))?; + let path_clone = auth_config_file.borrow().get_path().to_string(); + Silencer::call(|| { + shirabe_php_shim::chmod(&path_clone, 0o600); + Ok(()) + }); + } + Ok(()) + } - let validator = &unique_config_values.get("preferred-install").unwrap().0; - if !validator(&PhpMixed::String(values[0].clone())) - .as_bool() - .unwrap_or(false) - { - return Err(RuntimeException { - message: format!( - "Invalid value for {}. Should be one of: auto, source, or dist", - setting_key - ), - code: 0, + fn execute( + &self, + input: std::rc::Rc>, + output: std::rc::Rc>, + ) -> anyhow::Result { + // Open file in editor + if input.borrow().get_option("editor")?.as_bool() == Some(true) { + let mut editor = Platform::get_env("EDITOR"); + if editor.is_none() || editor.as_deref() == Some("") { + if Platform::is_windows() { + editor = Some("notepad".to_string()); + } else { + for candidate in &["editor", "vim", "vi", "nano", "pico", "ed"] { + if !exec(&format!("which {}", candidate), None, None) + .unwrap_or_default() + .is_empty() + { + editor = Some(candidate.to_string()); + break; + } + } } - .into()); + } else { + editor = Some(escapeshellcmd(&editor.unwrap())); } - self.config_source - .borrow_mut() - .as_mut() - .unwrap() - .add_config_setting(&setting_key, PhpMixed::String(values[0].clone())); + let file = if input.borrow().get_option("auth")?.as_bool() == Some(true) { + self.auth_config_file + .borrow() + .as_ref() + .unwrap() + .borrow() + .get_path() + .to_string() + } else { + self.config_file + .borrow() + .as_ref() + .unwrap() + .borrow() + .get_path() + .to_string() + }; + system( + &format!( + "{} {}{}", + editor.unwrap_or_default(), + file, + if Platform::is_windows() { + "" + } else { + " > `tty`" + } + ), + None, + ); return Ok(0); } - // handle allow-plugins config setting elements true or false to add/remove - let mut matches: IndexMap = IndexMap::new(); - if Preg::is_match3( - php_regex!("{^allow-plugins\\.([a-zA-Z0-9/*-]+)}"), - &setting_key, - Some(&mut matches), - ) { - if input.borrow().get_option("unset")?.as_bool() == Some(true) { - self.config_source - .borrow_mut() - .as_mut() - .unwrap() - .remove_config_setting(&setting_key); - - return Ok(0); - } - - if !boolean_validator(&PhpMixed::String(values[0].clone())) { - return Err(RuntimeException { - message: format!("\"{}\" is an invalid value", values[0].clone()), - code: 0, - } - .into()); - } - - let normalized_value = boolean_normalizer(&PhpMixed::String(values[0].clone())); + let config = self.config.borrow().as_ref().unwrap().clone(); + let config_file = self.config_file.borrow().as_ref().unwrap().clone(); + let auth_config_file = self.auth_config_file.borrow().as_ref().unwrap().clone(); + if input.borrow().get_option("global")?.as_bool() != Some(true) { + let config_read = config_file.borrow_mut().read()?; + let config_map = match config_read { + PhpMixed::Array(m) => m, + _ => IndexMap::new(), + }; + let config_file_path = config_file.borrow().get_path().to_string(); + config.borrow_mut().merge(&config_map, &config_file_path); + let auth_data: PhpMixed = if auth_config_file.borrow().exists() { + auth_config_file.borrow_mut().read()? + } else { + PhpMixed::Array(IndexMap::new()) + }; + let mut wrap: IndexMap = IndexMap::new(); + wrap.insert("config".to_string(), auth_data); + let auth_config_file_path = auth_config_file.borrow().get_path().to_string(); + config.borrow_mut().merge(&wrap, &auth_config_file_path); + } - self.config_source + { + let config_rc = config.clone(); + self.get_io() .borrow_mut() - .as_mut() - .unwrap() - .add_config_setting(&setting_key, normalized_value); + .load_configuration(&mut config_rc.borrow_mut())?; + } + + // List the configuration of the file settings + if input.borrow().get_option("list")?.as_bool() == Some(true) { + let all_map = config.borrow_mut().all(0)?; + let raw_map = config.borrow().raw(); + let to_mixed = |m: IndexMap| -> PhpMixed { + PhpMixed::Array(m.into_iter().collect()) + }; + self.list_configuration( + to_mixed(all_map), + to_mixed(raw_map), + output, + None, + input.borrow().get_option("source")?.as_bool() == Some(true), + ); return Ok(0); } - // handle properties - let unique_props = build_unique_props(); - let multi_props = build_multi_props(); + let setting_key_arg = input.borrow().get_argument("setting-key")?; + let setting_key = match setting_key_arg.as_string() { + Some(s) => s.to_string(), + None => return Ok(0), + }; - if input.borrow().get_option("global")?.as_bool() == Some(true) - && (unique_props.contains_key(&setting_key) - || multi_props.contains_key(&setting_key) - || strpos(&setting_key, "extra.") == Some(0)) + // If the user enters in a config variable, parse it and save to file + let setting_values_raw = input.borrow().get_argument("setting-value")?; + let setting_values: Vec = setting_values_raw + .as_list() + .map(|l| { + l.iter() + .filter_map(|v| v.as_string().map(|s| s.to_string())) + .collect() + }) + .unwrap_or_default(); + if !setting_values.is_empty() && input.borrow().get_option("unset")?.as_bool() == Some(true) { - return Err(InvalidArgumentException { - message: format!("The {} property can not be set in the global config.json file. Use `composer global config` to apply changes to the global composer.json", setting_key), + return Err(RuntimeException { + message: "You can not combine a setting value with --unset".to_string(), code: 0, } .into()); } - if input.borrow().get_option("unset")?.as_bool() == Some(true) - && (unique_props.contains_key(&setting_key) || multi_props.contains_key(&setting_key)) - { - self.config_source - .borrow_mut() - .as_mut() - .unwrap() - .remove_property(&setting_key); - return Ok(0); - } - if let Some(callbacks) = unique_props.get(&setting_key) { - self.handle_single_value(&setting_key, callbacks, &values, "addProperty")?; + // show the value if no value is provided + if setting_values.is_empty() && input.borrow().get_option("unset")?.as_bool() != Some(true) + { + let properties: Vec<&'static str> = Self::CONFIGURABLE_PACKAGE_PROPERTIES.to_vec(); + let mut properties_defaults: IndexMap = IndexMap::new(); + properties_defaults.insert("type".to_string(), PhpMixed::String("library".to_string())); + properties_defaults.insert("description".to_string(), PhpMixed::String(String::new())); + properties_defaults.insert("homepage".to_string(), PhpMixed::String(String::new())); + properties_defaults.insert( + "minimum-stability".to_string(), + PhpMixed::String("stable".to_string()), + ); + properties_defaults.insert("prefer-stable".to_string(), PhpMixed::Bool(false)); + properties_defaults.insert("keywords".to_string(), PhpMixed::List(vec![])); + properties_defaults.insert("license".to_string(), PhpMixed::List(vec![])); + properties_defaults.insert("suggest".to_string(), PhpMixed::List(vec![])); + properties_defaults.insert("extra".to_string(), PhpMixed::List(vec![])); + let raw_data = config_file.borrow_mut().read()?; + let data = config.borrow_mut().all(0)?; + let mut source = config.borrow_mut().get_source_of_value(&setting_key); - return Ok(0); - } - if let Some(callbacks) = multi_props.get(&setting_key) { - self.handle_multi_value(&setting_key, callbacks, &values, "addProperty")?; + let mut value: PhpMixed; + let mut matches: IndexMap = IndexMap::new(); + if Preg::is_match3( + php_regex!("/^repos?(?:itories)?(?:\\.(.+))?/"), + &setting_key, + Some(&mut matches), + ) { + if matches.get(&CaptureKey::ByIndex(1)).is_none() { + value = data + .get("repositories") + .cloned() + .unwrap_or_else(|| PhpMixed::Array(IndexMap::new())); + } else { + let repo_key = matches + .get(&CaptureKey::ByIndex(1)) + .cloned() + .unwrap_or_default(); + let repos = data.get("repositories").cloned(); + value = match repos + .as_ref() + .and_then(|r| r.as_array().and_then(|a| a.get(&repo_key))) + { + Some(v) => v.clone(), + None => { + return Err(InvalidArgumentException { + message: format!("There is no {} repository defined", repo_key), + code: 0, + } + .into()); + } + }; + } + } else if strpos(&setting_key, ".").is_some() { + let bits = explode(".", &setting_key); + // PHP: $data here is the mixed dot-segment cursor; the rest of the loop walks it. + let mut cursor: PhpMixed = if bits[0] == "extra" || bits[0] == "suggest" { + PhpMixed::Array(raw_data.as_array().cloned().unwrap_or_else(IndexMap::new)) + } else { + data.get("config").cloned().unwrap_or(PhpMixed::Null) + }; + let mut r#match = false; + let mut key_acc: Option = None; + for bit in &bits { + let new_key = match &key_acc { + Some(k) => format!("{}.{}", k, bit), + None => bit.clone(), + }; + key_acc = Some(new_key.clone()); + r#match = false; + if let Some(arr) = cursor.as_array() + && let Some(v) = arr.get(&new_key) + { + r#match = true; + cursor = v.clone(); + key_acc = None; + } + } - return Ok(0); - } + if !r#match { + return Err(RuntimeException { + message: format!("{} is not defined.", setting_key), + code: 0, + } + .into()); + } - // handle repositories - let mut matches: IndexMap = IndexMap::new(); - if Preg::is_match3( - php_regex!("/^repos?(?:itories)?\\.(.+)/"), - &setting_key, - Some(&mut matches), - ) { - if input.borrow().get_option("unset")?.as_bool() == Some(true) { - self.config_source - .borrow_mut() - .as_mut() + value = cursor; + } else if data + .get("config") + .and_then(|c| c.as_array()) + .map(|c| c.contains_key(&setting_key)) + .unwrap_or(false) + { + value = config.borrow_mut().get_with_flags( + &setting_key, + if input.borrow().get_option("absolute")?.as_bool() == Some(true) { + 0 + } else { + Config::RELATIVE_PATHS + }, + )?; + // ensure we get {} output for properties which are objects + if value.as_array().map(|a| a.is_empty()).unwrap_or(false) { + let schema = JsonFile::parse_json( + Some(JsonFile::COMPOSER_SCHEMA_JSON), + Some("composer.schema.json"), + )?; + let type_value = schema + .as_array() + .and_then(|a| a.get("properties")) + .and_then(|v| v.as_array()) + .and_then(|a| a.get("config")) + .and_then(|v| v.as_array()) + .and_then(|a| a.get("properties")) + .and_then(|v| v.as_array()) + .and_then(|a| a.get(&setting_key)) + .and_then(|v| v.as_array()) + .and_then(|a| a.get("type")) + .cloned(); + if let Some(tv) = type_value { + let type_array = match &tv { + PhpMixed::List(_) | PhpMixed::Array(_) => tv, + other => PhpMixed::List(vec![other.clone()]), + }; + let type_strings: Vec = type_array + .as_list() + .map(|l| { + l.iter() + .filter_map(|v| v.as_string().map(|s| s.to_string())) + .collect::>() + }) + .unwrap_or_default(); + if type_strings.iter().any(|s| s == "object") { + value = PhpMixed::Object(IndexMap::new()); + } + } + } + } else if raw_data + .as_array() + .and_then(|a| a.get(&setting_key)) + .is_some() + && in_array_strict( + setting_key.as_str(), + &properties + .iter() + .map(|s| PhpMixed::String(s.to_string())) + .collect::>(), + ) + { + value = raw_data + .as_array() .unwrap() - .remove_repository(&matches[1]); - - return Ok(0); - } - - if 2 == values.len() { - let mut repo: IndexMap = IndexMap::new(); - repo.insert("type".to_string(), PhpMixed::String(values[0].clone())); - repo.insert("url".to_string(), PhpMixed::String(values[1].clone())); - self.config_source - .borrow_mut() - .as_mut() + .get(&setting_key) .unwrap() - .add_repository( - &matches[1], - PhpMixed::Array(repo), - input.borrow().get_option("append")?.as_bool() == Some(true), - ); - - return Ok(0); + .clone(); + source = config_file.borrow().get_path().to_string(); + } else if let Some(v) = properties_defaults.get(&setting_key) { + value = v.clone(); + source = "defaults".to_string(); + } else { + return Err(RuntimeException { + message: format!("{} is not defined", setting_key), + code: 0, + } + .into()); } - if 1 == values.len() { + let value_str = if is_array(&value) || is_object(&value) || is_bool(&value) { + JsonFile::encode_with_options( + &value, + JsonEncodeOptions { + pretty_print: false, + ..Default::default() + }, + )? + } else { + value.as_string().unwrap_or("").to_string() + }; + + let mut source_of_config_value = String::new(); + if input.borrow().get_option("source")?.as_bool() == Some(true) { + source_of_config_value = format!(" ({})", source); + } + + self.get_io().write3( + &format!("{}{}", value_str, source_of_config_value), + true, + io_interface::QUIET, + ); + + return Ok(0); + } + + let values: Vec = setting_values; // what the user is trying to add/change + + let boolean_validator = |val: &PhpMixed| -> bool { + matches!(val.as_string().unwrap_or(""), "true" | "false" | "1" | "0") + }; + let boolean_normalizer = |val: &PhpMixed| -> PhpMixed { + let s = val.as_string().unwrap_or(""); + PhpMixed::Bool(s != "false" && !s.is_empty() && s != "0") + }; + + // handle config values + let unique_config_values = build_unique_config_values(); + let multi_config_values = build_multi_config_values(); + + // allow unsetting audit config entirely + if input.borrow().get_option("unset")?.as_bool() == Some(true) && setting_key == "audit" { + self.config_source + .borrow_mut() + .as_mut() + .unwrap() + .remove_config_setting(&setting_key); + + return Ok(0); + } + + if input.borrow().get_option("unset")?.as_bool() == Some(true) + && (unique_config_values.contains_key(&setting_key) + || multi_config_values.contains_key(&setting_key)) + { + if setting_key == "disable-tls" + && config + .borrow() + .get("disable-tls") + .as_bool() + .unwrap_or(false) + { + self.get_io().write_error( + "You are now running Composer with SSL/TLS protection enabled.", + ); + } + + self.config_source + .borrow_mut() + .as_mut() + .unwrap() + .remove_config_setting(&setting_key); + + return Ok(0); + } + if let Some(callbacks) = unique_config_values.get(&setting_key) { + self.handle_single_value(&setting_key, callbacks, &values, "addConfigSetting")?; + + return Ok(0); + } + if let Some(callbacks) = multi_config_values.get(&setting_key) { + self.handle_multi_value(&setting_key, callbacks, &values, "addConfigSetting")?; + + return Ok(0); + } + // handle preferred-install per-package config + let mut matches: IndexMap = IndexMap::new(); + if Preg::is_match3( + php_regex!("/^preferred-install\\.(.+)/"), + &setting_key, + Some(&mut matches), + ) { + if input.borrow().get_option("unset")?.as_bool() == Some(true) { + self.config_source + .borrow_mut() + .as_mut() + .unwrap() + .remove_config_setting(&setting_key); + + return Ok(0); + } + + let validator = &unique_config_values.get("preferred-install").unwrap().0; + if !validator(&PhpMixed::String(values[0].clone())) + .as_bool() + .unwrap_or(false) + { + return Err(RuntimeException { + message: format!( + "Invalid value for {}. Should be one of: auto, source, or dist", + setting_key + ), + code: 0, + } + .into()); + } + + self.config_source + .borrow_mut() + .as_mut() + .unwrap() + .add_config_setting(&setting_key, PhpMixed::String(values[0].clone())); + + return Ok(0); + } + + // handle allow-plugins config setting elements true or false to add/remove + let mut matches: IndexMap = IndexMap::new(); + if Preg::is_match3( + php_regex!("{^allow-plugins\\.([a-zA-Z0-9/*-]+)}"), + &setting_key, + Some(&mut matches), + ) { + if input.borrow().get_option("unset")?.as_bool() == Some(true) { + self.config_source + .borrow_mut() + .as_mut() + .unwrap() + .remove_config_setting(&setting_key); + + return Ok(0); + } + + if !boolean_validator(&PhpMixed::String(values[0].clone())) { + return Err(RuntimeException { + message: format!("\"{}\" is an invalid value", values[0].clone()), + code: 0, + } + .into()); + } + + let normalized_value = boolean_normalizer(&PhpMixed::String(values[0].clone())); + + self.config_source + .borrow_mut() + .as_mut() + .unwrap() + .add_config_setting(&setting_key, normalized_value); + + return Ok(0); + } + + // handle properties + let unique_props = build_unique_props(); + let multi_props = build_multi_props(); + + if input.borrow().get_option("global")?.as_bool() == Some(true) + && (unique_props.contains_key(&setting_key) + || multi_props.contains_key(&setting_key) + || strpos(&setting_key, "extra.") == Some(0)) + { + return Err(InvalidArgumentException { + message: format!("The {} property can not be set in the global config.json file. Use `composer global config` to apply changes to the global composer.json", setting_key), + code: 0, + } + .into()); + } + if input.borrow().get_option("unset")?.as_bool() == Some(true) + && (unique_props.contains_key(&setting_key) || multi_props.contains_key(&setting_key)) + { + self.config_source + .borrow_mut() + .as_mut() + .unwrap() + .remove_property(&setting_key); + + return Ok(0); + } + if let Some(callbacks) = unique_props.get(&setting_key) { + self.handle_single_value(&setting_key, callbacks, &values, "addProperty")?; + + return Ok(0); + } + if let Some(callbacks) = multi_props.get(&setting_key) { + self.handle_multi_value(&setting_key, callbacks, &values, "addProperty")?; + + return Ok(0); + } + + // handle repositories + let mut matches: IndexMap = IndexMap::new(); + if Preg::is_match3( + php_regex!("/^repos?(?:itories)?\\.(.+)/"), + &setting_key, + Some(&mut matches), + ) { + if input.borrow().get_option("unset")?.as_bool() == Some(true) { + self.config_source + .borrow_mut() + .as_mut() + .unwrap() + .remove_repository(&matches[1]); + + return Ok(0); + } + + if 2 == values.len() { + let mut repo: IndexMap = IndexMap::new(); + repo.insert("type".to_string(), PhpMixed::String(values[0].clone())); + repo.insert("url".to_string(), PhpMixed::String(values[1].clone())); + self.config_source + .borrow_mut() + .as_mut() + .unwrap() + .add_repository( + &matches[1], + PhpMixed::Array(repo), + input.borrow().get_option("append")?.as_bool() == Some(true), + ); + + return Ok(0); + } + + if 1 == values.len() { let value = strtolower(&values[0]); if boolean_validator(&PhpMixed::String(value.clone())) { if !boolean_normalizer(&PhpMixed::String(value)) @@ -1099,542 +1457,180 @@ impl Command for ConfigCommand { .as_mut() .unwrap() .add_config_setting(&key, PhpMixed::String(values[0].clone())); - } else if matches[1] == "http-basic" { - if 2 != values.len() { - return Err(RuntimeException { - message: format!( - "Expected two arguments (username, password), got {}", - values.len() - ), - code: 0, - } - .into()); - } - self.config_source - .borrow_mut() - .as_mut() - .unwrap() - .remove_config_setting(&key); - let mut obj: IndexMap = IndexMap::new(); - obj.insert("username".to_string(), PhpMixed::String(values[0].clone())); - obj.insert("password".to_string(), PhpMixed::String(values[1].clone())); - self.auth_config_source - .borrow_mut() - .as_mut() - .unwrap() - .add_config_setting(&key, PhpMixed::Array(obj)); - } else if matches[1] == "custom-headers" { - if values.is_empty() { - return Err(RuntimeException { - message: "Expected at least one argument (header), got none".to_string(), - code: 0, - } - .into()); - } - - // Validate headers format - let mut formatted_headers: Vec = vec![]; - for header in &values { - if !is_string(&PhpMixed::String(header.clone())) { - return Err(RuntimeException { - message: - "Headers must be strings in \"Header-Name: Header-Value\" format" - .to_string(), - code: 0, - } - .into()); - } - - // Check if the header is in correct "Name: Value" format - let mut header_parts: IndexMap = IndexMap::new(); - if !Preg::is_match3( - php_regex!("/^[^:]+:\\s*.+$/"), - header, - Some(&mut header_parts), - ) { - return Err(RuntimeException { - message: format!( - "Header \"{}\" is not in \"Header-Name: Header-Value\" format", - header - ), - code: 0, - } - .into()); - } - - formatted_headers.push(PhpMixed::String(header.clone())); - } - - self.config_source - .borrow_mut() - .as_mut() - .unwrap() - .remove_config_setting(&key); - self.auth_config_source - .borrow_mut() - .as_mut() - .unwrap() - .add_config_setting(&key, PhpMixed::List(formatted_headers)); - } else if matches[1] == "forgejo-token" { - if 2 != values.len() { - return Err(RuntimeException { - message: format!( - "Expected two arguments (username, access token), got {}", - values.len() - ), - code: 0, - } - .into()); - } - self.config_source - .borrow_mut() - .as_mut() - .unwrap() - .remove_config_setting(&key); - let mut obj: IndexMap = IndexMap::new(); - obj.insert("username".to_string(), PhpMixed::String(values[0].clone())); - obj.insert("token".to_string(), PhpMixed::String(values[1].clone())); - self.auth_config_source - .borrow_mut() - .as_mut() - .unwrap() - .add_config_setting(&key, PhpMixed::Array(obj)); - } - - return Ok(0); - } - - // handle script - let mut matches: IndexMap = IndexMap::new(); - if Preg::is_match3( - php_regex!("/^scripts\\.(.+)/"), - &setting_key, - Some(&mut matches), - ) { - if input.borrow().get_option("unset")?.as_bool() == Some(true) { - self.config_source - .borrow_mut() - .as_mut() - .unwrap() - .remove_property(&setting_key); - - return Ok(0); - } - - let value: PhpMixed = if values.len() > 1 { - PhpMixed::List(values.iter().map(|s| PhpMixed::String(s.clone())).collect()) - } else { - PhpMixed::String(values[0].clone()) - }; - self.config_source - .borrow_mut() - .as_mut() - .unwrap() - .add_property(&setting_key, value); - - return Ok(0); - } - - // handle unsetting other top level properties - if input.borrow().get_option("unset")?.as_bool() == Some(true) { - self.config_source - .borrow_mut() - .as_mut() - .unwrap() - .remove_property(&setting_key); - - return Ok(0); - } - - Err(InvalidArgumentException { - message: format!( - "Setting {} does not exist or is not supported by this command", - setting_key - ), - code: 0, - } - .into()) - } - - fn complete( - &self, - input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput, - suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions, - ) -> anyhow::Result<()> { - crate::command::base_command::base_command_complete(self, input, suggestions) - } - - shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); -} - -impl BaseCommand for ConfigCommand { - fn base_command_data(&self) -> &crate::command::BaseCommandData { - &self.base_command_data - } - - crate::delegate_base_command_trait_impls_to_inner!(base_command_data); -} - -impl ConfigCommand { - pub(crate) fn handle_single_value( - &self, - key: &str, - callbacks: &(ValidatorFn, NormalizerFn), - values: &[String], - method: &str, - ) -> anyhow::Result<()> { - let (validator, normalizer) = callbacks; - if 1 != values.len() { - return Err(RuntimeException { - message: "You can only pass one value. Example: shirabe config process-timeout 300" - .to_string(), - code: 0, - } - .into()); - } - - let validation = validator(&PhpMixed::String(values[0].clone())); - if validation.as_bool() != Some(true) { - let suffix = if !validation.is_null() && validation.as_bool() != Some(false) { - format!(" ({})", validation.as_string().unwrap_or("")) - } else { - String::new() - }; - return Err(RuntimeException { - message: format!("\"{}\" is an invalid value{}", values[0].clone(), suffix), - code: 0, - } - .into()); - } - - let normalized_value = normalizer(&PhpMixed::String(values[0].clone())); - - if key == "disable-tls" { - let config = self.config.borrow().as_ref().unwrap().clone(); - if !normalized_value.as_bool().unwrap_or(false) - && config - .borrow() - .get("disable-tls") - .as_bool() - .unwrap_or(false) - { - self.get_io().write_error( - "You are now running Composer with SSL/TLS protection enabled.", - ); - } else if normalized_value.as_bool().unwrap_or(false) - && !config - .borrow() - .get("disable-tls") - .as_bool() - .unwrap_or(false) - { - self.get_io().write_error("You are now running Composer with SSL/TLS protection disabled."); - } - } - - let mut config_source = self.config_source.borrow_mut(); - let config_source = config_source.as_mut().unwrap(); - match method { - "addConfigSetting" => config_source.add_config_setting(key, normalized_value)?, - "addProperty" => config_source.add_property(key, normalized_value)?, - _ => unreachable!(), - } - Ok(()) - } - - pub(crate) fn handle_multi_value( - &self, - key: &str, - callbacks: &(ValidatorFn, NormalizerFn), - values: &[String], - method: &str, - ) -> anyhow::Result<()> { - let (validator, normalizer) = callbacks; - let values_mixed = - PhpMixed::List(values.iter().map(|s| PhpMixed::String(s.clone())).collect()); - let validation = validator(&values_mixed); - if validation.as_bool() != Some(true) { - let suffix = if !validation.is_null() && validation.as_bool() != Some(false) { - format!(" ({})", validation.as_string().unwrap_or("")) - } else { - String::new() - }; - return Err(RuntimeException { - message: format!( - "{} is an invalid value{}", - PhpMixed::from(json_encode(&values_mixed).ok()), - suffix - ), - code: 0, - } - .into()); - } - - let mut config_source = self.config_source.borrow_mut(); - let config_source = config_source.as_mut().unwrap(); - match method { - "addConfigSetting" => { - config_source.add_config_setting(key, normalizer(&values_mixed))? - } - "addProperty" => config_source.add_property(key, normalizer(&values_mixed))?, - _ => unreachable!(), - } - Ok(()) - } - - /// Display the contents of the file in a pretty formatted way - pub(crate) fn list_configuration( - &self, - contents: PhpMixed, - raw_contents: PhpMixed, - output: std::rc::Rc>, - k: Option, - show_source: bool, - ) { - let orig_k = k.clone(); - let contents_arr = contents.as_array().cloned().unwrap_or_default(); - let raw_contents_arr = raw_contents.as_array().cloned().unwrap_or_default(); - let mut k = k; - for (key, value) in &contents_arr { - if k.is_none() && !matches!(key.as_str(), "config" | "repositories") { - continue; - } - - let raw_val = raw_contents_arr.get(key).cloned().unwrap_or(PhpMixed::Null); - - let value_inner = value.clone(); - - if is_array(&value_inner) - && (!is_numeric(&key_first_key(&value_inner).unwrap_or_default().into()) - || (key == "repositories" && k.is_none())) - { - let mut new_k = k.clone().unwrap_or_default(); - new_k.push_str(&Preg::replace( - php_regex!("{^config\\.}"), - "", - &format!("{}.", key), - )); - k = Some(new_k); - self.list_configuration( - value_inner, - raw_val, - output.clone(), - k.clone(), - show_source, - ); - k = orig_k.clone(); - - continue; - } - - let value_display: String = if is_array(&value_inner) { - let arr_strs: Vec = value_inner - .as_list() - .map(|l| { - l.iter() - .map(|val| { - if is_array(val) { - json_encode(val).unwrap_or_default() - } else { - val.as_string().unwrap_or("").to_string() - } - }) - .collect::>() - }) - .unwrap_or_default(); - format!("[{}]", implode(", ", &arr_strs)) - } else if is_bool(&value_inner) { - var_export(&value_inner, true) - } else { - value_inner.as_string().unwrap_or("").to_string() - }; - - let source = if show_source { - format!( - " ({})", - self.config - .borrow() - .as_ref() - .unwrap() - .borrow_mut() - .get_source_of_value(&format!("{}{}", k.clone().unwrap_or_default(), key)) - ) - } else { - String::new() - }; - - let link: String = - if k.is_some() && strpos(k.as_ref().unwrap(), "repositories") == Some(0) { - "https://getcomposer.org/doc/05-repositories.md".to_string() - } else { - let id_source = if k.as_deref() == Some("") || k.is_none() { - key.clone() - } else { - k.clone().unwrap() - }; - let id = Preg::replace(php_regex!("{\\..*$}"), "", &id_source); - let id = Preg::replace( - php_regex!("{[^a-z0-9]}i"), - "-", - &strtolower(&shirabe_php_shim::trim(&id, Some(" \t\n\r\0\u{0B}"))), - ); - let id = Preg::replace(php_regex!("{-+}"), "-", &id); - format!("https://getcomposer.org/doc/06-config.md#{}", id) - }; - if is_string(&raw_val) - && raw_val - .as_string() - .map(|s| s.to_string()) - .unwrap_or_default() - != value_display - { - self.get_io().write3( - &format!( - "[{}{}] {} ({}){}", - link, - k.clone().unwrap_or_default(), - key, - raw_val.as_string().unwrap_or(""), - value_display, - source - ), - true, - io_interface::QUIET, - ); - } else { - self.get_io().write3( - &format!( - "[{}{}] {}{}", - link, - k.clone().unwrap_or_default(), - key, - value_display, - source - ), - true, - io_interface::QUIET, - ); - } - } - } + } else if matches[1] == "http-basic" { + if 2 != values.len() { + return Err(RuntimeException { + message: format!( + "Expected two arguments (username, password), got {}", + values.len() + ), + code: 0, + } + .into()); + } + self.config_source + .borrow_mut() + .as_mut() + .unwrap() + .remove_config_setting(&key); + let mut obj: IndexMap = IndexMap::new(); + obj.insert("username".to_string(), PhpMixed::String(values[0].clone())); + obj.insert("password".to_string(), PhpMixed::String(values[1].clone())); + self.auth_config_source + .borrow_mut() + .as_mut() + .unwrap() + .add_config_setting(&key, PhpMixed::Array(obj)); + } else if matches[1] == "custom-headers" { + if values.is_empty() { + return Err(RuntimeException { + message: "Expected at least one argument (header), got none".to_string(), + code: 0, + } + .into()); + } - /// Suggest setting-keys, while taking given options in account. - fn suggest_setting_keys(&self) -> crate::console::input::SuggestedValues { - crate::console::input::SuggestedValues::Closure(Box::new(|this, input, _suggestions| { - if input.get_option("list")?.to_bool() - || input.get_option("editor")?.to_bool() - || input.get_option("auth")?.to_bool() - { - return Ok(vec![]); - } + // Validate headers format + let mut formatted_headers: Vec = vec![]; + for header in &values { + if !is_string(&PhpMixed::String(header.clone())) { + return Err(RuntimeException { + message: + "Headers must be strings in \"Header-Name: Header-Value\" format" + .to_string(), + code: 0, + } + .into()); + } - let this = this - .as_any() - .downcast_ref::() - .expect("suggestSettingKeys is bound to ConfigCommand"); - // PHP passes the CompletionInput itself; the accessors only read from it, so a - // clone behind a fresh handle is equivalent. - let input_handle: std::rc::Rc< - std::cell::RefCell< - dyn shirabe_external_packages::symfony::console::input::InputInterface, - >, - > = std::rc::Rc::new(std::cell::RefCell::new(input.clone())); + // Check if the header is in correct "Name: Value" format + let mut header_parts: IndexMap = IndexMap::new(); + if !Preg::is_match3( + php_regex!("/^[^:]+:\\s*.+$/"), + header, + Some(&mut header_parts), + ) { + return Err(RuntimeException { + message: format!( + "Header \"{}\" is not in \"Header-Name: Header-Value\" format", + header + ), + code: 0, + } + .into()); + } - // initialize configuration - let mut config = Factory::create_config(None, None)?; + formatted_headers.push(PhpMixed::String(header.clone())); + } - // load configuration - let config_file = JsonFile::new( - this.get_composer_config_file(input_handle.clone(), &config)?, - None, - None, - )?; - if config_file.exists() { - let path = config_file.get_path().to_string(); - let data = config_file.read()?.as_array().cloned().unwrap_or_default(); - config.merge(&data, &path); + self.config_source + .borrow_mut() + .as_mut() + .unwrap() + .remove_config_setting(&key); + self.auth_config_source + .borrow_mut() + .as_mut() + .unwrap() + .add_config_setting(&key, PhpMixed::List(formatted_headers)); + } else if matches[1] == "forgejo-token" { + if 2 != values.len() { + return Err(RuntimeException { + message: format!( + "Expected two arguments (username, access token), got {}", + values.len() + ), + code: 0, + } + .into()); + } + self.config_source + .borrow_mut() + .as_mut() + .unwrap() + .remove_config_setting(&key); + let mut obj: IndexMap = IndexMap::new(); + obj.insert("username".to_string(), PhpMixed::String(values[0].clone())); + obj.insert("token".to_string(), PhpMixed::String(values[1].clone())); + self.auth_config_source + .borrow_mut() + .as_mut() + .unwrap() + .add_config_setting(&key, PhpMixed::Array(obj)); } - // load auth-configuration - let auth_config_file = JsonFile::new( - this.get_auth_config_file(input_handle.clone(), &config)?, - None, - None, - )?; - if auth_config_file.exists() { - let path = auth_config_file.get_path().to_string(); - let mut data = IndexMap::new(); - data.insert("config".to_string(), auth_config_file.read()?); - config.merge(&data, &path); - } + return Ok(0); + } - // collect all configuration setting-keys - let raw_config = config.raw(); - let mut keys = flatten_setting_keys( - raw_config.get("config").cloned().unwrap_or(PhpMixed::Null), - "", - ); - keys.extend(flatten_setting_keys( - raw_config - .get("repositories") - .cloned() - .unwrap_or(PhpMixed::Null), - "repositories.", - )); + // handle script + let mut matches: IndexMap = IndexMap::new(); + if Preg::is_match3( + php_regex!("/^scripts\\.(.+)/"), + &setting_key, + Some(&mut matches), + ) { + if input.borrow().get_option("unset")?.as_bool() == Some(true) { + self.config_source + .borrow_mut() + .as_mut() + .unwrap() + .remove_property(&setting_key); - // if unsetting … - if input.get_option("unset")?.to_bool() { - // … keep only the currently customized setting-keys … - let sources = [ - config_file.get_path().to_string(), - auth_config_file.get_path().to_string(), - ]; - keys.retain(|key| sources.contains(&config.get_source_of_value(key))); + return Ok(0); + } - // … else if showing or setting a value … + let value: PhpMixed = if values.len() > 1 { + PhpMixed::List(values.iter().map(|s| PhpMixed::String(s.clone())).collect()) } else { - // … add all configurable package-properties, no matter if it exist - keys.extend( - Self::CONFIGURABLE_PACKAGE_PROPERTIES - .iter() - .map(|property| property.to_string()), - ); + PhpMixed::String(values[0].clone()) + }; + self.config_source + .borrow_mut() + .as_mut() + .unwrap() + .add_property(&setting_key, value); - // it would be nice to distinguish between showing and setting - // a value, but that makes the implementation much more complex - // and partially impossible because symfony's implementation - // does not complete arguments followed by other arguments - } + return Ok(0); + } - // add all existing configurable package-properties - if config_file.exists() { - let properties: IndexMap = config_file - .read()? - .as_array() - .cloned() - .unwrap_or_default() - .into_iter() - .filter(|(key, _)| { - Self::CONFIGURABLE_PACKAGE_PROPERTIES.contains(&key.as_str()) - }) - .collect(); + // handle unsetting other top level properties + if input.borrow().get_option("unset")?.as_bool() == Some(true) { + self.config_source + .borrow_mut() + .as_mut() + .unwrap() + .remove_property(&setting_key); - keys.extend(flatten_setting_keys(PhpMixed::Array(properties), "")); - } + return Ok(0); + } - // filter settings-keys by completion value - let completion_value = input.get_completion_value(); + Err(InvalidArgumentException { + message: format!( + "Setting {} does not exist or is not supported by this command", + setting_key + ), + code: 0, + } + .into()) + } - if !completion_value.is_empty() { - keys.retain(|key| key.starts_with(&completion_value)); - } + fn complete( + &self, + input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput, + suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions, + ) -> anyhow::Result<()> { + crate::command::base_command::base_command_complete(self, input, suggestions) + } - keys.sort(); + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} - keys.dedup(); - Ok(keys) - })) +impl BaseCommand for ConfigCommand { + fn base_command_data(&self) -> &crate::command::BaseCommandData { + &self.base_command_data } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } // PHP signature: function ($val): bool / ($val) -> bool/string -- cgit v1.3.1