Skip to main content

macho_objc/
encoding.rs

1use crate::error::{Error, Result};
2use std::fmt;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
5/// The ObjCQualifiedType type.
6pub struct ObjCQualifiedType {
7    /// The qualifiers field.
8    pub qualifiers: Vec<TypeQualifier>,
9    /// The ty field.
10    pub ty: ObjCType,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14/// The ObjCType type.
15#[non_exhaustive]
16pub enum ObjCType {
17    /// The Void variant.
18    Void,
19    /// The Bool variant.
20    Bool,
21    /// The Char variant.
22    Char,
23    /// The UnsignedChar variant.
24    UnsignedChar,
25    /// The Short variant.
26    Short,
27    /// The UnsignedShort variant.
28    UnsignedShort,
29    /// The Int variant.
30    Int,
31    /// The UnsignedInt variant.
32    UnsignedInt,
33    /// The Long variant.
34    Long,
35    /// The UnsignedLong variant.
36    UnsignedLong,
37    /// The LongLong variant.
38    LongLong,
39    /// The UnsignedLongLong variant.
40    UnsignedLongLong,
41    /// The Float variant.
42    Float,
43    /// The Double variant.
44    Double,
45    /// The CharPtr variant.
46    CharPtr,
47    /// The Selector variant.
48    Selector,
49    /// The Class variant.
50    Class,
51    /// The Object variant.
52    Object {
53        /// The item field.
54        class_name: Option<String>,
55        /// The item field.
56        protocols: Vec<String>,
57        /// The bool field.
58        is_block: bool,
59    },
60    /// The CString variant.
61    CString,
62    /// The Pointer variant.
63    Pointer(Box<ObjCQualifiedType>),
64    /// The Array variant.
65    Array {
66        /// The usize field.
67        len: usize,
68        /// The item field.
69        element: Box<ObjCQualifiedType>,
70    },
71    /// The Struct variant.
72    Struct {
73        /// The String field.
74        name: String,
75        /// The item field.
76        fields: Vec<ObjCQualifiedType>,
77    },
78    /// The Union variant.
79    Union {
80        /// The String field.
81        name: String,
82        /// The item field.
83        fields: Vec<ObjCQualifiedType>,
84    },
85    /// The BitField variant.
86    BitField(usize),
87    /// The Unknown variant.
88    Unknown(char),
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92/// The TypeQualifier type.
93#[non_exhaustive]
94pub enum TypeQualifier {
95    /// The Const variant.
96    Const,
97    /// The In variant.
98    In,
99    /// The InOut variant.
100    InOut,
101    /// The Out variant.
102    Out,
103    /// The ByCopy variant.
104    ByCopy,
105    /// The ByRef variant.
106    ByRef,
107    /// The OneWay variant.
108    OneWay,
109    /// The Atomic variant.
110    Atomic,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
114/// The ObjCMethodArg type.
115pub struct ObjCMethodArg {
116    /// The ty field.
117    pub ty: ObjCQualifiedType,
118    /// The stack_offset field.
119    pub stack_offset: Option<usize>,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
123/// The ObjCMethodSignature type.
124pub struct ObjCMethodSignature {
125    /// The return_type field.
126    pub return_type: ObjCQualifiedType,
127    /// The return_offset field.
128    pub return_offset: Option<usize>,
129    /// The self_type field.
130    pub self_type: Option<ObjCMethodArg>,
131    /// The cmd_type field.
132    pub cmd_type: Option<ObjCMethodArg>,
133    /// The arguments field.
134    pub arguments: Vec<ObjCMethodArg>,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Default)]
138/// The ObjCPropertyAttributes type.
139pub struct ObjCPropertyAttributes {
140    /// The ty field.
141    pub ty: Option<ObjCQualifiedType>,
142    /// The readonly field.
143    pub readonly: bool,
144    /// The nonatomic field.
145    pub nonatomic: bool,
146    /// The dynamic field.
147    pub dynamic: bool,
148    /// The weak field.
149    pub weak: bool,
150    /// The copy field.
151    pub copy: bool,
152    /// The strong field.
153    pub strong: bool,
154    /// The getter field.
155    pub getter: Option<String>,
156    /// The setter field.
157    pub setter: Option<String>,
158    /// The ivar field.
159    pub ivar: Option<String>,
160    /// The old_type_encoding field.
161    pub old_type_encoding: Option<String>,
162    /// The unknown_flags field.
163    pub unknown_flags: Vec<String>,
164}
165
166impl ObjCQualifiedType {
167    /// Performs parse.
168    pub fn parse(encoding: &str) -> Result<Self> {
169        let mut parser = Parser::new(encoding);
170        let ty = parser.parse_qualified_type()?;
171        parser.skip_digits();
172        parser.expect_eof()?;
173        Ok(ty)
174    }
175
176    /// Performs render.
177    pub fn render(&self) -> String {
178        match &self.ty {
179            ObjCType::Pointer(inner) => {
180                let inner = inner.render();
181                let pointer = if inner.ends_with('*') {
182                    format!("{inner}*")
183                } else {
184                    format!("{inner} *")
185                };
186                self.qualify(pointer)
187            }
188            ObjCType::Array { len, element } => {
189                self.qualify(format!("{}[{len}]", element.render()))
190            }
191            _ => self.qualify(self.ty.render_base()),
192        }
193    }
194
195    /// Performs render_named.
196    pub fn render_named(&self, name: &str) -> String {
197        match &self.ty {
198            ObjCType::BitField(bits) => self.qualify(attach_declarator(
199                "unsigned int".to_string(),
200                format!("{name} : {bits}"),
201            )),
202            ObjCType::Pointer(inner) => {
203                if let ObjCType::Array { len, element } = &inner.ty {
204                    return self.qualify(attach_declarator(
205                        element.render(),
206                        format!("(*{name})[{len}]"),
207                    ));
208                }
209
210                let inner_rendered = inner.render();
211                let declarator = if inner_rendered.ends_with('*') {
212                    format!("*{name}")
213                } else {
214                    format!(" *{name}")
215                };
216                self.qualify(format!("{inner_rendered}{declarator}"))
217            }
218            ObjCType::Array { len, element } => self.qualify(attach_declarator(
219                element.render(),
220                format!("{name}[{len}]"),
221            )),
222            _ => self.qualify(attach_declarator(self.ty.render_base(), name.to_string())),
223        }
224    }
225
226    fn qualify(&self, rendered: String) -> String {
227        if self.qualifiers.is_empty() {
228            rendered
229        } else {
230            format!(
231                "{} {rendered}",
232                self.qualifiers
233                    .iter()
234                    .map(|qualifier| qualifier.as_str())
235                    .collect::<Vec<_>>()
236                    .join(" ")
237            )
238        }
239    }
240}
241
242impl ObjCMethodSignature {
243    /// Performs parse.
244    pub fn parse(encoding: &str) -> Result<Self> {
245        let mut parser = Parser::new(encoding);
246
247        let return_type = parser.parse_qualified_type()?;
248        let return_offset = parser.parse_number();
249        let self_type = parser.parse_method_arg()?;
250        let cmd_type = parser.parse_method_arg()?;
251
252        let mut arguments = Vec::new();
253        while !parser.is_eof() {
254            let Some(arg) = parser.parse_method_arg()? else {
255                break;
256            };
257            arguments.push(arg);
258        }
259
260        Ok(Self {
261            return_type,
262            return_offset,
263            self_type,
264            cmd_type,
265            arguments,
266        })
267    }
268}
269
270impl ObjCPropertyAttributes {
271    /// Performs parse.
272    pub fn parse(attrs: &str) -> Self {
273        let mut parsed = Self::default();
274
275        for component in split_property_attribute_components(attrs) {
276            if component.is_empty() {
277                continue;
278            }
279
280            let mut chars = component.chars();
281            let Some(flag) = chars.next() else {
282                continue;
283            };
284            let value = chars.as_str();
285
286            match flag {
287                'T' => parsed.ty = ObjCQualifiedType::parse(value).ok(),
288                'R' => parsed.readonly = true,
289                'N' => parsed.nonatomic = true,
290                'D' => parsed.dynamic = true,
291                'W' => parsed.weak = true,
292                'C' => parsed.copy = true,
293                '&' => parsed.strong = true,
294                'G' if !value.is_empty() => parsed.getter = Some(value.to_string()),
295                'S' if !value.is_empty() => parsed.setter = Some(value.to_string()),
296                'V' if !value.is_empty() => parsed.ivar = Some(value.to_string()),
297                't' if !value.is_empty() => parsed.old_type_encoding = Some(value.to_string()),
298                _ => parsed.unknown_flags.push(component.to_string()),
299            }
300        }
301
302        parsed
303    }
304
305    /// Performs effective_type.
306    pub fn effective_type(&self) -> Option<ObjCQualifiedType> {
307        let legacy = self
308            .old_type_encoding
309            .as_deref()
310            .and_then(|encoding| ObjCQualifiedType::parse(encoding).ok());
311
312        match (&self.ty, legacy) {
313            (Some(current), Some(legacy)) if current.is_less_specific_than(&legacy) => Some(legacy),
314            (Some(current), _) => Some(current.clone()),
315            (None, Some(legacy)) => Some(legacy),
316            (None, None) => None,
317        }
318    }
319}
320
321impl fmt::Display for ObjCQualifiedType {
322    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323        write!(f, "{}", self.render())
324    }
325}
326
327impl fmt::Display for ObjCType {
328    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
329        write!(f, "{}", self.render_base())
330    }
331}
332
333impl TypeQualifier {
334    pub(crate) fn as_str(self) -> &'static str {
335        match self {
336            Self::Const => "const",
337            Self::In => "in",
338            Self::InOut => "inout",
339            Self::Out => "out",
340            Self::ByCopy => "bycopy",
341            Self::ByRef => "byref",
342            Self::OneWay => "oneway",
343            Self::Atomic => "_Atomic",
344        }
345    }
346}
347
348impl fmt::Display for TypeQualifier {
349    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350        f.write_str(self.as_str())
351    }
352}
353
354impl ObjCType {
355    fn render_base(&self) -> String {
356        match self {
357            Self::Void => "void".to_string(),
358            Self::Bool => "BOOL".to_string(),
359            Self::Char => "char".to_string(),
360            Self::UnsignedChar => "unsigned char".to_string(),
361            Self::Short => "short".to_string(),
362            Self::UnsignedShort => "unsigned short".to_string(),
363            Self::Int => "int".to_string(),
364            Self::UnsignedInt => "unsigned int".to_string(),
365            Self::Long => "long".to_string(),
366            Self::UnsignedLong => "unsigned long".to_string(),
367            Self::LongLong => "long long".to_string(),
368            Self::UnsignedLongLong => "unsigned long long".to_string(),
369            Self::Float => "float".to_string(),
370            Self::Double => "double".to_string(),
371            Self::CharPtr | Self::CString => "char *".to_string(),
372            Self::Selector => "SEL".to_string(),
373            Self::Class => "Class".to_string(),
374            Self::Object {
375                class_name,
376                protocols,
377                is_block,
378            } => {
379                if *is_block {
380                    "id /* block */".to_string()
381                } else if let Some(name) = class_name {
382                    if protocols.is_empty() {
383                        format!("{name} *")
384                    } else {
385                        format!("{name}<{}> *", protocols.join(", "))
386                    }
387                } else if protocols.is_empty() {
388                    "id".to_string()
389                } else {
390                    format!("id<{}>", protocols.join(", "))
391                }
392            }
393            Self::Pointer(inner) => {
394                let inner = inner.render();
395                if inner.ends_with('*') {
396                    format!("{inner}*")
397                } else {
398                    format!("{inner} *")
399                }
400            }
401            Self::Array { len, element } => format!("{}[{len}]", element.render()),
402            Self::Struct { name, .. } => {
403                if name == "?" || name.is_empty() {
404                    "struct ?".to_string()
405                } else {
406                    format!("struct {name}")
407                }
408            }
409            Self::Union { name, .. } => {
410                if name == "?" || name.is_empty() {
411                    "union ?".to_string()
412                } else {
413                    format!("union {name}")
414                }
415            }
416            Self::BitField(bits) => format!("unsigned int : {bits}"),
417            Self::Unknown(code) => format!("unknown /* {code} */"),
418        }
419    }
420
421    fn is_less_specific_than(&self, other: &Self) -> bool {
422        match self {
423            Self::Unknown(_) => !matches!(other, Self::Unknown(_)),
424            Self::Object {
425                class_name: None,
426                protocols,
427                is_block: false,
428            } => match other {
429                Self::Object {
430                    class_name,
431                    protocols: other_protocols,
432                    ..
433                } => class_name.is_some() || other_protocols.len() > protocols.len(),
434                _ => false,
435            },
436            _ => false,
437        }
438    }
439}
440
441impl ObjCQualifiedType {
442    fn is_less_specific_than(&self, other: &Self) -> bool {
443        self.qualifiers == other.qualifiers && self.ty.is_less_specific_than(&other.ty)
444    }
445}
446
447fn attach_declarator(base: String, declarator: String) -> String {
448    if declarator.is_empty() {
449        base
450    } else if base.ends_with('*') {
451        format!("{base}{declarator}")
452    } else {
453        format!("{base} {declarator}")
454    }
455}
456
457struct Parser<'a> {
458    input: &'a str,
459    pos: usize,
460}
461
462impl<'a> Parser<'a> {
463    fn new(input: &'a str) -> Self {
464        Self { input, pos: 0 }
465    }
466
467    fn is_eof(&self) -> bool {
468        self.pos >= self.input.len()
469    }
470
471    fn expect_eof(&self) -> Result<()> {
472        if self.is_eof() {
473            Ok(())
474        } else {
475            Err(Error::format(format!(
476                "unexpected trailing ObjC encoding at byte {}: {}",
477                self.pos,
478                &self.input[self.pos..]
479            )))
480        }
481    }
482
483    fn peek(&self) -> Option<char> {
484        self.input[self.pos..].chars().next()
485    }
486
487    fn bump(&mut self) -> Option<char> {
488        let ch = self.peek()?;
489        self.pos += ch.len_utf8();
490        Some(ch)
491    }
492
493    fn consume(&mut self, expected: char) -> Result<()> {
494        match self.bump() {
495            Some(ch) if ch == expected => Ok(()),
496            Some(ch) => Err(Error::format(format!(
497                "expected '{}' in ObjC encoding, found '{}'",
498                expected, ch
499            ))),
500            None => Err(Error::format(format!(
501                "expected '{}' in ObjC encoding, found EOF",
502                expected
503            ))),
504        }
505    }
506
507    fn parse_method_arg(&mut self) -> Result<Option<ObjCMethodArg>> {
508        if self.is_eof() {
509            return Ok(None);
510        }
511        let ty = self.parse_qualified_type()?;
512        let stack_offset = self.parse_number();
513        Ok(Some(ObjCMethodArg { ty, stack_offset }))
514    }
515
516    fn parse_qualified_type(&mut self) -> Result<ObjCQualifiedType> {
517        let mut qualifiers = Vec::new();
518        while let Some(qualifier) = self.parse_qualifier() {
519            qualifiers.push(qualifier);
520        }
521        let ty = self.parse_type()?;
522        Ok(ObjCQualifiedType { qualifiers, ty })
523    }
524
525    fn parse_qualifier(&mut self) -> Option<TypeQualifier> {
526        let qualifier = match self.peek()? {
527            'r' => TypeQualifier::Const,
528            'n' => TypeQualifier::In,
529            'N' => TypeQualifier::InOut,
530            'o' => TypeQualifier::Out,
531            'O' => TypeQualifier::ByCopy,
532            'R' => TypeQualifier::ByRef,
533            'V' => TypeQualifier::OneWay,
534            'A' => TypeQualifier::Atomic,
535            _ => return None,
536        };
537        self.bump();
538        Some(qualifier)
539    }
540
541    fn parse_type(&mut self) -> Result<ObjCType> {
542        let Some(ch) = self.bump() else {
543            return Err(Error::format("unexpected EOF in ObjC type encoding"));
544        };
545
546        match ch {
547            'v' => Ok(ObjCType::Void),
548            'B' => Ok(ObjCType::Bool),
549            'c' => Ok(ObjCType::Char),
550            'C' => Ok(ObjCType::UnsignedChar),
551            's' => Ok(ObjCType::Short),
552            'S' => Ok(ObjCType::UnsignedShort),
553            'i' => Ok(ObjCType::Int),
554            'I' => Ok(ObjCType::UnsignedInt),
555            'l' => Ok(ObjCType::Long),
556            'L' => Ok(ObjCType::UnsignedLong),
557            'q' => Ok(ObjCType::LongLong),
558            'Q' => Ok(ObjCType::UnsignedLongLong),
559            'f' => Ok(ObjCType::Float),
560            'd' => Ok(ObjCType::Double),
561            '*' => Ok(ObjCType::CharPtr),
562            ':' => Ok(ObjCType::Selector),
563            '#' => Ok(ObjCType::Class),
564            '%' => Ok(ObjCType::CString),
565            '@' => self.parse_object_type(),
566            '^' => Ok(ObjCType::Pointer(Box::new(self.parse_qualified_type()?))),
567            '[' => self.parse_array_type(),
568            '{' => self.parse_record_type('}', true),
569            '(' => self.parse_record_type(')', false),
570            'b' => Ok(ObjCType::BitField(self.parse_number().unwrap_or(0))),
571            '?' => Ok(ObjCType::Unknown('?')),
572            _ => Ok(ObjCType::Unknown(ch)),
573        }
574    }
575
576    fn parse_object_type(&mut self) -> Result<ObjCType> {
577        match self.peek() {
578            Some('?') => {
579                self.bump();
580                Ok(ObjCType::Object {
581                    class_name: None,
582                    protocols: Vec::new(),
583                    is_block: true,
584                })
585            }
586            Some('"') => {
587                self.bump();
588                let quoted = self.take_until('"')?;
589                self.consume('"')?;
590                let (class_name, protocols) = parse_quoted_object_metadata(&quoted);
591                Ok(ObjCType::Object {
592                    class_name,
593                    protocols,
594                    is_block: false,
595                })
596            }
597            _ => Ok(ObjCType::Object {
598                class_name: None,
599                protocols: Vec::new(),
600                is_block: false,
601            }),
602        }
603    }
604
605    fn parse_array_type(&mut self) -> Result<ObjCType> {
606        let len = self.parse_number().unwrap_or(0);
607        let element = self.parse_qualified_type()?;
608        self.consume(']')?;
609        Ok(ObjCType::Array {
610            len,
611            element: Box::new(element),
612        })
613    }
614
615    fn parse_record_type(&mut self, terminator: char, is_struct: bool) -> Result<ObjCType> {
616        let name = self.take_record_name(terminator);
617        let mut fields = Vec::new();
618
619        if self.peek() == Some('=') {
620            self.bump();
621            while self.peek() != Some(terminator) {
622                self.skip_quoted_field_name();
623                fields.push(self.parse_qualified_type()?);
624            }
625        }
626
627        self.consume(terminator)?;
628        if is_struct {
629            Ok(ObjCType::Struct { name, fields })
630        } else {
631            Ok(ObjCType::Union { name, fields })
632        }
633    }
634
635    fn take_record_name(&mut self, terminator: char) -> String {
636        let start = self.pos;
637        while let Some(ch) = self.peek() {
638            if ch == '=' || ch == terminator {
639                break;
640            }
641            self.bump();
642        }
643        self.input[start..self.pos].to_string()
644    }
645
646    fn skip_quoted_field_name(&mut self) {
647        if self.peek() != Some('"') {
648            return;
649        }
650        let _ = self.bump();
651        while let Some(ch) = self.bump() {
652            if ch == '"' {
653                break;
654            }
655        }
656    }
657
658    fn take_until(&mut self, terminator: char) -> Result<String> {
659        let start = self.pos;
660        while let Some(ch) = self.peek() {
661            if ch == terminator {
662                return Ok(self.input[start..self.pos].to_string());
663            }
664            self.bump();
665        }
666        Err(Error::format(format!(
667            "unterminated quoted ObjC encoding fragment starting at byte {}",
668            start.saturating_sub(1)
669        )))
670    }
671
672    fn parse_number(&mut self) -> Option<usize> {
673        let start = self.pos;
674        while matches!(self.peek(), Some('0'..='9')) {
675            self.bump();
676        }
677        if self.pos == start {
678            None
679        } else {
680            self.input[start..self.pos].parse().ok()
681        }
682    }
683
684    fn skip_digits(&mut self) {
685        while matches!(self.peek(), Some('0'..='9')) {
686            self.bump();
687        }
688    }
689}
690
691fn parse_quoted_object_metadata(quoted: &str) -> (Option<String>, Vec<String>) {
692    if quoted.is_empty() {
693        return (None, Vec::new());
694    }
695
696    let mut class_name = String::new();
697    let mut protocols = Vec::new();
698    let mut rest = quoted;
699
700    while let Some(start) = rest.find('<') {
701        if class_name.is_empty() {
702            class_name.push_str(rest[..start].trim());
703        }
704        let Some(end) = rest[start + 1..].find('>') else {
705            break;
706        };
707        let proto = &rest[start + 1..start + 1 + end];
708        for proto in proto
709            .split(',')
710            .map(str::trim)
711            .filter(|proto| !proto.is_empty())
712        {
713            protocols.push(proto.to_string());
714        }
715        rest = &rest[start + 2 + end..];
716    }
717
718    if class_name.is_empty() {
719        let trimmed = rest.trim();
720        if !trimmed.is_empty() {
721            class_name.push_str(trimmed);
722        }
723    }
724
725    let class_name = if class_name.is_empty() || class_name == "<>" {
726        None
727    } else {
728        Some(class_name)
729    };
730
731    (class_name, protocols)
732}
733
734fn split_property_attribute_components(attrs: &str) -> Vec<&str> {
735    let mut parts = Vec::new();
736    let mut start = 0;
737    let mut in_quotes = false;
738    let mut angle_depth = 0usize;
739    let mut brace_depth = 0usize;
740    let mut paren_depth = 0usize;
741    let mut bracket_depth = 0usize;
742
743    for (idx, ch) in attrs.char_indices() {
744        match ch {
745            '"' => in_quotes = !in_quotes,
746            '<' if !in_quotes => angle_depth += 1,
747            '>' if !in_quotes && angle_depth > 0 => angle_depth -= 1,
748            '{' if !in_quotes => brace_depth += 1,
749            '}' if !in_quotes && brace_depth > 0 => brace_depth -= 1,
750            '(' if !in_quotes => paren_depth += 1,
751            ')' if !in_quotes && paren_depth > 0 => paren_depth -= 1,
752            '[' if !in_quotes => bracket_depth += 1,
753            ']' if !in_quotes && bracket_depth > 0 => bracket_depth -= 1,
754            ',' if !in_quotes
755                && angle_depth == 0
756                && brace_depth == 0
757                && paren_depth == 0
758                && bracket_depth == 0 =>
759            {
760                parts.push(&attrs[start..idx]);
761                start = idx + ch.len_utf8();
762            }
763            _ => {}
764        }
765    }
766
767    parts.push(&attrs[start..]);
768    parts
769}
770
771#[cfg(test)]
772mod tests {
773    use super::{
774        ObjCMethodSignature, ObjCPropertyAttributes, ObjCQualifiedType, ObjCType, TypeQualifier,
775    };
776
777    #[test]
778    fn parses_named_object_type() {
779        let ty = ObjCQualifiedType::parse("@\"NSString\"").expect("parse object type");
780        assert_eq!(
781            ty.ty,
782            ObjCType::Object {
783                class_name: Some("NSString".into()),
784                protocols: Vec::new(),
785                is_block: false,
786            }
787        );
788        assert_eq!(ty.to_string(), "NSString *");
789    }
790
791    #[test]
792    fn parses_protocol_qualified_object_type() {
793        let ty = ObjCQualifiedType::parse("@\"NSObject<NSCopying><NSSecureCoding>\"")
794            .expect("parse qualified object");
795        assert_eq!(ty.to_string(), "NSObject<NSCopying, NSSecureCoding> *");
796    }
797
798    #[test]
799    fn parses_nested_pointer_and_struct_type() {
800        let ty = ObjCQualifiedType::parse("^{CGRect={CGPoint=dd}{CGSize=dd}}")
801            .expect("parse struct pointer");
802        assert_eq!(ty.to_string(), "struct CGRect *");
803    }
804
805    #[test]
806    fn parses_qualified_scalar_type() {
807        let ty = ObjCQualifiedType::parse("ri").expect("parse qualified scalar");
808        assert_eq!(ty.qualifiers, vec![TypeQualifier::Const]);
809        assert_eq!(ty.to_string(), "const int");
810    }
811
812    #[test]
813    fn parses_method_signature_with_offsets() {
814        let sig =
815            ObjCMethodSignature::parse("v24@0:8@\"NSString\"16").expect("parse method signature");
816        assert_eq!(sig.return_type.to_string(), "void");
817        assert_eq!(sig.return_offset, Some(24));
818        assert_eq!(sig.arguments.len(), 1);
819        assert_eq!(sig.arguments[0].ty.to_string(), "NSString *");
820        assert_eq!(sig.arguments[0].stack_offset, Some(16));
821    }
822
823    #[test]
824    fn parses_method_signature_with_block_argument() {
825        let sig = ObjCMethodSignature::parse("v32@0:8@?16q24").expect("parse block signature");
826        assert_eq!(sig.arguments.len(), 2);
827        assert_eq!(sig.arguments[0].ty.to_string(), "id /* block */");
828        assert_eq!(sig.arguments[1].ty.to_string(), "long long");
829    }
830
831    #[test]
832    fn parses_property_attributes() {
833        let attrs = ObjCPropertyAttributes::parse(
834            "T@\"NSString\",&,N,GcustomTitle,SsetCustomTitle:,V_title",
835        );
836        assert_eq!(attrs.ty.expect("typed property").to_string(), "NSString *");
837        assert!(attrs.strong);
838        assert!(attrs.nonatomic);
839        assert_eq!(attrs.getter.as_deref(), Some("customTitle"));
840        assert_eq!(attrs.setter.as_deref(), Some("setCustomTitle:"));
841        assert_eq!(attrs.ivar.as_deref(), Some("_title"));
842    }
843
844    #[test]
845    fn parses_property_attributes_with_commas_inside_quoted_type_metadata() {
846        let attrs =
847            ObjCPropertyAttributes::parse("T@\"NSObject<NSCopying, NSSecureCoding>\",&,N,V_value");
848        assert_eq!(
849            attrs.effective_type().expect("typed property").to_string(),
850            "NSObject<NSCopying, NSSecureCoding> *"
851        );
852        assert!(attrs.strong);
853        assert!(attrs.nonatomic);
854        assert_eq!(attrs.ivar.as_deref(), Some("_value"));
855    }
856
857    #[test]
858    fn prefers_more_specific_legacy_property_type() {
859        let attrs = ObjCPropertyAttributes {
860            ty: Some(ObjCQualifiedType::parse("@").expect("parse id type")),
861            old_type_encoding: Some("@\"NSString\"".into()),
862            ..Default::default()
863        };
864        assert_eq!(
865            attrs.effective_type().expect("effective type").to_string(),
866            "NSString *"
867        );
868    }
869
870    #[test]
871    fn renders_named_nested_pointer_types_without_extra_spaces() {
872        let ty = ObjCQualifiedType::parse("^^@\"NSError\"").expect("parse nested pointer");
873        assert_eq!(ty.to_string(), "NSError ***");
874        assert_eq!(ty.render_named("error"), "NSError ***error");
875    }
876
877    #[test]
878    fn renders_pointer_to_array_declarators() {
879        let ty = ObjCQualifiedType::parse("^[4i]").expect("parse pointer to array");
880        assert_eq!(ty.render_named("matrix"), "int (*matrix)[4]");
881    }
882
883    #[test]
884    fn renders_bitfield_declarators() {
885        let ty = ObjCQualifiedType::parse("b3").expect("parse bitfield");
886        assert_eq!(ty.render_named("flags"), "unsigned int flags : 3");
887    }
888}