diff options
Diffstat (limited to 'crates/shirabe/src')
| -rw-r--r-- | crates/shirabe/src/command/config_command.rs | 29 | ||||
| -rw-r--r-- | crates/shirabe/src/package/loader/validating_array_loader.rs | 19 | ||||
| -rw-r--r-- | crates/shirabe/src/package/root_alias_package.rs | 17 | ||||
| -rw-r--r-- | crates/shirabe/src/repository/repository_factory.rs | 15 | ||||
| -rw-r--r-- | crates/shirabe/src/util/forgejo.rs | 2 | ||||
| -rw-r--r-- | crates/shirabe/src/util/mod.rs | 2 | ||||
| -rw-r--r-- | crates/shirabe/src/util/tls_helper.rs | 149 |
7 files changed, 207 insertions, 26 deletions
diff --git a/crates/shirabe/src/command/config_command.rs b/crates/shirabe/src/command/config_command.rs index 7bd65b6..308d557 100644 --- a/crates/shirabe/src/command/config_command.rs +++ b/crates/shirabe/src/command/config_command.rs @@ -823,14 +823,27 @@ impl Command for ConfigCommand { PhpMixed::List(value.as_list().cloned().unwrap_or_default()), ); } else { - // PHP "+" operator on arrays: keep keys from left, fill from right - let mut merged: IndexMap<String, PhpMixed> = - value.as_array().cloned().unwrap_or_default(); - if let Some(cv) = current_value.as_array() { - for (k, v) in cv { - if !merged.contains_key(k) { - merged.insert(k.clone(), v.clone()); - } + // PHP "+" operator on arrays: keep keys from left, fill from right. + // A list participates with its integer indices as keys. + let mut merged: IndexMap<String, PhpMixed> = match &value { + PhpMixed::List(l) => l + .iter() + .enumerate() + .map(|(i, v)| (i.to_string(), v.clone())) + .collect(), + _ => value.as_array().cloned().unwrap_or_default(), + }; + let fill: IndexMap<String, PhpMixed> = match ¤t_value { + PhpMixed::List(l) => l + .iter() + .enumerate() + .map(|(i, v)| (i.to_string(), v.clone())) + .collect(), + _ => current_value.as_array().cloned().unwrap_or_default(), + }; + for (k, v) in fill { + if !merged.contains_key(&k) { + merged.insert(k, v); } } value = PhpMixed::Array(merged); diff --git a/crates/shirabe/src/package/loader/validating_array_loader.rs b/crates/shirabe/src/package/loader/validating_array_loader.rs index 342e1cb..6a85e37 100644 --- a/crates/shirabe/src/package/loader/validating_array_loader.rs +++ b/crates/shirabe/src/package/loader/validating_array_loader.rs @@ -1063,19 +1063,22 @@ impl ValidatingArrayLoader { .and_then(|v| v.as_array()) .cloned() .unwrap_or_default(); - if !section.contains_key("type") { + // Mirror PHP `isset()`, which is false for both missing keys and null values. + let isset = + |key: &str| matches!(section.get(key), Some(v) if !matches!(v, PhpMixed::Null)); + if !isset("type") { self.errors .push(format!("{}.type : must be present", src_type)); } - if !section.contains_key("url") { + if !isset("url") { self.errors .push(format!("{}.url : must be present", src_type)); } - if src_type == "source" && !section.contains_key("reference") { + if src_type == "source" && !isset("reference") { self.errors .push(format!("{}.reference : must be present", src_type)); } - if let Some(type_val) = section.get("type") + if let Some(type_val) = section.get("type").filter(|_| isset("type")) && !is_string(type_val) { self.errors.push(format!( @@ -1084,7 +1087,7 @@ impl ValidatingArrayLoader { get_debug_type(type_val) )); } - if let Some(url_val) = section.get("url") + if let Some(url_val) = section.get("url").filter(|_| isset("url")) && !is_string(url_val) { self.errors.push(format!( @@ -1093,7 +1096,7 @@ impl ValidatingArrayLoader { get_debug_type(url_val) )); } - if let Some(ref_val) = section.get("reference") + if let Some(ref_val) = section.get("reference").filter(|_| isset("reference")) && !is_string(ref_val) && !is_int(ref_val) { @@ -1103,7 +1106,7 @@ impl ValidatingArrayLoader { get_debug_type(ref_val) )); } - if let Some(ref_val) = section.get("reference") { + if let Some(ref_val) = section.get("reference").filter(|_| isset("reference")) { let ref_str = php_to_string(ref_val); if Preg::is_match("{^\\s*-}", &ref_str) { self.errors.push(format!( @@ -1112,7 +1115,7 @@ impl ValidatingArrayLoader { )); } } - if let Some(url_val) = section.get("url") { + if let Some(url_val) = section.get("url").filter(|_| isset("url")) { let url_str = php_to_string(url_val); if Preg::is_match("{^\\s*-}", &url_str) { self.errors.push(format!( diff --git a/crates/shirabe/src/package/root_alias_package.rs b/crates/shirabe/src/package/root_alias_package.rs index 140c96d..4d8bd6e 100644 --- a/crates/shirabe/src/package/root_alias_package.rs +++ b/crates/shirabe/src/package/root_alias_package.rs @@ -92,18 +92,35 @@ impl RootPackageInterface for RootAliasPackage { } fn set_dev_requires(&mut self, dev_requires: IndexMap<String, Link>) { + self.inner.inner.dev_requires = self + .inner + .inner + .replace_self_version_dependencies(dev_requires.clone(), Link::TYPE_DEV_REQUIRE); + self.alias_of.set_dev_requires(dev_requires); } fn set_conflicts(&mut self, conflicts: IndexMap<String, Link>) { + self.inner.inner.conflicts = self + .inner + .inner + .replace_self_version_dependencies(conflicts.clone(), Link::TYPE_CONFLICT); self.alias_of.set_conflicts(conflicts); } fn set_provides(&mut self, provides: IndexMap<String, Link>) { + self.inner.inner.provides = self + .inner + .inner + .replace_self_version_dependencies(provides.clone(), Link::TYPE_PROVIDE); self.alias_of.set_provides(provides); } fn set_replaces(&mut self, replaces: IndexMap<String, Link>) { + self.inner.inner.replaces = self + .inner + .inner + .replace_self_version_dependencies(replaces.clone(), Link::TYPE_REPLACE); self.alias_of.set_replaces(replaces); } diff --git a/crates/shirabe/src/repository/repository_factory.rs b/crates/shirabe/src/repository/repository_factory.rs index 47caf59..9cc4e4d 100644 --- a/crates/shirabe/src/repository/repository_factory.rs +++ b/crates/shirabe/src/repository/repository_factory.rs @@ -4,6 +4,7 @@ use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::Preg; use shirabe_php_shim::{ InvalidArgumentException, PhpMixed, UnexpectedValueException, get_debug_type, json_encode, + php_to_string, }; use crate::config::Config; @@ -318,15 +319,11 @@ impl RepositoryFactory { repo: &IndexMap<String, PhpMixed>, existing_repos: &IndexMap<String, T>, ) -> String { - let mut name = match index { - PhpMixed::Int(_) => { - if let Some(url) = repo.get("url").and_then(|v| v.as_string()) { - Preg::replace("{^https?://}i", "", url) - } else { - index.as_string().unwrap_or("").to_string() - } - } - _ => index.as_string().unwrap_or("").to_string(), + let mut name = if matches!(index, PhpMixed::Int(_)) && repo.contains_key("url") { + let url = repo.get("url").and_then(|v| v.as_string()).unwrap_or(""); + Preg::replace("{^https?://}i", "", url) + } else { + php_to_string(index) }; while existing_repos.contains_key(&name) { name.push('2'); diff --git a/crates/shirabe/src/util/forgejo.rs b/crates/shirabe/src/util/forgejo.rs index 4ca4710..3a51be5 100644 --- a/crates/shirabe/src/util/forgejo.rs +++ b/crates/shirabe/src/util/forgejo.rs @@ -124,7 +124,7 @@ impl Forgejo { Err(e) => { let code = e .downcast_ref::<crate::downloader::TransportException>() - .and_then(|te| te.get_status_code()) + .map(|te| te.code) .unwrap_or(0); if [403, 401, 404].contains(&code) { self.io.write_error3( diff --git a/crates/shirabe/src/util/mod.rs b/crates/shirabe/src/util/mod.rs index d34fce5..bd51b66 100644 --- a/crates/shirabe/src/util/mod.rs +++ b/crates/shirabe/src/util/mod.rs @@ -27,6 +27,7 @@ pub mod stream_context_factory; pub mod svn; pub mod sync_helper; pub mod tar; +pub mod tls_helper; pub mod url; pub mod zip; @@ -58,5 +59,6 @@ pub use stream_context_factory::*; pub use svn::*; pub use sync_helper::*; pub use tar::*; +pub use tls_helper::*; pub use url::*; pub use zip::*; diff --git a/crates/shirabe/src/util/tls_helper.rs b/crates/shirabe/src/util/tls_helper.rs new file mode 100644 index 0000000..6422be3 --- /dev/null +++ b/crates/shirabe/src/util/tls_helper.rs @@ -0,0 +1,149 @@ +//! ref: composer/src/Composer/Util/TlsHelper.php + +use shirabe_external_packages::composer::ca_bundle::ca_bundle::CaBundle; +use shirabe_external_packages::composer::pcre::Preg; +use shirabe_php_shim::{ + PhpMixed, ltrim, preg_quote, str_replace, strtolower, substr, substr_count, +}; + +/// Extracted certificate names. Mirrors PHP's `array{cn: string, san: string[]}`. +#[derive(Debug, Clone)] +pub struct CertificateNames { + pub cn: String, + pub san: Vec<String>, +} + +/// Match hostname against a certificate. +/// +/// @deprecated Use composer/ca-bundle and composer/composer 2.2 if you still need PHP 5 +/// compatibility, this class will be removed in Composer 3.0 +#[derive(Debug)] +pub struct TlsHelper; + +impl TlsHelper { + /// Match hostname against a certificate. Sets `cn` to the common name of the + /// certificate iff a match is found. + pub fn check_certificate_host( + certificate: &PhpMixed, + hostname: &str, + cn: &mut Option<String>, + ) -> bool { + let names = Self::get_certificate_names(certificate); + + let Some(names) = names else { + return false; + }; + + let mut combined_names = names.san.clone(); + combined_names.push(names.cn.clone()); + let hostname = strtolower(hostname); + + for cert_name in &combined_names { + let matcher = Self::cert_name_matcher(cert_name); + + if let Some(matcher) = matcher + && matcher(&hostname) + { + *cn = Some(names.cn.clone()); + + return true; + } + } + + false + } + + /// Extract DNS names out of an X.509 certificate. + pub fn get_certificate_names(certificate: &PhpMixed) -> Option<CertificateNames> { + let info: Option<&PhpMixed> = if certificate.as_array().is_some() { + Some(certificate) + } else if CaBundle::is_openssl_parse_safe() { + // TODO(phase-c): openssl_x509_parse on a PEM string certificate. + todo!("openssl_x509_parse for non-array certificates") + } else { + None + }; + + let info = info?.as_array()?; + + let common_name = info + .get("subject") + .and_then(|s| s.as_array()) + .and_then(|s| s.get("commonName")) + .and_then(|c| c.as_string()); + + let common_name = strtolower(common_name?); + let mut subject_alt_names: Vec<String> = Vec::new(); + + if let Some(san) = info + .get("extensions") + .and_then(|e| e.as_array()) + .and_then(|e| e.get("subjectAltName")) + .and_then(|s| s.as_string()) + { + let split = Preg::split("{\\s*,\\s*}", san); + subject_alt_names = split + .into_iter() + .filter_map(|name| { + if name.starts_with("DNS:") { + Some(strtolower(<rim(&substr(&name, 4, None), None))) + } else { + None + } + }) + .collect(); + } + + Some(CertificateNames { + cn: common_name, + san: subject_alt_names, + }) + } + + /// Get the certificate pin. + pub fn get_certificate_fingerprint(_certificate: &str) -> String { + todo!("openssl public key extraction and sha1 fingerprint") + } + + /// Test if it is safe to use the PHP function openssl_x509_parse(). + pub fn is_openssl_parse_safe() -> bool { + CaBundle::is_openssl_parse_safe() + } + + /// Convert certificate name into matching function. + fn cert_name_matcher(cert_name: &str) -> Option<Box<dyn Fn(&str) -> bool>> { + let wildcards = substr_count(cert_name, "*"); + + if wildcards == 0 { + // Literal match. + let cert_name = cert_name.to_string(); + return Some(Box::new(move |hostname: &str| hostname == cert_name)); + } + + if wildcards == 1 { + let components: Vec<&str> = cert_name.split('.').collect(); + + if components.len() < 3 { + // Must have 3+ components + return None; + } + + let first_component = components[0]; + + // Wildcard must be the last character. + if !first_component.ends_with('*') { + return None; + } + + let mut wildcard_regex = preg_quote(cert_name, None); + wildcard_regex = str_replace("\\*", "[a-z0-9-]+", &wildcard_regex); + let wildcard_regex = format!("{{^{}$}}", wildcard_regex); + + return Some(Box::new(move |hostname: &str| { + Preg::is_match(&wildcard_regex, hostname) + })); + } + + None + } +} |
