aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe/src')
-rw-r--r--crates/shirabe/src/command/config_command.rs29
-rw-r--r--crates/shirabe/src/package/loader/validating_array_loader.rs19
-rw-r--r--crates/shirabe/src/package/root_alias_package.rs17
-rw-r--r--crates/shirabe/src/repository/repository_factory.rs15
-rw-r--r--crates/shirabe/src/util/forgejo.rs2
-rw-r--r--crates/shirabe/src/util/mod.rs2
-rw-r--r--crates/shirabe/src/util/tls_helper.rs149
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 &current_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(&ltrim(&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
+ }
+}