aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-22 23:41:12 +0900
committernsfisis <nsfisis@gmail.com>2026-06-22 23:41:12 +0900
commitb291e714bc739262140323e08fe2fb9e91e00ee7 (patch)
treec95f742064b9000e72b902f88e8905b4017d5dbc /crates/shirabe
parent99ef82a9807578c1e5749156a027949efaba75c4 (diff)
downloadphp-shirabe-b291e714bc739262140323e08fe2fb9e91e00ee7.tar.gz
php-shirabe-b291e714bc739262140323e08fe2fb9e91e00ee7.tar.zst
php-shirabe-b291e714bc739262140323e08fe2fb9e91e00ee7.zip
feat(php-shim): implement fopen-family stream API on PhpResource
Redesign PhpResource into a real stream handle (File/Memory backing with tracked position, eof, closed state) and unify the whole fopen family (fopen/fwrite/fread/fgets/fgetc/feof/fclose/ftell/fseek/rewind/fstat/ ftruncate/fflush and stream_get_contents/stream_copy_to_stream) on &PhpResource, replacing the split PhpMixed/PhpResource APIs and their todo!() stubs. fopen now returns Result; read functions stay String for now (TODO(phase-e) to move to byte strings). Propagate the signatures through callers: Process stdout/stderr, Cursor input, curl header/body handles (extracted into typed maps keyed by job id), Filesystem copy/safe_copy/files_are_equal, BufferIO, error_handler, platform, perforce, zip. The proc_open pipe paths cannot carry a PhpResource in a PhpMixed list, so they are left as todo!() with notes.
Diffstat (limited to 'crates/shirabe')
-rw-r--r--crates/shirabe/src/downloader/gzip_downloader.rs6
-rw-r--r--crates/shirabe/src/installer/binary_installer.rs11
-rw-r--r--crates/shirabe/src/io/buffer_io.rs50
-rw-r--r--crates/shirabe/src/util/error_handler.rs10
-rw-r--r--crates/shirabe/src/util/filesystem.rs85
-rw-r--r--crates/shirabe/src/util/http/curl_downloader.rs173
-rw-r--r--crates/shirabe/src/util/perforce.rs73
-rw-r--r--crates/shirabe/src/util/platform.rs21
-rw-r--r--crates/shirabe/src/util/zip.rs4
-rw-r--r--crates/shirabe/tests/question/strict_confirmation_question_test.rs6
10 files changed, 242 insertions, 197 deletions
diff --git a/crates/shirabe/src/downloader/gzip_downloader.rs b/crates/shirabe/src/downloader/gzip_downloader.rs
index f59fe11..aefc6f9 100644
--- a/crates/shirabe/src/downloader/gzip_downloader.rs
+++ b/crates/shirabe/src/downloader/gzip_downloader.rs
@@ -52,16 +52,16 @@ impl GzipDownloader {
fn extract_using_ext(&self, file: &str, target_filepath: &str) {
let archive_file = gzopen(file, "rb");
- let target_file = fopen(target_filepath, "wb");
+ let target_file = fopen(target_filepath, "wb").unwrap();
loop {
let string = gzread(archive_file.clone(), 4096);
if string.is_empty() {
break;
}
- fwrite(target_file.clone(), &string, Platform::strlen(&string));
+ fwrite(&target_file, &string, Some(Platform::strlen(&string)));
}
gzclose(archive_file);
- fclose(target_file);
+ fclose(&target_file);
}
}
diff --git a/crates/shirabe/src/installer/binary_installer.rs b/crates/shirabe/src/installer/binary_installer.rs
index b9c56f6..8e7ade5 100644
--- a/crates/shirabe/src/installer/binary_installer.rs
+++ b/crates/shirabe/src/installer/binary_installer.rs
@@ -169,9 +169,14 @@ impl BinaryInstaller {
return "call".to_string();
}
- let handle = fopen(bin, "r");
- let line = fgets(handle.clone()).unwrap_or_default();
- fclose(handle);
+ let line = match fopen(bin, "r") {
+ Ok(handle) => {
+ let line = fgets(&handle, None).unwrap_or_default();
+ fclose(&handle);
+ line
+ }
+ Err(_) => String::new(),
+ };
let mut m: IndexMap<CaptureKey, String> = IndexMap::new();
if Preg::is_match3(
r"{^#!/(?:usr/bin/env )?(?:[^/]+/)*(.+)$}m",
diff --git a/crates/shirabe/src/io/buffer_io.rs b/crates/shirabe/src/io/buffer_io.rs
index 2b07c6b..91d22d7 100644
--- a/crates/shirabe/src/io/buffer_io.rs
+++ b/crates/shirabe/src/io/buffer_io.rs
@@ -12,8 +12,8 @@ use shirabe_external_packages::symfony::console::input::InputInterface;
use shirabe_external_packages::symfony::console::input::StringInput;
use shirabe_external_packages::symfony::console::output::OutputInterface;
use shirabe_php_shim::{
- PHP_EOL, PhpMixed, RuntimeException, fopen, fseek, fwrite, rewind, stream_get_contents,
- strip_tags,
+ PHP_EOL, PhpMixed, PhpResource, RuntimeException, SEEK_SET, fopen, fseek, fwrite, rewind,
+ stream_get_contents, strip_tags,
};
#[derive(Debug)]
@@ -30,14 +30,16 @@ impl BufferIO {
let mut input_obj = StringInput::new(&input)?;
input_obj.set_interactive(false);
- let stream = fopen("php://memory", "rw");
- if matches!(stream, PhpMixed::Bool(false)) {
- return Err(RuntimeException {
- message: "Unable to open memory output stream".to_string(),
- code: 0,
+ let stream = match fopen("php://memory", "rw") {
+ Ok(stream) => stream,
+ Err(_) => {
+ return Err(RuntimeException {
+ message: "Unable to open memory output stream".to_string(),
+ code: 0,
+ }
+ .into());
}
- .into());
- }
+ };
let _decorated = formatter.as_ref().is_some_and(|f| f.is_decorated());
// TODO(phase-c): wire StreamOutput as the output. StreamOutput::new requires a
@@ -75,11 +77,11 @@ impl BufferIO {
pub fn get_output(&self) -> String {
// TODO(phase-c): OutputInterface::get_stream returns PhpResource, while
// fseek/stream_get_contents take PhpMixed. The PhpResource stream model is not yet defined.
- let stream: PhpMixed =
- todo!("PhpResource -> PhpMixed conversion for OutputInterface::get_stream");
- fseek(stream.clone(), 0);
+ let stream: PhpResource =
+ todo!("retrieve the StreamOutput's PhpResource from OutputInterface::get_stream");
+ fseek(&stream, 0, SEEK_SET);
- let output = stream_get_contents(stream).unwrap_or_default();
+ let output = stream_get_contents(&stream).unwrap_or_default();
Preg::replace_callback(
r"{(?<=^|\n|\x08)(.+?)(\x08+)}",
@@ -119,21 +121,23 @@ impl BufferIO {
todo!("BufferIO::set_user_inputs: needs an as_streamable accessor on InputInterface")
}
- fn create_stream(&self, inputs: Vec<String>) -> Result<PhpMixed> {
- let stream = fopen("php://memory", "r+");
- if matches!(stream, PhpMixed::Bool(false)) {
- return Err(RuntimeException {
- message: "Unable to open memory output stream".to_string(),
- code: 0,
+ fn create_stream(&self, inputs: Vec<String>) -> Result<PhpResource> {
+ let stream = match fopen("php://memory", "r+") {
+ Ok(stream) => stream,
+ Err(_) => {
+ return Err(RuntimeException {
+ message: "Unable to open memory output stream".to_string(),
+ code: 0,
+ }
+ .into());
}
- .into());
- }
+ };
for input in inputs {
- fwrite(stream.clone(), &format!("{}{}", input, PHP_EOL), -1);
+ fwrite(&stream, &format!("{}{}", input, PHP_EOL), None);
}
- rewind(stream.clone());
+ rewind(&stream);
Ok(stream)
}
diff --git a/crates/shirabe/src/util/error_handler.rs b/crates/shirabe/src/util/error_handler.rs
index b9210fc..263d80c 100644
--- a/crates/shirabe/src/util/error_handler.rs
+++ b/crates/shirabe/src/util/error_handler.rs
@@ -4,8 +4,8 @@ use crate::io::IOInterface;
use crate::io::IOInterfaceImmutable;
use shirabe_php_shim::{
E_ALL, E_DEPRECATED, E_USER_DEPRECATED, E_USER_WARNING, E_WARNING, ErrorException, PHP_EOL,
- PhpMixed, STDERR, debug_backtrace, error_reporting, filter_var_boolean, ini_get, is_resource,
- set_error_handler,
+ PhpMixed, STDERR, debug_backtrace, error_reporting, filter_var_boolean, ini_get,
+ is_resource_value, set_error_handler,
};
use std::cell::{Cell, RefCell};
use std::rc::Rc;
@@ -126,11 +126,11 @@ impl ErrorHandler {
}
if output_even_without_io {
- if is_resource(&PhpMixed::Int(STDERR)) {
+ if is_resource_value(&STDERR) {
shirabe_php_shim::fwrite(
- PhpMixed::Int(STDERR),
+ &STDERR,
&format!("Warning: {}{}", message, PHP_EOL),
- -1,
+ None,
);
} else {
print!("Warning: {}{}", message, PHP_EOL);
diff --git a/crates/shirabe/src/util/filesystem.rs b/crates/shirabe/src/util/filesystem.rs
index 37ffabb..90deba1 100644
--- a/crates/shirabe/src/util/filesystem.rs
+++ b/crates/shirabe/src/util/filesystem.rs
@@ -417,20 +417,21 @@ impl Filesystem {
// if copy fails we attempt to copy it manually as this can help bypass issues with VirtualBox shared folders
// see https://github.com/composer/composer/issues/12057
if str_contains(&e.message, "Bad address") {
- let source_handle = fopen(source, "r");
- let target_handle = fopen(&target, "w");
- if source_handle.is_null() || target_handle.is_null() {
- return Err(e.into());
- }
- while !feof(source_handle.clone()) {
- let chunk =
- fread(source_handle.clone(), 1024 * 1024).unwrap_or_default();
- if fwrite(target_handle.clone(), &chunk, chunk.len() as i64).is_none() {
+ let (source_handle, target_handle) =
+ match (fopen(source, "r"), fopen(&target, "w")) {
+ (Ok(source_handle), Ok(target_handle)) => {
+ (source_handle, target_handle)
+ }
+ _ => return Err(e.into()),
+ };
+ while !feof(&source_handle) {
+ let chunk = fread(&source_handle, 1024 * 1024).unwrap_or_default();
+ if fwrite(&target_handle, &chunk, None).is_none() {
return Err(e.into());
}
}
- fclose(source_handle);
- fclose(target_handle);
+ fclose(&source_handle);
+ fclose(&target_handle);
return Ok(true);
}
@@ -1041,24 +1042,28 @@ impl Filesystem {
/// Copy file using stream_copy_to_stream to work around https://bugs.php.net/bug.php?id=6463
pub fn safe_copy(&self, source: &str, target: &str) -> anyhow::Result<()> {
if !file_exists(target) || !file_exists(source) || !self.files_are_equal(source, target) {
- let source_handle = fopen(source, "r");
- if source_handle.is_null() {
- return Err(anyhow::anyhow!(
- "Could not open \"{}\" for reading.",
- source
- ));
- }
- let target_handle = fopen(target, "w+");
- if target_handle.is_null() {
- return Err(anyhow::anyhow!(
- "Could not open \"{}\" for writing.",
- target
- ));
- }
+ let source_handle = match fopen(source, "r") {
+ Ok(source_handle) => source_handle,
+ Err(_) => {
+ return Err(anyhow::anyhow!(
+ "Could not open \"{}\" for reading.",
+ source
+ ));
+ }
+ };
+ let target_handle = match fopen(target, "w+") {
+ Ok(target_handle) => target_handle,
+ Err(_) => {
+ return Err(anyhow::anyhow!(
+ "Could not open \"{}\" for writing.",
+ target
+ ));
+ }
+ };
- shirabe_php_shim::stream_copy_to_stream(source_handle.clone(), target_handle.clone());
- fclose(source_handle);
- fclose(target_handle);
+ shirabe_php_shim::stream_copy_to_stream(&source_handle, &target_handle);
+ fclose(&source_handle);
+ fclose(&target_handle);
touch(target);
// PHP also passes filemtime/fileatime — skipping detailed timestamp restore here.
@@ -1076,25 +1081,25 @@ impl Filesystem {
}
// Check if content is different
- let a_handle = fopen(a, "rb");
- if a_handle.is_null() {
- return false;
- }
- let b_handle = fopen(b, "rb");
- if b_handle.is_null() {
- return false;
- }
+ let a_handle = match fopen(a, "rb") {
+ Ok(a_handle) => a_handle,
+ Err(_) => return false,
+ };
+ let b_handle = match fopen(b, "rb") {
+ Ok(b_handle) => b_handle,
+ Err(_) => return false,
+ };
let mut result = true;
- while !feof(a_handle.clone()) {
- if fread(a_handle.clone(), 8192) != fread(b_handle.clone(), 8192) {
+ while !feof(&a_handle) {
+ if fread(&a_handle, 8192) != fread(&b_handle, 8192) {
result = false;
break;
}
}
- fclose(a_handle);
- fclose(b_handle);
+ fclose(&a_handle);
+ fclose(&b_handle);
result
}
diff --git a/crates/shirabe/src/util/http/curl_downloader.rs b/crates/shirabe/src/util/http/curl_downloader.rs
index 0519f26..94e703d 100644
--- a/crates/shirabe/src/util/http/curl_downloader.rs
+++ b/crates/shirabe/src/util/http/curl_downloader.rs
@@ -14,9 +14,9 @@ use shirabe_php_shim::{
CURLOPT_FILE, CURLOPT_FOLLOWLOCATION, CURLOPT_HTTP_VERSION, CURLOPT_IPRESOLVE,
CURLOPT_PROTOCOLS, CURLOPT_SHARE, CURLOPT_TIMEOUT, CURLOPT_URL, CURLOPT_WRITEHEADER,
CURLPROTO_HTTP, CURLPROTO_HTTPS, CURLSHOPT_SHARE, CurlMultiHandle, CurlShareHandle,
- LogicException, PHP_VERSION_ID, PhpMixed, RuntimeException, array_diff, array_diff_key,
- array_merge, count, curl_errno, curl_error, curl_getinfo, curl_handle_id, curl_init,
- curl_multi_add_handle, curl_multi_exec, curl_multi_info_read, curl_multi_init,
+ LogicException, PHP_VERSION_ID, PhpMixed, PhpResource, RuntimeException, array_diff,
+ array_diff_key, array_merge, count, curl_errno, curl_error, curl_getinfo, curl_handle_id,
+ curl_init, curl_multi_add_handle, curl_multi_exec, curl_multi_info_read, curl_multi_init,
curl_multi_select, curl_multi_setopt, curl_setopt, curl_setopt_array, curl_share_init,
curl_share_setopt, curl_strerror, curl_version, defined, explode, fclose, fopen,
function_exists, implode, in_array, ini_get, is_resource, json_decode, parse_url, preg_quote,
@@ -47,6 +47,10 @@ pub struct CurlDownloader {
share_handle: Option<CurlShareHandle>,
/// @var Job[]
jobs: IndexMap<i64, IndexMap<String, PhpMixed>>,
+ /// The `headerHandle`/`bodyHandle` stream resources of each job, keyed by job id. PHP keeps
+ /// them inside the Job array, but a PhpResource cannot live in the PhpMixed-typed job map.
+ header_handles: IndexMap<i64, PhpResource>,
+ body_handles: IndexMap<i64, PhpResource>,
/// @var IOInterface
io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
/// @var Config
@@ -232,6 +236,8 @@ impl CurlDownloader {
multi_handle,
share_handle,
jobs: IndexMap::new(),
+ header_handles: IndexMap::new(),
+ body_handles: IndexMap::new(),
io,
config,
auth_helper,
@@ -329,16 +335,18 @@ impl CurlDownloader {
}
let curl_handle = curl_init();
- let header_handle = fopen("php://temp/maxmemory:32768", "w+b");
- if matches!(header_handle, PhpMixed::Bool(false)) {
- anyhow::bail!(
- RuntimeException {
- message: "Failed to open a temp stream to store curl headers".to_string(),
- code: 0,
- }
- .message
- );
- }
+ let header_handle = match fopen("php://temp/maxmemory:32768", "w+b") {
+ Ok(header_handle) => header_handle,
+ Err(_) => {
+ anyhow::bail!(
+ RuntimeException {
+ message: "Failed to open a temp stream to store curl headers".to_string(),
+ code: 0,
+ }
+ .message
+ );
+ }
+ };
let body_target: String = if let Some(copy_to) = copy_to {
format!("{}~", copy_to)
@@ -350,18 +358,20 @@ impl CurlDownloader {
// (stripping the "fopen(...): " prefix) into the message. Rust I/O reports failures through
// return values, not warnings, so error_message stays empty until fopen surfaces its reason.
let error_message = String::new();
- let body_handle = fopen(&body_target, "w+b");
- if matches!(body_handle, PhpMixed::Bool(false)) {
- return Ok(Err(TransportException::new(
- format!(
- "The \"{}\" file could not be written to {}: {}",
- url,
- copy_to.unwrap_or("a temporary file"),
- error_message
- ),
- 0,
- )));
- }
+ let body_handle = match fopen(&body_target, "w+b") {
+ Ok(body_handle) => body_handle,
+ Err(_) => {
+ return Ok(Err(TransportException::new(
+ format!(
+ "The \"{}\" file could not be written to {}: {}",
+ url,
+ copy_to.unwrap_or("a temporary file"),
+ error_message
+ ),
+ 0,
+ )));
+ }
+ };
curl_setopt(&curl_handle, CURLOPT_URL, PhpMixed::String(url.to_string()));
curl_setopt(&curl_handle, CURLOPT_FOLLOWLOCATION, PhpMixed::Bool(false));
@@ -378,8 +388,10 @@ impl CurlDownloader {
.max(300),
),
);
- curl_setopt(&curl_handle, CURLOPT_WRITEHEADER, header_handle.clone());
- curl_setopt(&curl_handle, CURLOPT_FILE, body_handle.clone());
+ // TODO(phase-d): CURLOPT_WRITEHEADER/CURLOPT_FILE hand the header/body stream resources to
+ // curl, but curl_setopt takes a PhpMixed and a PhpResource cannot be passed through it.
+ // curl_setopt(&curl_handle, CURLOPT_WRITEHEADER, header_handle);
+ // curl_setopt(&curl_handle, CURLOPT_FILE, body_handle);
curl_setopt(
&curl_handle,
CURLOPT_ENCODING,
@@ -601,8 +613,11 @@ impl CurlDownloader {
.map(|s| PhpMixed::String(s.to_string()))
.unwrap_or(PhpMixed::Null),
);
- job.insert("headerHandle".to_string(), header_handle.clone());
- job.insert("bodyHandle".to_string(), body_handle.clone());
+ // headerHandle/bodyHandle are PHP stream resources; they live in the Rust-side
+ // header_handles/body_handles maps (keyed by job id) since a PhpResource cannot be stored
+ // in the PhpMixed job map.
+ job.insert("headerHandle".to_string(), PhpMixed::Null);
+ job.insert("bodyHandle".to_string(), PhpMixed::Null);
job.insert("resolve".to_string(), PhpMixed::Null);
job.insert("reject".to_string(), PhpMixed::Null);
job.insert("primaryIp".to_string(), PhpMixed::String(String::new()));
@@ -611,7 +626,10 @@ impl CurlDownloader {
// above) — blocked on the typed-Job refactor and the React\Promise model.
let _ = (resolve, reject);
- self.jobs.insert(curl_handle_id(&curl_handle), job);
+ let job_id = curl_handle_id(&curl_handle);
+ self.header_handles.insert(job_id, header_handle);
+ self.body_handles.insert(job_id, body_handle);
+ self.jobs.insert(job_id, job);
let using_proxy = proxy
.get_status(Some(" using proxy (%s)"))
@@ -677,15 +695,17 @@ impl CurlDownloader {
if PHP_VERSION_ID < 80000 {
// curl_close($job['curlHandle']);
}
- if is_resource(job.get("headerHandle").unwrap_or(&PhpMixed::Null)) {
- fclose(job.get("headerHandle").cloned().unwrap_or(PhpMixed::Null));
+ if let Some(handle) = self.header_handles.get(&id) {
+ fclose(handle);
}
- if is_resource(job.get("bodyHandle").unwrap_or(&PhpMixed::Null)) {
- fclose(job.get("bodyHandle").cloned().unwrap_or(PhpMixed::Null));
+ if let Some(handle) = self.body_handles.get(&id) {
+ fclose(handle);
}
if let Some(PhpMixed::String(filename)) = job.get("filename") {
unlink_silent(&format!("{}~", filename));
}
+ self.header_handles.shift_remove(&id);
+ self.body_handles.shift_remove(&id);
self.jobs.shift_remove(&id);
}
}
@@ -939,18 +959,19 @@ impl CurlDownloader {
)));
}
status_code = progress.get("http_code").and_then(|b| b.as_int());
- rewind(job.get("headerHandle").cloned().unwrap_or(PhpMixed::Null));
- headers = Some(explode(
- "\r\n",
- &rtrim(
- &stream_get_contents(
- job.get("headerHandle").cloned().unwrap_or(PhpMixed::Null),
- )
- .unwrap_or_default(),
- None,
- ),
- ));
- fclose(job.get("headerHandle").cloned().unwrap_or(PhpMixed::Null));
+ let header_handle = self.header_handles.get(&i).cloned();
+ let header_contents = match &header_handle {
+ Some(handle) => {
+ rewind(handle);
+ stream_get_contents(handle).unwrap_or_default()
+ }
+ None => String::new(),
+ };
+ headers = Some(explode("\r\n", &rtrim(&header_contents, None)));
+ if let Some(handle) = &header_handle {
+ fclose(handle);
+ }
+ self.header_handles.shift_remove(&i);
if status_code == Some(0) {
anyhow::bail!(
@@ -984,17 +1005,15 @@ impl CurlDownloader {
}
// prepare response object
+ let body_handle = self.body_handles.get(&i).cloned();
let contents: PhpMixed;
if let Some(PhpMixed::String(filename)) = job.get("filename") {
let mut c: PhpMixed = PhpMixed::String(format!("{}~", filename));
if status_code.unwrap_or(0) >= 300 {
- rewind(job.get("bodyHandle").cloned().unwrap_or(PhpMixed::Null));
- c = PhpMixed::String(
- stream_get_contents(
- job.get("bodyHandle").cloned().unwrap_or(PhpMixed::Null),
- )
- .unwrap_or_default(),
- );
+ if let Some(handle) = &body_handle {
+ rewind(handle);
+ c = PhpMixed::String(stream_get_contents(handle).unwrap_or_default());
+ }
}
contents = c;
response = Some(CurlResponse::new(
@@ -1027,12 +1046,16 @@ impl CurlDownloader {
.and_then(|v| v.as_array())
.and_then(|a| a.get("max_file_size"))
.and_then(|b| b.as_int());
- rewind(job.get("bodyHandle").cloned().unwrap_or(PhpMixed::Null));
+ if let Some(handle) = &body_handle {
+ rewind(handle);
+ }
if let Some(max_file_size) = max_file_size {
- let c = stream_get_contents_with_max(
- job.get("bodyHandle").cloned().unwrap_or(PhpMixed::Null),
- Some(max_file_size),
- );
+ let c = match &body_handle {
+ Some(handle) => {
+ stream_get_contents_with_max(handle, Some(max_file_size))
+ }
+ None => None,
+ };
// Gzipped responses with missing Content-Length header cannot be detected during the file download
// because $progress['size_download'] refers to the gzipped size downloaded, not the actual file size
if let Some(c_str) = c.as_deref()
@@ -1053,12 +1076,10 @@ impl CurlDownloader {
}
contents = PhpMixed::String(c.unwrap_or_default());
} else {
- contents = PhpMixed::String(
- stream_get_contents(
- job.get("bodyHandle").cloned().unwrap_or(PhpMixed::Null),
- )
- .unwrap_or_default(),
- );
+ contents = PhpMixed::String(match &body_handle {
+ Some(handle) => stream_get_contents(handle).unwrap_or_default(),
+ None => String::new(),
+ });
}
response = Some(CurlResponse::new(
@@ -1086,7 +1107,10 @@ impl CurlDownloader {
crate::io::DEBUG,
);
}
- fclose(job.get("bodyHandle").cloned().unwrap_or(PhpMixed::Null));
+ if let Some(handle) = &body_handle {
+ fclose(handle);
+ }
+ self.body_handles.shift_remove(&i);
let response_ref = response.as_ref().unwrap();
if response_ref.inner.get_status_code() >= 300
@@ -1310,11 +1334,11 @@ impl CurlDownloader {
e.set_response(r.inner.get_body().map(|s| s.to_string()));
}
e.set_response_info(progress.values().cloned().collect::<Vec<_>>());
- self.reject_job(&job, anyhow::anyhow!(e.message));
+ self.reject_job(i, &job, anyhow::anyhow!(e.message));
}
Err(e) => {
// Non-TransportException fatal error: pass through reject_job to mirror PHP catch (\Exception $e).
- self.reject_job(&job, e);
+ self.reject_job(i, &job, e);
}
}
}
@@ -1369,6 +1393,7 @@ impl CurlDownloader {
if max_file_size < download_content_length {
let job = self.jobs.get(&i).cloned().unwrap_or_default();
self.reject_job(
+ i,
&job,
anyhow::anyhow!(
MaxFileSizeExceededException(TransportException::new(
@@ -1392,6 +1417,7 @@ impl CurlDownloader {
if max_file_size < size_download {
let job = self.jobs.get(&i).cloned().unwrap_or_default();
self.reject_job(
+ i,
&job,
anyhow::anyhow!(
MaxFileSizeExceededException(TransportException::new(
@@ -1438,6 +1464,7 @@ impl CurlDownloader {
if blocked {
let job = self.jobs.get(&i).cloned().unwrap_or_default();
self.reject_job(
+ i,
&job,
anyhow::anyhow!(
TransportException::new(
@@ -1801,13 +1828,15 @@ impl CurlDownloader {
}
/// @param Job $job
- fn reject_job(&mut self, job: &IndexMap<String, PhpMixed>, _e: anyhow::Error) {
- if is_resource(job.get("headerHandle").unwrap_or(&PhpMixed::Null)) {
- fclose(job.get("headerHandle").cloned().unwrap_or(PhpMixed::Null));
+ fn reject_job(&mut self, id: i64, job: &IndexMap<String, PhpMixed>, _e: anyhow::Error) {
+ if let Some(handle) = self.header_handles.get(&id) {
+ fclose(handle);
}
- if is_resource(job.get("bodyHandle").unwrap_or(&PhpMixed::Null)) {
- fclose(job.get("bodyHandle").cloned().unwrap_or(PhpMixed::Null));
+ if let Some(handle) = self.body_handles.get(&id) {
+ fclose(handle);
}
+ self.header_handles.shift_remove(&id);
+ self.body_handles.shift_remove(&id);
if let Some(PhpMixed::String(filename)) = job.get("filename") {
unlink_silent(&format!("{}~", filename));
}
diff --git a/crates/shirabe/src/util/perforce.rs b/crates/shirabe/src/util/perforce.rs
index d5facd2..7ca3f26 100644
--- a/crates/shirabe/src/util/perforce.rs
+++ b/crates/shirabe/src/util/perforce.rs
@@ -6,7 +6,7 @@ use shirabe_external_packages::composer::pcre::Preg;
use shirabe_external_packages::symfony::process::ExecutableFinder;
use shirabe_external_packages::symfony::process::Process;
use shirabe_php_shim::{
- Exception, PHP_EOL, PhpMixed, chdir, count, date, explode, fclose, feof, fgets,
+ Exception, PHP_EOL, PhpMixed, PhpResource, chdir, count, date, explode, fclose, feof, fgets,
file_get_contents, fopen, fwrite, gethostname, json_decode, str_replace_array, strcmp, strlen,
strpos, strrpos, substr, time, trim,
};
@@ -416,73 +416,73 @@ impl Perforce {
}
/// @param resource|false $spec
- pub fn write_client_spec_to_file(&mut self, spec: PhpMixed) {
+ pub fn write_client_spec_to_file(&mut self, spec: &PhpResource) {
fwrite(
- spec.clone(),
+ spec,
&format!("Client: {}{}{}", self.get_client(), PHP_EOL, PHP_EOL),
- 0,
+ None,
);
fwrite(
- spec.clone(),
+ spec,
&format!(
"Update: {}{}{}",
date("Y/m/d H:i:s", None),
PHP_EOL,
PHP_EOL
),
- 0,
+ None,
);
fwrite(
- spec.clone(),
+ spec,
&format!("Access: {}{}", date("Y/m/d H:i:s", None), PHP_EOL),
- 0,
+ None,
);
fwrite(
- spec.clone(),
+ spec,
&format!(
"Owner: {}{}{}",
self.get_user().unwrap_or_default(),
PHP_EOL,
PHP_EOL
),
- 0,
+ None,
);
- fwrite(spec.clone(), &format!("Description:{}", PHP_EOL), 0);
+ fwrite(spec, &format!("Description:{}", PHP_EOL), None);
fwrite(
- spec.clone(),
+ spec,
&format!(
" Created by {} from composer.{}{}",
self.get_user().unwrap_or_default(),
PHP_EOL,
PHP_EOL
),
- 0,
+ None,
);
fwrite(
- spec.clone(),
+ spec,
&format!("Root: {}{}{}", self.get_path(), PHP_EOL, PHP_EOL),
- 0,
+ None,
);
fwrite(
- spec.clone(),
+ spec,
&format!(
"Options: noallwrite noclobber nocompress unlocked modtime rmdir{}{}",
PHP_EOL, PHP_EOL
),
- 0,
+ None,
);
fwrite(
- spec.clone(),
+ spec,
&format!("SubmitOptions: revertunchanged{}{}", PHP_EOL, PHP_EOL),
- 0,
+ None,
);
fwrite(
- spec.clone(),
+ spec,
&format!("LineEnd: local{}{}", PHP_EOL, PHP_EOL),
- 0,
+ None,
);
if self.is_stream() {
- fwrite(spec.clone(), &format!("Stream:{}", PHP_EOL), 0);
+ fwrite(spec, &format!("Stream:{}", PHP_EOL), None);
let stream_clone = self.p4_stream.clone().unwrap_or_default();
fwrite(
spec,
@@ -491,7 +491,7 @@ impl Perforce {
self.get_stream_without_label(&stream_clone),
PHP_EOL
),
- 0,
+ None,
);
} else {
let stream = self.get_stream();
@@ -499,38 +499,47 @@ impl Perforce {
fwrite(
spec,
&format!("View: {}/... //{}/... {}", stream, client, PHP_EOL),
- 0,
+ None,
);
}
}
pub fn write_p4_client_spec(&mut self) -> Result<()> {
let client_spec = self.get_p4_client_spec();
- let spec = fopen(&client_spec, "w");
+ let spec = match fopen(&client_spec, "w") {
+ Ok(spec) => spec,
+ Err(e) => {
+ return Err(Exception {
+ message: e.to_string(),
+ code: 0,
+ }
+ .into());
+ }
+ };
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
- self.write_client_spec_to_file(spec.clone());
+ self.write_client_spec_to_file(&spec);
}));
if let Err(e) = result {
- fclose(spec);
+ fclose(&spec);
return Err(Exception {
message: format!("{:?}", e),
code: 0,
}
.into());
}
- fclose(spec);
+ fclose(&spec);
Ok(())
}
/// @param resource $pipe
/// @param mixed $name
- pub(crate) fn read(&self, pipe: PhpMixed, _name: PhpMixed) {
- if feof(pipe.clone()) {
+ pub(crate) fn read(&self, pipe: &PhpResource, _name: PhpMixed) {
+ if feof(pipe) {
return;
}
- let mut line = fgets(pipe.clone());
+ let mut line = fgets(pipe, None);
while line.is_some() {
- line = fgets(pipe.clone());
+ line = fgets(pipe, None);
}
}
diff --git a/crates/shirabe/src/util/platform.rs b/crates/shirabe/src/util/platform.rs
index 449874e..442b4d9 100644
--- a/crates/shirabe/src/util/platform.rs
+++ b/crates/shirabe/src/util/platform.rs
@@ -291,13 +291,7 @@ impl Platform {
/// @param ?resource $fd Open file descriptor or null to default to STDOUT
pub fn is_tty(fd: Option<PhpResource>) -> bool {
- let fd = match fd {
- Some(f) => f,
- None => {
- // TODO(phase-c): STDOUT is not yet modeled as a `PhpResource` constant.
- todo!("STDOUT resource constant")
- }
- };
+ let fd = fd.unwrap_or(shirabe_php_shim::STDOUT);
// detect msysgit/mingw and assume this is a tty because detection
// does not work correctly, see https://github.com/composer/composer/issues/9690
@@ -323,19 +317,18 @@ impl Platform {
return true;
}
- let stat = Silencer::call(|| Ok(fstat(fd)));
+ let stat = Silencer::call(|| Ok(fstat(&fd)));
let stat = match stat {
Ok(s) => s,
Err(_) => return false,
};
- if matches!(stat, PhpMixed::Bool(false)) {
- return false;
- }
+ let stat = match stat {
+ Some(stat) => stat,
+ None => return false,
+ };
// Check if formatted mode is S_IFCHR
- if let Some(arr) = stat.as_array()
- && let Some(mode) = arr.get("mode").and_then(|v| v.as_int())
- {
+ if let Some(mode) = stat.get("mode").and_then(|v| v.as_int()) {
return 0o020000 == (mode & 0o170000);
}
diff --git a/crates/shirabe/src/util/zip.rs b/crates/shirabe/src/util/zip.rs
index 4231db4..dbb2627 100644
--- a/crates/shirabe/src/util/zip.rs
+++ b/crates/shirabe/src/util/zip.rs
@@ -34,8 +34,8 @@ impl Zip {
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());
+ if let Some(stream) = &stream {
+ content = stream_get_contents(stream);
}
zip.close();
diff --git a/crates/shirabe/tests/question/strict_confirmation_question_test.rs b/crates/shirabe/tests/question/strict_confirmation_question_test.rs
index 4444289..c59a0fd 100644
--- a/crates/shirabe/tests/question/strict_confirmation_question_test.rs
+++ b/crates/shirabe/tests/question/strict_confirmation_question_test.rs
@@ -5,19 +5,19 @@
// validator are private, so they cannot be exercised directly either.
#[test]
-#[ignore = "ArrayInput exposes no public set_stream (its inner Input is pub(crate)), and fopen returns PhpMixed which cannot be bridged to StreamOutput::new's PhpResource"]
+#[ignore = "ArrayInput exposes no public set_stream (its inner Input is pub(crate)); fopen now returns a PhpResource that bridges to StreamOutput::new, but the input-stream wiring is still missing"]
fn test_ask_confirmation_bad_answer() {
todo!()
}
#[test]
-#[ignore = "ArrayInput exposes no public set_stream (its inner Input is pub(crate)), and fopen returns PhpMixed which cannot be bridged to StreamOutput::new's PhpResource"]
+#[ignore = "ArrayInput exposes no public set_stream (its inner Input is pub(crate)); fopen now returns a PhpResource that bridges to StreamOutput::new, but the input-stream wiring is still missing"]
fn test_ask_confirmation() {
todo!()
}
#[test]
-#[ignore = "ArrayInput exposes no public set_stream (its inner Input is pub(crate)), and fopen returns PhpMixed which cannot be bridged to StreamOutput::new's PhpResource"]
+#[ignore = "ArrayInput exposes no public set_stream (its inner Input is pub(crate)); fopen now returns a PhpResource that bridges to StreamOutput::new, but the input-stream wiring is still missing"]
fn test_ask_confirmation_with_custom_true_and_false_answer() {
todo!()
}