Class: AttributedStringExtractor

Inherits:
Object
  • Object
show all
Defined in:
lib/imsg-grep/apple/attr_str.rb

Overview

Extract NSString value from unkeyed-archived (typedstream) NSAttributedString

Constant Summary collapse

TAG_INTEGER_2 =

Indicates 2-byte integer follows

-127  # Indicates 2-byte integer follows
TAG_INTEGER_4 =

Indicates 4-byte integer follows

-126  # Indicates 4-byte integer follows

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(data) ⇒ AttributedStringExtractor

Returns a new instance of AttributedStringExtractor.



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/imsg-grep/apple/attr_str.rb', line 12

def initialize(data)
  return if data.nil?
  # Find NSString position first and fail fast if not found
  @nsstring_pos = data.index("NSString") or raise "NSString not found in data"
  @data = data.b
  @pos = 0

  # Read and validate header
  version = read.ord
  sig_length = read.ord
  raise "Only version 4 supported, got #{version}" unless version == 4
  raise "Invalid signature length #{sig_length}" unless sig_length == 11

  signature = read(sig_length)
  case signature
  when "streamtyped" then @int16_format, @int32_format = "v", "V"
  when "typedstream" then @int16_format, @int32_format = "n", "N"
  else raise "Invalid signature: #{signature.inspect}"
  end
end

Class Method Details

.extract(data) ⇒ Object



7
# File 'lib/imsg-grep/apple/attr_str.rb', line 7

def self.extract(data) = (new(data).extract if data)

Instance Method Details

#extractObject



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/imsg-grep/apple/attr_str.rb', line 33

def extract
  return if @data.nil?

  # Jump to after NSString and look for '+' string marker
  marker_pos = @data.index(?+, @nsstring_pos + 8)
  return unless marker_pos

  @pos = marker_pos + 1  # Skip '+' marker

  length_byte = read.ord                                             # Read string length
  length_byte = length_byte > 127 ? length_byte - 256 : length_byte  # Convert to signed byte for tag comparison

  length = case length_byte
  when TAG_INTEGER_2 then read(2).unpack1(@int16_format) # 2-byte length
  when TAG_INTEGER_4 then read(4).unpack1(@int32_format) # 4-byte length
  else length_byte                                       # Single byte length
  end

  return unless length && length > 0

  read(length).force_encoding("UTF-8") # Extract and return string
end