//! ref: composer/src/Composer/Repository/Vcs/VcsDriver.php use chrono::{DateTime, Utc}; use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::preg::Preg; use shirabe_php_shim::{ JSON_UNESCAPED_SLASHES, JSON_UNESCAPED_UNICODE, PhpMixed, extension_loaded, }; use crate::cache::Cache; use crate::config::Config; use crate::downloader::transport_exception::TransportException; use crate::io::io_interface::IOInterface; use crate::json::json_file::JsonFile; use crate::repository::vcs::vcs_driver_interface::VcsDriverInterface; use crate::util::filesystem::Filesystem; use crate::util::http::response::Response; use crate::util::http_downloader::HttpDownloader; use crate::util::process_executor::ProcessExecutor; #[derive(Debug)] pub struct VcsDriverBase { pub url: String, pub origin_url: String, pub repo_config: IndexMap, pub io: Box, pub config: std::rc::Rc>, pub process: std::rc::Rc>, pub http_downloader: std::rc::Rc>, pub info_cache: IndexMap>>, pub cache: Option, } impl VcsDriverBase { pub fn new( repo_config: IndexMap, io: Box, config: std::rc::Rc>, http_downloader: std::rc::Rc>, process: std::rc::Rc>, ) -> Self { let url = repo_config .get("url") .and_then(|v| v.as_string()) .unwrap_or("") .to_string(); let origin_url = url.clone(); Self { url, origin_url, repo_config, io, config, process, http_downloader, info_cache: IndexMap::new(), cache: None, } } pub fn should_cache(&self, identifier: &str) -> bool { self.cache.is_some() && Preg::is_match("{^[a-f0-9]{40}$}iD", identifier).unwrap_or(false) } pub fn get_scheme(&self) -> &str { if extension_loaded("openssl") { return "https"; } "http" } pub fn get_contents(&self, url: &str) -> anyhow::Result { let options = self .repo_config .get("options") .cloned() .unwrap_or(PhpMixed::Array(IndexMap::new())); self.http_downloader.borrow_mut().get(url, &options) } // Helper for concrete drivers: produces the same value as the trait default // `get_base_composer_information`, but receives a pre-fetched composer.json // body and a lazy change-date callback. Concrete drivers in the Rust port // wrap `VcsDriverBase` as `self.inner` instead of inheriting from it, so // they cannot dispatch back into a base method that calls `get_file_content` // / `get_change_date` hooks; the caller threads those calls in itself. pub fn finish_base_composer_information( identifier: &str, composer_file_content: Option, change_date: impl FnOnce() -> anyhow::Result>>, ) -> anyhow::Result>> { let content = match composer_file_content { None => return Ok(None), Some(c) if c.is_empty() => return Ok(None), Some(c) => c, }; let parsed = JsonFile::parse_json( Some(&content), Some(&format!("{}:composer.json", identifier)), )?; let array = match parsed { PhpMixed::Array(a) if !a.is_empty() => a, _ => return Ok(None), }; // PHP arrays own their nested values; the Rust representation wraps them // in Box. Unbox the outer level so callers can mutate keys. let mut composer: IndexMap = array.into_iter().map(|(k, v)| (k, *v)).collect(); if !composer.contains_key("time") || composer .get("time") .map_or(true, |v| v.as_string().map_or(true, |s| s.is_empty())) { if let Some(d) = change_date()? { composer.insert("time".to_string(), PhpMixed::String(d.to_rfc3339())); } } Ok(Some(composer)) } } // TODO(phase-b): the constructor is `final` in PHP; concrete implementations must replicate the // initialization logic (local-path normalization etc.) from the original new() body. pub trait VcsDriver: VcsDriverInterface { fn url(&self) -> &str; fn url_mut(&mut self) -> &mut String; fn origin_url(&self) -> &str; fn origin_url_mut(&mut self) -> &mut String; fn repo_config(&self) -> &IndexMap; fn repo_config_mut(&mut self) -> &mut IndexMap; fn io(&self) -> &dyn IOInterface; fn io_mut(&mut self) -> &mut dyn IOInterface; fn config(&self) -> &Config; fn config_mut(&mut self) -> &mut Config; fn process(&self) -> &ProcessExecutor; fn process_mut(&mut self) -> &mut ProcessExecutor; fn http_downloader(&self) -> &std::rc::Rc>; fn info_cache(&self) -> &IndexMap>>; fn info_cache_mut(&mut self) -> &mut IndexMap>>; fn cache(&self) -> Option<&Cache>; fn cache_mut(&mut self) -> Option<&mut Cache>; fn should_cache(&self, identifier: &str) -> bool { self.cache().is_some() && Preg::is_match("{^[a-f0-9]{40}$}iD", identifier).unwrap_or(false) } fn get_composer_information( &mut self, identifier: &str, ) -> anyhow::Result>> { if !self.info_cache().contains_key(identifier) { if self.should_cache(identifier) { if let Some(res) = self.cache().and_then(|c| c.read(identifier)) { let parsed = JsonFile::parse_json(&res, None)?; self.info_cache_mut().insert(identifier.to_string(), parsed); return Ok(self.info_cache().get(identifier).and_then(|v| v.clone())); } } let composer = self.get_base_composer_information(identifier)?; if self.should_cache(identifier) { if let Some(ref composer_map) = composer { let encoded = JsonFile::encode_with_options( composer_map, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES, ); self.cache().map(|c| c.write(identifier, &encoded)); } } self.info_cache_mut() .insert(identifier.to_string(), composer); } Ok(self.info_cache().get(identifier).and_then(|v| v.clone())) } fn get_base_composer_information( &mut self, identifier: &str, ) -> anyhow::Result>> { let composer_file_content = self.get_file_content("composer.json", identifier)?; let composer_file_content = match composer_file_content { None => return Ok(None), Some(c) if c.is_empty() => return Ok(None), Some(c) => c, }; let composer = JsonFile::parse_json( &composer_file_content, Some(&format!("{}:composer.json", identifier)), )?; let mut composer = match composer { None => return Ok(None), Some(c) if c.is_empty() => return Ok(None), Some(c) => c, }; if !composer.contains_key("time") || composer .get("time") .map_or(true, |v| v.as_string().map_or(true, |s| s.is_empty())) { if let Some(change_date) = self.get_change_date(identifier)? { composer.insert( "time".to_string(), PhpMixed::String(change_date.to_rfc3339()), ); } } Ok(Some(composer)) } fn has_composer_file(&mut self, identifier: &str) -> bool { match self.get_composer_information(identifier) { Ok(Some(_)) => true, _ => false, } } fn get_scheme(&self) -> &str { if extension_loaded("openssl") { return "https"; } "http" } fn get_contents(&self, url: &str) -> anyhow::Result { let options = self .repo_config() .get("options") .cloned() .unwrap_or(PhpMixed::Array(IndexMap::new())); self.http_downloader().borrow_mut().get(url, &options) } fn cleanup(&self) {} }