diff options
Diffstat (limited to 'crates/shirabe/src/package/loader')
| -rw-r--r-- | crates/shirabe/src/package/loader/array_loader.rs | 296 | ||||
| -rw-r--r-- | crates/shirabe/src/package/loader/validating_array_loader.rs | 554 |
2 files changed, 423 insertions, 427 deletions
diff --git a/crates/shirabe/src/package/loader/array_loader.rs b/crates/shirabe/src/package/loader/array_loader.rs index f0399e89..0940183c 100644 --- a/crates/shirabe/src/package/loader/array_loader.rs +++ b/crates/shirabe/src/package/loader/array_loader.rs @@ -39,156 +39,7 @@ impl ArrayLoader { load_options, } } -} - -enum CompleteOrRootPackage { - Complete(CompletePackage), - Root(RootPackage), -} - -impl CompleteOrRootPackage { - fn package(&self) -> &Package { - match self { - Self::Complete(p) => &p.inner, - Self::Root(p) => &p.inner.inner, - } - } - - fn package_mut(&mut self) -> &mut Package { - match self { - Self::Complete(p) => &mut p.inner, - Self::Root(p) => &mut p.inner.inner, - } - } - - fn complete_mut(&mut self) -> &mut dyn CompletePackageInterface { - match self { - Self::Complete(p) => p, - Self::Root(p) => p, - } - } - - fn is_root(&self) -> bool { - matches!(self, Self::Root(_)) - } - - fn get_name(&self) -> &str { - self.package().get_name() - } - - fn get_pretty_version(&self) -> &str { - self.package().get_pretty_version() - } - - fn into_handle(self) -> PackageInterfaceHandle { - match self { - Self::Complete(p) => CompletePackageHandle::from_complete_package(p).into(), - Self::Root(p) => RootPackageHandle::from_root_package(p).into(), - } - } -} - -fn php_to_map(value: &PhpMixed) -> IndexMap<String, PhpMixed> { - match value { - PhpMixed::Array(m) => m.clone(), - _ => IndexMap::new(), - } -} - -fn php_to_string_vec(value: &PhpMixed) -> Vec<String> { - match value { - PhpMixed::List(l) => l.iter().map(strval).collect(), - PhpMixed::Array(m) => m.values().map(strval).collect(), - _ => Vec::new(), - } -} - -fn apply_link_setter(package: &mut Package, method: &str, links: IndexMap<String, Link>) { - if method == Link::TYPE_REQUIRE { - package.set_requires(links); - } else if method == Link::TYPE_DEV_REQUIRE { - package.set_dev_requires(links); - } else if method == Link::TYPE_CONFLICT { - package.set_conflicts(links); - } else if method == Link::TYPE_PROVIDE { - package.set_provides(links); - } else if method == Link::TYPE_REPLACE { - package.set_replaces(links); - } -} - -fn php_to_mirrors(value: &PhpMixed) -> Vec<Mirror> { - let entries: Vec<&PhpMixed> = match value { - PhpMixed::List(l) => l.iter().collect(), - PhpMixed::Array(m) => m.values().collect(), - _ => Vec::new(), - }; - entries - .into_iter() - .filter_map(|entry| match entry { - PhpMixed::Array(m) => Some(Mirror { - url: m - .get("url") - .and_then(|v| v.as_string()) - .unwrap_or("") - .to_string(), - preferred: m.get("preferred").is_some_and(|v| v.to_bool()), - }), - _ => None, - }) - .collect() -} - -impl LoaderInterface for ArrayLoader { - fn as_any(&self) -> &dyn std::any::Any { - self - } - - fn load( - &self, - mut config: IndexMap<String, PhpMixed>, - class: Option<String>, - ) -> anyhow::Result<PackageInterfaceHandle> { - let class = class.unwrap_or_else(|| "Composer\\Package\\CompletePackage".to_string()); - - if class != "Composer\\Package\\CompletePackage" - && class != "Composer\\Package\\RootPackage" - { - trigger_error( - "The $class arg is deprecated, please reach out to Composer maintainers ASAP if you still need this.", - E_USER_DEPRECATED, - ); - } - - let mut package = self.create_object(&config, &class)?; - - for (r#type, opts) in SUPPORTED_LINK_TYPES.iter() { - let entry = config.get(*r#type); - let entry_is_array = entry - .map(|v| matches!(v, PhpMixed::Array(_))) - .unwrap_or(false); - if entry.is_none() || !entry_is_array { - continue; - } - let links = self.parse_links( - package.get_name(), - package.get_pretty_version(), - opts.method, - match entry.unwrap() { - PhpMixed::Array(arr) => arr.clone(), - _ => IndexMap::new(), - }, - )?; - apply_link_setter(package.package_mut(), opts.method, links); - } - - let package = self.configure_object(package, &mut config)?; - - Ok(package) - } -} -impl ArrayLoader { #[tracing::instrument(skip_all)] pub fn load_packages( &self, @@ -938,3 +789,150 @@ impl ArrayLoader { Ok(None) } } + +enum CompleteOrRootPackage { + Complete(CompletePackage), + Root(RootPackage), +} + +impl CompleteOrRootPackage { + fn package(&self) -> &Package { + match self { + Self::Complete(p) => &p.inner, + Self::Root(p) => &p.inner.inner, + } + } + + fn package_mut(&mut self) -> &mut Package { + match self { + Self::Complete(p) => &mut p.inner, + Self::Root(p) => &mut p.inner.inner, + } + } + + fn complete_mut(&mut self) -> &mut dyn CompletePackageInterface { + match self { + Self::Complete(p) => p, + Self::Root(p) => p, + } + } + + fn is_root(&self) -> bool { + matches!(self, Self::Root(_)) + } + + fn get_name(&self) -> &str { + self.package().get_name() + } + + fn get_pretty_version(&self) -> &str { + self.package().get_pretty_version() + } + + fn into_handle(self) -> PackageInterfaceHandle { + match self { + Self::Complete(p) => CompletePackageHandle::from_complete_package(p).into(), + Self::Root(p) => RootPackageHandle::from_root_package(p).into(), + } + } +} + +fn php_to_map(value: &PhpMixed) -> IndexMap<String, PhpMixed> { + match value { + PhpMixed::Array(m) => m.clone(), + _ => IndexMap::new(), + } +} + +fn php_to_string_vec(value: &PhpMixed) -> Vec<String> { + match value { + PhpMixed::List(l) => l.iter().map(strval).collect(), + PhpMixed::Array(m) => m.values().map(strval).collect(), + _ => Vec::new(), + } +} + +fn apply_link_setter(package: &mut Package, method: &str, links: IndexMap<String, Link>) { + if method == Link::TYPE_REQUIRE { + package.set_requires(links); + } else if method == Link::TYPE_DEV_REQUIRE { + package.set_dev_requires(links); + } else if method == Link::TYPE_CONFLICT { + package.set_conflicts(links); + } else if method == Link::TYPE_PROVIDE { + package.set_provides(links); + } else if method == Link::TYPE_REPLACE { + package.set_replaces(links); + } +} + +fn php_to_mirrors(value: &PhpMixed) -> Vec<Mirror> { + let entries: Vec<&PhpMixed> = match value { + PhpMixed::List(l) => l.iter().collect(), + PhpMixed::Array(m) => m.values().collect(), + _ => Vec::new(), + }; + entries + .into_iter() + .filter_map(|entry| match entry { + PhpMixed::Array(m) => Some(Mirror { + url: m + .get("url") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(), + preferred: m.get("preferred").is_some_and(|v| v.to_bool()), + }), + _ => None, + }) + .collect() +} + +impl LoaderInterface for ArrayLoader { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn load( + &self, + mut config: IndexMap<String, PhpMixed>, + class: Option<String>, + ) -> anyhow::Result<PackageInterfaceHandle> { + let class = class.unwrap_or_else(|| "Composer\\Package\\CompletePackage".to_string()); + + if class != "Composer\\Package\\CompletePackage" + && class != "Composer\\Package\\RootPackage" + { + trigger_error( + "The $class arg is deprecated, please reach out to Composer maintainers ASAP if you still need this.", + E_USER_DEPRECATED, + ); + } + + let mut package = self.create_object(&config, &class)?; + + for (r#type, opts) in SUPPORTED_LINK_TYPES.iter() { + let entry = config.get(*r#type); + let entry_is_array = entry + .map(|v| matches!(v, PhpMixed::Array(_))) + .unwrap_or(false); + if entry.is_none() || !entry_is_array { + continue; + } + let links = self.parse_links( + package.get_name(), + package.get_pretty_version(), + opts.method, + match entry.unwrap() { + PhpMixed::Array(arr) => arr.clone(), + _ => IndexMap::new(), + }, + )?; + apply_link_setter(package.package_mut(), opts.method, links); + } + + let package = self.configure_object(package, &mut config)?; + + Ok(package) + } +} diff --git a/crates/shirabe/src/package/loader/validating_array_loader.rs b/crates/shirabe/src/package/loader/validating_array_loader.rs index 0be0ef38..a28df595 100644 --- a/crates/shirabe/src/package/loader/validating_array_loader.rs +++ b/crates/shirabe/src/package/loader/validating_array_loader.rs @@ -60,6 +60,282 @@ impl ValidatingArrayLoader { flags, } } + + pub fn get_warnings(&self) -> Vec<String> { + self.warnings.borrow().clone() + } + + pub fn get_errors(&self) -> Vec<String> { + self.errors.borrow().clone() + } + + pub fn has_package_naming_error(name: &str, is_link: bool) -> Option<String> { + if PlatformRepository::is_platform_package(name) { + return None; + } + + if !Preg::is_match( + php_regex!( + "{^[a-z0-9](?:[_.-]?[a-z0-9]++)*+/[a-z0-9](?:(?:[_.]|-{1,2})?[a-z0-9]++)*+$}iD" + ), + name, + ) { + return Some(format!( + "{} is invalid, it should have a vendor name, a forward slash, and a package name. The vendor and package name can be words separated by -, . or _. The complete name should match \"^[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9](([_.]?|-{{0,2}})[a-z0-9]+)*$\".", + name + )); + } + + let reserved_names = [ + "nul", "con", "prn", "aux", "com1", "com2", "com3", "com4", "com5", "com6", "com7", + "com8", "com9", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9", + ]; + let lower = strtolower(name); + let bits: Vec<&str> = lower.split('/').collect(); + if reserved_names.contains(&bits[0]) || reserved_names.contains(&bits[1]) { + return Some(format!( + "{} is reserved, package and vendor names can not match any of: {}.", + name, + reserved_names.join(", ") + )); + } + + if Preg::is_match(php_regex!("{\\.json$}"), name) { + return Some(format!( + "{} is invalid, package names can not end in .json, consider renaming it or perhaps using a -json suffix instead.", + name + )); + } + + if Preg::is_match(php_regex!("{[A-Z]}"), name) { + if is_link { + return Some(format!( + "{} is invalid, it should not contain uppercase characters. Please use {} instead.", + name, + strtolower(name) + )); + } + + let suggest_name = Preg::replace( + php_regex!("{(?:([a-z])([A-Z])|([A-Z])([A-Z][a-z]))}"), + "\\1\\3-\\2\\4", + name, + ); + let suggest_name = strtolower(&suggest_name); + + return Some(format!( + "{} is invalid, it should not contain uppercase characters. We suggest using {} instead.", + name, suggest_name + )); + } + + None + } + + fn validate_regex(&self, property: &str, regex: &str, mandatory: bool) -> bool { + if !self.validate_string(property, mandatory) { + return false; + } + + let value = self.config.borrow()[property] + .as_string() + .unwrap_or("") + .to_string(); + if !Preg::is_match(format!("{{^{}$}}u", regex), &value) { + let message = format!( + "{} : invalid value ({}), must match {}", + property, value, regex + ); + if mandatory { + self.errors.borrow_mut().push(message); + } else { + self.warnings.borrow_mut().push(message); + } + self.config.borrow_mut().shift_remove(property); + + return false; + } + + true + } + + fn validate_string(&self, property: &str, mandatory: bool) -> bool { + if self.config.borrow().contains_key(property) + && !is_string(&self.config.borrow()[property]) + { + self.errors.borrow_mut().push(format!( + "{} : should be a string, {} given", + property, + get_debug_type(&self.config.borrow()[property]) + )); + self.config.borrow_mut().shift_remove(property); + + return false; + } + + let is_empty = !self.config.borrow().contains_key(property) + || trim( + self.config.borrow()[property].as_string().unwrap_or(""), + Some(" \t\n\r\0\u{0B}"), + ) + .is_empty(); + if is_empty { + if mandatory { + self.errors + .borrow_mut() + .push(format!("{} : must be present", property)); + } + self.config.borrow_mut().shift_remove(property); + + return false; + } + + true + } + + fn validate_array(&self, property: &str, mandatory: bool) -> bool { + if self.config.borrow().contains_key(property) && !is_array(&self.config.borrow()[property]) + { + self.errors.borrow_mut().push(format!( + "{} : should be an array, {} given", + property, + get_debug_type(&self.config.borrow()[property]) + )); + self.config.borrow_mut().shift_remove(property); + + return false; + } + + let is_empty = !self.config.borrow().contains_key(property) + || match &self.config.borrow()[property] { + PhpMixed::Array(m) => m.is_empty(), + PhpMixed::List(l) => l.is_empty(), + // is_array() above guarantees the value is Array or List here. + _ => unreachable!("validate_array: non-array value survived the is_array check"), + }; + if is_empty { + if mandatory { + self.errors.borrow_mut().push(format!( + "{} : must be present and contain at least one element", + property + )); + } + self.config.borrow_mut().shift_remove(property); + + return false; + } + + true + } + + fn validate_flat_array(&self, property: &str, regex: Option<&str>, mandatory: bool) -> bool { + if !self.validate_array(property, mandatory) { + return false; + } + + let mut pass = true; + let entries: Vec<(String, PhpMixed)> = self.config.borrow()[property] + .as_array() + .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect()) + .unwrap_or_default(); + for (key, value) in entries { + if !is_string(&value) && !is_numeric(&value) { + self.errors.borrow_mut().push(format!( + "{}.{} : must be a string or int, {} given", + property, + key, + get_debug_type(&value) + )); + if let Some(PhpMixed::Array(arr)) = self.config.borrow_mut().get_mut(property) { + arr.shift_remove(&key); + } + pass = false; + + continue; + } + + if let Some(regex_str) = regex { + let value_str = php_to_string(&value); + if !Preg::is_match(format!("{{^{}$}}u", regex_str), &value_str) { + self.warnings.borrow_mut().push(format!( + "{}.{} : invalid value ({}), must match {}", + property, key, value_str, regex_str + )); + if let Some(PhpMixed::Array(arr)) = self.config.borrow_mut().get_mut(property) { + arr.shift_remove(&key); + } + pass = false; + } + } + } + + pass + } + + fn validate_url(&self, property: &str, mandatory: bool) -> bool { + if !self.validate_string(property, mandatory) { + return false; + } + + let value = self.config.borrow()[property] + .as_string() + .unwrap_or("") + .to_string(); + if !self.filter_url(&value, &["http", "https"]) { + self.warnings.borrow_mut().push(format!( + "{} : invalid value ({}), must be an http/https URL", + property, value + )); + self.config.borrow_mut().shift_remove(property); + + return false; + } + + true + } + + fn filter_url(&self, value: &str, schemes: &[&str]) -> bool { + if value.is_empty() { + return true; + } + + let bits = parse_url_all(value); + let bits_map = match bits { + PhpMixed::Array(m) => m, + _ => return false, + }; + let scheme = bits_map + .get("scheme") + .and_then(|v| v.as_string()) + .unwrap_or(""); + let host = bits_map + .get("host") + .and_then(|v| v.as_string()) + .unwrap_or(""); + if scheme.is_empty() || host.is_empty() { + return false; + } + + if !schemes.contains(&scheme) { + return false; + } + + true + } + + fn is_empty_array(val: Option<&PhpMixed>) -> bool { + match val { + Some(v) => match v { + PhpMixed::Array(m) => m.is_empty(), + PhpMixed::Null => true, + PhpMixed::Bool(false) => true, + PhpMixed::String(s) => s.is_empty(), + PhpMixed::Int(0) => true, + _ => false, + }, + None => true, + } + } } impl LoaderInterface for ValidatingArrayLoader { @@ -1327,281 +1603,3 @@ impl LoaderInterface for ValidatingArrayLoader { Ok(package) } } - -impl ValidatingArrayLoader { - pub fn get_warnings(&self) -> Vec<String> { - self.warnings.borrow().clone() - } - - pub fn get_errors(&self) -> Vec<String> { - self.errors.borrow().clone() - } - - pub fn has_package_naming_error(name: &str, is_link: bool) -> Option<String> { - if PlatformRepository::is_platform_package(name) { - return None; - } - - if !Preg::is_match( - php_regex!( - "{^[a-z0-9](?:[_.-]?[a-z0-9]++)*+/[a-z0-9](?:(?:[_.]|-{1,2})?[a-z0-9]++)*+$}iD" - ), - name, - ) { - return Some(format!( - "{} is invalid, it should have a vendor name, a forward slash, and a package name. The vendor and package name can be words separated by -, . or _. The complete name should match \"^[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9](([_.]?|-{{0,2}})[a-z0-9]+)*$\".", - name - )); - } - - let reserved_names = [ - "nul", "con", "prn", "aux", "com1", "com2", "com3", "com4", "com5", "com6", "com7", - "com8", "com9", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9", - ]; - let lower = strtolower(name); - let bits: Vec<&str> = lower.split('/').collect(); - if reserved_names.contains(&bits[0]) || reserved_names.contains(&bits[1]) { - return Some(format!( - "{} is reserved, package and vendor names can not match any of: {}.", - name, - reserved_names.join(", ") - )); - } - - if Preg::is_match(php_regex!("{\\.json$}"), name) { - return Some(format!( - "{} is invalid, package names can not end in .json, consider renaming it or perhaps using a -json suffix instead.", - name - )); - } - - if Preg::is_match(php_regex!("{[A-Z]}"), name) { - if is_link { - return Some(format!( - "{} is invalid, it should not contain uppercase characters. Please use {} instead.", - name, - strtolower(name) - )); - } - - let suggest_name = Preg::replace( - php_regex!("{(?:([a-z])([A-Z])|([A-Z])([A-Z][a-z]))}"), - "\\1\\3-\\2\\4", - name, - ); - let suggest_name = strtolower(&suggest_name); - - return Some(format!( - "{} is invalid, it should not contain uppercase characters. We suggest using {} instead.", - name, suggest_name - )); - } - - None - } - - fn validate_regex(&self, property: &str, regex: &str, mandatory: bool) -> bool { - if !self.validate_string(property, mandatory) { - return false; - } - - let value = self.config.borrow()[property] - .as_string() - .unwrap_or("") - .to_string(); - if !Preg::is_match(format!("{{^{}$}}u", regex), &value) { - let message = format!( - "{} : invalid value ({}), must match {}", - property, value, regex - ); - if mandatory { - self.errors.borrow_mut().push(message); - } else { - self.warnings.borrow_mut().push(message); - } - self.config.borrow_mut().shift_remove(property); - - return false; - } - - true - } - - fn validate_string(&self, property: &str, mandatory: bool) -> bool { - if self.config.borrow().contains_key(property) - && !is_string(&self.config.borrow()[property]) - { - self.errors.borrow_mut().push(format!( - "{} : should be a string, {} given", - property, - get_debug_type(&self.config.borrow()[property]) - )); - self.config.borrow_mut().shift_remove(property); - - return false; - } - - let is_empty = !self.config.borrow().contains_key(property) - || trim( - self.config.borrow()[property].as_string().unwrap_or(""), - Some(" \t\n\r\0\u{0B}"), - ) - .is_empty(); - if is_empty { - if mandatory { - self.errors - .borrow_mut() - .push(format!("{} : must be present", property)); - } - self.config.borrow_mut().shift_remove(property); - - return false; - } - - true - } - - fn validate_array(&self, property: &str, mandatory: bool) -> bool { - if self.config.borrow().contains_key(property) && !is_array(&self.config.borrow()[property]) - { - self.errors.borrow_mut().push(format!( - "{} : should be an array, {} given", - property, - get_debug_type(&self.config.borrow()[property]) - )); - self.config.borrow_mut().shift_remove(property); - - return false; - } - - let is_empty = !self.config.borrow().contains_key(property) - || match &self.config.borrow()[property] { - PhpMixed::Array(m) => m.is_empty(), - PhpMixed::List(l) => l.is_empty(), - // is_array() above guarantees the value is Array or List here. - _ => unreachable!("validate_array: non-array value survived the is_array check"), - }; - if is_empty { - if mandatory { - self.errors.borrow_mut().push(format!( - "{} : must be present and contain at least one element", - property - )); - } - self.config.borrow_mut().shift_remove(property); - - return false; - } - - true - } - - fn validate_flat_array(&self, property: &str, regex: Option<&str>, mandatory: bool) -> bool { - if !self.validate_array(property, mandatory) { - return false; - } - - let mut pass = true; - let entries: Vec<(String, PhpMixed)> = self.config.borrow()[property] - .as_array() - .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect()) - .unwrap_or_default(); - for (key, value) in entries { - if !is_string(&value) && !is_numeric(&value) { - self.errors.borrow_mut().push(format!( - "{}.{} : must be a string or int, {} given", - property, - key, - get_debug_type(&value) - )); - if let Some(PhpMixed::Array(arr)) = self.config.borrow_mut().get_mut(property) { - arr.shift_remove(&key); - } - pass = false; - - continue; - } - - if let Some(regex_str) = regex { - let value_str = php_to_string(&value); - if !Preg::is_match(format!("{{^{}$}}u", regex_str), &value_str) { - self.warnings.borrow_mut().push(format!( - "{}.{} : invalid value ({}), must match {}", - property, key, value_str, regex_str - )); - if let Some(PhpMixed::Array(arr)) = self.config.borrow_mut().get_mut(property) { - arr.shift_remove(&key); - } - pass = false; - } - } - } - - pass - } - - fn validate_url(&self, property: &str, mandatory: bool) -> bool { - if !self.validate_string(property, mandatory) { - return false; - } - - let value = self.config.borrow()[property] - .as_string() - .unwrap_or("") - .to_string(); - if !self.filter_url(&value, &["http", "https"]) { - self.warnings.borrow_mut().push(format!( - "{} : invalid value ({}), must be an http/https URL", - property, value - )); - self.config.borrow_mut().shift_remove(property); - - return false; - } - - true - } - - fn filter_url(&self, value: &str, schemes: &[&str]) -> bool { - if value.is_empty() { - return true; - } - - let bits = parse_url_all(value); - let bits_map = match bits { - PhpMixed::Array(m) => m, - _ => return false, - }; - let scheme = bits_map - .get("scheme") - .and_then(|v| v.as_string()) - .unwrap_or(""); - let host = bits_map - .get("host") - .and_then(|v| v.as_string()) - .unwrap_or(""); - if scheme.is_empty() || host.is_empty() { - return false; - } - - if !schemes.contains(&scheme) { - return false; - } - - true - } - - fn is_empty_array(val: Option<&PhpMixed>) -> bool { - match val { - Some(v) => match v { - PhpMixed::Array(m) => m.is_empty(), - PhpMixed::Null => true, - PhpMixed::Bool(false) => true, - PhpMixed::String(s) => s.is_empty(), - PhpMixed::Int(0) => true, - _ => false, - }, - None => true, - } - } -} |
