diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-22 23:41:12 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-22 23:41:12 +0900 |
| commit | b291e714bc739262140323e08fe2fb9e91e00ee7 (patch) | |
| tree | c95f742064b9000e72b902f88e8905b4017d5dbc /crates | |
| parent | 99ef82a9807578c1e5749156a027949efaba75c4 (diff) | |
| download | php-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')
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!() } |
