ext_php_rs/zend/streams.rs
1use std::ffi::CString;
2use std::ptr::{self, NonNull};
3
4use crate::{
5 error::Error,
6 ffi::{
7 php_register_url_stream_wrapper, php_register_url_stream_wrapper_volatile, php_stream,
8 php_stream_context, php_stream_locate_url_wrapper, php_stream_wrapper,
9 php_stream_wrapper_ops, php_unregister_url_stream_wrapper,
10 php_unregister_url_stream_wrapper_volatile, zend_string,
11 },
12 types::ZendStr,
13};
14
15/// Wrapper for PHP streams
16pub type StreamWrapper = php_stream_wrapper;
17
18/// Stream opener function
19pub type StreamOpener = unsafe extern "C" fn(
20 *mut StreamWrapper,
21 *const std::ffi::c_char,
22 *const std::ffi::c_char,
23 i32,
24 *mut *mut zend_string,
25 *mut php_stream_context,
26 i32,
27 *const std::ffi::c_char,
28 u32,
29 *const std::ffi::c_char,
30 u32,
31) -> *mut Stream;
32
33impl StreamWrapper {
34 /// Locates the stream wrapper registered for `name`.
35 ///
36 /// # Safety
37 ///
38 /// The returned wrapper is owned by PHP and lives in the per-request stream
39 /// wrapper table, so the caller must not hold `'a` beyond the current
40 /// request. The lifetime is unconstrained by the arguments and must be
41 /// chosen to reflect that.
42 #[inline]
43 #[must_use]
44 pub unsafe fn get<'a>(name: &str) -> Option<&'a Self> {
45 let ptr = Self::locate(name)?;
46 // SAFETY: `locate` returned a non-null wrapper pointer owned by PHP, and
47 // the caller guarantees `'a` does not outlive the current request.
48 Some(unsafe { ptr.as_ref() })
49 }
50
51 /// Locates the stream wrapper registered for `name` for mutation.
52 ///
53 /// # Safety
54 ///
55 /// In addition to the request-scoped validity required by [`Self::get`], the
56 /// caller must ensure no other reference to the same wrapper is live for
57 /// `'a`. The wrapper table is process-wide state shared with PHP and with
58 /// every other extension, so this function cannot check exclusivity.
59 #[inline]
60 #[must_use]
61 pub unsafe fn get_mut<'a>(name: &str) -> Option<&'a mut Self> {
62 let mut ptr = Self::locate(name)?;
63 // SAFETY: `locate` returned a non-null wrapper pointer owned by PHP, and
64 // the caller guarantees both request-scoped validity and exclusivity.
65 Some(unsafe { ptr.as_mut() })
66 }
67
68 /// Looks up the wrapper pointer for `name`.
69 ///
70 /// `name` is copied into a NUL-terminated C string because
71 /// `php_stream_locate_url_wrapper` scans the path until it reaches a
72 /// character outside `[A-Za-z0-9+-.]`, which would read past the end of a
73 /// non-terminated Rust string slice.
74 #[inline]
75 fn locate(name: &str) -> Option<NonNull<Self>> {
76 let name = CString::new(name).ok()?;
77 // SAFETY: `name` is a valid NUL-terminated C string that outlives the
78 // call, and a null `path_for_open` is accepted by PHP.
79 let result = unsafe { php_stream_locate_url_wrapper(name.as_ptr(), ptr::null_mut(), 0) };
80 NonNull::new(result)
81 }
82
83 /// Register stream wrapper for name
84 ///
85 /// # Errors
86 ///
87 /// * `Error::StreamWrapperRegistrationFailure` - If the stream wrapper
88 /// could not be registered
89 /// * `Error::InvalidCString` - If the name contains a NUL byte
90 pub fn register(self, name: &str) -> Result<Self, Error> {
91 let name = std::ffi::CString::new(name)?;
92 // We have to convert it to a static so owned streamwrapper doesn't get dropped.
93 let copy = Box::new(self);
94 let copy = Box::leak(copy);
95 let result = unsafe { php_register_url_stream_wrapper(name.as_ptr(), copy) };
96 if result == 0 {
97 Ok(*copy)
98 } else {
99 Err(Error::StreamWrapperRegistrationFailure)
100 }
101 }
102
103 /// Register volatile stream wrapper for name
104 ///
105 /// # Errors
106 ///
107 /// * `Error::StreamWrapperRegistrationFailure` - If the stream wrapper
108 /// could not be registered
109 pub fn register_volatile(self, name: &str) -> Result<Self, Error> {
110 // We have to convert it to a static so owned streamwrapper doesn't get dropped.
111 let copy = Box::new(self);
112 let copy = Box::leak(copy);
113 let name = ZendStr::new(name, false);
114 let result =
115 unsafe { php_register_url_stream_wrapper_volatile((*name).as_ptr().cast_mut(), copy) };
116 if result == 0 {
117 Ok(*copy)
118 } else {
119 Err(Error::StreamWrapperRegistrationFailure)
120 }
121 }
122
123 /// Unregister stream wrapper by name
124 ///
125 /// # Errors
126 ///
127 /// * `Error::StreamWrapperUnregistrationFailure` - If the stream wrapper
128 /// could not be unregistered
129 /// * `Error::InvalidCString` - If the name contains a NUL byte
130 pub fn unregister(name: &str) -> Result<(), Error> {
131 let name = std::ffi::CString::new(name)?;
132 match unsafe { php_unregister_url_stream_wrapper(name.as_ptr()) } {
133 0 => Ok(()),
134 _ => Err(Error::StreamWrapperUnregistrationFailure),
135 }
136 }
137
138 /// Unregister volatile stream wrapper by name
139 ///
140 /// # Errors
141 ///
142 /// * `Error::StreamWrapperUnregistrationFailure` - If the stream wrapper
143 /// could not be unregistered
144 pub fn unregister_volatile(name: &str) -> Result<(), Error> {
145 let name = ZendStr::new(name, false);
146 match unsafe { php_unregister_url_stream_wrapper_volatile((*name).as_ptr().cast_mut()) } {
147 0 => Ok(()),
148 _ => Err(Error::StreamWrapperUnregistrationFailure),
149 }
150 }
151
152 /// Get the operations the stream wrapper can perform
153 #[must_use]
154 pub fn wops(&self) -> &php_stream_wrapper_ops {
155 unsafe { &*self.wops }
156 }
157
158 /// Get the mutable operations the stream can perform
159 pub fn wops_mut(&mut self) -> &mut php_stream_wrapper_ops {
160 unsafe { &mut *(self.wops.cast_mut()) }
161 }
162}
163
164/// A PHP stream
165pub type Stream = php_stream;
166
167/// Operations that can be performed with a stream wrapper
168pub type StreamWrapperOps = php_stream_wrapper_ops;
169
170#[cfg(feature = "embed")]
171#[cfg(test)]
172mod tests {
173 use super::StreamWrapper;
174 use crate::error::Error;
175
176 #[test]
177 fn unregister_rejects_a_name_with_a_nul_byte() {
178 let err = StreamWrapper::unregister("we\0ird");
179
180 assert!(matches!(err, Err(Error::InvalidCString { position: 2 })));
181 }
182}