aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/cursor.rs20
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs56
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/terminal.rs19
-rw-r--r--crates/shirabe-external-packages/src/symfony/process/pipes/abstract_pipes.rs9
-rw-r--r--crates/shirabe-external-packages/src/symfony/process/pipes/unix_pipes.rs9
-rw-r--r--crates/shirabe-external-packages/src/symfony/process/process.rs89
-rw-r--r--crates/shirabe-php-shim/src/fs.rs504
-rw-r--r--crates/shirabe-php-shim/src/lib.rs51
-rw-r--r--crates/shirabe-php-shim/src/process.rs4
-rw-r--r--crates/shirabe-php-shim/src/stream.rs138
-rw-r--r--crates/shirabe-php-shim/src/zip.rs2
-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
21 files changed, 902 insertions, 438 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/console/cursor.rs b/crates/shirabe-external-packages/src/symfony/console/cursor.rs
index 42efff7..811553f 100644
--- a/crates/shirabe-external-packages/src/symfony/console/cursor.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/cursor.rs
@@ -8,25 +8,15 @@ use std::rc::Rc;
#[derive(Debug)]
pub struct Cursor {
output: Rc<RefCell<dyn OutputInterface>>,
- input: shirabe_php_shim::PhpMixed,
+ input: shirabe_php_shim::PhpResource,
}
impl Cursor {
pub fn new(
output: Rc<RefCell<dyn OutputInterface>>,
- input: Option<shirabe_php_shim::PhpMixed>,
+ input: Option<shirabe_php_shim::PhpResource>,
) -> Self {
- let input = input.unwrap_or_else(|| {
- if shirabe_php_shim::defined("STDIN") {
- // PHP uses the `STDIN` resource constant here, but the shim models it as
- // `PhpResource` while this field is `PhpMixed` (which has no resource
- // variant). Fall back to opening `php://input` so the value stays a
- // PhpMixed stream handle.
- shirabe_php_shim::fopen("php://input", "r+")
- } else {
- shirabe_php_shim::fopen("php://input", "r+")
- }
- });
+ let input = input.unwrap_or(shirabe_php_shim::STDIN);
Self { output, input }
}
@@ -228,10 +218,10 @@ impl Cursor {
let stty_mode = shirabe_php_shim::shell_exec("stty -g");
shirabe_php_shim::shell_exec("stty -icanon -echo");
- shirabe_php_shim::fwrite(self.input.clone(), "\x1b[6n", 0);
+ shirabe_php_shim::fwrite(&self.input, "\x1b[6n", None);
let code = shirabe_php_shim::trim(
- shirabe_php_shim::fread(self.input.clone(), 1024)
+ shirabe_php_shim::fread(&self.input, 1024)
.as_deref()
.unwrap_or(""),
None,
diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs b/crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs
index c36f77a..db9c094 100644
--- a/crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs
@@ -383,7 +383,7 @@ impl QuestionHelper {
);
// Read a keypress
- while !feof_resource(input_stream) {
+ while !shirabe_php_shim::feof(input_stream) {
while is_stdin
&& 0 == shirabe_php_shim::stream_select(
&mut r,
@@ -396,7 +396,7 @@ impl QuestionHelper {
// Give signal handlers a chance to run
r = vec![input_stream.clone()];
}
- let mut c = fread_resource(input_stream, 1);
+ let mut c = shirabe_php_shim::fread(input_stream, 1);
// as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false.
if c.is_none()
@@ -430,7 +430,7 @@ impl QuestionHelper {
ret = QuestionHelper::substr(Some(&ret), 0, Some(i));
} else if c.as_deref() == Some("\u{1b}") {
// Did we read an escape sequence?
- let escape = fread_resource(input_stream, 2).unwrap_or_default();
+ let escape = shirabe_php_shim::fread(input_stream, 2).unwrap_or_default();
let cc = format!("{}{}", c.clone().unwrap_or_default(), escape);
c = Some(cc.clone());
@@ -510,7 +510,7 @@ impl QuestionHelper {
"\u{f0}" => 3,
_ => 0,
};
- let extra = fread_resource(input_stream, len).unwrap_or_default();
+ let extra = shirabe_php_shim::fread(input_stream, len).unwrap_or_default();
c = Some(format!("{}{}", cur, extra));
}
@@ -649,7 +649,7 @@ impl QuestionHelper {
})));
}
- let value = fgets_resource(input_stream, Some(4096));
+ let value = shirabe_php_shim::fgets(input_stream, Some(4096));
if STTY.load(std::sync::atomic::Ordering::SeqCst) && Terminal::has_stty_available() {
shirabe_php_shim::shell_exec(&format!("stty {}", stty_mode));
@@ -764,7 +764,7 @@ impl QuestionHelper {
) -> PhpMixed {
if !question.is_multiline() {
let cp = self.set_io_codepage();
- let ret = fgets_resource(input_stream, Some(4096));
+ let ret = shirabe_php_shim::fgets(input_stream, Some(4096));
return self.reset_io_codepage(
cp,
@@ -856,16 +856,16 @@ impl QuestionHelper {
let uri = uri?;
- let clone_stream = shirabe_php_shim::php_fopen_resource(&uri, &mode);
+ let clone_stream = shirabe_php_shim::fopen(&uri, &mode).ok()?;
// For seekable and writable streams, add all the same data to the
// cloned stream and then seek to the same offset.
if matches!(seekable, PhpMixed::Bool(true)) && !["r", "rb", "rt"].contains(&mode.as_str()) {
- let offset = shirabe_php_shim::ftell(input_stream);
- rewind_resource(input_stream);
- stream_copy_to_stream_resource(input_stream, &clone_stream);
- fseek_resource(input_stream, offset);
- fseek_resource(&clone_stream, offset);
+ let offset = shirabe_php_shim::ftell(input_stream).unwrap_or(0);
+ shirabe_php_shim::rewind(input_stream);
+ shirabe_php_shim::stream_copy_to_stream(input_stream, &clone_stream);
+ shirabe_php_shim::fseek(input_stream, offset, shirabe_php_shim::SEEK_SET);
+ shirabe_php_shim::fseek(&clone_stream, offset, shirabe_php_shim::SEEK_SET);
}
Some(clone_stream)
@@ -882,44 +882,12 @@ fn question_as_choice_question(question: &Question) -> Option<&ChoiceQuestion> {
question.as_any().downcast_ref::<ChoiceQuestion>()
}
-// PHP operates on raw stream resources, but the shim models `feof`/`fread`/
-// `fgets`/`fseek`/`rewind`/`stream_copy_to_stream` over `PhpMixed`, which has no
-// resource variant. These thin resource-typed wrappers stand in until the shim
-// grows `*_resource` counterparts (see report). Each defers to the real
-// implementation once it exists.
// PHP `__FILE__` magic constant. The shim's `file()` is PHP's file() function,
// not the magic constant, and there is no `__FILE__` shim yet (see report).
fn magic_file() -> String {
todo!("magic_file: shim needs a __FILE__ magic-constant equivalent")
}
-fn feof_resource(_stream: &shirabe_php_shim::PhpResource) -> bool {
- todo!("feof_resource: shim needs a PhpResource-based feof")
-}
-
-fn fread_resource(_stream: &shirabe_php_shim::PhpResource, _length: i64) -> Option<String> {
- todo!("fread_resource: shim needs a PhpResource-based fread")
-}
-
-fn fgets_resource(_stream: &shirabe_php_shim::PhpResource, _length: Option<i64>) -> Option<String> {
- todo!("fgets_resource: shim needs a PhpResource-based fgets")
-}
-
-fn fseek_resource(_stream: &shirabe_php_shim::PhpResource, _offset: i64) -> i64 {
- todo!("fseek_resource: shim needs a PhpResource-based fseek")
-}
-
-fn rewind_resource(_stream: &shirabe_php_shim::PhpResource) -> bool {
- todo!("rewind_resource: shim needs a PhpResource-based rewind")
-}
-
-fn stream_copy_to_stream_resource(
- _source: &shirabe_php_shim::PhpResource,
- _dest: &shirabe_php_shim::PhpResource,
-) -> Option<i64> {
- todo!("stream_copy_to_stream_resource: shim needs a PhpResource-based stream_copy_to_stream")
-}
-
impl HelperInterface for QuestionHelper {
fn set_helper_set(&mut self, helper_set: Option<Rc<RefCell<HelperSet>>>) {
self.inner.set_helper_set(helper_set);
diff --git a/crates/shirabe-external-packages/src/symfony/console/terminal.rs b/crates/shirabe-external-packages/src/symfony/console/terminal.rs
index e0de3f2..ac4009e 100644
--- a/crates/shirabe-external-packages/src/symfony/console/terminal.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/terminal.rs
@@ -219,7 +219,7 @@ impl Terminal {
]),
];
- let cp = if shirabe_php_shim::function_exists("sapi_windows_cp_set") {
+ let _cp = if shirabe_php_shim::function_exists("sapi_windows_cp_set") {
shirabe_php_shim::sapi_windows_cp_get(None)
} else {
0
@@ -233,16 +233,13 @@ impl Terminal {
}
// $pipes[1] and $pipes[2] from proc_open's output array.
- let info = shirabe_php_shim::stream_get_contents(php_pipe(&pipes, 1));
- shirabe_php_shim::fclose(php_pipe(&pipes, 1));
- shirabe_php_shim::fclose(php_pipe(&pipes, 2));
- shirabe_php_shim::proc_close(process);
-
- if cp != 0 {
- shirabe_php_shim::sapi_windows_cp_set(cp);
- }
-
- info
+ let _pipe1 = php_pipe(&pipes, 1);
+ let _pipe2 = php_pipe(&pipes, 2);
+ // TODO(phase-d): the pipe contents are read via stream_get_contents() and the pipes are
+ // fclose()d, but they are PHP stream resources the PhpMixed pipe list cannot hold.
+ todo!(
+ "Terminal: reading proc_open pipes needs PhpResource handles the PhpMixed pipe list cannot carry"
+ );
}
}
diff --git a/crates/shirabe-external-packages/src/symfony/process/pipes/abstract_pipes.rs b/crates/shirabe-external-packages/src/symfony/process/pipes/abstract_pipes.rs
index 47db6b9..d8ced43 100644
--- a/crates/shirabe-external-packages/src/symfony/process/pipes/abstract_pipes.rs
+++ b/crates/shirabe-external-packages/src/symfony/process/pipes/abstract_pipes.rs
@@ -37,13 +37,8 @@ impl AbstractPipes {
}
pub fn close(&mut self) {
- if let PhpMixed::List(pipes) = &self.pipes {
- for pipe in pipes {
- if php::is_resource(pipe) {
- php::fclose(pipe.clone());
- }
- }
- }
+ // TODO(phase-d): each pipe is a PHP stream resource that should be fclose()d, but the pipe
+ // list is a PhpMixed that cannot hold a PhpResource; the handles are dropped instead.
self.pipes = PhpMixed::List(Vec::new());
}
diff --git a/crates/shirabe-external-packages/src/symfony/process/pipes/unix_pipes.rs b/crates/shirabe-external-packages/src/symfony/process/pipes/unix_pipes.rs
index 387bcdf..983ed07 100644
--- a/crates/shirabe-external-packages/src/symfony/process/pipes/unix_pipes.rs
+++ b/crates/shirabe-external-packages/src/symfony/process/pipes/unix_pipes.rs
@@ -44,9 +44,12 @@ fn descriptor(items: &[&str]) -> PhpMixed {
impl PipesInterface for UnixPipes {
fn get_descriptors(&mut self) -> Vec<PhpMixed> {
if !self.have_read_support {
- let nullstream = shirabe_php_shim::fopen("/dev/null", "c");
-
- return vec![descriptor(&["pipe", "r"]), nullstream.clone(), nullstream];
+ // TODO(phase-d): /dev/null is opened as a stream resource and placed directly into the
+ // proc_open descriptor spec, but the descriptor list is a Vec<PhpMixed> that cannot
+ // carry a PhpResource.
+ todo!(
+ "UnixPipes::get_descriptors: the /dev/null resource cannot be represented in a PhpMixed descriptor list"
+ );
}
if self.tty_mode == Some(true) {
diff --git a/crates/shirabe-external-packages/src/symfony/process/process.rs b/crates/shirabe-external-packages/src/symfony/process/process.rs
index fe7b06c..d0d104e 100644
--- a/crates/shirabe-external-packages/src/symfony/process/process.rs
+++ b/crates/shirabe-external-packages/src/symfony/process/process.rs
@@ -3,7 +3,7 @@
use std::sync::OnceLock;
use indexmap::IndexMap;
-use shirabe_php_shim::{self as php, PhpMixed};
+use shirabe_php_shim::{self as php, PhpMixed, PhpResource};
use crate::symfony::process::exception::invalid_argument_exception::InvalidArgumentException;
use crate::symfony::process::exception::logic_exception::LogicException;
@@ -48,8 +48,8 @@ pub struct Process {
fallback_status: IndexMap<String, PhpMixed>,
process_information: Option<IndexMap<String, PhpMixed>>,
output_disabled: bool,
- stdout: PhpMixed,
- stderr: PhpMixed,
+ stdout: Option<PhpResource>,
+ stderr: Option<PhpResource>,
process: PhpMixed,
status: String,
incremental_output_offset: i64,
@@ -199,8 +199,8 @@ impl Process {
fallback_status: IndexMap::new(),
process_information: None,
output_disabled: false,
- stdout: PhpMixed::Null,
- stderr: PhpMixed::Null,
+ stdout: None,
+ stderr: None,
process: PhpMixed::Null,
status: Self::STATUS_READY.to_string(),
incremental_output_offset: 0,
@@ -407,12 +407,12 @@ impl Process {
self.status = Self::STATUS_STARTED.to_string();
if descriptors.len() > 3 {
- let pipe3 = php_pipe(self.process_pipes.as_ref().unwrap().pipes(), 3);
- let pid = php::fgets(pipe3)
- .and_then(|s| s.trim().parse::<i64>().ok())
- .unwrap_or(0);
- self.fallback_status
- .insert("pid".to_string(), PhpMixed::Int(pid));
+ let _pipe3 = php_pipe(self.process_pipes.as_ref().unwrap().pipes(), 3);
+ // TODO(phase-d): the pid is read with fgets() from pipe 3, a PHP stream resource. The
+ // pipe list is a PhpMixed and cannot hold a PhpResource, so this cannot be expressed.
+ todo!(
+ "Process::start: reading the pid from pipe 3 needs a PhpResource the PhpMixed pipe list cannot carry"
+ );
}
if self.tty {
@@ -610,16 +610,19 @@ impl Process {
pub fn get_output(&mut self) -> anyhow::Result<String> {
self.read_pipes_for_output("getOutput", false)?;
- Ok(php::stream_get_contents3(self.stdout.clone(), -1, 0).unwrap_or_default())
+ Ok(php::stream_get_contents3(self.stdout.as_ref().unwrap(), -1, 0).unwrap_or_default())
}
/// Returns the output incrementally.
pub fn get_incremental_output(&mut self) -> anyhow::Result<String> {
self.read_pipes_for_output("getIncrementalOutput", false)?;
- let latest =
- php::stream_get_contents3(self.stdout.clone(), -1, self.incremental_output_offset);
- self.incremental_output_offset = php::ftell_stream(&self.stdout);
+ let latest = php::stream_get_contents3(
+ self.stdout.as_ref().unwrap(),
+ -1,
+ self.incremental_output_offset,
+ );
+ self.incremental_output_offset = php::ftell(self.stdout.as_ref().unwrap()).unwrap_or(0);
Ok(latest.unwrap_or_default())
}
@@ -637,15 +640,15 @@ impl Process {
let mut yields = Vec::new();
while self.callback.is_some()
- || (yield_out && !php::feof(self.stdout.clone()))
- || (yield_err && !php::feof(self.stderr.clone()))
+ || (yield_out && !php::feof(self.stdout.as_ref().unwrap()))
+ || (yield_err && !php::feof(self.stderr.as_ref().unwrap()))
{
let mut got_out = false;
let mut got_err = false;
if yield_out {
let out = php::stream_get_contents3(
- self.stdout.clone(),
+ self.stdout.as_ref().unwrap(),
-1,
self.incremental_output_offset,
)
@@ -656,7 +659,8 @@ impl Process {
if clear_output {
self.clear_output();
} else {
- self.incremental_output_offset = php::ftell_stream(&self.stdout);
+ self.incremental_output_offset =
+ php::ftell(self.stdout.as_ref().unwrap()).unwrap_or(0);
}
yields.push((Self::OUT.to_string(), out));
@@ -665,7 +669,7 @@ impl Process {
if yield_err {
let err = php::stream_get_contents3(
- self.stderr.clone(),
+ self.stderr.as_ref().unwrap(),
-1,
self.incremental_error_output_offset,
)
@@ -676,7 +680,8 @@ impl Process {
if clear_output {
self.clear_error_output();
} else {
- self.incremental_error_output_offset = php::ftell_stream(&self.stderr);
+ self.incremental_error_output_offset =
+ php::ftell(self.stderr.as_ref().unwrap()).unwrap_or(0);
}
yields.push((Self::ERR.to_string(), err));
@@ -696,8 +701,8 @@ impl Process {
/// Clears the process output.
pub fn clear_output(&mut self) -> &mut Self {
- php::ftruncate(&self.stdout, 0);
- php::fseek(self.stdout.clone(), 0);
+ php::ftruncate(self.stdout.as_ref().unwrap(), 0);
+ php::fseek(self.stdout.as_ref().unwrap(), 0, php::SEEK_SET);
self.incremental_output_offset = 0;
self
@@ -707,7 +712,7 @@ impl Process {
pub fn get_error_output(&mut self) -> anyhow::Result<String> {
self.read_pipes_for_output("getErrorOutput", false)?;
- Ok(php::stream_get_contents3(self.stderr.clone(), -1, 0).unwrap_or_default())
+ Ok(php::stream_get_contents3(self.stderr.as_ref().unwrap(), -1, 0).unwrap_or_default())
}
/// Returns the errorOutput incrementally.
@@ -715,19 +720,20 @@ impl Process {
self.read_pipes_for_output("getIncrementalErrorOutput", false)?;
let latest = php::stream_get_contents3(
- self.stderr.clone(),
+ self.stderr.as_ref().unwrap(),
-1,
self.incremental_error_output_offset,
);
- self.incremental_error_output_offset = php::ftell_stream(&self.stderr);
+ self.incremental_error_output_offset =
+ php::ftell(self.stderr.as_ref().unwrap()).unwrap_or(0);
Ok(latest.unwrap_or_default())
}
/// Clears the process error output.
pub fn clear_error_output(&mut self) -> &mut Self {
- php::ftruncate(&self.stderr, 0);
- php::fseek(self.stderr.clone(), 0);
+ php::ftruncate(self.stderr.as_ref().unwrap(), 0);
+ php::fseek(self.stderr.as_ref().unwrap(), 0, php::SEEK_SET);
self.incremental_error_output_offset = 0;
self
@@ -882,18 +888,20 @@ impl Process {
pub fn add_output(&mut self, line: &str) {
self.last_output_time = Some(php::microtime());
- php::fseek3(self.stdout.clone(), 0, php::SEEK_END);
- php::fwrite(self.stdout.clone(), line, line.len() as i64);
- php::fseek(self.stdout.clone(), self.incremental_output_offset);
+ let stdout = self.stdout.as_ref().unwrap();
+ php::fseek(stdout, 0, php::SEEK_END);
+ php::fwrite(stdout, line, Some(line.len() as i64));
+ php::fseek(stdout, self.incremental_output_offset, php::SEEK_SET);
}
/// Adds a line to the STDERR stream.
pub fn add_error_output(&mut self, line: &str) {
self.last_output_time = Some(php::microtime());
- php::fseek3(self.stderr.clone(), 0, php::SEEK_END);
- php::fwrite(self.stderr.clone(), line, line.len() as i64);
- php::fseek(self.stderr.clone(), self.incremental_error_output_offset);
+ let stderr = self.stderr.as_ref().unwrap();
+ php::fseek(stderr, 0, php::SEEK_END);
+ php::fwrite(stderr, line, Some(line.len() as i64));
+ php::fseek(stderr, self.incremental_error_output_offset, php::SEEK_SET);
}
/// Gets the last output time in seconds.
@@ -1398,8 +1406,11 @@ impl Process {
self.exitcode = None;
self.fallback_status = IndexMap::new();
self.process_information = None;
- self.stdout = php::fopen(&format!("php://temp/maxmemory:{}", 1024 * 1024), "w+");
- self.stderr = php::fopen(&format!("php://temp/maxmemory:{}", 1024 * 1024), "w+");
+ // php://temp is an in-memory stream; fopen never fails for it.
+ self.stdout =
+ Some(php::fopen(&format!("php://temp/maxmemory:{}", 1024 * 1024), "w+").unwrap());
+ self.stderr =
+ Some(php::fopen(&format!("php://temp/maxmemory:{}", 1024 * 1024), "w+").unwrap());
self.process = PhpMixed::Null;
self.latest_signal = None;
self.status = Self::STATUS_READY.to_string();
@@ -1458,7 +1469,11 @@ impl Process {
None,
None,
);
- ok = php::php_truthy(&opened) && php::fgets(php_pipe(&pipes, 2)).is_none();
+ let _pipe2 = php_pipe(&pipes, 2);
+ // TODO(phase-d): the original also requires `fgets($pipes[2]) === false`, reading
+ // from a PHP stream resource the PhpMixed pipe list cannot hold; that read is
+ // dropped here.
+ ok = php::php_truthy(&opened);
}
if !ok {
if throw_exception {
diff --git a/crates/shirabe-php-shim/src/fs.rs b/crates/shirabe-php-shim/src/fs.rs
index e55b689..ecbe5c4 100644
--- a/crates/shirabe-php-shim/src/fs.rs
+++ b/crates/shirabe-php-shim/src/fs.rs
@@ -1,5 +1,7 @@
use crate::PhpMixed;
use crate::PhpResource;
+use crate::StreamBacking;
+use crate::StreamState;
use crate::UnexpectedValueException;
use indexmap::IndexMap;
@@ -8,6 +10,8 @@ pub const PHP_EOL: &str = "\n";
pub const FILE_APPEND: i64 = 8;
pub const STDIN: PhpResource = PhpResource::Stdin;
+pub const STDOUT: PhpResource = PhpResource::Stdout;
+pub const STDERR: PhpResource = PhpResource::Stderr;
pub const PATHINFO_FILENAME: i64 = 64;
pub const PATHINFO_EXTENSION: i64 = 4;
@@ -234,63 +238,361 @@ pub fn directory_iterator(_path: &str) -> Vec<DirectoryIteratorEntry> {
todo!()
}
-// TODO(phase-d): the fopen-family stream API is keyed on PhpMixed, but PhpMixed has no stream/
-// resource variant, so an opened stream cannot be represented or threaded through fread/fwrite/etc.
-// Wiring a stream representation into PhpMixed is Phase C type-design work.
-pub fn fopen(_file: &str, _mode: &str) -> PhpMixed {
- todo!()
+/// PHP `fopen()`. Returns a stream resource, or an error mirroring PHP's `false`-on-failure
+/// (the I/O error carries the reason so callers that surface the warning text can use it).
+pub fn fopen(file: &str, mode: &str) -> Result<PhpResource, std::io::Error> {
+ match file {
+ "php://output" | "php://stdout" => return Ok(PhpResource::Stdout),
+ "php://stderr" => return Ok(PhpResource::Stderr),
+ "php://stdin" | "php://input" => return Ok(PhpResource::Stdin),
+ _ => {}
+ }
+ // Strip the binary/text flags PHP accepts as part of the mode.
+ let base_mode: String = mode.chars().filter(|c| *c != 'b' && *c != 't').collect();
+ let (readable, writable) = match base_mode.as_str() {
+ "r" => (true, false),
+ "w" | "a" | "x" | "c" => (false, true),
+ _ => (true, true), // r+, w+, a+, x+, c+, rw, ...
+ };
+ // php://memory and php://temp are in-memory streams.
+ if file == "php://memory" || file.starts_with("php://temp") {
+ return Ok(StreamState::new(
+ StreamBacking::Memory(std::io::Cursor::new(Vec::new())),
+ readable,
+ writable,
+ ));
+ }
+ let mut options = std::fs::OpenOptions::new();
+ match base_mode.as_str() {
+ "r" => options.read(true),
+ "r+" => options.read(true).write(true),
+ "w" => options.write(true).create(true).truncate(true),
+ "w+" => options.read(true).write(true).create(true).truncate(true),
+ "a" => options.append(true).create(true),
+ "a+" => options.read(true).append(true).create(true),
+ "x" => options.write(true).create_new(true),
+ "x+" => options.read(true).write(true).create_new(true),
+ // "c"/"c+": open or create without truncating, position at start.
+ "c" => options.write(true).create(true),
+ "c+" => options.read(true).write(true).create(true),
+ _ => options.read(true),
+ };
+ let file = options.open(file)?;
+ Ok(StreamState::new(
+ StreamBacking::File(file),
+ readable,
+ writable,
+ ))
}
-pub fn fwrite(_file: PhpMixed, _data: &str, _length: i64) -> Option<i64> {
- // TODO(phase-d): see fopen; no PhpMixed stream representation exists.
- todo!()
+/// PHP `fwrite()`. `length` caps the number of bytes written (`None` = whole string).
+/// Returns the byte count written, or `None` for PHP's `false`-on-failure.
+pub fn fwrite(stream: &PhpResource, data: &str, length: Option<i64>) -> Option<i64> {
+ use std::io::Write;
+ let bytes = data.as_bytes();
+ let bytes = match length {
+ Some(l) if l >= 0 => &bytes[..(l as usize).min(bytes.len())],
+ _ => bytes,
+ };
+ match stream {
+ PhpResource::Stdin => None,
+ PhpResource::Stdout => std::io::stdout()
+ .write_all(bytes)
+ .ok()
+ .map(|_| bytes.len() as i64),
+ PhpResource::Stderr => std::io::stderr()
+ .write_all(bytes)
+ .ok()
+ .map(|_| bytes.len() as i64),
+ PhpResource::Stream(state) => {
+ let mut state = state.borrow_mut();
+ if state.closed || !state.writable {
+ return None;
+ }
+ let n = state.backing.as_rws().write(bytes).ok()?;
+ Some(n as i64)
+ }
+ }
}
-pub fn fread(_handle: PhpMixed, _length: i64) -> Option<String> {
- // TODO(phase-d): see fopen; no PhpMixed stream representation exists.
- todo!()
+/// PHP `fread()`. Reads up to `length` bytes.
+/// TODO(phase-e): byte-string semantics — should return Vec<u8>; from_utf8_lossy can corrupt
+/// binary reads (filesAreEqual / binary copy).
+pub fn fread(stream: &PhpResource, length: i64) -> Option<String> {
+ use std::io::Read;
+ let cap = length.max(0) as usize;
+ match stream {
+ PhpResource::Stdin => {
+ let mut buf = vec![0u8; cap];
+ let n = std::io::stdin().read(&mut buf).ok()?;
+ buf.truncate(n);
+ Some(String::from_utf8_lossy(&buf).into_owned())
+ }
+ PhpResource::Stdout | PhpResource::Stderr => None,
+ PhpResource::Stream(state) => {
+ let mut state = state.borrow_mut();
+ if state.closed || !state.readable {
+ return None;
+ }
+ let mut buf = vec![0u8; cap];
+ let n = state.backing.as_rws().read(&mut buf).ok()?;
+ if cap > 0 && n == 0 {
+ state.eof = true;
+ }
+ buf.truncate(n);
+ Some(String::from_utf8_lossy(&buf).into_owned())
+ }
+ }
}
-pub fn feof(_stream: PhpMixed) -> bool {
- // TODO(phase-d): see fopen; no PhpMixed stream representation exists.
- todo!()
+/// PHP `feof()`: true only after a read has hit end-of-stream.
+pub fn feof(stream: &PhpResource) -> bool {
+ match stream {
+ PhpResource::Stdin | PhpResource::Stdout | PhpResource::Stderr => false,
+ PhpResource::Stream(state) => state.borrow().eof,
+ }
}
-pub fn fclose(_file: PhpMixed) {
- // TODO(phase-d): see fopen; no PhpMixed stream representation exists.
- todo!()
+/// PHP `fclose()`. Marks the stream closed; the backing is released when the last clone drops.
+pub fn fclose(stream: &PhpResource) -> bool {
+ match stream {
+ PhpResource::Stdin | PhpResource::Stdout | PhpResource::Stderr => true,
+ PhpResource::Stream(state) => {
+ let mut state = state.borrow_mut();
+ if state.closed {
+ return false;
+ }
+ use std::io::Write;
+ let _ = state.backing.as_rws().flush();
+ state.closed = true;
+ true
+ }
+ }
}
-pub fn fgets(_handle: PhpMixed) -> Option<String> {
- // TODO(phase-d): see fopen; no PhpMixed stream representation exists.
- todo!()
+/// PHP `fgets()`. Reads one line, including the trailing newline, capped at `length-1` bytes
+/// when given (matching PHP's `length` parameter).
+/// TODO(phase-e): byte-string semantics — should return Vec<u8>; from_utf8_lossy can corrupt
+/// binary reads.
+pub fn fgets(stream: &PhpResource, length: Option<i64>) -> Option<String> {
+ let limit = match length {
+ Some(l) if l > 0 => Some((l - 1) as usize),
+ _ => None,
+ };
+ match stream {
+ PhpResource::Stdin => {
+ let stdin = std::io::stdin();
+ let line = fgets_read_line(&mut stdin.lock(), limit).ok()?;
+ if line.is_empty() {
+ return None;
+ }
+ Some(String::from_utf8_lossy(&line).into_owned())
+ }
+ PhpResource::Stdout | PhpResource::Stderr => None,
+ PhpResource::Stream(state) => {
+ let mut state = state.borrow_mut();
+ if state.closed || !state.readable {
+ return None;
+ }
+ let line = fgets_read_line(state.backing.as_rws(), limit).ok()?;
+ if line.is_empty() {
+ state.eof = true;
+ return None;
+ }
+ Some(String::from_utf8_lossy(&line).into_owned())
+ }
+ }
}
-pub fn fgetc(_resource: &PhpResource) -> Option<String> {
- // TODO(phase-d): PhpResource models stdio/file write sinks (see fwrite_resource) but not
- // buffered reads with a tracked position; fgetc needs a readable, seekable stream wrapper.
- todo!()
+// Reads one byte at a time up to and including the next newline, stopping early at `limit` bytes.
+fn fgets_read_line<R: std::io::Read + ?Sized>(
+ r: &mut R,
+ limit: Option<usize>,
+) -> std::io::Result<Vec<u8>> {
+ let mut line = Vec::new();
+ let mut byte = [0u8; 1];
+ loop {
+ if let Some(max) = limit {
+ if line.len() >= max {
+ break;
+ }
+ }
+ let n = r.read(&mut byte)?;
+ if n == 0 {
+ break;
+ }
+ line.push(byte[0]);
+ if byte[0] == b'\n' {
+ break;
+ }
+ }
+ Ok(line)
}
-pub fn ftell(_resource: &PhpResource) -> i64 {
- // TODO(phase-d): PhpResource does not track a stream position; see fgetc.
- todo!()
+/// PHP `fgetc()`: reads a single byte, or `None` at end-of-stream.
+/// TODO(phase-e): byte-string semantics — should return Vec<u8>.
+pub fn fgetc(stream: &PhpResource) -> Option<String> {
+ use std::io::Read;
+ let mut byte = [0u8; 1];
+ match stream {
+ PhpResource::Stdin => {
+ let n = std::io::stdin().read(&mut byte).ok()?;
+ if n == 0 {
+ return None;
+ }
+ Some(String::from_utf8_lossy(&byte).into_owned())
+ }
+ PhpResource::Stdout | PhpResource::Stderr => None,
+ PhpResource::Stream(state) => {
+ let mut state = state.borrow_mut();
+ if state.closed || !state.readable {
+ return None;
+ }
+ let n = state.backing.as_rws().read(&mut byte).ok()?;
+ if n == 0 {
+ state.eof = true;
+ return None;
+ }
+ Some(String::from_utf8_lossy(&byte).into_owned())
+ }
+ }
}
-pub fn fseek(_stream: PhpMixed, _offset: i64) -> i64 {
- // TODO(phase-d): see fopen; no PhpMixed stream representation exists.
- todo!()
+/// PHP `ftell()`: the current position, or `None` for `false`-on-failure.
+pub fn ftell(stream: &PhpResource) -> Option<i64> {
+ use std::io::Seek;
+ match stream {
+ PhpResource::Stdin | PhpResource::Stdout | PhpResource::Stderr => None,
+ PhpResource::Stream(state) => {
+ let mut state = state.borrow_mut();
+ if state.closed {
+ return None;
+ }
+ state
+ .backing
+ .as_rws()
+ .stream_position()
+ .ok()
+ .map(|p| p as i64)
+ }
+ }
}
-pub fn rewind(_stream: PhpMixed) -> bool {
- // TODO(phase-d): see fopen; no PhpMixed stream representation exists.
- todo!()
+/// PHP `fseek()`. Returns 0 on success, -1 on failure.
+pub fn fseek(stream: &PhpResource, offset: i64, whence: i64) -> i64 {
+ use std::io::Seek;
+ let from = match whence {
+ SEEK_CUR => std::io::SeekFrom::Current(offset),
+ SEEK_END => std::io::SeekFrom::End(offset),
+ _ => std::io::SeekFrom::Start(offset.max(0) as u64),
+ };
+ match stream {
+ PhpResource::Stdin | PhpResource::Stdout | PhpResource::Stderr => -1,
+ PhpResource::Stream(state) => {
+ let mut state = state.borrow_mut();
+ if state.closed {
+ return -1;
+ }
+ match state.backing.as_rws().seek(from) {
+ Ok(_) => {
+ state.eof = false;
+ 0
+ }
+ Err(_) => -1,
+ }
+ }
+ }
}
-pub fn fstat(_stream: PhpResource) -> PhpMixed {
- // TODO(phase-d): PhpResource::File holds a File, but the stdio variants have no fd to stat; a
- // faithful fstat needs a uniform stream handle.
- todo!()
+/// PHP `rewind()`: seek to the start.
+pub fn rewind(stream: &PhpResource) -> bool {
+ fseek(stream, 0, SEEK_SET) == 0
+}
+
+/// PHP `fstat()`: the stat array of an open stream, or `None` for `false`-on-failure.
+pub fn fstat(stream: &PhpResource) -> Option<IndexMap<String, PhpMixed>> {
+ match stream {
+ // TODO(phase-d): the stdio streams expose no fd to stat without a syscall crate; report
+ // failure rather than fabricate fields.
+ PhpResource::Stdin | PhpResource::Stdout | PhpResource::Stderr => None,
+ PhpResource::Stream(state) => {
+ let mut state = state.borrow_mut();
+ if state.closed {
+ return None;
+ }
+ let (size, file_meta) = match &mut state.backing {
+ StreamBacking::File(f) => {
+ let m = f.metadata().ok()?;
+ (m.len(), Some(m))
+ }
+ StreamBacking::Memory(c) => (c.get_ref().len() as u64, None),
+ };
+ Some(build_stat_map(size, file_meta.as_ref()))
+ }
+ }
+}
+
+// Builds the 13-field PHP stat array (indexed 0..12 and by name). For in-memory streams only
+// `size` is meaningful; the rest are reported as 0, matching PHP fstat on php://temp.
+fn build_stat_map(size: u64, file_meta: Option<&std::fs::Metadata>) -> IndexMap<String, PhpMixed> {
+ use std::os::unix::fs::MetadataExt;
+ let fields: [(&str, i64); 13] = match file_meta {
+ Some(m) => [
+ ("dev", m.dev() as i64),
+ ("ino", m.ino() as i64),
+ ("mode", m.mode() as i64),
+ ("nlink", m.nlink() as i64),
+ ("uid", m.uid() as i64),
+ ("gid", m.gid() as i64),
+ ("rdev", m.rdev() as i64),
+ ("size", m.size() as i64),
+ ("atime", m.atime()),
+ ("mtime", m.mtime()),
+ ("ctime", m.ctime()),
+ ("blksize", m.blksize() as i64),
+ ("blocks", m.blocks() as i64),
+ ],
+ None => [
+ ("dev", 0),
+ ("ino", 0),
+ ("mode", 0),
+ ("nlink", 0),
+ ("uid", 0),
+ ("gid", 0),
+ ("rdev", 0),
+ ("size", size as i64),
+ ("atime", 0),
+ ("mtime", 0),
+ ("ctime", 0),
+ ("blksize", 0),
+ ("blocks", 0),
+ ],
+ };
+ let mut map = IndexMap::new();
+ for (i, (_, v)) in fields.iter().enumerate() {
+ map.insert(i.to_string(), PhpMixed::Int(*v));
+ }
+ for (name, v) in &fields {
+ map.insert(name.to_string(), PhpMixed::Int(*v));
+ }
+ map
+}
+
+/// PHP `fflush()`.
+pub fn fflush(stream: &PhpResource) -> bool {
+ use std::io::Write;
+ match stream {
+ PhpResource::Stdin => true,
+ PhpResource::Stdout => std::io::stdout().flush().is_ok(),
+ PhpResource::Stderr => std::io::stderr().flush().is_ok(),
+ PhpResource::Stream(state) => {
+ let mut state = state.borrow_mut();
+ if state.closed {
+ return false;
+ }
+ state.backing.as_rws().flush().is_ok()
+ }
+ }
}
pub fn lstat(_filename: &str) -> Option<IndexMap<String, PhpMixed>> {
@@ -322,18 +624,6 @@ pub fn lstat(_filename: &str) -> Option<IndexMap<String, PhpMixed>> {
Some(map)
}
-/// PHP `ftell()` over a PhpMixed stream resource. (`ftell` itself is already defined for the
-/// `PhpResource`-typed stream API used elsewhere.)
-pub fn ftell_stream(_stream: &PhpMixed) -> i64 {
- // TODO(phase-d): see fopen; no PhpMixed stream representation exists.
- todo!()
-}
-
-pub fn fseek3(_stream: PhpMixed, _offset: i64, _whence: i64) -> i64 {
- // TODO(phase-d): see fopen; no PhpMixed stream representation exists.
- todo!()
-}
-
pub fn touch(_path: &str) -> bool {
// TODO(phase-d): for an existing file PHP also bumps its mtime/atime to now; std exposes no
// portable utime, so only the create-if-absent case is handled here.
@@ -346,36 +636,11 @@ pub fn touch(_path: &str) -> bool {
}
pub fn fflush_resource(resource: &PhpResource) {
- use std::io::Write;
- match resource {
- PhpResource::Stdin => {}
- PhpResource::Stdout => {
- let _ = std::io::stdout().flush();
- }
- PhpResource::Stderr => {
- let _ = std::io::stderr().flush();
- }
- PhpResource::File(file) => {
- let _ = file.borrow_mut().flush();
- }
- }
+ fflush(resource);
}
pub fn fwrite_resource(resource: &PhpResource, data: &str) {
- use std::io::Write;
- let bytes = data.as_bytes();
- match resource {
- PhpResource::Stdin => {}
- PhpResource::Stdout => {
- let _ = std::io::stdout().write_all(bytes);
- }
- PhpResource::Stderr => {
- let _ = std::io::stderr().write_all(bytes);
- }
- PhpResource::File(file) => {
- let _ = file.borrow_mut().write_all(bytes);
- }
- }
+ fwrite(resource, data, None);
}
pub fn touch2(_path: &str, _mtime: i64) -> bool {
@@ -621,9 +886,26 @@ pub fn copy(_source: &str, _dest: &str) -> bool {
std::fs::copy(_source, _dest).is_ok()
}
-pub fn ftruncate(_stream: &PhpMixed, _size: i64) -> bool {
- // TODO(phase-d): see fopen; no PhpMixed stream representation exists to truncate.
- todo!()
+pub fn ftruncate(stream: &PhpResource, size: i64) -> bool {
+ match stream {
+ PhpResource::Stdin | PhpResource::Stdout | PhpResource::Stderr => false,
+ PhpResource::Stream(state) => {
+ let mut state = state.borrow_mut();
+ if state.closed || !state.writable {
+ return false;
+ }
+ let size = size.max(0) as u64;
+ match &mut state.backing {
+ StreamBacking::File(f) => f.set_len(size).is_ok(),
+ StreamBacking::Memory(c) => {
+ // Grow or shrink the buffer; PHP ftruncate leaves the position unchanged.
+ let buf = c.get_mut();
+ buf.resize(size as usize, 0);
+ true
+ }
+ }
+ }
+ }
}
pub fn symlink(_target: &str, _link: &str) -> bool {
@@ -657,10 +939,20 @@ pub fn tempnam(_dir: &str, _prefix: &str) -> Option<String> {
None
}
-pub fn opendir(_path: &str) -> Option<PhpMixed> {
- // TODO(phase-d): opendir returns a directory-handle resource consumed by readdir/closedir, but
- // PhpMixed has no resource variant to carry it (see fopen).
- todo!()
+// A directory-handle resource. This is a distinct resource kind from the byte streams modeled by
+// PhpResource; readdir/closedir have no callers yet, so it only records the opened path.
+// TODO(phase-d): give it real readdir/closedir behavior (cursor over the entries) when needed.
+#[derive(Debug)]
+pub struct PhpDirHandle {
+ pub path: std::path::PathBuf,
+}
+
+pub fn opendir(path: &str) -> Option<PhpDirHandle> {
+ // opendir succeeds iff the path is a readable directory.
+ std::fs::read_dir(path).ok()?;
+ Some(PhpDirHandle {
+ path: std::path::PathBuf::from(path),
+ })
}
pub fn pathinfo(path: PhpMixed, option: i64) -> PhpMixed {
@@ -985,3 +1277,49 @@ fn glob_split_top_commas(inner: &str) -> Vec<String> {
parts.push(inner[start..].to_string());
parts
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn memory_stream_round_trips() {
+ let stream = fopen("php://temp", "w+").unwrap();
+
+ assert_eq!(fwrite(&stream, "hello\n", None), Some(6));
+ assert_eq!(fwrite(&stream, "world", None), Some(5));
+ assert_eq!(ftell(&stream), Some(11));
+
+ // Truncation past the end leaves the position untouched and reports the new size.
+ assert!(rewind(&stream));
+ assert_eq!(ftell(&stream), Some(0));
+ assert!(!feof(&stream));
+
+ // fgets reads up to and including the newline.
+ assert_eq!(fgets(&stream, None).as_deref(), Some("hello\n"));
+ // fread reads the requested number of bytes.
+ assert_eq!(fread(&stream, 3).as_deref(), Some("wor"));
+ assert_eq!(fgetc(&stream).as_deref(), Some("l"));
+ assert_eq!(fread(&stream, 10).as_deref(), Some("d"));
+ // A read past the end sets eof.
+ assert_eq!(fread(&stream, 10).as_deref(), Some(""));
+ assert!(feof(&stream));
+
+ // fstat reports the buffer size.
+ let stat = fstat(&stream).unwrap();
+ assert_eq!(stat.get("size"), Some(&PhpMixed::Int(11)));
+
+ // seek + truncate.
+ assert_eq!(fseek(&stream, 5, SEEK_SET), 0);
+ assert!(!feof(&stream)); // seek clears eof
+ assert!(ftruncate(&stream, 5));
+ assert_eq!(fstat(&stream).unwrap().get("size"), Some(&PhpMixed::Int(5)));
+
+ assert!(fclose(&stream));
+ }
+
+ #[test]
+ fn fopen_missing_file_is_err() {
+ assert!(fopen("/nonexistent/shirabe/test/path", "r").is_err());
+ }
+}
diff --git a/crates/shirabe-php-shim/src/lib.rs b/crates/shirabe-php-shim/src/lib.rs
index 10e89fe..267c3b7 100644
--- a/crates/shirabe-php-shim/src/lib.rs
+++ b/crates/shirabe-php-shim/src/lib.rs
@@ -345,5 +345,54 @@ pub enum PhpResource {
Stdin,
Stdout,
Stderr,
- File(std::rc::Rc<std::cell::RefCell<std::fs::File>>),
+ Stream(std::rc::Rc<std::cell::RefCell<StreamState>>),
+}
+
+/// Combined capability of every seekable byte stream backing. Both `std::fs::File`
+/// and `std::io::Cursor<Vec<u8>>` satisfy it, so a stream can be driven uniformly.
+pub trait ReadWriteSeek: std::io::Read + std::io::Write + std::io::Seek {}
+impl<T: std::io::Read + std::io::Write + std::io::Seek> ReadWriteSeek for T {}
+
+#[derive(Debug)]
+pub enum StreamBacking {
+ /// A real file on disk (also `/dev/null`); the OS tracks the position.
+ File(std::fs::File),
+ /// `php://memory` and `php://temp` — an in-memory growable buffer.
+ /// TODO(phase-d): `php://temp/maxmemory:N` spills to a temp file past N bytes;
+ /// the threshold is ignored here and everything stays in memory.
+ Memory(std::io::Cursor<Vec<u8>>),
+}
+
+impl StreamBacking {
+ pub(crate) fn as_rws(&mut self) -> &mut dyn ReadWriteSeek {
+ match self {
+ StreamBacking::File(f) => f,
+ StreamBacking::Memory(c) => c,
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct StreamState {
+ pub(crate) backing: StreamBacking,
+ /// Whether the mode opened the stream for reading.
+ pub(crate) readable: bool,
+ /// Whether the mode opened the stream for writing.
+ pub(crate) writable: bool,
+ /// Set once a read attempt sees end-of-stream, mirroring PHP's `feof()` which
+ /// only reports true after a read has hit the end; cleared by a seek.
+ pub(crate) eof: bool,
+ pub(crate) closed: bool,
+}
+
+impl StreamState {
+ pub(crate) fn new(backing: StreamBacking, readable: bool, writable: bool) -> PhpResource {
+ PhpResource::Stream(std::rc::Rc::new(std::cell::RefCell::new(StreamState {
+ backing,
+ readable,
+ writable,
+ eof: false,
+ closed: false,
+ })))
+ }
}
diff --git a/crates/shirabe-php-shim/src/process.rs b/crates/shirabe-php-shim/src/process.rs
index 8551d27..b47c579 100644
--- a/crates/shirabe-php-shim/src/process.rs
+++ b/crates/shirabe-php-shim/src/process.rs
@@ -210,8 +210,8 @@ pub fn posix_isatty(stream: PhpResource) -> bool {
PhpResource::Stdin => std::io::stdin().is_terminal(),
PhpResource::Stdout => std::io::stdout().is_terminal(),
PhpResource::Stderr => std::io::stderr().is_terminal(),
- // A regular file is never a tty.
- PhpResource::File(_) => false,
+ // A regular file or in-memory stream is never a tty.
+ PhpResource::Stream(_) => false,
}
}
diff --git a/crates/shirabe-php-shim/src/stream.rs b/crates/shirabe-php-shim/src/stream.rs
index 0747cc1..44ba0a4 100644
--- a/crates/shirabe-php-shim/src/stream.rs
+++ b/crates/shirabe-php-shim/src/stream.rs
@@ -1,14 +1,15 @@
-use crate::{PhpMixed, PhpResource};
+use crate::{PhpMixed, PhpResource, StreamBacking};
use indexmap::IndexMap;
pub const STREAM_NOTIFY_FAILURE: i64 = 9;
pub const STREAM_NOTIFY_FILE_SIZE_IS: i64 = 5;
pub const STREAM_NOTIFY_PROGRESS: i64 = 7;
-pub const STDERR: i64 = 2;
-
-pub fn stream_get_contents(_stream: PhpMixed) -> Option<String> {
- todo!()
+/// PHP `stream_get_contents()`: read the remaining bytes from the stream's current position.
+/// TODO(phase-e): byte-string semantics — should return Vec<u8>; from_utf8_lossy can corrupt
+/// binary reads.
+pub fn stream_get_contents(stream: &PhpResource) -> Option<String> {
+ stream_read_remaining(stream, None)
}
pub fn stream_resolve_include_path(filename: &str) -> Option<String> {
@@ -16,9 +17,47 @@ pub fn stream_resolve_include_path(filename: &str) -> Option<String> {
todo!()
}
-pub fn stream_get_contents_with_max(stream: PhpMixed, max_length: Option<i64>) -> Option<String> {
- let _ = (stream, max_length);
- todo!()
+/// PHP `stream_get_contents()` with an explicit max length.
+pub fn stream_get_contents_with_max(
+ stream: &PhpResource,
+ max_length: Option<i64>,
+) -> Option<String> {
+ stream_read_remaining(stream, max_length)
+}
+
+// Reads from the stream's current position: all remaining bytes, or up to `max_length` when given
+// (a negative max means "until end").
+fn stream_read_remaining(stream: &PhpResource, max_length: Option<i64>) -> Option<String> {
+ use std::io::Read;
+ match stream {
+ PhpResource::Stdin => {
+ let mut buf = Vec::new();
+ match max_length {
+ Some(l) if l >= 0 => {
+ let mut limited = std::io::stdin().take(l as u64);
+ limited.read_to_end(&mut buf).ok()?;
+ }
+ _ => {
+ std::io::stdin().read_to_end(&mut buf).ok()?;
+ }
+ }
+ Some(String::from_utf8_lossy(&buf).into_owned())
+ }
+ PhpResource::Stdout | PhpResource::Stderr => None,
+ PhpResource::Stream(state) => {
+ let mut state = state.borrow_mut();
+ if state.closed || !state.readable {
+ return None;
+ }
+ let mut buf = Vec::new();
+ let res = match max_length {
+ Some(l) if l >= 0 => state.backing.as_rws().take(l as u64).read_to_end(&mut buf),
+ _ => state.backing.as_rws().read_to_end(&mut buf),
+ };
+ res.ok()?;
+ Some(String::from_utf8_lossy(&buf).into_owned())
+ }
+ }
}
pub fn stream_context_create(
@@ -36,8 +75,43 @@ pub fn stream_get_wrappers() -> Vec<String> {
todo!()
}
-pub fn stream_copy_to_stream(_source: PhpMixed, _dest: PhpMixed) -> Option<i64> {
- todo!()
+/// PHP `stream_copy_to_stream()`: copy the remaining bytes of `source` into `dest`, returning the
+/// number of bytes copied (or `None` for `false`-on-failure).
+pub fn stream_copy_to_stream(source: &PhpResource, dest: &PhpResource) -> Option<i64> {
+ use std::io::{Read, Write};
+ let mut buf = Vec::new();
+ match source {
+ PhpResource::Stdin => {
+ std::io::stdin().read_to_end(&mut buf).ok()?;
+ }
+ PhpResource::Stdout | PhpResource::Stderr => return None,
+ PhpResource::Stream(state) => {
+ let mut state = state.borrow_mut();
+ if state.closed || !state.readable {
+ return None;
+ }
+ state.backing.as_rws().read_to_end(&mut buf).ok()?;
+ }
+ }
+ match dest {
+ PhpResource::Stdin => None,
+ PhpResource::Stdout => std::io::stdout()
+ .write_all(&buf)
+ .ok()
+ .map(|_| buf.len() as i64),
+ PhpResource::Stderr => std::io::stderr()
+ .write_all(&buf)
+ .ok()
+ .map(|_| buf.len() as i64),
+ PhpResource::Stream(state) => {
+ let mut state = state.borrow_mut();
+ if state.closed || !state.writable {
+ return None;
+ }
+ state.backing.as_rws().write_all(&buf).ok()?;
+ Some(buf.len() as i64)
+ }
+ }
}
pub fn stream_isatty_resource(resource: &PhpResource) -> bool {
@@ -46,7 +120,7 @@ pub fn stream_isatty_resource(resource: &PhpResource) -> bool {
PhpResource::Stdin => std::io::stdin().is_terminal(),
PhpResource::Stdout => std::io::stdout().is_terminal(),
PhpResource::Stderr => std::io::stderr().is_terminal(),
- PhpResource::File(_) => false,
+ PhpResource::Stream(_) => false,
}
}
@@ -68,8 +142,18 @@ pub fn stream_select(
todo!()
}
-pub fn stream_get_contents3(_stream: PhpMixed, _max_length: i64, _offset: i64) -> Option<String> {
- todo!()
+/// PHP `stream_get_contents($stream, $maxlength, $offset)`. A non-negative `offset` seeks there
+/// first; `max_length < 0` reads until end.
+pub fn stream_get_contents3(stream: &PhpResource, max_length: i64, offset: i64) -> Option<String> {
+ if offset >= 0 {
+ crate::fs::fseek(stream, offset, crate::fs::SEEK_SET);
+ }
+ let max = if max_length < 0 {
+ None
+ } else {
+ Some(max_length)
+ };
+ stream_read_remaining(stream, max)
}
pub fn is_resource_value(_resource: &PhpResource) -> bool {
@@ -80,31 +164,11 @@ pub fn get_resource_type(_resource: &PhpResource) -> String {
"stream".to_string()
}
+/// Convenience wrapper over `fopen` for callers that open never-failing `php://` stdio streams and
+/// want an infallible `PhpResource`. Panics on failure, matching the previous behavior.
pub fn php_fopen_resource(path: &str, mode: &str) -> PhpResource {
- match path {
- "php://output" | "php://stdout" => return PhpResource::Stdout,
- "php://stderr" => return PhpResource::Stderr,
- "php://stdin" | "php://input" => return PhpResource::Stdin,
- _ => {}
- }
- // Strip the binary/text flags PHP accepts as part of the mode.
- let base_mode: String = mode.chars().filter(|c| *c != 'b' && *c != 't').collect();
- let mut options = std::fs::OpenOptions::new();
- match base_mode.as_str() {
- "r" => options.read(true),
- "r+" => options.read(true).write(true),
- "w" => options.write(true).create(true).truncate(true),
- "w+" => options.read(true).write(true).create(true).truncate(true),
- "a" => options.append(true).create(true),
- "a+" => options.read(true).append(true).create(true),
- "x" => options.write(true).create_new(true),
- "x+" => options.read(true).write(true).create_new(true),
- _ => options.read(true),
- };
- let file = options
- .open(path)
- .unwrap_or_else(|e| panic!("php_fopen_resource failed to open {path:?}: {e}"));
- PhpResource::File(std::rc::Rc::new(std::cell::RefCell::new(file)))
+ crate::fs::fopen(path, mode)
+ .unwrap_or_else(|e| panic!("php_fopen_resource failed to open {path:?}: {e}"))
}
pub fn php_stdout_resource() -> PhpResource {
diff --git a/crates/shirabe-php-shim/src/zip.rs b/crates/shirabe-php-shim/src/zip.rs
index 1258c3d..5ef99bb 100644
--- a/crates/shirabe-php-shim/src/zip.rs
+++ b/crates/shirabe-php-shim/src/zip.rs
@@ -49,7 +49,7 @@ impl ZipArchive {
todo!()
}
- pub fn get_stream(&self, _name: &str) -> Option<PhpMixed> {
+ pub fn get_stream(&self, _name: &str) -> Option<crate::PhpResource> {
todo!()
}
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!()
}