tilia-vobject 4.0.0.pre.alpha2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (193) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rubocop.yml +32 -0
  4. data/.simplecov +4 -0
  5. data/.travis.yml +3 -0
  6. data/CHANGELOG.sabre.md +626 -0
  7. data/CONTRIBUTING.md +25 -0
  8. data/Gemfile +17 -0
  9. data/Gemfile.lock +68 -0
  10. data/LICENSE +27 -0
  11. data/LICENSE.sabre +27 -0
  12. data/README.md +63 -0
  13. data/Rakefile +17 -0
  14. data/bin/vobject +7 -0
  15. data/lib/tilia/v_object/birthday_calendar_generator.rb +142 -0
  16. data/lib/tilia/v_object/cli.rb +582 -0
  17. data/lib/tilia/v_object/component/available.rb +107 -0
  18. data/lib/tilia/v_object/component/v_alarm.rb +114 -0
  19. data/lib/tilia/v_object/component/v_availability.rb +128 -0
  20. data/lib/tilia/v_object/component/v_calendar.rb +468 -0
  21. data/lib/tilia/v_object/component/v_card.rb +457 -0
  22. data/lib/tilia/v_object/component/v_event.rb +127 -0
  23. data/lib/tilia/v_object/component/v_free_busy.rb +81 -0
  24. data/lib/tilia/v_object/component/v_journal.rb +75 -0
  25. data/lib/tilia/v_object/component/v_time_zone.rb +51 -0
  26. data/lib/tilia/v_object/component/v_todo.rb +147 -0
  27. data/lib/tilia/v_object/component.rb +591 -0
  28. data/lib/tilia/v_object/date_time_parser.rb +486 -0
  29. data/lib/tilia/v_object/document.rb +218 -0
  30. data/lib/tilia/v_object/element_list.rb +18 -0
  31. data/lib/tilia/v_object/eof_exception.rb +8 -0
  32. data/lib/tilia/v_object/free_busy_data.rb +149 -0
  33. data/lib/tilia/v_object/free_busy_generator.rb +465 -0
  34. data/lib/tilia/v_object/i_tip/broker.rb +909 -0
  35. data/lib/tilia/v_object/i_tip/i_tip_exception.rb +9 -0
  36. data/lib/tilia/v_object/i_tip/message.rb +109 -0
  37. data/lib/tilia/v_object/i_tip/same_organizer_for_all_components_exception.rb +13 -0
  38. data/lib/tilia/v_object/i_tip.rb +10 -0
  39. data/lib/tilia/v_object/node.rb +192 -0
  40. data/lib/tilia/v_object/parameter.rb +327 -0
  41. data/lib/tilia/v_object/parse_exception.rb +7 -0
  42. data/lib/tilia/v_object/parser/json.rb +149 -0
  43. data/lib/tilia/v_object/parser/mime_dir.rb +543 -0
  44. data/lib/tilia/v_object/parser/parser.rb +61 -0
  45. data/lib/tilia/v_object/parser/xml/element/key_value.rb +60 -0
  46. data/lib/tilia/v_object/parser/xml/element.rb +11 -0
  47. data/lib/tilia/v_object/parser/xml.rb +322 -0
  48. data/lib/tilia/v_object/parser.rb +10 -0
  49. data/lib/tilia/v_object/property/binary.rb +96 -0
  50. data/lib/tilia/v_object/property/boolean.rb +57 -0
  51. data/lib/tilia/v_object/property/flat_text.rb +52 -0
  52. data/lib/tilia/v_object/property/float_value.rb +107 -0
  53. data/lib/tilia/v_object/property/i_calendar/cal_address.rb +49 -0
  54. data/lib/tilia/v_object/property/i_calendar/date.rb +15 -0
  55. data/lib/tilia/v_object/property/i_calendar/date_time.rb +330 -0
  56. data/lib/tilia/v_object/property/i_calendar/duration.rb +65 -0
  57. data/lib/tilia/v_object/property/i_calendar/period.rb +124 -0
  58. data/lib/tilia/v_object/property/i_calendar/recur.rb +173 -0
  59. data/lib/tilia/v_object/property/i_calendar.rb +14 -0
  60. data/lib/tilia/v_object/property/integer_value.rb +60 -0
  61. data/lib/tilia/v_object/property/text.rb +352 -0
  62. data/lib/tilia/v_object/property/time.rb +85 -0
  63. data/lib/tilia/v_object/property/unknown.rb +30 -0
  64. data/lib/tilia/v_object/property/uri.rb +78 -0
  65. data/lib/tilia/v_object/property/utc_offset.rb +56 -0
  66. data/lib/tilia/v_object/property/v_card/date.rb +31 -0
  67. data/lib/tilia/v_object/property/v_card/date_and_or_time.rb +343 -0
  68. data/lib/tilia/v_object/property/v_card/date_time.rb +22 -0
  69. data/lib/tilia/v_object/property/v_card/language_tag.rb +41 -0
  70. data/lib/tilia/v_object/property/v_card/time_stamp.rb +74 -0
  71. data/lib/tilia/v_object/property/v_card.rb +13 -0
  72. data/lib/tilia/v_object/property.rb +532 -0
  73. data/lib/tilia/v_object/reader.rb +73 -0
  74. data/lib/tilia/v_object/recur/event_iterator.rb +417 -0
  75. data/lib/tilia/v_object/recur/no_instances_exception.rb +11 -0
  76. data/lib/tilia/v_object/recur/r_date_iterator.rb +138 -0
  77. data/lib/tilia/v_object/recur/r_rule_iterator.rb +717 -0
  78. data/lib/tilia/v_object/recur.rb +10 -0
  79. data/lib/tilia/v_object/settings.rb +32 -0
  80. data/lib/tilia/v_object/splitter/i_calendar.rb +95 -0
  81. data/lib/tilia/v_object/splitter/splitter_interface.rb +31 -0
  82. data/lib/tilia/v_object/splitter/v_card.rb +56 -0
  83. data/lib/tilia/v_object/splitter.rb +9 -0
  84. data/lib/tilia/v_object/string_util.rb +58 -0
  85. data/lib/tilia/v_object/time_zone_data/exchange_zones.rb +96 -0
  86. data/lib/tilia/v_object/time_zone_data/lotus_zones.rb +104 -0
  87. data/lib/tilia/v_object/time_zone_data/php_zones.rb +49 -0
  88. data/lib/tilia/v_object/time_zone_data/windows_zones.rb +121 -0
  89. data/lib/tilia/v_object/time_zone_data.rb +10 -0
  90. data/lib/tilia/v_object/time_zone_util.rb +213 -0
  91. data/lib/tilia/v_object/uuid_util.rb +51 -0
  92. data/lib/tilia/v_object/v_card_converter.rb +354 -0
  93. data/lib/tilia/v_object/version.rb +9 -0
  94. data/lib/tilia/v_object/writer.rb +56 -0
  95. data/lib/tilia/v_object.rb +45 -0
  96. data/lib/tilia/vobject.rb +1 -0
  97. data/resources/schema/xcal.rng +1192 -0
  98. data/resources/schema/xcard.rng +388 -0
  99. data/test/test_helper.rb +56 -0
  100. data/test/v_object/attach_issue_test.rb +19 -0
  101. data/test/v_object/birthday_calendar_generator_test.rb +463 -0
  102. data/test/v_object/cli_mock.rb +19 -0
  103. data/test/v_object/cli_test.rb +460 -0
  104. data/test/v_object/component/available_test.rb +59 -0
  105. data/test/v_object/component/v_alarm_test.rb +160 -0
  106. data/test/v_object/component/v_availability_test.rb +388 -0
  107. data/test/v_object/component/v_calendar_test.rb +646 -0
  108. data/test/v_object/component/v_card_test.rb +258 -0
  109. data/test/v_object/component/v_event_test.rb +85 -0
  110. data/test/v_object/component/v_free_busy_test.rb +59 -0
  111. data/test/v_object/component/v_journal_test.rb +85 -0
  112. data/test/v_object/component/v_time_zone_test.rb +47 -0
  113. data/test/v_object/component/v_todo_test.rb +172 -0
  114. data/test/v_object/component_test.rb +419 -0
  115. data/test/v_object/date_time_parser_test.rb +526 -0
  116. data/test/v_object/document_test.rb +71 -0
  117. data/test/v_object/element_list_test.rb +27 -0
  118. data/test/v_object/em_client_test.rb +53 -0
  119. data/test/v_object/empty_parameter_test.rb +65 -0
  120. data/test/v_object/empty_value_issue_test.rb +25 -0
  121. data/test/v_object/fake_component.rb +21 -0
  122. data/test/v_object/free_busy_data_test.rb +285 -0
  123. data/test/v_object/free_busy_generator_test.rb +637 -0
  124. data/test/v_object/google_colon_escaping_test.rb +27 -0
  125. data/test/v_object/i_calendar/attach_parse_test.rb +24 -0
  126. data/test/v_object/i_tip/broker_attendee_reply_test.rb +1042 -0
  127. data/test/v_object/i_tip/broker_delete_event_test.rb +175 -0
  128. data/test/v_object/i_tip/broker_new_event_test.rb +440 -0
  129. data/test/v_object/i_tip/broker_process_message_test.rb +153 -0
  130. data/test/v_object/i_tip/broker_process_reply_test.rb +402 -0
  131. data/test/v_object/i_tip/broker_tester.rb +71 -0
  132. data/test/v_object/i_tip/broker_update_event_test.rb +763 -0
  133. data/test/v_object/i_tip/evolution_test.rb +2644 -0
  134. data/test/v_object/i_tip/message_test.rb +25 -0
  135. data/test/v_object/issue153.vcf +352 -0
  136. data/test/v_object/issue153_test.rb +12 -0
  137. data/test/v_object/issue26_test.rb +25 -0
  138. data/test/v_object/issue36_work_around_test.rb +37 -0
  139. data/test/v_object/issue40_test.rb +26 -0
  140. data/test/v_object/issue64.vcf +351 -0
  141. data/test/v_object/issue64_test.rb +17 -0
  142. data/test/v_object/issue96_test.rb +22 -0
  143. data/test/v_object/issue_undefined_index_test.rb +24 -0
  144. data/test/v_object/j_cal_test.rb +150 -0
  145. data/test/v_object/j_card_test.rb +192 -0
  146. data/test/v_object/line_folding_issue_test.rb +19 -0
  147. data/test/v_object/mock_document.rb +6 -0
  148. data/test/v_object/parameter_test.rb +109 -0
  149. data/test/v_object/parser/json_test.rb +370 -0
  150. data/test/v_object/parser/mime_dir_test.rb +14 -0
  151. data/test/v_object/parser/quoted_printable_test.rb +78 -0
  152. data/test/v_object/parser/xml_test.rb +2563 -0
  153. data/test/v_object/property/binary_test.rb +12 -0
  154. data/test/v_object/property/boolean_test.rb +18 -0
  155. data/test/v_object/property/compound_test.rb +43 -0
  156. data/test/v_object/property/float_test.rb +20 -0
  157. data/test/v_object/property/i_calendar/cal_address_test.rb +26 -0
  158. data/test/v_object/property/i_calendar/date_time_test.rb +303 -0
  159. data/test/v_object/property/i_calendar/duration_test.rb +14 -0
  160. data/test/v_object/property/i_calendar/recur_test.rb +39 -0
  161. data/test/v_object/property/text_test.rb +81 -0
  162. data/test/v_object/property/v_card/date_and_or_time_test.rb +205 -0
  163. data/test/v_object/property/v_card/language_tag_test.rb +35 -0
  164. data/test/v_object/property_test.rb +338 -0
  165. data/test/v_object/reader_test.rb +403 -0
  166. data/test/v_object/recur/event_iterator/by_month_in_daily_test.rb +52 -0
  167. data/test/v_object/recur/event_iterator/by_set_pos_hang_test.rb +55 -0
  168. data/test/v_object/recur/event_iterator/expand_floating_times_test.rb +109 -0
  169. data/test/v_object/recur/event_iterator/fifth_tuesday_problem_test.rb +45 -0
  170. data/test/v_object/recur/event_iterator/incorrect_expand_test.rb +53 -0
  171. data/test/v_object/recur/event_iterator/infinite_loop_problem_test.rb +75 -0
  172. data/test/v_object/recur/event_iterator/issue48_test.rb +43 -0
  173. data/test/v_object/recur/event_iterator/issue50_test.rb +123 -0
  174. data/test/v_object/recur/event_iterator/main_test.rb +1222 -0
  175. data/test/v_object/recur/event_iterator/missing_overridden_test.rb +55 -0
  176. data/test/v_object/recur/event_iterator/no_instances_test.rb +32 -0
  177. data/test/v_object/recur/event_iterator/override_first_event_test.rb +106 -0
  178. data/test/v_object/recur/r_date_iterator_test.rb +44 -0
  179. data/test/v_object/recur/r_rule_iterator_test.rb +608 -0
  180. data/test/v_object/recurrence_iterator/UntilRespectsTimezoneTest.ics +39 -0
  181. data/test/v_object/slash_r_test.rb +15 -0
  182. data/test/v_object/splitter/i_calendar_test.rb +299 -0
  183. data/test/v_object/splitter/v_card_test.rb +173 -0
  184. data/test/v_object/string_util_test.rb +37 -0
  185. data/test/v_object/test_case.rb +42 -0
  186. data/test/v_object/time_zone_util_test.rb +271 -0
  187. data/test/v_object/uuid_util_test.rb +18 -0
  188. data/test/v_object/v_card21_test.rb +43 -0
  189. data/test/v_object/v_card_converter_test.rb +419 -0
  190. data/test/v_object/version_test.rb +15 -0
  191. data/test/v_object/writer_test.rb +33 -0
  192. data/tilia-vobject.gemspec +17 -0
  193. metadata +308 -0
@@ -0,0 +1,149 @@
1
+ require 'json'
2
+ module Tilia
3
+ module VObject
4
+ module Parser
5
+ # Json Parser.
6
+ #
7
+ # This parser parses both the jCal and jCard formats.
8
+ class Json < Parser
9
+ # The input data.
10
+ #
11
+ # @var array
12
+ # RUBY: attr_accessor :input
13
+
14
+ # Root component.
15
+ #
16
+ # @var Document
17
+ # RUBY: attr_accessor :root
18
+
19
+ # This method starts the parsing process.
20
+ #
21
+ # If the input was not supplied during construction, it's possible to pass
22
+ # it here instead.
23
+ #
24
+ # If either input or options are not supplied, the defaults will be used.
25
+ #
26
+ # @param resource|string|array|null input
27
+ # @param int options
28
+ #
29
+ # @return Sabre\VObject\Document
30
+ def parse(input = nil, options = 0)
31
+ self.input = input unless input.nil?
32
+ if @input.nil?
33
+ fail Tilia::VObject::EofException, 'End of input stream, or no input supplied'
34
+ end
35
+
36
+ @options = options if 0 != options
37
+
38
+ case @input[0]
39
+ when 'vcalendar'
40
+ @root = Tilia::VObject::Component::VCalendar.new({}, false)
41
+ when 'vcard'
42
+ @root = Tilia::VObject::Component::VCard.new({}, false)
43
+ else
44
+ fail Tilia::VObject::ParseException, 'The root component must either be a vcalendar, or a vcard'
45
+ end
46
+
47
+ @input[1].each do |prop|
48
+ @root.add(parse_property(prop))
49
+ end
50
+ if @input[2]
51
+ @input[2].each do |comp|
52
+ @root.add(parse_component(comp))
53
+ end
54
+ end
55
+
56
+ # Resetting the input so we can throw an feof exception the next time.
57
+ @input = nil
58
+
59
+ @root
60
+ end
61
+
62
+ # Parses a component.
63
+ #
64
+ # @param array j_comp
65
+ #
66
+ # @return \Sabre\VObject\Component
67
+ def parse_component(j_comp)
68
+ properties = j_comp[1].map do |j_prop|
69
+ parse_property(j_prop)
70
+ end
71
+
72
+ if j_comp[2]
73
+ components = j_comp[2].map do |j|
74
+ parse_component(j)
75
+ end
76
+ else
77
+ components = []
78
+ end
79
+
80
+ @root.create_component(
81
+ j_comp[0],
82
+ components + properties,
83
+ false
84
+ )
85
+ end
86
+
87
+ # Parses properties.
88
+ #
89
+ # @param array j_prop
90
+ #
91
+ # @return \Sabre\VObject\Property
92
+ def parse_property(j_prop)
93
+ (
94
+ property_name,
95
+ parameters,
96
+ value_type
97
+ ) = j_prop
98
+
99
+ property_name = property_name.upcase
100
+
101
+ # This is the default class we would be using if we didn't know the
102
+ # value type. We're using this value later in this function.
103
+ default_property_class = @root.class_name_for_property_name(property_name)
104
+
105
+ # parameters = (array)parameters
106
+
107
+ value = j_prop[3..-1]
108
+
109
+ value_type = value_type.upcase
110
+
111
+ if parameters.key?('group')
112
+ property_name = parameters['group'] + '.' + property_name
113
+ parameters.delete('group')
114
+ end
115
+
116
+ prop = @root.create_property(property_name, nil, parameters, value_type)
117
+ prop.json_value = value
118
+
119
+ # We have to do something awkward here. FlatText as well as Text
120
+ # represents TEXT values. We have to normalize these here. In the
121
+ # future we can get rid of FlatText once we're allowed to break BC
122
+ # again.
123
+ if default_property_class == Tilia::VObject::Property::FlatText
124
+ default_property_class = Tilia::VObject::Property::Text
125
+ end
126
+
127
+ # If the value type we received (e.g.: TEXT) was not the default value
128
+ # type for the given property (e.g.: BDAY), we need to add a VALUE=
129
+ # parameter.
130
+ prop['VALUE'] = value_type if default_property_class != prop.class
131
+
132
+ prop
133
+ end
134
+
135
+ # Sets the input data.
136
+ #
137
+ # @param resource|string|array input
138
+ #
139
+ # @return void
140
+ def input=(input)
141
+ input = input.readlines.join('') if input.respond_to?(:readlines)
142
+ input = JSON.parse(input) if input.is_a?(String)
143
+
144
+ @input = input
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,543 @@
1
+ require 'stringio'
2
+ module Tilia
3
+ module VObject
4
+ module Parser
5
+ # MimeDir parser.
6
+ #
7
+ # This class parses iCalendar 2.0 and vCard 2.1, 3.0 and 4.0 files. This
8
+ # parser will return one of the following two objects from the parse method:
9
+ #
10
+ # Sabre\VObject\Component\VCalendar
11
+ # Sabre\VObject\Component\VCard
12
+ class MimeDir < Parser
13
+ # The input stream.
14
+ #
15
+ # @var resource
16
+ # RUBY: attr_accessor :input
17
+
18
+ # Root component.
19
+ #
20
+ # @var Component
21
+ # RUBY: attr_accessor :root
22
+
23
+ # Parses an iCalendar or vCard file.
24
+ #
25
+ # Pass a stream or a string. If null is parsed, the existing buffer is
26
+ # used.
27
+ #
28
+ # @param string|resource|null input
29
+ # @param int options
30
+ #
31
+ # @return Sabre\VObject\Document
32
+ def parse(input = nil, options = 0)
33
+ @root = nil
34
+
35
+ self.input = input unless input.nil?
36
+ @options = options if options != 0
37
+
38
+ parse_document
39
+
40
+ @root
41
+ end
42
+
43
+ # Sets the input buffer. Must be a string or stream.
44
+ #
45
+ # @param resource|string input
46
+ #
47
+ # @return void
48
+ def input=(input)
49
+ # Resetting the parser
50
+ @line_index = 0
51
+ @start_line = 0
52
+
53
+ if input.is_a?(String)
54
+ # Convering to a stream.
55
+ stream = StringIO.new
56
+ stream.write(input)
57
+ stream.rewind
58
+ @input = stream
59
+ elsif input.respond_to?(:readlines)
60
+ @input = input
61
+ else
62
+ fail ArgumentError, 'This parser can only read from strings or streams.'
63
+ end
64
+ end
65
+
66
+ protected
67
+
68
+ # Parses an entire document.
69
+ #
70
+ # @return void
71
+ def parse_document
72
+ line = read_line
73
+
74
+ # BOM is ZERO WIDTH NO-BREAK SPACE (U+FEFF).
75
+ # It's 0xEF 0xBB 0xBF in UTF-8 hex.
76
+ line.sub!("\xEF\xBB\xBF", '')
77
+
78
+ case line.upcase
79
+ when 'BEGIN:VCALENDAR'
80
+ klass = Tilia::VObject::Component::VCalendar
81
+ when 'BEGIN:VCARD'
82
+ klass = Tilia::VObject::Component::VCard
83
+ else
84
+ fail Tilia::VObject::ParseException, 'This parser only supports VCARD and VCALENDAR files'
85
+ end
86
+
87
+ @root = klass.new({}, false)
88
+
89
+ loop do
90
+ # Reading until we hit END:
91
+ line = read_line
92
+ break if line[0...4].upcase == 'END:'
93
+ result = parse_line(line)
94
+ @root.add(result) if result
95
+ end
96
+
97
+ name = line[4..-1].upcase
98
+ if name != @root.name
99
+ fail Tilia::VObject::ParseException, "Invalid MimeDir file. expected: \"END:#{@root.name}\" got: \"END:#{name}\""
100
+ end
101
+ end
102
+
103
+ # Parses a line, and if it hits a component, it will also attempt to parse
104
+ # the entire component.
105
+ #
106
+ # @param string line Unfolded line
107
+ #
108
+ # @return Node
109
+ def parse_line(line)
110
+ # Start of a new component
111
+ if line[0...6].upcase == 'BEGIN:'
112
+ component = @root.create_component(line[6..-1], [], false)
113
+
114
+ loop do
115
+ # Reading until we hit END:
116
+ line = read_line
117
+ break if line[0...4].upcase == 'END:'
118
+
119
+ result = parse_line(line)
120
+ component.add(result) if result
121
+ end
122
+
123
+ name = line[4..-1].upcase
124
+ if name != component.name
125
+ fail Tilia::VObject::ParseException, "Invalid MimeDir file. expected: \"END:#{component.name}\" got: \"END:#{name}\""
126
+ end
127
+
128
+ component
129
+ else
130
+ # Property reader
131
+ property = read_property(line)
132
+ unless property
133
+ # Ignored line
134
+ return false
135
+ end
136
+
137
+ property
138
+ end
139
+ end
140
+
141
+ # We need to look ahead 1 line every time to see if we need to 'unfold'
142
+ # the next line.
143
+ #
144
+ # If that was not the case, we store it here.
145
+ #
146
+ # @var null|string
147
+ # RUBY: attr_accessor :protected line_buffer
148
+
149
+ # The real current line number.
150
+ # RUBY: attr_accessor :protected line_index
151
+
152
+ # In the case of unfolded lines, this property holds the line number for
153
+ # the start of the line.
154
+ #
155
+ # @var int
156
+ # RUBY: attr_accessor :start_line
157
+
158
+ # Contains a 'raw' representation of the current line.
159
+ #
160
+ # @var string
161
+ # RUBY: attr_accessor :raw_line
162
+
163
+ # Reads a single line from the buffer.
164
+ #
165
+ # This method strips any newlines and also takes care of unfolding.
166
+ #
167
+ # @throws \Sabre\VObject\EofException
168
+ #
169
+ # @return string
170
+ def read_line
171
+ if !@line_buffer.nil?
172
+ raw_line = @line_buffer
173
+ @line_buffer = nil
174
+ else
175
+ loop do
176
+ if @input.eof?
177
+ fail Tilia::VObject::EofException, 'End of document reached prematurely'
178
+ end
179
+
180
+ raw_line = @input.readline
181
+
182
+ unless raw_line
183
+ fail Tilia::VObject::ParseException, 'Error reading from input stream'
184
+ end
185
+
186
+ raw_line.chomp!
187
+ break unless raw_line == '' # Skipping empty lines
188
+ end
189
+
190
+ @line_index += 1
191
+ end
192
+ line = raw_line
193
+
194
+ @start_line = @line_index
195
+
196
+ # Looking ahead for folded lines.
197
+ loop do
198
+ begin
199
+ next_line = @input.readline.chomp
200
+ rescue EOFError
201
+ next_line = ''
202
+ end
203
+
204
+ @line_index += 1
205
+ break if next_line == ''
206
+ if next_line[0] == "\t" || next_line[0] == ' '
207
+ line += next_line[1..-1]
208
+ raw_line += "\n " + next_line[1..-1]
209
+ else
210
+ @line_buffer = next_line
211
+ break
212
+ end
213
+ end
214
+
215
+ @raw_line = raw_line
216
+ line
217
+ end
218
+
219
+ # Reads a property or component from a line.
220
+ #
221
+ # @return void
222
+ def read_property(line)
223
+ if @options & self.class::OPTION_FORGIVING > 0
224
+ prop_name_token = 'A-Z0-9\\-\\._\\/'
225
+ else
226
+ prop_name_token = 'A-Z0-9\\-\\.'
227
+ end
228
+
229
+ param_name_token = 'A-Z0-9\\-'
230
+ safe_char = '^";:,'
231
+ q_safe_char = '^"'
232
+
233
+ regex = /
234
+ ^(?<name> [#{prop_name_token}]+ ) (?=[;:]) # property name
235
+ |
236
+ (?<=:)(?<propValue> .+)$ # property value
237
+ |
238
+ ;(?<paramName> [#{param_name_token}]+) (?=[=;:]) # parameter name
239
+ |
240
+ (=|,)(?<paramValue> # parameter value
241
+ (?: [#{safe_char}]*) |
242
+ \"(?: [#{q_safe_char}]+)\"
243
+ ) (?=[;:,])
244
+ /xi
245
+
246
+ matches = line.scan(regex)
247
+
248
+ property = {
249
+ 'name' => nil,
250
+ 'parameters' => {},
251
+ 'value' => nil
252
+ }
253
+
254
+ last_param = nil
255
+
256
+ # Looping through all the tokens.
257
+ #
258
+ # Note that we are looping through them in reverse order, because if a
259
+ # sub-pattern matched, the subsequent named patterns will not show up
260
+ # in the result.
261
+ matches.each do |match|
262
+ match = Hash[['name', 'propValue', 'paramName', 'paramValue'].zip(match)]
263
+ match.delete_if { |_k, v| v.nil? }
264
+
265
+ if match.key?('paramValue')
266
+ if match['paramValue'] && match['paramValue'][0] == '"'
267
+ value = match['paramValue'][1..-2]
268
+ else
269
+ value = match['paramValue']
270
+ end
271
+
272
+ value = unescape_param(value)
273
+
274
+ if last_param.nil?
275
+ fail Tilia::VObject::ParseException, "Invalid Mimedir file. Line starting at #{@start_line} did not follow iCalendar/vCard conventions"
276
+ end
277
+
278
+ if property['parameters'][last_param].nil?
279
+ property['parameters'][last_param] = value
280
+ elsif property['parameters'][last_param].is_a?(Array)
281
+ property['parameters'][last_param] << value
282
+ else
283
+ property['parameters'][last_param] = [
284
+ property['parameters'][last_param],
285
+ value
286
+ ]
287
+ end
288
+ next
289
+ end
290
+
291
+ if match.key?('paramName')
292
+ last_param = match['paramName'].upcase
293
+ unless property['parameters'].key?(last_param)
294
+ property['parameters'][last_param] = nil
295
+ end
296
+ next
297
+ end
298
+ if match.key?('propValue')
299
+ property['value'] = match['propValue']
300
+ next
301
+ end
302
+ if match.key?('name') && !match['name'].blank?
303
+ property['name'] = match['name'].upcase
304
+ next
305
+ end
306
+
307
+ # @codeCoverageIgnoreStart
308
+ fail 'This code should not be reachable'
309
+ # @codeCoverageIgnoreEnd
310
+ end
311
+
312
+ property['value'] = '' if property['value'].nil?
313
+ if property['name'].blank?
314
+ if @options & self.class::OPTION_IGNORE_INVALID_LINES > 0
315
+ return false
316
+ end
317
+ fail Tilia::VObject::ParseException, "Invalid Mimedir file. Line starting at #{@start_line} did not follow iCalendar/vCard conventions"
318
+ end
319
+
320
+ # vCard 2.1 states that parameters may appear without a name, and only
321
+ # a value. We can deduce the value based on it's name.
322
+ #
323
+ # Our parser will get those as parameters without a value instead, so
324
+ # we're filtering these parameters out first.
325
+ named_parameters = {}
326
+ nameless_parameters = []
327
+
328
+ property['parameters'].each do |name, value|
329
+ if !value.nil?
330
+ named_parameters[name] = value
331
+ else
332
+ nameless_parameters << name
333
+ end
334
+ end
335
+
336
+ prop_obj = @root.create_property(property['name'], nil, named_parameters)
337
+
338
+ nameless_parameters.each do |nameless_parameter|
339
+ prop_obj.add(nil, nameless_parameter)
340
+ end
341
+
342
+ if prop_obj.key?('ENCODING') && prop_obj['ENCODING'].to_s.upcase == 'QUOTED-PRINTABLE'
343
+ prop_obj.quoted_printable_value = extract_quoted_printable_value
344
+ else
345
+ prop_obj.raw_mime_dir_value = property['value']
346
+ end
347
+
348
+ prop_obj
349
+ end
350
+
351
+ public
352
+
353
+ # Unescapes a property value.
354
+ #
355
+ # vCard 2.1 says:
356
+ # * Semi-colons must be escaped in some property values, specifically
357
+ # ADR, ORG and N.
358
+ # * Semi-colons must be escaped in parameter values, because semi-colons
359
+ # are also use to separate values.
360
+ # * No mention of escaping backslashes with another backslash.
361
+ # * newlines are not escaped either, instead QUOTED-PRINTABLE is used to
362
+ # span values over more than 1 line.
363
+ #
364
+ # vCard 3.0 says:
365
+ # * (rfc2425) Backslashes, newlines (\n or \N) and comma's must be
366
+ # escaped, all time time.
367
+ # * Comma's are used for delimeters in multiple values
368
+ # * (rfc2426) Adds to to this that the semi-colon MUST also be escaped,
369
+ # as in some properties semi-colon is used for separators.
370
+ # * Properties using semi-colons: N, ADR, GEO, ORG
371
+ # * Both ADR and N's individual parts may be broken up further with a
372
+ # comma.
373
+ # * Properties using commas: NICKNAME, CATEGORIES
374
+ #
375
+ # vCard 4.0 (rfc6350) says:
376
+ # * Commas must be escaped.
377
+ # * Semi-colons may be escaped, an unescaped semi-colon _may_ be a
378
+ # delimiter, depending on the property.
379
+ # * Backslashes must be escaped
380
+ # * Newlines must be escaped as either \N or \n.
381
+ # * Some compound properties may contain multiple parts themselves, so a
382
+ # comma within a semi-colon delimited property may also be unescaped
383
+ # to denote multiple parts _within_ the compound property.
384
+ # * Text-properties using semi-colons: N, ADR, ORG, CLIENTPIDMAP.
385
+ # * Text-properties using commas: NICKNAME, RELATED, CATEGORIES, PID.
386
+ #
387
+ # Even though the spec says that commas must always be escaped, the
388
+ # example for GEO in Section 6.5.2 seems to violate this.
389
+ #
390
+ # iCalendar 2.0 (rfc5545) says:
391
+ # * Commas or semi-colons may be used as delimiters, depending on the
392
+ # property.
393
+ # * Commas, semi-colons, backslashes, newline (\N or \n) are always
394
+ # escaped, unless they are delimiters.
395
+ # * Colons shall not be escaped.
396
+ # * Commas can be considered the 'default delimiter' and is described as
397
+ # the delimiter in cases where the order of the multiple values is
398
+ # insignificant.
399
+ # * Semi-colons are described as the delimiter for 'structured values'.
400
+ # They are specifically used in Semi-colons are used as a delimiter in
401
+ # REQUEST-STATUS, RRULE, GEO and EXRULE. EXRULE is deprecated however.
402
+ #
403
+ # Now for the parameters
404
+ #
405
+ # If delimiter is not set (null) this method will just return a string.
406
+ # If it's a comma or a semi-colon the string will be split on those
407
+ # characters, and always return an array.
408
+ #
409
+ # @param string input
410
+ # @param string delimiter
411
+ #
412
+ # @return string|string[]
413
+ def self.unescape_value(input, delimiter = ';')
414
+ regex = '(?: (\\\\ (?: \\\\ | N | n | ; | , ) )'
415
+ regex += ' | (' + delimiter + ')' unless delimiter.blank?
416
+ regex += ')'
417
+
418
+ regexp = Regexp.compile(regex, Regexp::EXTENDED)
419
+ matches = input.split(regexp)
420
+
421
+ result_array = []
422
+ result = ''
423
+
424
+ matches.each do |match|
425
+ case match
426
+ when '\\\\'
427
+ result += '\\'
428
+ when '\\N', '\\n'
429
+ result += "\n"
430
+ when '\\;'
431
+ result += ';'
432
+ when '\\,'
433
+ result += ','
434
+ when delimiter
435
+ result_array << result
436
+ result = ''
437
+ else
438
+ result += match
439
+ end
440
+ end
441
+
442
+ result_array << result
443
+ delimiter ? result_array : result
444
+ end
445
+
446
+ private
447
+
448
+ # Unescapes a parameter value.
449
+ #
450
+ # vCard 2.1:
451
+ # * Does not mention a mechanism for this. In addition, double quotes
452
+ # are never used to wrap values.
453
+ # * This means that parameters can simply not contain colons or
454
+ # semi-colons.
455
+ #
456
+ # vCard 3.0 (rfc2425, rfc2426):
457
+ # * Parameters _may_ be surrounded by double quotes.
458
+ # * If this is not the case, semi-colon, colon and comma may simply not
459
+ # occur (the comma used for multiple parameter values though).
460
+ # * If it is surrounded by double-quotes, it may simply not contain
461
+ # double-quotes.
462
+ # * This means that a parameter can in no case encode double-quotes, or
463
+ # newlines.
464
+ #
465
+ # vCard 4.0 (rfc6350)
466
+ # * Behavior seems to be identical to vCard 3.0
467
+ #
468
+ # iCalendar 2.0 (rfc5545)
469
+ # * Behavior seems to be identical to vCard 3.0
470
+ #
471
+ # Parameter escaping mechanism (rfc6868) :
472
+ # * This rfc describes a new way to escape parameter values.
473
+ # * New-line is encoded as ^n
474
+ # * ^ is encoded as ^^.
475
+ # * " is encoded as ^'
476
+ #
477
+ # @param string input
478
+ #
479
+ # @return void
480
+ def unescape_param(input)
481
+ input.gsub(/(\^(\^|n|\'))/) do |match|
482
+ case match
483
+ when '^n'
484
+ "\n"
485
+ when '^^'
486
+ '^'
487
+ when '^\''
488
+ '"'
489
+ end
490
+ end
491
+ end
492
+
493
+ # Gets the full quoted printable value.
494
+ #
495
+ # We need a special method for this, because newlines have both a meaning
496
+ # in vCards, and in QuotedPrintable.
497
+ #
498
+ # This method does not do any decoding.
499
+ #
500
+ # @return string
501
+ def extract_quoted_printable_value
502
+ # We need to parse the raw line again to get the start of the value.
503
+ #
504
+ # We are basically looking for the first colon (:), but we need to
505
+ # skip over the parameters first, as they may contain one.
506
+ regex = /^
507
+ (?: [^:])+ # Anything but a colon
508
+ (?: "[^"]")* # A parameter in double quotes
509
+ : # start of the value we really care about
510
+ (.*)$
511
+ /xm
512
+
513
+ matches = regex.match(@raw_line)
514
+
515
+ value = matches[1]
516
+ # Removing the first whitespace character from every line. Kind of
517
+ # like unfolding, but we keep the newline.
518
+ value = value.gsub("\n ", "\n")
519
+
520
+ # Microsoft products don't always correctly fold lines, they may be
521
+ # missing a whitespace. So if 'forgiving' is turned on, we will take
522
+ # those as well.
523
+ if @options & self.class::OPTION_FORGIVING > 0
524
+ while value[-1] == '='
525
+ # Reading the line
526
+ read_line
527
+ # Grabbing the raw form
528
+ value += "\n" + @raw_line
529
+ end
530
+ end
531
+
532
+ value
533
+ end
534
+
535
+ def initialize(*args)
536
+ super(*args)
537
+ @start_line = 0
538
+ @line_index = 0
539
+ end
540
+ end
541
+ end
542
+ end
543
+ end