aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe')
-rw-r--r--crates/shirabe/src/dependency_resolver/request.rs224
-rw-r--r--crates/shirabe/src/downloader/svn_downloader.rs400
-rw-r--r--crates/shirabe/src/repository/path_repository.rs329
-rw-r--r--crates/shirabe/src/repository/vcs/fossil_driver.rs311
-rw-r--r--crates/shirabe/src/util/stream_context_factory.rs381
5 files changed, 1645 insertions, 0 deletions
diff --git a/crates/shirabe/src/dependency_resolver/request.rs b/crates/shirabe/src/dependency_resolver/request.rs
index d6edc98..9aef975 100644
--- a/crates/shirabe/src/dependency_resolver/request.rs
+++ b/crates/shirabe/src/dependency_resolver/request.rs
@@ -1 +1,225 @@
//! ref: composer/src/Composer/DependencyResolver/Request.php
+
+use indexmap::IndexMap;
+use shirabe_php_shim::{spl_object_hash, strtolower, LogicException};
+use shirabe_semver::constraint::constraint_interface::ConstraintInterface;
+use shirabe_semver::constraint::match_all_constraint::MatchAllConstraint;
+
+use crate::package::base_package::BasePackage;
+use crate::package::package_interface::PackageInterface;
+use crate::repository::lock_array_repository::LockArrayRepository;
+
+/// Identifies a partial update for listed packages only, all dependencies will remain at locked versions
+pub const UPDATE_ONLY_LISTED: i64 = 0;
+
+/// Identifies a partial update for listed packages and recursively all their dependencies, however
+/// dependencies also directly required by the root composer.json and their dependencies will remain
+/// at the locked version.
+pub const UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE: i64 = 1;
+
+/// Identifies a partial update for listed packages and recursively all their dependencies, even
+/// dependencies also directly required by the root composer.json will be updated.
+pub const UPDATE_LISTED_WITH_TRANSITIVE_DEPS: i64 = 2;
+
+/// Represents the value of updateAllowTransitiveDependencies, which is false|UPDATE_* in PHP.
+#[derive(Debug, Clone, PartialEq)]
+pub enum UpdateAllowTransitiveDeps {
+ /// Corresponds to PHP false (initial value)
+ False,
+ UpdateOnlyListed,
+ UpdateListedWithTransitiveDepsNoRootRequire,
+ UpdateListedWithTransitiveDeps,
+}
+
+#[derive(Debug)]
+pub struct Request {
+ pub(crate) locked_repository: Option<LockArrayRepository>,
+ pub(crate) requires: IndexMap<String, Box<dyn ConstraintInterface>>,
+ pub(crate) fixed_packages: IndexMap<String, BasePackage>,
+ pub(crate) locked_packages: IndexMap<String, BasePackage>,
+ pub(crate) fixed_locked_packages: IndexMap<String, BasePackage>,
+ pub(crate) update_allow_list: Vec<String>,
+ pub(crate) update_allow_transitive_dependencies: UpdateAllowTransitiveDeps,
+ restrict_packages: Option<Vec<String>>,
+}
+
+impl Request {
+ pub fn new(locked_repository: Option<LockArrayRepository>) -> Self {
+ Self {
+ locked_repository,
+ requires: IndexMap::new(),
+ fixed_packages: IndexMap::new(),
+ locked_packages: IndexMap::new(),
+ fixed_locked_packages: IndexMap::new(),
+ update_allow_list: vec![],
+ update_allow_transitive_dependencies: UpdateAllowTransitiveDeps::False,
+ restrict_packages: None,
+ }
+ }
+
+ pub fn require_name(
+ &mut self,
+ package_name: &str,
+ constraint: Option<Box<dyn ConstraintInterface>>,
+ ) -> anyhow::Result<()> {
+ let package_name = strtolower(package_name);
+ let constraint = constraint.unwrap_or_else(|| Box::new(MatchAllConstraint::new()));
+ if self.requires.contains_key(&package_name) {
+ return Err(LogicException {
+ message: format!(
+ "Overwriting requires seems like a bug ({} {} => {}, check why it is happening, might be a root alias",
+ package_name,
+ self.requires[&package_name].get_pretty_string(),
+ constraint.get_pretty_string()
+ ),
+ code: 0,
+ }
+ .into());
+ }
+ self.requires.insert(package_name, constraint);
+ Ok(())
+ }
+
+ /// Mark a package as currently present and having to remain installed.
+ ///
+ /// This is used for platform packages which cannot be modified by Composer. A rule enforcing
+ /// their installation is generated for dependency resolution. Partial updates with dependencies
+ /// cannot in any way modify these packages.
+ pub fn fix_package(&mut self, package: BasePackage) {
+ let hash = spl_object_hash(&package);
+ self.fixed_packages.insert(hash, package);
+ }
+
+ /// Mark a package as locked to a specific version but removable.
+ ///
+ /// This is used for lock file packages which need to be treated similar to fixed packages by
+ /// the pool builder in that by default they should really only have the currently present
+ /// version loaded and no remote alternatives.
+ ///
+ /// However unlike fixed packages there will not be a special rule enforcing their installation
+ /// for the solver, so if nothing requires these packages they will be removed. Additionally in
+ /// a partial update these packages can be unlocked, meaning other versions can be installed if
+ /// explicitly requested as part of the update.
+ pub fn lock_package(&mut self, package: BasePackage) {
+ let hash = spl_object_hash(&package);
+ self.locked_packages.insert(hash, package);
+ }
+
+ /// Marks a locked package fixed. So it's treated irremovable like a platform package.
+ ///
+ /// This is necessary for the composer install step which verifies the lock file integrity and
+ /// should not allow removal of any packages. At the same time lock packages there cannot simply
+ /// be marked fixed, as error reporting would then report them as platform packages, so this
+ /// still marks them as locked packages at the same time.
+ pub fn fix_locked_package(&mut self, package: BasePackage) {
+ let hash = spl_object_hash(&package);
+ self.fixed_packages.insert(hash.clone(), package.clone());
+ self.fixed_locked_packages.insert(hash, package);
+ }
+
+ pub fn unlock_package(&mut self, package: &BasePackage) {
+ self.locked_packages.remove(&spl_object_hash(package));
+ }
+
+ pub fn set_update_allow_list(
+ &mut self,
+ update_allow_list: Vec<String>,
+ update_allow_transitive_dependencies: UpdateAllowTransitiveDeps,
+ ) {
+ self.update_allow_list = update_allow_list;
+ self.update_allow_transitive_dependencies = update_allow_transitive_dependencies;
+ }
+
+ pub fn get_update_allow_list(&self) -> &Vec<String> {
+ &self.update_allow_list
+ }
+
+ pub fn get_update_allow_transitive_dependencies(&self) -> bool {
+ // PHP: $this->updateAllowTransitiveDependencies !== self::UPDATE_ONLY_LISTED
+ // false !== 0 is true in PHP (strict inequality, different types)
+ self.update_allow_transitive_dependencies != UpdateAllowTransitiveDeps::UpdateOnlyListed
+ }
+
+ pub fn get_update_allow_transitive_root_dependencies(&self) -> bool {
+ self.update_allow_transitive_dependencies
+ == UpdateAllowTransitiveDeps::UpdateListedWithTransitiveDeps
+ }
+
+ pub fn get_requires(&self) -> &IndexMap<String, Box<dyn ConstraintInterface>> {
+ &self.requires
+ }
+
+ pub fn get_fixed_packages(&self) -> &IndexMap<String, BasePackage> {
+ &self.fixed_packages
+ }
+
+ pub fn is_fixed_package(&self, package: &BasePackage) -> bool {
+ self.fixed_packages.contains_key(&spl_object_hash(package))
+ }
+
+ pub fn get_locked_packages(&self) -> &IndexMap<String, BasePackage> {
+ &self.locked_packages
+ }
+
+ pub fn is_locked_package(&self, package: &dyn PackageInterface) -> bool {
+ let hash = spl_object_hash(package);
+ self.locked_packages.contains_key(&hash) || self.fixed_locked_packages.contains_key(&hash)
+ }
+
+ pub fn get_fixed_or_locked_packages(&self) -> IndexMap<String, BasePackage> {
+ let mut result = self.fixed_packages.clone();
+ result.extend(self.locked_packages.clone());
+ result
+ }
+
+ /// @TODO look into removing the packageIds option, the only place true is used
+ /// is for the installed map in the solver problems.
+ /// Some locked packages may not be in the pool,
+ /// so they have a package->id of -1
+ pub fn get_present_map(&self, package_ids: bool) -> IndexMap<String, BasePackage> {
+ let mut present_map: IndexMap<String, BasePackage> = IndexMap::new();
+
+ if let Some(ref locked_repository) = self.locked_repository {
+ for package in locked_repository.get_packages() {
+ let key = if package_ids {
+ package.get_id().to_string()
+ } else {
+ spl_object_hash(&package)
+ };
+ present_map.insert(key, package);
+ }
+ }
+
+ for (_, package) in &self.fixed_packages {
+ let key = if package_ids {
+ package.get_id().to_string()
+ } else {
+ spl_object_hash(package)
+ };
+ present_map.insert(key, package.clone());
+ }
+
+ present_map
+ }
+
+ pub fn get_fixed_packages_map(&self) -> IndexMap<i64, BasePackage> {
+ let mut fixed_packages_map: IndexMap<i64, BasePackage> = IndexMap::new();
+ for (_, package) in &self.fixed_packages {
+ fixed_packages_map.insert(package.get_id(), package.clone());
+ }
+ fixed_packages_map
+ }
+
+ pub fn get_locked_repository(&self) -> Option<&LockArrayRepository> {
+ self.locked_repository.as_ref()
+ }
+
+ /// Restricts the pool builder from loading other packages than those listed here.
+ pub fn restrict_packages(&mut self, names: Vec<String>) {
+ self.restrict_packages = Some(names);
+ }
+
+ pub fn get_restricted_packages(&self) -> Option<&Vec<String>> {
+ self.restrict_packages.as_ref()
+ }
+}
diff --git a/crates/shirabe/src/downloader/svn_downloader.rs b/crates/shirabe/src/downloader/svn_downloader.rs
index d66e7d9..75e5c02 100644
--- a/crates/shirabe/src/downloader/svn_downloader.rs
+++ b/crates/shirabe/src/downloader/svn_downloader.rs
@@ -1 +1,401 @@
//! ref: composer/src/Composer/Downloader/SvnDownloader.php
+
+use shirabe_external_packages::composer::pcre::preg::Preg;
+use shirabe_external_packages::react::promise;
+use shirabe_external_packages::react::promise::promise_interface::PromiseInterface;
+use shirabe_php_shim::{is_dir, version_compare, PhpMixed, RuntimeException};
+
+use crate::downloader::vcs_downloader::VcsDownloader;
+use crate::io::io_interface::IOInterface;
+use crate::package::package_interface::PackageInterface;
+use crate::repository::vcs_repository::VcsRepository;
+use crate::util::svn::Svn as SvnUtil;
+
+#[derive(Debug)]
+pub struct SvnDownloader {
+ inner: VcsDownloader,
+ pub(crate) cache_credentials: bool,
+}
+
+impl SvnDownloader {
+ pub(crate) fn do_download(
+ &mut self,
+ package: &dyn PackageInterface,
+ path: &str,
+ url: &str,
+ prev_package: Option<&dyn PackageInterface>,
+ ) -> anyhow::Result<Box<dyn PromiseInterface>> {
+ SvnUtil::clean_env();
+ let util = SvnUtil::new(url, &*self.inner.io, &self.inner.config, &self.inner.process);
+ if util.binary_version().is_none() {
+ return Err(RuntimeException {
+ message: "svn was not found in your PATH, skipping source download".to_string(),
+ code: 0,
+ }
+ .into());
+ }
+
+ Ok(promise::resolve(None))
+ }
+
+ pub(crate) fn do_install(
+ &mut self,
+ package: &dyn PackageInterface,
+ path: &str,
+ url: &str,
+ ) -> anyhow::Result<Box<dyn PromiseInterface>> {
+ SvnUtil::clean_env();
+ let r#ref = package.get_source_reference();
+
+ let repo = package.get_repository();
+ if let Some(repo) = repo {
+ if let Some(vcs_repo) = repo.as_any().downcast_ref::<VcsRepository>() {
+ let repo_config = vcs_repo.get_repo_config();
+ if repo_config.contains_key("svn-cache-credentials") {
+ if let Some(val) = repo_config.get("svn-cache-credentials").and_then(|v| v.as_bool()) {
+ self.cache_credentials = val;
+ }
+ }
+ }
+ }
+
+ self.inner.io.write_error(
+ PhpMixed::String(format!(" Checking out {}", package.get_source_reference())),
+ true,
+ IOInterface::NORMAL,
+ );
+ self.execute(
+ package,
+ url,
+ vec!["svn".to_string(), "co".to_string()],
+ &format!("{}/{}", url, r#ref),
+ None,
+ Some(path),
+ )?;
+
+ Ok(promise::resolve(None))
+ }
+
+ pub(crate) fn do_update(
+ &mut self,
+ initial: &dyn PackageInterface,
+ target: &dyn PackageInterface,
+ path: &str,
+ url: &str,
+ ) -> anyhow::Result<Box<dyn PromiseInterface>> {
+ SvnUtil::clean_env();
+ let r#ref = target.get_source_reference();
+
+ if !self.has_metadata_repository(path) {
+ return Err(RuntimeException {
+ message: format!(
+ "The .svn directory is missing from {}, see https://getcomposer.org/commit-deps for more information",
+ path
+ ),
+ code: 0,
+ }
+ .into());
+ }
+
+ let util = SvnUtil::new(url, &*self.inner.io, &self.inner.config, &self.inner.process);
+ let mut flags: Vec<String> = vec![];
+ if version_compare(&util.binary_version().unwrap_or_default(), "1.7.0", ">=") {
+ flags.push("--ignore-ancestry".to_string());
+ }
+
+ self.inner.io.write_error(
+ PhpMixed::String(format!(" Checking out {}", r#ref)),
+ true,
+ IOInterface::NORMAL,
+ );
+ let mut command = vec!["svn".to_string(), "switch".to_string()];
+ command.extend(flags);
+ self.execute(
+ target,
+ url,
+ command,
+ &format!("{}/{}", url, r#ref),
+ Some(path),
+ None,
+ )?;
+
+ Ok(promise::resolve(None))
+ }
+
+ pub fn get_local_changes(&self, package: &dyn PackageInterface, path: &str) -> Option<String> {
+ if !self.has_metadata_repository(path) {
+ return None;
+ }
+
+ let mut output = String::new();
+ self.inner.process.execute(
+ &["svn", "status", "--ignore-externals"]
+ .map(|s| s.to_string())
+ .to_vec(),
+ &mut output,
+ Some(path.to_string()),
+ );
+
+ if Preg::is_match("{^ *[^X ] +}m", &output).unwrap_or(false) {
+ Some(output)
+ } else {
+ None
+ }
+ }
+
+ pub(crate) fn execute(
+ &self,
+ package: &dyn PackageInterface,
+ base_url: &str,
+ command: Vec<String>,
+ url: &str,
+ cwd: Option<&str>,
+ path: Option<&str>,
+ ) -> anyhow::Result<String> {
+ let mut util = SvnUtil::new(
+ base_url,
+ &*self.inner.io,
+ &self.inner.config,
+ &self.inner.process,
+ );
+ util.set_cache_credentials(self.cache_credentials);
+ util.execute(command, url, cwd, path, self.inner.io.is_verbose())
+ .map_err(|e| {
+ anyhow::anyhow!(
+ "{} could not be downloaded, {}",
+ package.get_pretty_name(),
+ e
+ )
+ })
+ }
+
+ pub(crate) fn clean_changes(
+ &mut self,
+ package: &dyn PackageInterface,
+ path: &str,
+ update: bool,
+ ) -> anyhow::Result<Box<dyn PromiseInterface>> {
+ let changes = self.get_local_changes(package, path);
+ if changes.is_none() {
+ return Ok(promise::resolve(None));
+ }
+
+ if !self.inner.io.is_interactive() {
+ if self.inner.config.get("discard-changes").as_bool() == Some(true) {
+ return self.discard_changes(path);
+ }
+
+ return self.inner.clean_changes(package, path, update);
+ }
+
+ let changes_str = changes.unwrap();
+ let changes: Vec<String> = Preg::split(r"{\s*\r?\n\s*}", &changes_str)
+ .into_iter()
+ .map(|elem| format!(" {}", elem))
+ .collect();
+ let count_changes = changes.len() as i64;
+ self.inner.io.write_error(
+ PhpMixed::String(format!(
+ " <error>{} has modified file{}:</error>",
+ package.get_pretty_name(),
+ if count_changes == 1 { "" } else { "s" }
+ )),
+ true,
+ IOInterface::NORMAL,
+ );
+ let slice_end = 10_usize.min(changes.len());
+ self.inner.io.write_error(
+ PhpMixed::List(
+ changes[..slice_end]
+ .iter()
+ .map(|s| Box::new(PhpMixed::String(s.clone())))
+ .collect(),
+ ),
+ true,
+ IOInterface::NORMAL,
+ );
+ if count_changes > 10 {
+ let remaining_changes = count_changes - 10;
+ self.inner.io.write_error(
+ PhpMixed::String(format!(
+ " <info>{} more file{} modified, choose \"v\" to view the full list</info>",
+ remaining_changes,
+ if remaining_changes == 1 { "" } else { "s" }
+ )),
+ true,
+ IOInterface::NORMAL,
+ );
+ }
+
+ loop {
+ match self
+ .inner
+ .io
+ .ask(
+ " <info>Discard changes [y,n,v,?]?</info> ".to_string(),
+ PhpMixed::String("?".to_string()),
+ )
+ .as_string()
+ {
+ Some("y") => {
+ self.discard_changes(path)?;
+ break;
+ }
+ Some("n") => {
+ return Err(RuntimeException {
+ message: "Update aborted".to_string(),
+ code: 0,
+ }
+ .into());
+ }
+ Some("v") => {
+ self.inner.io.write_error(
+ PhpMixed::List(
+ changes
+ .iter()
+ .map(|s| Box::new(PhpMixed::String(s.clone())))
+ .collect(),
+ ),
+ true,
+ IOInterface::NORMAL,
+ );
+ }
+ _ => {
+ self.inner.io.write_error(
+ PhpMixed::List(vec![
+ Box::new(PhpMixed::String(format!(
+ " y - discard changes and apply the {}",
+ if update { "update" } else { "uninstall" }
+ ))),
+ Box::new(PhpMixed::String(format!(
+ " n - abort the {} and let you manually clean things up",
+ if update { "update" } else { "uninstall" }
+ ))),
+ Box::new(PhpMixed::String(
+ " v - view modified files".to_string(),
+ )),
+ Box::new(PhpMixed::String(" ? - print help".to_string())),
+ ]),
+ true,
+ IOInterface::NORMAL,
+ );
+ }
+ }
+ }
+
+ Ok(promise::resolve(None))
+ }
+
+ pub(crate) fn get_commit_logs(
+ &self,
+ from_reference: &str,
+ to_reference: &str,
+ path: &str,
+ ) -> anyhow::Result<String> {
+ if Preg::is_match(r"{@(\d+)$}", from_reference).unwrap_or(false)
+ && Preg::is_match(r"{@(\d+)$}", to_reference).unwrap_or(false)
+ {
+ // retrieve the svn base url from the checkout folder
+ let command = vec![
+ "svn".to_string(),
+ "info".to_string(),
+ "--non-interactive".to_string(),
+ "--xml".to_string(),
+ "--".to_string(),
+ path.to_string(),
+ ];
+ let mut output = String::new();
+ if self
+ .inner
+ .process
+ .execute(&command, &mut output, Some(path.to_string()))
+ != 0
+ {
+ return Err(RuntimeException {
+ message: format!(
+ "Failed to execute {}\n\n{}",
+ command.join(" "),
+ self.inner.process.get_error_output()
+ ),
+ code: 0,
+ }
+ .into());
+ }
+
+ let url_pattern = "#<url>(.*)</url>#";
+ let base_url = if let Some(matches) = Preg::match_strict_groups(url_pattern, &output) {
+ matches.get("1").cloned().unwrap_or_default()
+ } else {
+ return Err(RuntimeException {
+ message: format!("Unable to determine svn url for path {}", path),
+ code: 0,
+ }
+ .into());
+ };
+
+ // strip paths from references and only keep the actual revision
+ let from_revision = Preg::replace(r"{.*@(\d+)$}", "$1", from_reference.to_string());
+ let to_revision = Preg::replace(r"{.*@(\d+)$}", "$1", to_reference.to_string());
+
+ let command = vec![
+ "svn".to_string(),
+ "log".to_string(),
+ "-r".to_string(),
+ format!("{}:{}", from_revision, to_revision),
+ "--incremental".to_string(),
+ ];
+
+ let mut util = SvnUtil::new(
+ &base_url,
+ &*self.inner.io,
+ &self.inner.config,
+ &self.inner.process,
+ );
+ util.set_cache_credentials(self.cache_credentials);
+ util.execute_local(command.clone(), path, None, self.inner.io.is_verbose())
+ .map_err(|e| {
+ RuntimeException {
+ message: format!(
+ "Failed to execute {}\n\n{}",
+ command.join(" "),
+ e
+ ),
+ code: 0,
+ }
+ .into()
+ })
+ } else {
+ Ok(format!(
+ "Could not retrieve changes between {} and {} due to missing revision information",
+ from_reference, to_reference
+ ))
+ }
+ }
+
+ pub(crate) fn discard_changes(&self, path: &str) -> anyhow::Result<Box<dyn PromiseInterface>> {
+ let mut output = String::new();
+ if self.inner.process.execute(
+ &["svn", "revert", "-R", "."]
+ .map(|s| s.to_string())
+ .to_vec(),
+ &mut output,
+ Some(path.to_string()),
+ ) != 0
+ {
+ return Err(RuntimeException {
+ message: format!(
+ "Could not reset changes\n\n:{}",
+ self.inner.process.get_error_output()
+ ),
+ code: 0,
+ }
+ .into());
+ }
+
+ Ok(promise::resolve(None))
+ }
+
+ pub(crate) fn has_metadata_repository(&self, path: &str) -> bool {
+ is_dir(&format!("{}/.svn", path))
+ }
+}
diff --git a/crates/shirabe/src/repository/path_repository.rs b/crates/shirabe/src/repository/path_repository.rs
index 894668b..f2b470d 100644
--- a/crates/shirabe/src/repository/path_repository.rs
+++ b/crates/shirabe/src/repository/path_repository.rs
@@ -1 +1,330 @@
//! ref: composer/src/Composer/Repository/PathRepository.php
+
+use indexmap::IndexMap;
+use shirabe_external_packages::composer::pcre::preg::Preg;
+use shirabe_php_shim::{
+ defined, file_exists, file_get_contents, glob_with_flags, hash, realpath, serialize,
+ PhpMixed, RuntimeException, DIRECTORY_SEPARATOR, GLOB_BRACE, GLOB_MARK, GLOB_ONLYDIR,
+};
+
+use crate::config::Config;
+use crate::event_dispatcher::event_dispatcher::EventDispatcher;
+use crate::io::io_interface::IOInterface;
+use crate::json::json_file::JsonFile;
+use crate::package::loader::array_loader::ArrayLoader;
+use crate::package::version::version_guesser::VersionGuesser;
+use crate::package::version::version_parser::VersionParser;
+use crate::repository::array_repository::ArrayRepository;
+use crate::repository::configurable_repository_interface::ConfigurableRepositoryInterface;
+use crate::util::filesystem::Filesystem;
+use crate::util::git::Git as GitUtil;
+use crate::util::http_downloader::HttpDownloader;
+use crate::util::platform::Platform;
+use crate::util::process_executor::ProcessExecutor;
+use crate::util::url::Url;
+
+#[derive(Debug)]
+pub struct PathRepository {
+ inner: ArrayRepository,
+ loader: ArrayLoader,
+ version_guesser: VersionGuesser,
+ url: String,
+ repo_config: IndexMap<String, PhpMixed>,
+ process: ProcessExecutor,
+ options: IndexMap<String, PhpMixed>,
+}
+
+impl ConfigurableRepositoryInterface for PathRepository {
+ fn get_repo_config(&self) -> IndexMap<String, PhpMixed> {
+ self.repo_config.clone()
+ }
+}
+
+impl PathRepository {
+ pub fn new(
+ repo_config: IndexMap<String, PhpMixed>,
+ io: Box<dyn IOInterface>,
+ config: Config,
+ http_downloader: Option<HttpDownloader>,
+ dispatcher: Option<EventDispatcher>,
+ process: Option<ProcessExecutor>,
+ ) -> anyhow::Result<Self> {
+ if !repo_config.contains_key("url") {
+ return Err(RuntimeException {
+ message: "You must specify the `url` configuration for the path repository"
+ .to_string(),
+ code: 0,
+ }
+ .into());
+ }
+
+ let url_str = repo_config
+ .get("url")
+ .and_then(|v| v.as_string())
+ .unwrap_or("")
+ .to_string();
+ let url = Platform::expand_path(&url_str);
+ let process = process.unwrap_or_else(|| ProcessExecutor::new(&*io));
+ let version_guesser =
+ VersionGuesser::new(&config, &process, VersionParser::new(), &*io);
+ let mut options = repo_config
+ .get("options")
+ .and_then(|v| v.as_array())
+ .cloned()
+ .unwrap_or_default()
+ .into_iter()
+ .map(|(k, v)| (k, *v))
+ .collect::<IndexMap<String, PhpMixed>>();
+ if !options.contains_key("relative") {
+ let filesystem = Filesystem::new();
+ let is_relative = !filesystem.is_absolute_path(&url);
+ options.insert("relative".to_string(), PhpMixed::Bool(is_relative));
+ }
+
+ Ok(Self {
+ inner: ArrayRepository::new(),
+ loader: ArrayLoader::new(None, true),
+ version_guesser,
+ url,
+ repo_config,
+ process,
+ options,
+ })
+ }
+
+ pub fn get_repo_name(&self) -> String {
+ format!(
+ "path repo ({})",
+ Url::sanitize(
+ self.repo_config
+ .get("url")
+ .and_then(|v| v.as_string())
+ .unwrap_or("")
+ .to_string()
+ )
+ )
+ }
+
+ pub(crate) fn initialize(&mut self) -> anyhow::Result<()> {
+ self.inner.initialize()?;
+
+ let url_matches = self.get_url_matches()?;
+
+ if url_matches.is_empty() {
+ if Preg::is_match(r"{[*{}]}", &self.url).unwrap_or(false) {
+ let mut url = self.url.clone();
+ while Preg::is_match(r"{[*{}]}", &url).unwrap_or(false) {
+ url = shirabe_php_shim::dirname(&url);
+ }
+ // the parent directory before any wildcard exists, so we assume it is correctly configured but simply empty
+ if shirabe_php_shim::is_dir(&url) {
+ return Ok(());
+ }
+ }
+
+ return Err(RuntimeException {
+ message: format!(
+ "The `url` supplied for the path ({}) repository does not exist",
+ self.url
+ ),
+ code: 0,
+ }
+ .into());
+ }
+
+ for url in url_matches {
+ let path = format!(
+ "{}/",
+ realpath(&url).unwrap_or_default()
+ );
+ let composer_file_path = format!("{}composer.json", path);
+
+ if !file_exists(&composer_file_path) {
+ continue;
+ }
+
+ let json = file_get_contents(&composer_file_path).unwrap_or_default();
+ let mut package = JsonFile::parse_json(&json, Some(&composer_file_path))?
+ .unwrap_or_default();
+ let dist = {
+ let mut dist = IndexMap::new();
+ dist.insert("type".to_string(), Box::new(PhpMixed::String("path".to_string())));
+ dist.insert("url".to_string(), Box::new(PhpMixed::String(url.clone())));
+ dist
+ };
+ package.insert("dist".to_string(), PhpMixed::Array(dist));
+
+ let reference = self
+ .options
+ .get("reference")
+ .and_then(|v| v.as_string())
+ .unwrap_or("auto")
+ .to_string();
+ if reference == "none" {
+ if let Some(PhpMixed::Array(ref mut dist)) = package.get_mut("dist") {
+ dist.insert("reference".to_string(), Box::new(PhpMixed::Null));
+ }
+ } else if reference == "config" || reference == "auto" {
+ let options_mixed = PhpMixed::Array(
+ self.options
+ .iter()
+ .map(|(k, v)| (k.clone(), Box::new(v.clone())))
+ .collect(),
+ );
+ let ref_hash = hash("sha1", &format!("{}{}", json, serialize(&options_mixed)));
+ if let Some(PhpMixed::Array(ref mut dist)) = package.get_mut("dist") {
+ dist.insert(
+ "reference".to_string(),
+ Box::new(PhpMixed::String(ref_hash)),
+ );
+ }
+ }
+
+ // copy symlink/relative options to transport options
+ let transport_options: IndexMap<String, Box<PhpMixed>> = self
+ .options
+ .iter()
+ .filter(|(k, _)| k.as_str() == "symlink" || k.as_str() == "relative")
+ .map(|(k, v)| (k.clone(), Box::new(v.clone())))
+ .collect();
+ package.insert(
+ "transport-options".to_string(),
+ PhpMixed::Array(transport_options),
+ );
+
+ // use the version provided as option if available
+ if let Some(name) = package.get("name").and_then(|v| v.as_string()).map(|s| s.to_string()) {
+ if let Some(version) = self
+ .options
+ .get("versions")
+ .and_then(|v| v.as_array())
+ .and_then(|a| a.get(&name))
+ .and_then(|v| v.as_string())
+ .map(|s| s.to_string())
+ {
+ package.insert("version".to_string(), PhpMixed::String(version));
+ }
+ }
+
+ // carry over the root package version if this path repo is in the same git repository as root package
+ if !package.contains_key("version") {
+ if let Some(root_version) = Platform::get_env("COMPOSER_ROOT_VERSION") {
+ if !root_version.is_empty() {
+ let mut ref1 = String::new();
+ let mut ref2 = String::new();
+ if self.process.execute(
+ &["git", "rev-parse", "HEAD"].map(|s| s.to_string()).to_vec(),
+ &mut ref1,
+ Some(path.clone()),
+ ) == 0
+ && self.process.execute(
+ &["git", "rev-parse", "HEAD"].map(|s| s.to_string()).to_vec(),
+ &mut ref2,
+ None,
+ ) == 0
+ && ref1 == ref2
+ {
+ package.insert(
+ "version".to_string(),
+ PhpMixed::String(
+ self.version_guesser.get_root_version_from_env(),
+ ),
+ );
+ }
+ }
+ }
+ }
+
+ let mut output = String::new();
+ let command = GitUtil::build_rev_list_command(
+ &self.process,
+ {
+ let mut args = vec!["-n1".to_string(), "--format=%H".to_string(), "HEAD".to_string()];
+ args.extend(GitUtil::get_no_show_signature_flags(&self.process));
+ args
+ },
+ );
+ if reference == "auto"
+ && shirabe_php_shim::is_dir(&format!("{}/.git", path.trim_end_matches('/')))
+ && self.process.execute(&command, &mut output, Some(path.clone())) == 0
+ {
+ let ref_val =
+ GitUtil::parse_rev_list_output(&output, &self.process).trim().to_string();
+ if let Some(PhpMixed::Array(ref mut dist)) = package.get_mut("dist") {
+ dist.insert("reference".to_string(), Box::new(PhpMixed::String(ref_val)));
+ }
+ }
+
+ if !package.contains_key("version") {
+ let version_data = self.version_guesser.guess_version(&package, &path);
+ if let Some(version_data) = version_data {
+ if let Some(pretty_version) = version_data
+ .get("pretty_version")
+ .and_then(|v| v.as_string())
+ .filter(|s| !s.is_empty())
+ .map(|s| s.to_string())
+ {
+ // if there is a feature branch detected, we add a second package with the feature branch version
+ if let Some(feature_pretty_version) = version_data
+ .get("feature_pretty_version")
+ .and_then(|v| v.as_string())
+ .filter(|s| !s.is_empty())
+ .map(|s| s.to_string())
+ {
+ package.insert(
+ "version".to_string(),
+ PhpMixed::String(feature_pretty_version),
+ );
+ self.inner.add_package(self.loader.load(package.clone())?);
+ }
+
+ package.insert("version".to_string(), PhpMixed::String(pretty_version));
+ } else {
+ package.insert(
+ "version".to_string(),
+ PhpMixed::String("dev-main".to_string()),
+ );
+ }
+ } else {
+ package.insert(
+ "version".to_string(),
+ PhpMixed::String("dev-main".to_string()),
+ );
+ }
+ }
+
+ self.inner
+ .add_package(self.loader.load(package.clone()).map_err(|e| {
+ RuntimeException {
+ message: format!("Failed loading the package in {}", composer_file_path),
+ code: 0,
+ }
+ })?);
+ }
+
+ Ok(())
+ }
+
+ fn get_url_matches(&self) -> anyhow::Result<Vec<String>> {
+ let mut flags = GLOB_MARK | GLOB_ONLYDIR;
+
+ if defined("GLOB_BRACE") {
+ flags |= GLOB_BRACE;
+ } else if self.url.contains('{') || self.url.contains('}') {
+ return Err(RuntimeException {
+ message: format!(
+ "The operating system does not support GLOB_BRACE which is required for the url {}",
+ self.url
+ ),
+ code: 0,
+ }
+ .into());
+ }
+
+ // Ensure environment-specific path separators are normalized to URL separators
+ Ok(glob_with_flags(&self.url, flags)
+ .into_iter()
+ .map(|val| val.replace(DIRECTORY_SEPARATOR, "/").trim_end_matches('/').to_string())
+ .collect())
+ }
+}
diff --git a/crates/shirabe/src/repository/vcs/fossil_driver.rs b/crates/shirabe/src/repository/vcs/fossil_driver.rs
index b1e7ac9..da81548 100644
--- a/crates/shirabe/src/repository/vcs/fossil_driver.rs
+++ b/crates/shirabe/src/repository/vcs/fossil_driver.rs
@@ -1 +1,312 @@
//! ref: composer/src/Composer/Repository/Vcs/FossilDriver.php
+
+use chrono::{DateTime, Utc};
+use indexmap::IndexMap;
+use shirabe_external_packages::composer::pcre::preg::Preg;
+use shirabe_php_shim::{dirname, is_dir, is_file, is_writable, PhpMixed, RuntimeException};
+
+use crate::cache::Cache;
+use crate::config::Config;
+use crate::io::io_interface::IOInterface;
+use crate::repository::vcs::vcs_driver::VcsDriver;
+use crate::util::filesystem::Filesystem;
+use crate::util::process_executor::ProcessExecutor;
+
+#[derive(Debug)]
+pub struct FossilDriver {
+ pub(crate) inner: VcsDriver,
+ pub(crate) tags: Option<IndexMap<String, String>>,
+ pub(crate) branches: Option<IndexMap<String, String>>,
+ pub(crate) root_identifier: Option<String>,
+ pub(crate) repo_file: Option<String>,
+ pub(crate) checkout_dir: String,
+}
+
+impl FossilDriver {
+ pub fn initialize(&mut self) -> anyhow::Result<()> {
+ // Make sure fossil is installed and reachable.
+ self.check_fossil()?;
+
+ // Ensure we are allowed to use this URL by config.
+ self.inner.config.prohibit_url_by_config(&self.inner.url, &*self.inner.io)?;
+
+ // Only if url points to a locally accessible directory, assume it's the checkout directory.
+ // Otherwise, it should be something fossil can clone from.
+ if Filesystem::is_local_path(&self.inner.url) && is_dir(&self.inner.url) {
+ self.checkout_dir = self.inner.url.clone();
+ } else {
+ let cache_repo_dir = self.inner.config.get("cache-repo-dir").as_string().unwrap_or("").to_string();
+ let cache_vcs_dir = self.inner.config.get("cache-vcs-dir").as_string().unwrap_or("").to_string();
+ if !Cache::is_usable(&cache_repo_dir) || !Cache::is_usable(&cache_vcs_dir) {
+ return Err(RuntimeException {
+ message: "FossilDriver requires a usable cache directory, and it looks like you set it to be disabled".to_string(),
+ code: 0,
+ }
+ .into());
+ }
+
+ let local_name = Preg::replace(r"{[^a-z0-9]}i", "-", self.inner.url.clone());
+ self.repo_file = Some(format!("{}/{}.fossil", cache_repo_dir, local_name));
+ self.checkout_dir = format!("{}/{}/", cache_vcs_dir, local_name);
+
+ self.update_local_repo()?;
+ }
+
+ self.get_tags()?;
+ self.get_branches()?;
+
+ Ok(())
+ }
+
+ pub(crate) fn check_fossil(&self) -> anyhow::Result<()> {
+ let mut ignored_output = String::new();
+ if self.inner.process.execute(
+ &["fossil", "version"].map(|s| s.to_string()).to_vec(),
+ &mut ignored_output,
+ None,
+ ) != 0
+ {
+ return Err(RuntimeException {
+ message: format!(
+ "fossil was not found, check that it is installed and in your PATH env.\n\n{}",
+ self.inner.process.get_error_output()
+ ),
+ code: 0,
+ }
+ .into());
+ }
+ Ok(())
+ }
+
+ pub(crate) fn update_local_repo(&mut self) -> anyhow::Result<()> {
+ assert!(self.repo_file.is_some());
+
+ let fs = Filesystem::new();
+ fs.ensure_directory_exists(&self.checkout_dir)?;
+
+ if !is_writable(&dirname(&self.checkout_dir)) {
+ return Err(RuntimeException {
+ message: format!(
+ "Can not clone {} to access package information. The \"{}\" directory is not writable by the current user.",
+ self.inner.url, self.checkout_dir
+ ),
+ code: 0,
+ }
+ .into());
+ }
+
+ let repo_file = self.repo_file.as_ref().unwrap().clone();
+
+ // update the repo if it is a valid fossil repository
+ if is_file(&repo_file)
+ && is_dir(&self.checkout_dir)
+ && self.inner.process.execute(
+ &["fossil", "info"].map(|s| s.to_string()).to_vec(),
+ &mut String::new(),
+ Some(self.checkout_dir.clone()),
+ ) == 0
+ {
+ if self.inner.process.execute(
+ &["fossil", "pull"].map(|s| s.to_string()).to_vec(),
+ &mut String::new(),
+ Some(self.checkout_dir.clone()),
+ ) != 0
+ {
+ self.inner.io.write_error(
+ PhpMixed::String(format!(
+ "<error>Failed to update {}, package information from this repository may be outdated ({})</error>",
+ self.inner.url,
+ self.inner.process.get_error_output()
+ )),
+ true,
+ IOInterface::NORMAL,
+ );
+ }
+ } else {
+ // clean up directory and do a fresh clone into it
+ fs.remove_directory(&self.checkout_dir)?;
+ fs.remove(&repo_file)?;
+ fs.ensure_directory_exists(&self.checkout_dir)?;
+
+ let mut output = String::new();
+ if self.inner.process.execute(
+ &["fossil", "clone", "--", &self.inner.url, &repo_file]
+ .map(|s| s.to_string())
+ .to_vec(),
+ &mut output,
+ None,
+ ) != 0
+ {
+ let output = self.inner.process.get_error_output();
+ return Err(RuntimeException {
+ message: format!(
+ "Failed to clone {} to repository {}\n\n{}",
+ self.inner.url, repo_file, output
+ ),
+ code: 0,
+ }
+ .into());
+ }
+
+ if self.inner.process.execute(
+ &["fossil", "open", "--nested", "--", &repo_file]
+ .map(|s| s.to_string())
+ .to_vec(),
+ &mut output,
+ Some(self.checkout_dir.clone()),
+ ) != 0
+ {
+ let output = self.inner.process.get_error_output();
+ return Err(RuntimeException {
+ message: format!(
+ "Failed to open repository {} in {}\n\n{}",
+ repo_file, self.checkout_dir, output
+ ),
+ code: 0,
+ }
+ .into());
+ }
+ }
+
+ Ok(())
+ }
+
+ pub fn get_root_identifier(&mut self) -> String {
+ if self.root_identifier.is_none() {
+ self.root_identifier = Some("trunk".to_string());
+ }
+ self.root_identifier.clone().unwrap()
+ }
+
+ pub fn get_url(&self) -> String {
+ self.inner.url.clone()
+ }
+
+ pub fn get_source(&self, identifier: &str) -> IndexMap<String, String> {
+ let mut map = IndexMap::new();
+ map.insert("type".to_string(), "fossil".to_string());
+ map.insert("url".to_string(), self.get_url());
+ map.insert("reference".to_string(), identifier.to_string());
+ map
+ }
+
+ pub fn get_dist(&self, _identifier: &str) -> Option<IndexMap<String, String>> {
+ None
+ }
+
+ pub fn get_file_content(&self, file: &str, identifier: &str) -> anyhow::Result<Option<String>> {
+ if identifier.starts_with('-') {
+ return Err(RuntimeException {
+ message: format!(
+ "Invalid fossil identifier detected. Identifier must not start with a -, given: {}",
+ identifier
+ ),
+ code: 0,
+ }
+ .into());
+ }
+
+ let mut content = String::new();
+ self.inner.process.execute(
+ &["fossil", "cat", "-r", identifier, "--", file]
+ .map(|s| s.to_string())
+ .to_vec(),
+ &mut content,
+ Some(self.checkout_dir.clone()),
+ );
+
+ if content.trim().is_empty() {
+ return Ok(None);
+ }
+
+ Ok(Some(content))
+ }
+
+ pub fn get_change_date(&self, _identifier: &str) -> anyhow::Result<Option<DateTime<Utc>>> {
+ let mut output = String::new();
+ self.inner.process.execute(
+ &["fossil", "finfo", "-b", "-n", "1", "composer.json"]
+ .map(|s| s.to_string())
+ .to_vec(),
+ &mut output,
+ Some(self.checkout_dir.clone()),
+ );
+ let parts: Vec<&str> = output.trim().splitn(3, ' ').collect();
+ let date = parts.get(1).copied().unwrap_or("");
+
+ let date = DateTime::parse_from_rfc3339(date).map(|d| d.with_timezone(&Utc))?;
+ Ok(Some(date))
+ }
+
+ pub fn get_tags(&mut self) -> anyhow::Result<IndexMap<String, String>> {
+ if self.tags.is_none() {
+ let mut tags: IndexMap<String, String> = IndexMap::new();
+ let mut output = String::new();
+ self.inner.process.execute(
+ &["fossil", "tag", "list"].map(|s| s.to_string()).to_vec(),
+ &mut output,
+ Some(self.checkout_dir.clone()),
+ );
+ for tag in self.inner.process.split_lines(&output) {
+ tags.insert(tag.clone(), tag);
+ }
+ self.tags = Some(tags);
+ }
+ Ok(self.tags.clone().unwrap_or_default())
+ }
+
+ pub fn get_branches(&mut self) -> anyhow::Result<IndexMap<String, String>> {
+ if self.branches.is_none() {
+ let mut branches: IndexMap<String, String> = IndexMap::new();
+ let mut output = String::new();
+ self.inner.process.execute(
+ &["fossil", "branch", "list"].map(|s| s.to_string()).to_vec(),
+ &mut output,
+ Some(self.checkout_dir.clone()),
+ );
+ for branch in self.inner.process.split_lines(&output) {
+ let branch = Preg::replace(r"/^\*/", "", branch.trim().to_string());
+ let branch = branch.trim().to_string();
+ branches.insert(branch.clone(), branch);
+ }
+ self.branches = Some(branches);
+ }
+ Ok(self.branches.clone().unwrap_or_default())
+ }
+
+ pub fn supports(io: &dyn IOInterface, config: &Config, url: &str, deep: bool) -> bool {
+ if Preg::is_match(
+ r"#(^(?:https?|ssh)://(?:[^@]@)?(?:chiselapp\.com|fossil\.))#i",
+ url,
+ )
+ .unwrap_or(false)
+ {
+ return true;
+ }
+
+ if Preg::is_match(r"!/fossil/|\.fossil!", url).unwrap_or(false) {
+ return true;
+ }
+
+ // local filesystem
+ if Filesystem::is_local_path(url) {
+ let url = Filesystem::get_platform_path(url);
+ if !is_dir(&url) {
+ return false;
+ }
+
+ let process = ProcessExecutor::new(io);
+ let mut output = String::new();
+ if process.execute(
+ &["fossil", "info"].map(|s| s.to_string()).to_vec(),
+ &mut output,
+ Some(url),
+ ) == 0
+ {
+ return true;
+ }
+ }
+
+ false
+ }
+}
diff --git a/crates/shirabe/src/util/stream_context_factory.rs b/crates/shirabe/src/util/stream_context_factory.rs
index 414d403..aba3a50 100644
--- a/crates/shirabe/src/util/stream_context_factory.rs
+++ b/crates/shirabe/src/util/stream_context_factory.rs
@@ -1 +1,382 @@
//! ref: composer/src/Composer/Util/StreamContextFactory.php
+
+use indexmap::IndexMap;
+use shirabe_external_packages::composer::ca_bundle::ca_bundle::CaBundle;
+use shirabe_external_packages::psr::log::logger_interface::LoggerInterface;
+use shirabe_php_shim::{
+ array_replace_recursive, curl_version, extension_loaded, function_exists, php_uname,
+ stream_context_create, stripos, uasort, PhpMixed, RuntimeException,
+ HHVM_VERSION, PHP_MAJOR_VERSION, PHP_MINOR_VERSION, PHP_RELEASE_VERSION,
+};
+
+use crate::composer::Composer;
+use crate::downloader::transport_exception::TransportException;
+use crate::repository::platform_repository::PlatformRepository;
+use crate::util::filesystem::Filesystem;
+use crate::util::http::proxy_manager::ProxyManager;
+use crate::util::platform::Platform;
+
+pub struct StreamContextFactory;
+
+impl StreamContextFactory {
+ /// Creates a context supporting HTTP proxies.
+ pub fn get_context(
+ url: &str,
+ default_options: IndexMap<String, PhpMixed>,
+ default_params: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<PhpMixed, TransportException> {
+ let mut options: IndexMap<String, PhpMixed> = {
+ let mut http = IndexMap::new();
+ // specify defaults again to try and work better with curlwrappers enabled
+ http.insert("follow_location".to_string(), PhpMixed::Int(1));
+ http.insert("max_redirects".to_string(), PhpMixed::Int(20));
+ let mut o = IndexMap::new();
+ o.insert("http".to_string(), PhpMixed::Array(
+ http.into_iter().map(|(k, v)| (k, Box::new(v))).collect()
+ ));
+ o
+ };
+
+ options = array_replace_recursive(options, Self::init_options(url, default_options.clone(), false)?);
+ let default_options = {
+ let mut o = default_options;
+ if let Some(PhpMixed::Array(ref mut http)) = o.get_mut("http") {
+ http.remove("header");
+ }
+ o
+ };
+ options = array_replace_recursive(options, default_options);
+
+ if let Some(PhpMixed::Array(ref mut http)) = options.get_mut("http") {
+ if let Some(header) = http.get("header").cloned() {
+ let fixed = Self::fix_http_header_field(&*header);
+ http.insert(
+ "header".to_string(),
+ Box::new(PhpMixed::List(
+ fixed.into_iter().map(|s| Box::new(PhpMixed::String(s))).collect(),
+ )),
+ );
+ }
+ }
+
+ Ok(stream_context_create(&options, Some(&default_params)))
+ }
+
+ pub fn init_options(
+ url: &str,
+ mut options: IndexMap<String, PhpMixed>,
+ for_curl: bool,
+ ) -> anyhow::Result<IndexMap<String, PhpMixed>, TransportException> {
+ // Make sure the headers are in an array form
+ let has_header = options
+ .get("http")
+ .and_then(|v| v.as_array())
+ .map(|a| a.contains_key("header"))
+ .unwrap_or(false);
+ if !has_header {
+ if let Some(PhpMixed::Array(ref mut http)) = options.get_mut("http") {
+ http.insert(
+ "header".to_string(),
+ Box::new(PhpMixed::List(vec![])),
+ );
+ }
+ }
+ // Convert string header to array
+ let header_is_string = options
+ .get("http")
+ .and_then(|v| v.as_array())
+ .and_then(|a| a.get("header"))
+ .map(|v| matches!(**v, PhpMixed::String(_)))
+ .unwrap_or(false);
+ if header_is_string {
+ if let Some(PhpMixed::Array(ref mut http)) = options.get_mut("http") {
+ if let Some(PhpMixed::String(header_str)) = http.get("header").map(|v| *v.clone()) {
+ let parts: Vec<Box<PhpMixed>> = header_str
+ .split("\r\n")
+ .map(|s| Box::new(PhpMixed::String(s.to_string())))
+ .collect();
+ http.insert("header".to_string(), Box::new(PhpMixed::List(parts)));
+ }
+ }
+ }
+
+ // Add stream proxy options if there is a proxy
+ if !for_curl {
+ let proxy_manager = ProxyManager::get_instance().lock().unwrap();
+ let proxy_manager = proxy_manager.as_ref().unwrap();
+ let proxy = proxy_manager.get_proxy_for_request(url)?;
+ let proxy_options = proxy.get_context_options();
+ if let Some(proxy_options) = proxy_options {
+ let is_https_request = url.starts_with("https://");
+
+ if proxy.is_secure() {
+ if !extension_loaded("openssl") {
+ return Err(TransportException::new(
+ "You must enable the openssl extension to use a secure proxy.".to_string(),
+ ));
+ }
+ if is_https_request {
+ return Err(TransportException::new(
+ "You must enable the curl extension to make https requests through a secure proxy.".to_string(),
+ ));
+ }
+ } else if is_https_request && !extension_loaded("openssl") {
+ return Err(TransportException::new(
+ "You must enable the openssl extension to make https requests through a proxy.".to_string(),
+ ));
+ }
+
+ // Header will be a Proxy-Authorization string or not set
+ let proxy_http = proxy_options.get("http");
+ if let Some(proxy_header) = proxy_http.and_then(|h| h.get("header")) {
+ if let Some(PhpMixed::Array(ref mut http)) = options.get_mut("http") {
+ if let Some(PhpMixed::List(ref mut headers)) = http.get_mut("header").map(|v| &mut **v) {
+ headers.push(Box::new(*proxy_header.clone()));
+ }
+ }
+ }
+
+ let proxy_options_flat: IndexMap<String, PhpMixed> = proxy_options
+ .iter()
+ .map(|(k, v)| {
+ let inner: IndexMap<String, Box<PhpMixed>> = v
+ .iter()
+ .filter(|(ik, _)| ik.as_str() != "header")
+ .map(|(ik, iv)| (ik.clone(), iv.clone()))
+ .collect();
+ (k.clone(), PhpMixed::Array(inner))
+ })
+ .collect();
+ options = array_replace_recursive(options, proxy_options_flat);
+ }
+ }
+
+ let php_version = if HHVM_VERSION.is_some() {
+ format!("HHVM {}", HHVM_VERSION.unwrap())
+ } else {
+ format!(
+ "PHP {}.{}.{}",
+ PHP_MAJOR_VERSION, PHP_MINOR_VERSION, PHP_RELEASE_VERSION
+ )
+ };
+
+ let http_version = if for_curl {
+ let curl = curl_version().unwrap_or_default();
+ let version = curl
+ .get("version")
+ .and_then(|v| v.as_string())
+ .unwrap_or("")
+ .to_string();
+ format!("cURL {}", version)
+ } else {
+ "streams".to_string()
+ };
+
+ let has_user_agent = options
+ .get("http")
+ .and_then(|v| v.as_array())
+ .and_then(|a| a.get("header"))
+ .and_then(|v| match **v {
+ PhpMixed::List(ref list) => {
+ let joined: String = list
+ .iter()
+ .filter_map(|item| item.as_string())
+ .collect::<Vec<_>>()
+ .join("");
+ Some(joined.to_lowercase().contains("user-agent"))
+ }
+ _ => None,
+ })
+ .unwrap_or(false);
+
+ if !has_user_agent {
+ let platform_php_version = PlatformRepository::get_platform_php_version();
+ let user_agent = format!(
+ "User-Agent: Composer/{} ({os}; {release}; {php_version}; {http_version}{platform}{ci})",
+ Composer::get_version(),
+ os = if function_exists("php_uname") { php_uname("s") } else { "Unknown".to_string() },
+ release = if function_exists("php_uname") { php_uname("r") } else { "Unknown".to_string() },
+ php_version = php_version,
+ http_version = http_version,
+ platform = platform_php_version
+ .as_deref()
+ .map(|v| format!("; Platform-PHP {}", v))
+ .unwrap_or_default(),
+ ci = if Platform::get_env("CI").is_some() { "; CI" } else { "" },
+ );
+ if let Some(PhpMixed::Array(ref mut http)) = options.get_mut("http") {
+ if let Some(PhpMixed::List(ref mut headers)) = http.get_mut("header").map(|v| &mut **v) {
+ headers.push(Box::new(PhpMixed::String(user_agent)));
+ }
+ }
+ }
+
+ Ok(options)
+ }
+
+ pub fn get_tls_defaults(
+ options: &IndexMap<String, PhpMixed>,
+ logger: Option<&dyn LoggerInterface>,
+ ) -> anyhow::Result<IndexMap<String, PhpMixed>, TransportException> {
+ let ciphers = [
+ "ECDHE-RSA-AES128-GCM-SHA256",
+ "ECDHE-ECDSA-AES128-GCM-SHA256",
+ "ECDHE-RSA-AES256-GCM-SHA384",
+ "ECDHE-ECDSA-AES256-GCM-SHA384",
+ "DHE-RSA-AES128-GCM-SHA256",
+ "DHE-DSS-AES128-GCM-SHA256",
+ "kEDH+AESGCM",
+ "ECDHE-RSA-AES128-SHA256",
+ "ECDHE-ECDSA-AES128-SHA256",
+ "ECDHE-RSA-AES128-SHA",
+ "ECDHE-ECDSA-AES128-SHA",
+ "ECDHE-RSA-AES256-SHA384",
+ "ECDHE-ECDSA-AES256-SHA384",
+ "ECDHE-RSA-AES256-SHA",
+ "ECDHE-ECDSA-AES256-SHA",
+ "DHE-RSA-AES128-SHA256",
+ "DHE-RSA-AES128-SHA",
+ "DHE-DSS-AES128-SHA256",
+ "DHE-RSA-AES256-SHA256",
+ "DHE-DSS-AES256-SHA",
+ "DHE-RSA-AES256-SHA",
+ "AES128-GCM-SHA256",
+ "AES256-GCM-SHA384",
+ "AES128-SHA256",
+ "AES256-SHA256",
+ "AES128-SHA",
+ "AES256-SHA",
+ "AES",
+ "CAMELLIA",
+ "!aNULL",
+ "!eNULL",
+ "!EXPORT",
+ "!DES",
+ "!3DES",
+ "!RC4",
+ "!MD5",
+ "!PSK",
+ "!aECDH",
+ "!EDH-DSS-DES-CBC3-SHA",
+ "!EDH-RSA-DES-CBC3-SHA",
+ "!KRB5-DES-CBC3-SHA",
+ ]
+ .join(":");
+
+ // CN_match and SNI_server_name are only known once a URL is passed.
+ // They will be set in the getOptionsForUrl() method which receives a URL.
+ //
+ // cafile or capath can be overridden by passing in those options to constructor.
+ let ssl_defaults: IndexMap<String, PhpMixed> = {
+ let mut ssl = IndexMap::new();
+ ssl.insert("ciphers".to_string(), PhpMixed::String(ciphers));
+ ssl.insert("verify_peer".to_string(), PhpMixed::Bool(true));
+ ssl.insert("verify_depth".to_string(), PhpMixed::Int(7));
+ ssl.insert("SNI_enabled".to_string(), PhpMixed::Bool(true));
+ ssl.insert("capture_peer_cert".to_string(), PhpMixed::Bool(true));
+ ssl
+ };
+
+ let mut defaults: IndexMap<String, PhpMixed> = {
+ let mut d = IndexMap::new();
+ d.insert("ssl".to_string(), PhpMixed::Array(
+ ssl_defaults.into_iter().map(|(k, v)| (k, Box::new(v))).collect()
+ ));
+ d
+ };
+
+ if let Some(ssl_options) = options.get("ssl") {
+ if let Some(ssl_defaults_mixed) = defaults.get("ssl").cloned() {
+ let merged = array_replace_recursive(
+ match ssl_defaults_mixed {
+ PhpMixed::Array(a) => a.into_iter().map(|(k, v)| (k, *v)).collect(),
+ _ => IndexMap::new(),
+ },
+ match ssl_options.clone() {
+ PhpMixed::Array(a) => a.into_iter().map(|(k, v)| (k, *v)).collect(),
+ _ => IndexMap::new(),
+ },
+ );
+ defaults.insert(
+ "ssl".to_string(),
+ PhpMixed::Array(merged.into_iter().map(|(k, v)| (k, Box::new(v))).collect()),
+ );
+ }
+ }
+
+ // Attempt to find a local cafile or throw an exception if none pre-set.
+ // The user may go download one if this occurs.
+ let ssl = defaults.get("ssl").and_then(|v| v.as_array());
+ let has_cafile = ssl.as_ref().and_then(|a| a.get("cafile")).is_some();
+ let has_capath = ssl.as_ref().and_then(|a| a.get("capath")).is_some();
+ if !has_cafile && !has_capath {
+ let result = CaBundle::get_system_ca_root_bundle_path(logger);
+ if shirabe_php_shim::is_dir(&result) {
+ if let Some(PhpMixed::Array(ref mut ssl)) = defaults.get_mut("ssl") {
+ ssl.insert("capath".to_string(), Box::new(PhpMixed::String(result)));
+ }
+ } else {
+ if let Some(PhpMixed::Array(ref mut ssl)) = defaults.get_mut("ssl") {
+ ssl.insert("cafile".to_string(), Box::new(PhpMixed::String(result)));
+ }
+ }
+ }
+
+ let cafile = defaults.get("ssl").and_then(|v| v.as_array())
+ .and_then(|a| a.get("cafile"))
+ .and_then(|v| v.as_string())
+ .map(|s| s.to_string());
+ if let Some(ref cafile) = cafile {
+ if !Filesystem::is_readable(cafile) || !CaBundle::validate_ca_file(cafile, logger) {
+ return Err(TransportException::new(
+ "The configured cafile was not valid or could not be read.".to_string(),
+ ));
+ }
+ }
+
+ let capath = defaults.get("ssl").and_then(|v| v.as_array())
+ .and_then(|a| a.get("capath"))
+ .and_then(|v| v.as_string())
+ .map(|s| s.to_string());
+ if let Some(ref capath) = capath {
+ if !shirabe_php_shim::is_dir(capath) || !Filesystem::is_readable(capath) {
+ return Err(TransportException::new(
+ "The configured capath was not valid or could not be read.".to_string(),
+ ));
+ }
+ }
+
+ // Disable TLS compression to prevent CRIME attacks where supported.
+ if let Some(PhpMixed::Array(ref mut ssl)) = defaults.get_mut("ssl") {
+ ssl.insert(
+ "disable_compression".to_string(),
+ Box::new(PhpMixed::Bool(true)),
+ );
+ }
+
+ Ok(defaults)
+ }
+
+ /// A bug in PHP prevents the headers from correctly being sent when a content-type header is
+ /// present and NOT at the end of the array. This method fixes the array by moving the
+ /// content-type header to the end.
+ fn fix_http_header_field(header: &PhpMixed) -> Vec<String> {
+ let mut headers: Vec<String> = match header {
+ PhpMixed::String(s) => s.split("\r\n").map(|p| p.to_string()).collect(),
+ PhpMixed::List(list) => list
+ .iter()
+ .filter_map(|v| v.as_string())
+ .map(|s| s.to_string())
+ .collect(),
+ _ => vec![],
+ };
+ uasort(&mut headers, |el, _| {
+ if stripos(el, "content-type") == Some(0) {
+ 1
+ } else {
+ -1
+ }
+ });
+ headers
+ }
+}