use crate::{objc::ObjcRuntime, util::read_u32_le_at};
const PROPERTY_ENTSIZE: u32 = 16;
#[derive(Debug, Clone)]
pub struct Property<'a> {
name: &'a str,
attributes: &'a str,
}
impl<'a> Property<'a> {
pub fn name(&self) -> &'a str {
self.name
}
pub fn attributes(&self) -> &'a str {
self.attributes
}
pub fn parsed(&self) -> ParsedAttributes<'a> {
parse_attributes(self.attributes)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParsedAttribute<'a> {
pub key: char,
pub value: &'a str,
}
#[derive(Debug, Clone)]
pub struct ParsedAttributes<'a> {
pub type_encoding: &'a str,
pub items: Vec<ParsedAttribute<'a>>,
}
pub struct PropertyIter<'a, 'p> {
rt: &'p ObjcRuntime<'a>,
layout: Option<PropertyListLayout>,
cursor: u32,
}
#[derive(Debug, Clone, Copy)]
struct PropertyListLayout {
base_va: u64,
entsize: u32,
count: u32,
}
impl<'a, 'p> PropertyIter<'a, 'p> {
pub(crate) fn empty(rt: &'p ObjcRuntime<'a>) -> Self {
Self {
rt,
layout: None,
cursor: 0,
}
}
}
impl<'a, 'p> Iterator for PropertyIter<'a, 'p> {
type Item = Property<'a>;
fn next(&mut self) -> Option<Self::Item> {
let layout = self.layout?;
loop {
if self.cursor >= layout.count {
return None;
}
let i = self.cursor;
self.cursor = self.cursor.checked_add(1)?;
let entry_off = u64::from(i).checked_mul(u64::from(layout.entsize))?;
let entry_va = layout.base_va.checked_add(entry_off)?;
if let Some(p) = decode_property(self.rt, entry_va) {
return Some(p);
}
#[cfg(feature = "tracing")]
tracing::debug!(
"darwinscope::objc: property row at 0x{:x} (idx={}) skipped - decode failed",
entry_va,
i
);
}
}
}
pub(crate) fn property_list_iter<'a, 'p>(
rt: &'p ObjcRuntime<'a>,
list_va: u64,
) -> PropertyIter<'a, 'p> {
if list_va == 0 || (list_va & 0x1) != 0 {
return PropertyIter::empty(rt);
}
let Some(header) = rt.read_bytes(list_va, 8) else {
return PropertyIter::empty(rt);
};
let Some(entsize_and_flags) = read_u32_le_at(header, 0) else {
return PropertyIter::empty(rt);
};
let Some(count) = read_u32_le_at(header, 4) else {
return PropertyIter::empty(rt);
};
let entsize = entsize_and_flags;
if entsize < PROPERTY_ENTSIZE {
return PropertyIter::empty(rt);
}
let base_va = match list_va.checked_add(8) {
Some(v) => v,
None => return PropertyIter::empty(rt),
};
PropertyIter {
rt,
layout: Some(PropertyListLayout {
base_va,
entsize,
count,
}),
cursor: 0,
}
}
fn decode_property<'a>(rt: &ObjcRuntime<'a>, entry_va: u64) -> Option<Property<'a>> {
rt.read_bytes(entry_va, PROPERTY_ENTSIZE as usize)?;
let name_va = rt.resolve_pointer(entry_va)?;
let attrs_va = rt.resolve_pointer(entry_va.checked_add(8)?).unwrap_or(0);
let name = rt.read_cstr(name_va)?;
let attributes = rt.read_cstr(attrs_va).unwrap_or("");
Some(Property { name, attributes })
}
fn parse_attributes(s: &str) -> ParsedAttributes<'_> {
let mut items: Vec<ParsedAttribute<'_>> = Vec::new();
let mut type_encoding: &str = "";
let bytes = s.as_bytes();
let mut i = 0usize;
while i < bytes.len() {
let Some(&key_byte) = bytes.get(i) else { break };
let key = key_byte as char;
let value_start = match i.checked_add(1) {
Some(v) => v,
None => break,
};
let mut j = value_start;
let mut in_quotes = false;
while j < bytes.len() {
let Some(&c) = bytes.get(j) else { break };
if c == b'"' {
in_quotes = !in_quotes;
} else if c == b',' && !in_quotes {
break;
}
j = match j.checked_add(1) {
Some(v) => v,
None => break,
};
}
let value = s.get(value_start..j).unwrap_or("");
let item = ParsedAttribute { key, value };
if key == 'T' && type_encoding.is_empty() {
type_encoding = value;
}
items.push(item);
i = j;
if bytes.get(i) == Some(&b',') {
i = match i.checked_add(1) {
Some(v) => v,
None => break,
};
}
}
ParsedAttributes {
type_encoding,
items,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_basic() {
let p = parse_attributes("T@\"NSString\",C,N,V_name");
assert_eq!(p.type_encoding, "@\"NSString\"");
assert_eq!(p.items.len(), 4);
assert_eq!(p.items[0].key, 'T');
assert_eq!(p.items[0].value, "@\"NSString\"");
assert_eq!(p.items[1].key, 'C');
assert_eq!(p.items[1].value, "");
assert_eq!(p.items[2].key, 'N');
assert_eq!(p.items[3].key, 'V');
assert_eq!(p.items[3].value, "_name");
}
#[test]
fn parse_empty() {
let p = parse_attributes("");
assert!(p.items.is_empty());
assert_eq!(p.type_encoding, "");
}
#[test]
fn parse_handles_quoted_commas() {
let p = parse_attributes("T@\"NSDictionary<NSString,NSNumber>\",R,V_dict");
assert_eq!(p.items.len(), 3);
assert_eq!(p.type_encoding, "@\"NSDictionary<NSString,NSNumber>\"");
assert_eq!(p.items[1].key, 'R');
assert_eq!(p.items[2].key, 'V');
assert_eq!(p.items[2].value, "_dict");
}
}