Skip to main content

mtl_foundation/
string.rs

1//! String type for Foundation.
2//!
3//! Corresponds to `Foundation/NSString.hpp`.
4//!
5//! # C++ Equivalent
6//!
7//! ```cpp
8//! namespace NS {
9//! _NS_ENUM(NS::UInteger, StringEncoding) {
10//!     ASCIIStringEncoding = 1,
11//!     NEXTSTEPStringEncoding = 2,
12//!     JapaneseEUCStringEncoding = 3,
13//!     UTF8StringEncoding = 4,
14//!     ISOLatin1StringEncoding = 5,
15//!     SymbolStringEncoding = 6,
16//!     NonLossyASCIIStringEncoding = 7,
17//!     ShiftJISStringEncoding = 8,
18//!     ISOLatin2StringEncoding = 9,
19//!     UnicodeStringEncoding = 10,
20//!     WindowsCP1251StringEncoding = 11,
21//!     WindowsCP1252StringEncoding = 12,
22//!     WindowsCP1253StringEncoding = 13,
23//!     WindowsCP1254StringEncoding = 14,
24//!     WindowsCP1250StringEncoding = 15,
25//!     ISO2022JPStringEncoding = 21,
26//!     MacOSRomanStringEncoding = 30,
27//!     UTF16StringEncoding = UnicodeStringEncoding,
28//!     UTF16BigEndianStringEncoding = 0x90000100,
29//!     UTF16LittleEndianStringEncoding = 0x94000100,
30//!     UTF32StringEncoding = 0x8c000100,
31//!     UTF32BigEndianStringEncoding = 0x98000100,
32//!     UTF32LittleEndianStringEncoding = 0x9c000100
33//! };
34//!
35//! _NS_OPTIONS(NS::UInteger, StringCompareOptions) {
36//!     CaseInsensitiveSearch = 1,
37//!     LiteralSearch = 2,
38//!     BackwardsSearch = 4,
39//!     AnchoredSearch = 8,
40//!     NumericSearch = 64,
41//!     DiacriticInsensitiveSearch = 128,
42//!     WidthInsensitiveSearch = 256,
43//!     ForcedOrderingSearch = 512,
44//!     RegularExpressionSearch = 1024
45//! };
46//!
47//! using unichar = unsigned short;
48//!
49//! class String : public Copying<String> {
50//! public:
51//!     static String*   string();
52//!     static String*   string(const String* pString);
53//!     static String*   string(const char* pString, StringEncoding encoding);
54//!     static String*   alloc();
55//!     String*          init();
56//!     String*          init(const String* pString);
57//!     String*          init(const char* pString, StringEncoding encoding);
58//!     String*          init(void* pBytes, UInteger len, StringEncoding encoding, bool freeBuffer);
59//!     unichar          character(UInteger index) const;
60//!     UInteger         length() const;
61//!     const char*      cString(StringEncoding encoding) const;
62//!     const char*      utf8String() const;
63//!     UInteger         maximumLengthOfBytes(StringEncoding encoding) const;
64//!     UInteger         lengthOfBytes(StringEncoding encoding) const;
65//!     bool             isEqualToString(const String* pString) const;
66//!     Range            rangeOfString(const String* pString, StringCompareOptions options) const;
67//!     const char*      fileSystemRepresentation() const;
68//!     String*          stringByAppendingString(const String* pString) const;
69//!     ComparisonResult caseInsensitiveCompare(const String* pString) const;
70//! };
71//! }
72//! ```
73
74use std::ffi::{c_char, c_void};
75use std::ptr::NonNull;
76
77use mtl_sys::{class, msg_send_0, msg_send_1, msg_send_2, msg_send_4, sel};
78
79use crate::objc_runtime::ComparisonResult;
80use crate::object::{Copying, Referencing};
81use crate::range::Range;
82use crate::types::UInteger;
83
84/// Unicode character type.
85///
86/// C++ equivalent: `NS::unichar`
87pub type Unichar = u16;
88
89/// String encoding options.
90///
91/// C++ equivalent: `NS::StringEncoding`
92#[repr(transparent)]
93#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
94pub struct StringEncoding(pub UInteger);
95
96impl StringEncoding {
97    /// ASCII encoding.
98    ///
99    /// C++ equivalent: `NS::ASCIIStringEncoding`
100    pub const ASCII: Self = Self(1);
101
102    /// NEXTSTEP encoding.
103    ///
104    /// C++ equivalent: `NS::NEXTSTEPStringEncoding`
105    pub const NEXTSTEP: Self = Self(2);
106
107    /// Japanese EUC encoding.
108    ///
109    /// C++ equivalent: `NS::JapaneseEUCStringEncoding`
110    pub const JAPANESE_EUC: Self = Self(3);
111
112    /// UTF-8 encoding.
113    ///
114    /// C++ equivalent: `NS::UTF8StringEncoding`
115    pub const UTF8: Self = Self(4);
116
117    /// ISO Latin 1 encoding.
118    ///
119    /// C++ equivalent: `NS::ISOLatin1StringEncoding`
120    pub const ISO_LATIN1: Self = Self(5);
121
122    /// Symbol encoding.
123    ///
124    /// C++ equivalent: `NS::SymbolStringEncoding`
125    pub const SYMBOL: Self = Self(6);
126
127    /// Non-lossy ASCII encoding.
128    ///
129    /// C++ equivalent: `NS::NonLossyASCIIStringEncoding`
130    pub const NON_LOSSY_ASCII: Self = Self(7);
131
132    /// Shift JIS encoding.
133    ///
134    /// C++ equivalent: `NS::ShiftJISStringEncoding`
135    pub const SHIFT_JIS: Self = Self(8);
136
137    /// ISO Latin 2 encoding.
138    ///
139    /// C++ equivalent: `NS::ISOLatin2StringEncoding`
140    pub const ISO_LATIN2: Self = Self(9);
141
142    /// Unicode encoding.
143    ///
144    /// C++ equivalent: `NS::UnicodeStringEncoding`
145    pub const UNICODE: Self = Self(10);
146
147    /// Windows CP1251 encoding.
148    ///
149    /// C++ equivalent: `NS::WindowsCP1251StringEncoding`
150    pub const WINDOWS_CP1251: Self = Self(11);
151
152    /// Windows CP1252 encoding.
153    ///
154    /// C++ equivalent: `NS::WindowsCP1252StringEncoding`
155    pub const WINDOWS_CP1252: Self = Self(12);
156
157    /// Windows CP1253 encoding.
158    ///
159    /// C++ equivalent: `NS::WindowsCP1253StringEncoding`
160    pub const WINDOWS_CP1253: Self = Self(13);
161
162    /// Windows CP1254 encoding.
163    ///
164    /// C++ equivalent: `NS::WindowsCP1254StringEncoding`
165    pub const WINDOWS_CP1254: Self = Self(14);
166
167    /// Windows CP1250 encoding.
168    ///
169    /// C++ equivalent: `NS::WindowsCP1250StringEncoding`
170    pub const WINDOWS_CP1250: Self = Self(15);
171
172    /// ISO 2022 JP encoding.
173    ///
174    /// C++ equivalent: `NS::ISO2022JPStringEncoding`
175    pub const ISO2022JP: Self = Self(21);
176
177    /// Mac OS Roman encoding.
178    ///
179    /// C++ equivalent: `NS::MacOSRomanStringEncoding`
180    pub const MAC_OS_ROMAN: Self = Self(30);
181
182    /// UTF-16 encoding (alias for Unicode).
183    ///
184    /// C++ equivalent: `NS::UTF16StringEncoding`
185    pub const UTF16: Self = Self(10);
186
187    /// UTF-16 big endian encoding.
188    ///
189    /// C++ equivalent: `NS::UTF16BigEndianStringEncoding`
190    pub const UTF16_BIG_ENDIAN: Self = Self(0x90000100);
191
192    /// UTF-16 little endian encoding.
193    ///
194    /// C++ equivalent: `NS::UTF16LittleEndianStringEncoding`
195    pub const UTF16_LITTLE_ENDIAN: Self = Self(0x94000100);
196
197    /// UTF-32 encoding.
198    ///
199    /// C++ equivalent: `NS::UTF32StringEncoding`
200    pub const UTF32: Self = Self(0x8c000100);
201
202    /// UTF-32 big endian encoding.
203    ///
204    /// C++ equivalent: `NS::UTF32BigEndianStringEncoding`
205    pub const UTF32_BIG_ENDIAN: Self = Self(0x98000100);
206
207    /// UTF-32 little endian encoding.
208    ///
209    /// C++ equivalent: `NS::UTF32LittleEndianStringEncoding`
210    pub const UTF32_LITTLE_ENDIAN: Self = Self(0x9c000100);
211
212    /// Returns the raw value.
213    #[inline]
214    pub const fn raw(&self) -> UInteger {
215        self.0
216    }
217
218    /// Creates from a raw value.
219    #[inline]
220    pub const fn from_raw(value: UInteger) -> Self {
221        Self(value)
222    }
223}
224
225/// String comparison options.
226///
227/// C++ equivalent: `NS::StringCompareOptions`
228#[repr(transparent)]
229#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
230pub struct StringCompareOptions(pub UInteger);
231
232impl StringCompareOptions {
233    /// No options.
234    pub const NONE: Self = Self(0);
235
236    /// Case insensitive search.
237    ///
238    /// C++ equivalent: `NS::CaseInsensitiveSearch`
239    pub const CASE_INSENSITIVE: Self = Self(1);
240
241    /// Literal search.
242    ///
243    /// C++ equivalent: `NS::LiteralSearch`
244    pub const LITERAL: Self = Self(2);
245
246    /// Backwards search.
247    ///
248    /// C++ equivalent: `NS::BackwardsSearch`
249    pub const BACKWARDS: Self = Self(4);
250
251    /// Anchored search.
252    ///
253    /// C++ equivalent: `NS::AnchoredSearch`
254    pub const ANCHORED: Self = Self(8);
255
256    /// Numeric search.
257    ///
258    /// C++ equivalent: `NS::NumericSearch`
259    pub const NUMERIC: Self = Self(64);
260
261    /// Diacritic insensitive search.
262    ///
263    /// C++ equivalent: `NS::DiacriticInsensitiveSearch`
264    pub const DIACRITIC_INSENSITIVE: Self = Self(128);
265
266    /// Width insensitive search.
267    ///
268    /// C++ equivalent: `NS::WidthInsensitiveSearch`
269    pub const WIDTH_INSENSITIVE: Self = Self(256);
270
271    /// Forced ordering search.
272    ///
273    /// C++ equivalent: `NS::ForcedOrderingSearch`
274    pub const FORCED_ORDERING: Self = Self(512);
275
276    /// Regular expression search.
277    ///
278    /// C++ equivalent: `NS::RegularExpressionSearch`
279    pub const REGULAR_EXPRESSION: Self = Self(1024);
280
281    /// Returns the raw bits.
282    #[inline]
283    pub const fn bits(&self) -> UInteger {
284        self.0
285    }
286
287    /// Creates from raw bits.
288    #[inline]
289    pub const fn from_bits(bits: UInteger) -> Self {
290        Self(bits)
291    }
292
293    /// Check if empty.
294    #[inline]
295    pub const fn is_empty(&self) -> bool {
296        self.0 == 0
297    }
298
299    /// Check if contains all flags in other.
300    #[inline]
301    pub const fn contains(&self, other: Self) -> bool {
302        (self.0 & other.0) == other.0
303    }
304
305    /// Insert flags.
306    #[inline]
307    pub fn insert(&mut self, other: Self) {
308        self.0 |= other.0;
309    }
310
311    /// Remove flags.
312    #[inline]
313    pub fn remove(&mut self, other: Self) {
314        self.0 &= !other.0;
315    }
316}
317
318impl std::ops::BitOr for StringCompareOptions {
319    type Output = Self;
320    #[inline]
321    fn bitor(self, rhs: Self) -> Self {
322        Self(self.0 | rhs.0)
323    }
324}
325
326impl std::ops::BitAnd for StringCompareOptions {
327    type Output = Self;
328    #[inline]
329    fn bitand(self, rhs: Self) -> Self {
330        Self(self.0 & rhs.0)
331    }
332}
333
334impl std::ops::BitXor for StringCompareOptions {
335    type Output = Self;
336    #[inline]
337    fn bitxor(self, rhs: Self) -> Self {
338        Self(self.0 ^ rhs.0)
339    }
340}
341
342impl std::ops::Not for StringCompareOptions {
343    type Output = Self;
344    #[inline]
345    fn not(self) -> Self {
346        Self(!self.0)
347    }
348}
349
350impl std::ops::BitOrAssign for StringCompareOptions {
351    #[inline]
352    fn bitor_assign(&mut self, rhs: Self) {
353        self.0 |= rhs.0;
354    }
355}
356
357impl std::ops::BitAndAssign for StringCompareOptions {
358    #[inline]
359    fn bitand_assign(&mut self, rhs: Self) {
360        self.0 &= rhs.0;
361    }
362}
363
364/// An Objective-C string object.
365///
366/// C++ equivalent: `NS::String`
367#[repr(transparent)]
368#[derive(Clone)]
369pub struct String(NonNull<c_void>);
370
371impl String {
372    /// Create an empty string.
373    ///
374    /// C++ equivalent: `static String* string()`
375    #[inline]
376    pub fn string() -> Option<Self> {
377        unsafe {
378            let ptr: *mut c_void = msg_send_0(class!(NSString).as_ptr(), sel!(string));
379            Self::from_ptr(ptr)
380        }
381    }
382
383    /// Create a string from another string.
384    ///
385    /// C++ equivalent: `static String* string(const String* pString)`
386    #[inline]
387    pub fn string_with_string(string: &String) -> Option<Self> {
388        unsafe {
389            let ptr: *mut c_void = msg_send_1(
390                class!(NSString).as_ptr(),
391                sel!(stringWithString:),
392                string.as_ptr(),
393            );
394            Self::from_ptr(ptr)
395        }
396    }
397
398    /// Create a string from a C string with the specified encoding.
399    ///
400    /// C++ equivalent: `static String* string(const char* pString, StringEncoding encoding)`
401    #[inline]
402    pub fn string_with_cstring(cstring: *const c_char, encoding: StringEncoding) -> Option<Self> {
403        unsafe {
404            let ptr: *mut c_void = msg_send_2(
405                class!(NSString).as_ptr(),
406                sel!(stringWithCString:encoding:),
407                cstring,
408                encoding.0,
409            );
410            Self::from_ptr(ptr)
411        }
412    }
413
414    /// Allocate a new string.
415    ///
416    /// C++ equivalent: `static String* alloc()`
417    #[inline]
418    pub fn alloc() -> Option<Self> {
419        unsafe {
420            let ptr: *mut c_void = msg_send_0(class!(NSString).as_ptr(), sel!(alloc));
421            Self::from_ptr(ptr)
422        }
423    }
424
425    /// Initialize an allocated string.
426    ///
427    /// C++ equivalent: `String* init()`
428    #[inline]
429    pub fn init(&self) -> Option<Self> {
430        unsafe {
431            let ptr: *mut c_void = msg_send_0(self.as_ptr(), sel!(init));
432            Self::from_ptr(ptr)
433        }
434    }
435
436    /// Initialize with another string.
437    ///
438    /// C++ equivalent: `String* init(const String* pString)`
439    #[inline]
440    pub fn init_with_string(&self, string: &String) -> Option<Self> {
441        unsafe {
442            let ptr: *mut c_void =
443                msg_send_1(self.as_ptr(), sel!(initWithString:), string.as_ptr());
444            Self::from_ptr(ptr)
445        }
446    }
447
448    /// Initialize with a C string and encoding.
449    ///
450    /// C++ equivalent: `String* init(const char* pString, StringEncoding encoding)`
451    #[inline]
452    pub fn init_with_cstring(
453        &self,
454        cstring: *const c_char,
455        encoding: StringEncoding,
456    ) -> Option<Self> {
457        unsafe {
458            let ptr: *mut c_void = msg_send_2(
459                self.as_ptr(),
460                sel!(initWithCString:encoding:),
461                cstring,
462                encoding.0,
463            );
464            Self::from_ptr(ptr)
465        }
466    }
467
468    /// Initialize with bytes, length, encoding, and free buffer flag.
469    ///
470    /// C++ equivalent: `String* init(void* pBytes, UInteger len, StringEncoding encoding, bool freeBuffer)`
471    #[inline]
472    pub fn init_with_bytes_no_copy(
473        &self,
474        bytes: *mut c_void,
475        len: UInteger,
476        encoding: StringEncoding,
477        free_when_done: bool,
478    ) -> Option<Self> {
479        unsafe {
480            let ptr: *mut c_void = msg_send_4(
481                self.as_ptr(),
482                sel!(initWithBytesNoCopy:length:encoding:freeWhenDone:),
483                bytes,
484                len,
485                encoding.0,
486                free_when_done,
487            );
488            Self::from_ptr(ptr)
489        }
490    }
491
492    /// Get the character at the specified index.
493    ///
494    /// C++ equivalent: `unichar character(UInteger index) const`
495    #[inline]
496    pub fn character(&self, index: UInteger) -> Unichar {
497        unsafe { msg_send_1(self.as_ptr(), sel!(characterAtIndex:), index) }
498    }
499
500    /// Get the length of the string in characters.
501    ///
502    /// C++ equivalent: `UInteger length() const`
503    #[inline]
504    pub fn length(&self) -> UInteger {
505        unsafe { msg_send_0(self.as_ptr(), sel!(length)) }
506    }
507
508    /// Get the string as a C string with the specified encoding.
509    ///
510    /// C++ equivalent: `const char* cString(StringEncoding encoding) const`
511    #[inline]
512    pub fn c_string(&self, encoding: StringEncoding) -> *const c_char {
513        unsafe { msg_send_1(self.as_ptr(), sel!(cStringUsingEncoding:), encoding.0) }
514    }
515
516    /// Get the string as a UTF-8 C string.
517    ///
518    /// C++ equivalent: `const char* utf8String() const`
519    #[inline]
520    pub fn utf8_string(&self) -> *const c_char {
521        unsafe { msg_send_0(self.as_ptr(), sel!(UTF8String)) }
522    }
523
524    /// Get the maximum length in bytes for the specified encoding.
525    ///
526    /// C++ equivalent: `UInteger maximumLengthOfBytes(StringEncoding encoding) const`
527    #[inline]
528    pub fn maximum_length_of_bytes(&self, encoding: StringEncoding) -> UInteger {
529        unsafe {
530            msg_send_1(
531                self.as_ptr(),
532                sel!(maximumLengthOfBytesUsingEncoding:),
533                encoding.0,
534            )
535        }
536    }
537
538    /// Get the actual length in bytes for the specified encoding.
539    ///
540    /// C++ equivalent: `UInteger lengthOfBytes(StringEncoding encoding) const`
541    #[inline]
542    pub fn length_of_bytes(&self, encoding: StringEncoding) -> UInteger {
543        unsafe { msg_send_1(self.as_ptr(), sel!(lengthOfBytesUsingEncoding:), encoding.0) }
544    }
545
546    /// Check if this string is equal to another string.
547    ///
548    /// C++ equivalent: `bool isEqualToString(const String* pString) const`
549    #[inline]
550    pub fn is_equal_to_string(&self, string: &String) -> bool {
551        unsafe { msg_send_1(self.as_ptr(), sel!(isEqualToString:), string.as_ptr()) }
552    }
553
554    /// Find the range of a substring with the specified options.
555    ///
556    /// C++ equivalent: `Range rangeOfString(const String* pString, StringCompareOptions options) const`
557    #[inline]
558    pub fn range_of_string(&self, string: &String, options: StringCompareOptions) -> Range {
559        unsafe {
560            msg_send_2(
561                self.as_ptr(),
562                sel!(rangeOfString:options:),
563                string.as_ptr(),
564                options.0,
565            )
566        }
567    }
568
569    /// Get the file system representation of the string.
570    ///
571    /// C++ equivalent: `const char* fileSystemRepresentation() const`
572    #[inline]
573    pub fn file_system_representation(&self) -> *const c_char {
574        unsafe { msg_send_0(self.as_ptr(), sel!(fileSystemRepresentation)) }
575    }
576
577    /// Create a new string by appending another string.
578    ///
579    /// C++ equivalent: `String* stringByAppendingString(const String* pString) const`
580    #[inline]
581    pub fn string_by_appending_string(&self, string: &String) -> Option<Self> {
582        unsafe {
583            let ptr: *mut c_void = msg_send_1(
584                self.as_ptr(),
585                sel!(stringByAppendingString:),
586                string.as_ptr(),
587            );
588            Self::from_ptr(ptr)
589        }
590    }
591
592    /// Perform a case-insensitive comparison with another string.
593    ///
594    /// C++ equivalent: `ComparisonResult caseInsensitiveCompare(const String* pString) const`
595    #[inline]
596    pub fn case_insensitive_compare(&self, string: &String) -> ComparisonResult {
597        unsafe {
598            msg_send_1(
599                self.as_ptr(),
600                sel!(caseInsensitiveCompare:),
601                string.as_ptr(),
602            )
603        }
604    }
605
606    /// Create a String from a raw pointer.
607    ///
608    /// # Safety
609    ///
610    /// The pointer must be a valid Objective-C NSString object.
611    #[inline]
612    pub unsafe fn from_ptr(ptr: *mut c_void) -> Option<Self> {
613        NonNull::new(ptr).map(Self)
614    }
615
616    // Convenience methods for Rust interop
617
618    /// Create a string from a Rust &str.
619    ///
620    /// This is a convenience method for Rust users.
621    #[inline]
622    pub fn from_str(s: &str) -> Option<Self> {
623        let c_str = std::ffi::CString::new(s).ok()?;
624        Self::string_with_cstring(c_str.as_ptr(), StringEncoding::UTF8)
625    }
626
627    /// Convert to a Rust String.
628    ///
629    /// This is a convenience method for Rust users.
630    #[inline]
631    pub fn to_string(&self) -> Option<std::string::String> {
632        let ptr = self.utf8_string();
633        if ptr.is_null() {
634            return None;
635        }
636        unsafe {
637            let c_str = std::ffi::CStr::from_ptr(ptr);
638            Some(c_str.to_string_lossy().into_owned())
639        }
640    }
641}
642
643impl Referencing for String {
644    #[inline]
645    fn as_ptr(&self) -> *const c_void {
646        self.0.as_ptr()
647    }
648}
649
650impl Copying for String {
651    #[inline]
652    fn copy(&self) -> Option<Self> {
653        unsafe {
654            let ptr: *mut c_void = msg_send_0(self.as_ptr(), sel!(copy));
655            Self::from_ptr(ptr)
656        }
657    }
658}
659
660// SAFETY: NSString is thread-safe for reference counting
661unsafe impl Send for String {}
662unsafe impl Sync for String {}
663
664impl std::fmt::Debug for String {
665    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
666        match self.to_string() {
667            Some(s) => write!(f, "String({:?})", s),
668            None => write!(f, "String(<invalid>)"),
669        }
670    }
671}
672
673impl std::fmt::Display for String {
674    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
675        match self.to_string() {
676            Some(s) => write!(f, "{}", s),
677            None => write!(f, "<invalid>"),
678        }
679    }
680}
681
682#[cfg(test)]
683mod tests {
684    use super::*;
685
686    #[test]
687    fn test_string_encoding_values() {
688        assert_eq!(StringEncoding::ASCII.0, 1);
689        assert_eq!(StringEncoding::UTF8.0, 4);
690        assert_eq!(StringEncoding::UNICODE.0, 10);
691        assert_eq!(StringEncoding::UTF16.0, 10);
692        assert_eq!(StringEncoding::UTF32.0, 0x8c000100);
693    }
694
695    #[test]
696    fn test_string_compare_options() {
697        let options = StringCompareOptions::CASE_INSENSITIVE | StringCompareOptions::BACKWARDS;
698        assert!(options.contains(StringCompareOptions::CASE_INSENSITIVE));
699        assert!(options.contains(StringCompareOptions::BACKWARDS));
700        assert!(!options.contains(StringCompareOptions::LITERAL));
701    }
702}