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/shirabe-php-shim/src/stream.rs | |
| 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/shirabe-php-shim/src/stream.rs')
| -rw-r--r-- | crates/shirabe-php-shim/src/stream.rs | 138 |
1 files changed, 101 insertions, 37 deletions
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 { |
