• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

extphprs / ext-php-rs / 26565713723

28 May 2026 09:11AM UTC coverage: 66.146% (-0.06%) from 66.207%
26565713723

Pull #743

github

web-flow
Merge 7dbb5b991 into cd7df3e47
Pull Request #743: feat: Support using ZendStr as ZendHashTable keys

36 of 68 new or added lines in 5 files covered. (52.94%)

1 existing line in 1 file now uncovered.

8685 of 13130 relevant lines covered (66.15%)

33.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

49.08
/src/types/string/mod.rs
1
//! Represents a string in the PHP world. Similar to a C string, but is
2
//! reference counted and contains the length of the string.
3

4
#[cfg(feature = "smartstring")]
5
mod smartstring_impl;
6

7
use std::{
8
    borrow::Cow,
9
    cmp::Ordering,
10
    convert::TryFrom,
11
    ffi::{CStr, CString},
12
    fmt::Debug,
13
    hash::{Hash, Hasher},
14
    ptr, slice,
15
};
16

17
use parking_lot::{const_mutex, Mutex};
18

19
use crate::{
20
    boxed::{ZBox, ZBoxable},
21
    convert::{FromZval, IntoZval},
22
    error::{Error, Result},
23
    ffi::{
24
        ext_php_rs_is_known_valid_utf8, ext_php_rs_set_known_valid_utf8,
25
        ext_php_rs_zend_string_init, ext_php_rs_zend_string_release, zend_string,
26
        zend_string_init_interned,
27
    },
28
    flags::DataType,
29
    types::Zval,
30
};
31

32
/// A borrowed Zend string.
33
///
34
/// Although this object does implement [`Sized`], it is in fact not sized. As C
35
/// cannot represent unsized types, an array of size 1 is used at the end of the
36
/// type to represent the contents of the string, therefore this type is
37
/// actually unsized. All constructors return [`ZBox<ZendStr>`], the owned
38
/// variant.
39
///
40
/// Once the `ptr_metadata` feature lands in stable rust, this type can
41
/// potentially be changed to a DST using slices and metadata. See the tracking issue here: <https://github.com/rust-lang/rust/issues/81513>
42
pub type ZendStr = zend_string;
43

44
// Adding to the Zend interned string hashtable is not atomic and can be
45
// contested when PHP is compiled with ZTS, so an empty mutex is used to ensure
46
// no collisions occur on the Rust side. Not much we can do about collisions
47
// on the PHP side, but some safety is better than none.
48
static INTERNED_LOCK: Mutex<()> = const_mutex(());
49

50
// Clippy complains about there being no `is_empty` function when implementing
51
// on the alias `ZendStr` :( <https://github.com/rust-lang/rust-clippy/issues/7702>
52
#[allow(clippy::len_without_is_empty)]
53
impl ZendStr {
54
    /// Creates a new Zend string from a slice of bytes.
55
    ///
56
    /// # Parameters
57
    ///
58
    /// * `str` - String content.
59
    /// * `persistent` - Whether the string should persist through the request
60
    ///   boundary.
61
    ///
62
    /// # Panics
63
    ///
64
    /// Panics if the function was unable to allocate memory for the Zend
65
    /// string.
66
    ///
67
    /// # Safety
68
    ///
69
    /// When passing `persistent` as `false`, the caller must ensure that the
70
    /// object does not attempt to live after the request finishes. When a
71
    /// request starts and finishes in PHP, the Zend heap is deallocated and a
72
    /// new one is created, which would leave a dangling pointer in the
73
    /// [`ZBox`].
74
    ///
75
    /// # Example
76
    ///
77
    /// ```no_run
78
    /// use ext_php_rs::types::ZendStr;
79
    ///
80
    /// let s = ZendStr::new("Hello, world!", false);
81
    /// let php = ZendStr::new([80, 72, 80], false);
82
    /// ```
83
    pub fn new(str: impl AsRef<[u8]>, persistent: bool) -> ZBox<Self> {
456✔
84
        let s = str.as_ref();
456✔
85
        unsafe {
86
            let ptr = ext_php_rs_zend_string_init(s.as_ptr().cast(), s.len(), persistent)
456✔
87
                .as_mut()
456✔
88
                .expect("Failed to allocate memory for new Zend string");
456✔
89
            ZBox::from_raw(ptr)
456✔
90
        }
91
    }
456✔
92

93
    /// Creates a new Zend string from a [`CStr`].
94
    ///
95
    /// # Parameters
96
    ///
97
    /// * `str` - String content.
98
    /// * `persistent` - Whether the string should persist through the request
99
    ///   boundary.
100
    ///
101
    /// # Panics
102
    ///
103
    /// Panics if the function was unable to allocate memory for the Zend
104
    /// string.
105
    ///
106
    /// # Safety
107
    ///
108
    /// When passing `persistent` as `false`, the caller must ensure that the
109
    /// object does not attempt to live after the request finishes. When a
110
    /// request starts and finishes in PHP, the Zend heap is deallocated and a
111
    /// new one is created, which would leave a dangling pointer in the
112
    /// [`ZBox`].
113
    ///
114
    /// # Example
115
    ///
116
    /// ```no_run
117
    /// use ext_php_rs::types::ZendStr;
118
    /// use std::ffi::CString;
119
    ///
120
    /// let c_s = CString::new("Hello world!").unwrap();
121
    /// let s = ZendStr::from_c_str(&c_s, false);
122
    /// ```
123
    #[must_use]
124
    pub fn from_c_str(str: &CStr, persistent: bool) -> ZBox<Self> {
×
125
        unsafe {
126
            let ptr =
×
127
                ext_php_rs_zend_string_init(str.as_ptr(), str.to_bytes().len() as _, persistent);
×
128

129
            ZBox::from_raw(
×
130
                ptr.as_mut()
×
131
                    .expect("Failed to allocate memory for new Zend string"),
×
132
            )
133
        }
134
    }
×
135

136
    /// Creates a new interned Zend string from a slice of bytes.
137
    ///
138
    /// An interned string is only ever stored once and is immutable. PHP stores
139
    /// the string in an internal hashtable which stores the interned
140
    /// strings.
141
    ///
142
    /// As Zend hashtables are not thread-safe, a mutex is used to prevent two
143
    /// interned strings from being created at the same time.
144
    ///
145
    /// Interned strings are not used very often. You should almost always use a
146
    /// regular zend string, except in the case that you know you will use a
147
    /// string that PHP will already have interned, such as "PHP".
148
    ///
149
    /// # Parameters
150
    ///
151
    /// * `str` - String content.
152
    /// * `persistent` - Whether the string should persist through the request
153
    ///   boundary.
154
    ///
155
    /// # Panics
156
    ///
157
    /// Panics under the following circumstances:
158
    ///
159
    /// * The function used to create interned strings has not been set.
160
    /// * The function could not allocate enough memory for the Zend string.
161
    ///
162
    /// # Safety
163
    ///
164
    /// When passing `persistent` as `false`, the caller must ensure that the
165
    /// object does not attempt to live after the request finishes. When a
166
    /// request starts and finishes in PHP, the Zend heap is deallocated and a
167
    /// new one is created, which would leave a dangling pointer in the
168
    /// [`ZBox`].
169
    ///
170
    /// # Example
171
    ///
172
    /// ```no_run
173
    /// use ext_php_rs::types::ZendStr;
174
    ///
175
    /// let s = ZendStr::new_interned("PHP", true);
176
    /// ```
177
    pub fn new_interned(str: impl AsRef<[u8]>, persistent: bool) -> ZBox<Self> {
1✔
178
        let _lock = INTERNED_LOCK.lock();
1✔
179
        let s = str.as_ref();
1✔
180
        unsafe {
181
            let init = zend_string_init_interned.expect("`zend_string_init_interned` not ready");
1✔
182
            let ptr = init(s.as_ptr().cast(), s.len() as _, persistent)
1✔
183
                .as_mut()
1✔
184
                .expect("Failed to allocate memory for new Zend string");
1✔
185
            ZBox::from_raw(ptr)
1✔
186
        }
187
    }
1✔
188

189
    /// Creates a new interned Zend string from a [`CStr`].
190
    ///
191
    /// An interned string is only ever stored once and is immutable. PHP stores
192
    /// the string in an internal hashtable which stores the interned
193
    /// strings.
194
    ///
195
    /// As Zend hashtables are not thread-safe, a mutex is used to prevent two
196
    /// interned strings from being created at the same time.
197
    ///
198
    /// Interned strings are not used very often. You should almost always use a
199
    /// regular zend string, except in the case that you know you will use a
200
    /// string that PHP will already have interned, such as "PHP".
201
    ///
202
    /// # Parameters
203
    ///
204
    /// * `str` - String content.
205
    /// * `persistent` - Whether the string should persist through the request
206
    ///   boundary.
207
    ///
208
    /// # Panics
209
    ///
210
    /// Panics under the following circumstances:
211
    ///
212
    /// * The function used to create interned strings has not been set.
213
    /// * The function could not allocate enough memory for the Zend string.
214
    ///
215
    /// # Safety
216
    ///
217
    /// When passing `persistent` as `false`, the caller must ensure that the
218
    /// object does not attempt to live after the request finishes. When a
219
    /// request starts and finishes in PHP, the Zend heap is deallocated and a
220
    /// new one is created, which would leave a dangling pointer in the
221
    /// [`ZBox`].
222
    ///
223
    /// # Example
224
    ///
225
    /// ```no_run
226
    /// use ext_php_rs::types::ZendStr;
227
    /// use std::ffi::CString;
228
    ///
229
    /// let c_s = CString::new("PHP").unwrap();
230
    /// let s = ZendStr::interned_from_c_str(&c_s, true);
231
    /// ```
232
    pub fn interned_from_c_str(str: &CStr, persistent: bool) -> ZBox<Self> {
×
233
        let _lock = INTERNED_LOCK.lock();
×
234

235
        unsafe {
236
            let init = zend_string_init_interned.expect("`zend_string_init_interned` not ready");
×
237
            let ptr = init(str.as_ptr(), str.to_bytes().len() as _, persistent);
×
238

239
            ZBox::from_raw(
×
240
                ptr.as_mut()
×
241
                    .expect("Failed to allocate memory for new Zend string"),
×
242
            )
243
        }
244
    }
×
245

246
    /// Returns the length of the string.
247
    ///
248
    /// # Example
249
    ///
250
    /// ```no_run
251
    /// use ext_php_rs::types::ZendStr;
252
    ///
253
    /// let s = ZendStr::new("hello, world!", false);
254
    /// assert_eq!(s.len(), 13);
255
    /// ```
256
    #[must_use]
257
    pub fn len(&self) -> usize {
319✔
258
        self.len
319✔
259
    }
319✔
260

261
    /// Returns true if the string is empty, false otherwise.
262
    ///
263
    /// # Example
264
    ///
265
    /// ```no_run
266
    /// use ext_php_rs::types::ZendStr;
267
    ///
268
    /// let s = ZendStr::new("hello, world!", false);
269
    /// assert_eq!(s.is_empty(), false);
270
    /// ```
271
    #[must_use]
272
    pub fn is_empty(&self) -> bool {
×
273
        self.len() == 0
×
274
    }
×
275

276
    /// Attempts to return a reference to the underlying bytes inside the Zend
277
    /// string as a [`CStr`].
278
    ///
279
    /// # Errors
280
    ///
281
    /// Returns an [`Error::InvalidCString`] variant if the string contains null
282
    /// bytes.
283
    pub fn as_c_str(&self) -> Result<&CStr> {
×
284
        let bytes_with_null =
×
285
            unsafe { slice::from_raw_parts(self.val.as_ptr().cast(), self.len() + 1) };
×
286
        CStr::from_bytes_with_nul(bytes_with_null).map_err(|_| Error::InvalidCString)
×
287
    }
×
288

289
    /// Attempts to return a reference to the underlying bytes inside the Zend
290
    /// string.
291
    ///
292
    /// # Errors
293
    ///
294
    /// Returns an [`Error::InvalidUtf8`] variant if the [`str`] contains
295
    /// non-UTF-8 characters.
296
    ///
297
    /// # Example
298
    ///
299
    /// ```no_run
300
    /// use ext_php_rs::types::ZendStr;
301
    ///
302
    /// let s = ZendStr::new("hello, world!", false);
303
    /// assert!(s.as_str().is_ok());
304
    /// ```
305
    #[inline]
306
    pub fn as_str(&self) -> Result<&str> {
305✔
307
        if unsafe { ext_php_rs_is_known_valid_utf8(self.as_ptr()) } {
305✔
308
            let str = unsafe { std::str::from_utf8_unchecked(self.as_bytes()) };
36✔
309
            return Ok(str);
36✔
310
        }
269✔
311
        let str = std::str::from_utf8(self.as_bytes()).map_err(|_| Error::InvalidUtf8)?;
269✔
312
        unsafe { ext_php_rs_set_known_valid_utf8(self.as_ptr().cast_mut()) };
269✔
313
        Ok(str)
269✔
314
    }
305✔
315

316
    /// Returns a reference to the underlying bytes inside the Zend string.
317
    #[must_use]
318
    pub fn as_bytes(&self) -> &[u8] {
312✔
319
        unsafe { slice::from_raw_parts(self.val.as_ptr().cast(), self.len()) }
312✔
320
    }
312✔
321

322
    /// Returns a raw pointer to this object
323
    #[must_use]
324
    pub fn as_ptr(&self) -> *const ZendStr {
789✔
325
        ptr::from_ref(self)
789✔
326
    }
789✔
327

328
    /// Returns a mutable pointer to this object
329
    pub fn as_mut_ptr(&mut self) -> *mut ZendStr {
×
330
        ptr::from_mut(self)
×
331
    }
×
332
}
333

334
unsafe impl ZBoxable for ZendStr {
335
    fn free(&mut self) {
214✔
336
        unsafe { ext_php_rs_zend_string_release(self) };
214✔
337
    }
214✔
338
}
339

340
impl Debug for ZendStr {
341
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
342
        self.as_str().fmt(f)
×
343
    }
×
344
}
345

346
impl AsRef<[u8]> for ZendStr {
347
    fn as_ref(&self) -> &[u8] {
×
348
        self.as_bytes()
×
349
    }
×
350
}
351

352
impl<T> PartialEq<T> for ZendStr
353
where
354
    T: AsRef<[u8]>,
355
{
356
    fn eq(&self, other: &T) -> bool {
×
357
        self.as_ref() == other.as_ref()
×
358
    }
×
359
}
360

361
impl Eq for ZendStr {}
362

363
impl PartialOrd for ZendStr {
NEW
364
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
×
NEW
365
        Some(self.cmp(other))
×
NEW
366
    }
×
367
}
368

369
impl Ord for ZendStr {
NEW
370
    fn cmp(&self, other: &Self) -> Ordering {
×
NEW
371
        self.as_ref().cmp(other.as_ref())
×
NEW
372
    }
×
373
}
374

375
impl Hash for ZendStr {
NEW
376
    fn hash<H: Hasher>(&self, state: &mut H) {
×
NEW
377
        state.write_usize(self.len());
×
NEW
378
        state.write(self.as_bytes());
×
NEW
379
    }
×
380
}
381

382
impl ToOwned for ZendStr {
383
    type Owned = ZBox<ZendStr>;
384

385
    fn to_owned(&self) -> Self::Owned {
×
386
        Self::new(self.as_bytes(), false)
×
387
    }
×
388
}
389

390
impl<'a> TryFrom<&'a ZendStr> for &'a CStr {
391
    type Error = Error;
392

393
    fn try_from(value: &'a ZendStr) -> Result<Self> {
×
394
        value.as_c_str()
×
395
    }
×
396
}
397

398
impl<'a> TryFrom<&'a ZendStr> for &'a str {
399
    type Error = Error;
400

401
    fn try_from(value: &'a ZendStr) -> Result<Self> {
×
402
        value.as_str()
×
403
    }
×
404
}
405

406
impl TryFrom<&ZendStr> for String {
407
    type Error = Error;
408

409
    fn try_from(value: &ZendStr) -> Result<Self> {
1✔
410
        value.as_str().map(ToString::to_string)
1✔
411
    }
1✔
412
}
413

414
impl<'a> From<&'a ZendStr> for Cow<'a, ZendStr> {
415
    fn from(value: &'a ZendStr) -> Self {
×
416
        Cow::Borrowed(value)
×
417
    }
×
418
}
419

420
impl From<&CStr> for ZBox<ZendStr> {
421
    fn from(value: &CStr) -> Self {
×
422
        ZendStr::from_c_str(value, false)
×
423
    }
×
424
}
425

426
impl From<CString> for ZBox<ZendStr> {
427
    fn from(value: CString) -> Self {
×
428
        ZendStr::from_c_str(&value, false)
×
429
    }
×
430
}
431

432
impl From<&str> for ZBox<ZendStr> {
433
    fn from(value: &str) -> Self {
×
434
        ZendStr::new(value.as_bytes(), false)
×
435
    }
×
436
}
437

438
impl From<String> for ZBox<ZendStr> {
439
    fn from(value: String) -> Self {
×
440
        ZendStr::new(value.as_str(), false)
×
441
    }
×
442
}
443

444
impl From<ZBox<ZendStr>> for Cow<'_, ZendStr> {
445
    fn from(value: ZBox<ZendStr>) -> Self {
×
446
        Cow::Owned(value)
×
447
    }
×
448
}
449

450
impl From<Cow<'_, ZendStr>> for ZBox<ZendStr> {
451
    fn from(value: Cow<'_, ZendStr>) -> Self {
×
452
        value.into_owned()
×
453
    }
×
454
}
455

456
macro_rules! try_into_zval_str {
457
    ($type: ty) => {
458
        impl TryFrom<$type> for Zval {
459
            type Error = Error;
460

461
            fn try_from(value: $type) -> Result<Self> {
×
462
                let mut zv = Self::new();
×
463
                zv.set_string(&value, false)?;
×
464
                Ok(zv)
×
465
            }
×
466
        }
467

468
        impl IntoZval for $type {
469
            const TYPE: DataType = DataType::String;
470
            const NULLABLE: bool = false;
471

472
            fn set_zval(self, zv: &mut Zval, persistent: bool) -> Result<()> {
201✔
473
                zv.set_string(&self, persistent)
201✔
474
            }
201✔
475
        }
476
    };
477
}
478

479
try_into_zval_str!(String);
480
try_into_zval_str!(&str);
481
try_from_zval!(String, string, String);
482

483
impl<'a> FromZval<'a> for &'a str {
484
    const TYPE: DataType = DataType::String;
485

486
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
487
        zval.str()
×
488
    }
×
489
}
490

491
#[cfg(test)]
492
#[cfg(feature = "embed")]
493
mod tests {
494
    use crate::embed::Embed;
495

496
    #[test]
497
    fn test_string() {
1✔
498
        Embed::run(|| {
1✔
499
            let result = Embed::eval("'foo';");
1✔
500

501
            assert!(result.is_ok());
1✔
502

503
            let zval = result.as_ref().expect("Unreachable");
1✔
504

505
            assert!(zval.is_string());
1✔
506
            assert_eq!(zval.string(), Some("foo".to_string()));
1✔
507
        });
1✔
508
    }
1✔
509

510
    #[test]
511
    fn test_zend_string_init_fast() {
1✔
512
        Embed::run(|| {
1✔
513
            let cases: &[(&[u8], usize, bool)] = &[
1✔
514
                // (input, expected_len, should_be_interned)
1✔
515
                (b"", 0, true),
1✔
516
                (b"a", 1, true),
1✔
517
                (b"x", 1, true),
1✔
518
                (b"\0", 1, true),
1✔
519
                (b"\xff", 1, true),
1✔
520
                (b"hello", 5, false),
1✔
521
                (b"ab", 2, false),
1✔
522
            ];
1✔
523

524
            for &(input, expected_len, interned) in cases {
7✔
525
                let s = crate::types::ZendStr::new(input, false);
7✔
526
                assert_eq!(s.len(), expected_len, "len mismatch for {input:?}");
7✔
527
                assert_eq!(s.as_bytes(), input, "content mismatch for {input:?}");
7✔
528

529
                if interned {
7✔
530
                    let s2 = crate::types::ZendStr::new(input, false);
5✔
531
                    assert!(
5✔
532
                        std::ptr::eq(s.as_ptr(), s2.as_ptr()),
5✔
533
                        "expected interned pointer for {input:?}"
534
                    );
535
                }
2✔
536
            }
537

538
            // All printable ASCII single-chars return interned pointers
539
            for c in b' '..=b'~' {
95✔
540
                let s1 = crate::types::ZendStr::new(&[c][..], false);
95✔
541
                let s2 = crate::types::ZendStr::new(&[c][..], false);
95✔
542
                assert!(
95✔
543
                    std::ptr::eq(s1.as_ptr(), s2.as_ptr()),
95✔
544
                    "expected interned pointer for single char {c:#x}"
545
                );
546
            }
547
        });
1✔
548
    }
1✔
549
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc