aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/util/zip.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-15 00:19:50 +0900
committernsfisis <nsfisis@gmail.com>2026-05-15 02:37:37 +0900
commitac46dc8e98629719370c98e0a4760f7cd6b25578 (patch)
tree99bb1c319c02648e6248b6c307c1fd7394455d8f /crates/shirabe/src/util/zip.rs
parent06d2d2f869cd4c8c48898fae28fb91d6716e35c2 (diff)
downloadphp-shirabe-ac46dc8e98629719370c98e0a4760f7cd6b25578.tar.gz
php-shirabe-ac46dc8e98629719370c98e0a4760f7cd6b25578.tar.zst
php-shirabe-ac46dc8e98629719370c98e0a4760f7cd6b25578.zip
feat(port): port Zip.php
Add zip crate dependency and ZipArchive stub to shirabe-php-shim. Also add dirname and stream_get_contents shim functions.
Diffstat (limited to 'crates/shirabe/src/util/zip.rs')
-rw-r--r--crates/shirabe/src/util/zip.rs109
1 files changed, 109 insertions, 0 deletions
diff --git a/crates/shirabe/src/util/zip.rs b/crates/shirabe/src/util/zip.rs
index 583d397..296345a 100644
--- a/crates/shirabe/src/util/zip.rs
+++ b/crates/shirabe/src/util/zip.rs
@@ -1 +1,110 @@
//! ref: composer/src/Composer/Util/Zip.php
+
+use anyhow::Result;
+use indexmap::IndexMap;
+use shirabe_php_shim::{dirname, extension_loaded, implode, stream_get_contents, RuntimeException, ZipArchive};
+
+pub struct Zip;
+
+impl Zip {
+ pub fn get_composer_json(path_to_zip: &str) -> Result<Option<String>> {
+ if !extension_loaded("zip") {
+ return Err(RuntimeException {
+ message: "The Zip Util requires PHP's zip extension".to_string(),
+ code: 0,
+ }
+ .into());
+ }
+
+ let mut zip = ZipArchive::new();
+ if !zip.open(path_to_zip) {
+ return Ok(None);
+ }
+
+ if zip.num_files == 0 {
+ zip.close();
+ return Ok(None);
+ }
+
+ let found_file_index = Self::locate_file(&zip, "composer.json")?;
+
+ let mut content: Option<String> = None;
+ let configuration_file_name = zip.get_name_index(found_file_index);
+ let stream = zip.get_stream(&configuration_file_name);
+
+ if stream.is_some() {
+ content = stream_get_contents(stream.unwrap());
+ }
+
+ zip.close();
+
+ Ok(content)
+ }
+
+ fn locate_file(zip: &ZipArchive, filename: &str) -> Result<i64> {
+ // return root composer.json if it is there and is a file
+ if let Some(index) = zip.locate_name(filename) {
+ if zip.get_from_index(index).is_some() {
+ return Ok(index);
+ }
+ }
+
+ let mut top_level_paths: IndexMap<String, bool> = IndexMap::new();
+ for i in 0..zip.num_files {
+ let name = zip.get_name_index(i);
+ let dir_name = dirname(&name);
+
+ // ignore OSX specific resource fork folder
+ if name.contains("__MACOSX") {
+ continue;
+ }
+
+ // handle archives with proper TOC
+ if dir_name == "." {
+ top_level_paths.insert(name, true);
+ if top_level_paths.len() > 1 {
+ return Err(RuntimeException {
+ message: format!(
+ "Archive has more than one top level directories, and no composer.json was found on the top level, so it's an invalid archive. Top level paths found were: {}",
+ implode(",", &top_level_paths.keys().cloned().collect::<Vec<_>>())
+ ),
+ code: 0,
+ }
+ .into());
+ }
+ continue;
+ }
+
+ // handle archives which do not have a TOC record for the directory itself
+ if !dir_name.contains('\\') && !dir_name.contains('/') {
+ top_level_paths.insert(format!("{}/", dir_name), true);
+ if top_level_paths.len() > 1 {
+ return Err(RuntimeException {
+ message: format!(
+ "Archive has more than one top level directories, and no composer.json was found on the top level, so it's an invalid archive. Top level paths found were: {}",
+ implode(",", &top_level_paths.keys().cloned().collect::<Vec<_>>())
+ ),
+ code: 0,
+ }
+ .into());
+ }
+ }
+ }
+
+ if !top_level_paths.is_empty() {
+ let first_key = top_level_paths.keys().next().unwrap().clone();
+ if let Some(index) = zip.locate_name(&format!("{}{}", first_key, filename)) {
+ if zip.get_from_index(index).is_some() {
+ return Ok(index);
+ }
+ }
+ }
+
+ Err(RuntimeException {
+ message: "No composer.json found either at the top level or within the topmost directory"
+ .to_string(),
+ code: 0,
+ }
+ .into())
+ }
+}