Skip to main content

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}