aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-shim/src/stream.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-22 23:41:12 +0900
committernsfisis <nsfisis@gmail.com>2026-06-22 23:41:12 +0900
commitb291e714bc739262140323e08fe2fb9e91e00ee7 (patch)
treec95f742064b9000e72b902f88e8905b4017d5dbc /crates/shirabe-php-shim/src/stream.rs
parent99ef82a9807578c1e5749156a027949efaba75c4 (diff)
downloadphp-shirabe-b291e714bc739262140323e08fe2fb9e91e00ee7.tar.gz
php-shirabe-b291e714bc739262140323e08fe2fb9e91e00ee7.tar.zst
php-shirabe-b291e714bc739262140323e08fe2fb9e91e00ee7.zip
feat(php-shim): implement fopen-family stream API on PhpResource
Redesign PhpResource into a real stream handle (File/Memory backing with tracked position, eof, closed state) and unify the whole fopen family (fopen/fwrite/fread/fgets/fgetc/feof/fclose/ftell/fseek/rewind/fstat/ ftruncate/fflush and stream_get_contents/stream_copy_to_stream) on &PhpResource, replacing the split PhpMixed/PhpResource APIs and their todo!() stubs. fopen now returns Result; read functions stay String for now (TODO(phase-e) to move to byte strings). Propagate the signatures through callers: Process stdout/stderr, Cursor input, curl header/body handles (extracted into typed maps keyed by job id), Filesystem copy/safe_copy/files_are_equal, BufferIO, error_handler, platform, perforce, zip. The proc_open pipe paths cannot carry a PhpResource in a PhpMixed list, so they are left as todo!() with notes.
Diffstat (limited to 'crates/shirabe-php-shim/src/stream.rs')
-rw-r--r--crates/shirabe-php-shim/src/stream.rs138
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 {