vobject 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/macos.yml +38 -0
  3. data/.github/workflows/ubuntu.yml +56 -0
  4. data/.github/workflows/windows.yml +40 -0
  5. data/.gitignore +9 -0
  6. data/.hound.yml +3 -0
  7. data/.rspec +2 -0
  8. data/.rubocop.tb.yml +650 -0
  9. data/.rubocop.yml +1077 -0
  10. data/CODE_OF_CONDUCT.md +49 -0
  11. data/Gemfile +4 -0
  12. data/LICENSE.txt +25 -0
  13. data/README.adoc +151 -0
  14. data/Rakefile +6 -0
  15. data/bin/console +14 -0
  16. data/bin/setup +8 -0
  17. data/lib/c.rb +173 -0
  18. data/lib/error.rb +19 -0
  19. data/lib/vcalendar.rb +77 -0
  20. data/lib/vcard.rb +67 -0
  21. data/lib/vobject.rb +25 -0
  22. data/lib/vobject/component.rb +126 -0
  23. data/lib/vobject/parameter.rb +116 -0
  24. data/lib/vobject/parametervalue.rb +26 -0
  25. data/lib/vobject/property.rb +162 -0
  26. data/lib/vobject/propertyvalue.rb +46 -0
  27. data/lib/vobject/vcalendar/component.rb +106 -0
  28. data/lib/vobject/vcalendar/grammar.rb +595 -0
  29. data/lib/vobject/vcalendar/paramcheck.rb +259 -0
  30. data/lib/vobject/vcalendar/propertyparent.rb +98 -0
  31. data/lib/vobject/vcalendar/propertyvalue.rb +606 -0
  32. data/lib/vobject/vcalendar/typegrammars.rb +605 -0
  33. data/lib/vobject/vcard/v3_0/component.rb +40 -0
  34. data/lib/vobject/vcard/v3_0/grammar.rb +175 -0
  35. data/lib/vobject/vcard/v3_0/paramcheck.rb +110 -0
  36. data/lib/vobject/vcard/v3_0/parameter.rb +17 -0
  37. data/lib/vobject/vcard/v3_0/property.rb +18 -0
  38. data/lib/vobject/vcard/v3_0/propertyvalue.rb +401 -0
  39. data/lib/vobject/vcard/v3_0/typegrammars.rb +425 -0
  40. data/lib/vobject/vcard/v4_0/component.rb +40 -0
  41. data/lib/vobject/vcard/v4_0/grammar.rb +224 -0
  42. data/lib/vobject/vcard/v4_0/paramcheck.rb +269 -0
  43. data/lib/vobject/vcard/v4_0/parameter.rb +18 -0
  44. data/lib/vobject/vcard/v4_0/property.rb +63 -0
  45. data/lib/vobject/vcard/v4_0/propertyvalue.rb +404 -0
  46. data/lib/vobject/vcard/v4_0/typegrammars.rb +539 -0
  47. data/lib/vobject/version.rb +3 -0
  48. data/vobject.gemspec +32 -0
  49. metadata +174 -0
@@ -0,0 +1,46 @@
1
+ module Vobject
2
+ class PropertyValue
3
+ attr_accessor :value, :type, :errors, :norm
4
+
5
+ def <=>(another)
6
+ self.value <=> another.value
7
+ end
8
+
9
+ def initialize(val)
10
+ self.value = val
11
+ self.type = "text" # safe default
12
+ self.norm = nil
13
+ end
14
+
15
+ # raise_invalid_initialization if key != name
16
+
17
+ def to_s
18
+ value
19
+ end
20
+
21
+ def to_norm
22
+ if norm.nil?
23
+ norm = to_s
24
+ end
25
+ norm
26
+ end
27
+
28
+ def to_hash
29
+ value
30
+ end
31
+
32
+ def name
33
+ type
34
+ end
35
+
36
+ private
37
+
38
+ def default_value_type
39
+ "text"
40
+ end
41
+
42
+ def raise_invalid_initialization
43
+ raise "vObject property initialization failed"
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,106 @@
1
+ require "vobject"
2
+ require "vobject/property"
3
+ require "vobject/vcalendar/grammar"
4
+ require "json"
5
+
6
+ class Vobject::Component::Vcalendar < Vobject::Component
7
+ attr_accessor :comp_name, :children
8
+
9
+ class << self
10
+ def parse(vcf, strict)
11
+ hash = Vobject::Vcalendar::Grammar.new(strict).parse(vcf)
12
+ comp_name = hash.keys.first
13
+
14
+ new comp_name, hash[comp_name], hash[:errors]
15
+ end
16
+
17
+ def initialize(key, cs)
18
+ # super key, cs
19
+ self.comp_name = key
20
+ raise_invalid_initialization if key != name
21
+
22
+ self.children = []
23
+ if cs.is_a?(Array)
24
+ cs.each do |component|
25
+ c = []
26
+ component.each_key do |k|
27
+ val = component[k]
28
+ # iteration of array || hash values is making the value a key!
29
+ next if k.class == Array
30
+ next if k.class == Hash
31
+ cc = child_class(k, val)
32
+ c << cc.new(k, val)
33
+ end
34
+ children << c
35
+ end
36
+ else
37
+ cs.each_key do |k|
38
+ val = cs[k]
39
+ # iteration of array || hash values is making the value a key!
40
+ next if k.class == Array
41
+ next if k.class == Hash
42
+ cc = child_class(k, val)
43
+ children << cc.new(k, val)
44
+ end
45
+ end
46
+ end
47
+
48
+ def child_class(key, val)
49
+ base_class = if key == :VTODO
50
+ Vobject::Component::Vcalendar::ToDo
51
+ elsif key == :VFREEBUSY
52
+ Vobject::Component::Vcalendar::FreeBusy
53
+ elsif key == :JOURNAL
54
+ Vobject::Component::Vcalendar::Journal
55
+ elsif key == :STANDARD
56
+ Vobject::Component::Vcalendar::Timezone::Standard
57
+ elsif key == :DAYLIGHT
58
+ Vobject::Component::Vcalendar::Timezone::Daylight
59
+ elsif key == :VTIMEZONE
60
+ Vobject::Component::Vcalendar::Timezone
61
+ elsif key == :VEVENT
62
+ Vobject::Component::Vcalendar::Event
63
+ elsif key == :VALARM
64
+ Vobject::Component::Vcalendar::Alarm
65
+ elsif key == :VAVAILABILITY
66
+ Vobject::Component::Vcalendar::Vavailability
67
+ elsif key == :AVAILABLE
68
+ Vobject::Component::Vcalendar::Vavailability::Available
69
+ elsif !(val.is_a?(Hash) && !val.has_key?(:value))
70
+ property_base_class
71
+ else
72
+ Vobject::Component::Vcalendar
73
+ end
74
+ return base_class if [:CLASS, :OBJECT, :METHOD].include? key
75
+ camelized_key = key.to_s.downcase.split("_").map(&:capitalize).join("")
76
+ base_class.const_get(camelized_key) rescue base_class
77
+ end
78
+
79
+ private
80
+
81
+ def raise_invalid_parsing
82
+ raise "Vobject component parse failed"
83
+ end
84
+ end
85
+ end
86
+
87
+ class Vobject::Component::Vcalendar::ToDo < Vobject::Component::Vcalendar
88
+ end
89
+ class Vobject::Component::Vcalendar::Freebusy < Vobject::Component::Vcalendar
90
+ end
91
+ class Vobject::Component::Vcalendar::Journal < Vobject::Component::Vcalendar
92
+ end
93
+ class Vobject::Component::Vcalendar::Timezone < Vobject::Component::Vcalendar
94
+ end
95
+ class Vobject::Component::Vcalendar::Timezone::Standard < Vobject::Component::Vcalendar::Timezone
96
+ end
97
+ class Vobject::Component::Vcalendar::Timezone::Daylight < Vobject::Component::Vcalendar::Timezone
98
+ end
99
+ class Vobject::Component::Vcalendar::Event < Vobject::Component::Vcalendar
100
+ end
101
+ class Vobject::Component::Vcalendar::Alarm < Vobject::Component::Vcalendar
102
+ end
103
+ class Vobject::Component::Vcalendar::Vavailability < Vobject::Component::Vcalendar
104
+ end
105
+ class Vobject::Component::Vcalendar::Vavailability::Available < Vobject::Component::Vcalendar
106
+ end
@@ -0,0 +1,595 @@
1
+ require "rsec"
2
+ require "set"
3
+ require "uri"
4
+ require "date"
5
+ require "tzinfo"
6
+ include Rsec::Helpers
7
+ require_relative "../../c"
8
+ require_relative "../../error"
9
+ require "vobject"
10
+ require "vobject/vcalendar/typegrammars"
11
+ require "vobject/vcalendar/paramcheck"
12
+
13
+ module Vobject::Vcalendar
14
+ class Grammar
15
+ include C
16
+ attr_accessor :strict, :errors
17
+
18
+ class << self
19
+ # RFC 6868
20
+ def rfc6868decode(x)
21
+ x.gsub(/\^n/, "\n").gsub(/\^\^/, "^").gsub(/\^'/, '"')
22
+ end
23
+
24
+ def unfold(str)
25
+ str.gsub(/(\r|\n|\r\n)[ \t]/, "")
26
+ end
27
+ end
28
+
29
+ def vobject_grammar
30
+ # properties with value cardinality 1
31
+ @cardinality1 = {}
32
+ @cardinality1[:ICAL] = Set.new [:PRODID, :VERSION, :CALSCALE, :METHOD, :UID, :LAST_MOD, :URL,
33
+ :REFRESH_INTERVAL, :SOURCE, :COLOR]
34
+ @cardinality1[:EVENT] = Set.new [:UID, :DTSTAMP, :DTSTART, :CLASS, :CREATED, :DESCRIPTION, :GEO, :LAST_MOD,
35
+ :LOCATION, :ORGANIZER, :PRIORITY, :SEQ, :STATUS, :TRANSP, :URL, :RECURID, :COLOR]
36
+ @cardinality1[:TODO] = Set.new [:UID, :DTSTAMP, :CLASS, :COMPLETED, :CREATED, :DESCRIPTION, :DTSTART, :GEO, :LAST_MOD,
37
+ :LOCATION, :ORGANIZER, :PERCENT_COMPLETE, :PRIORITY, :SEQ, :STATUS, :SUMMARY, :URL, :RECURID, :COLOR]
38
+ @cardinality1[:JOURNAL] = Set.new [:UID, :DTSTAMP, :CLASS, :CREATED, :DTSTART, :LAST_MOD,
39
+ :ORGANIZER, :SEQ, :STATUS, :SUMMARY, :URL, :RECURID, :COLOR]
40
+ @cardinality1[:FREEBUSY] = Set.new [:UID, :DTSTAMP, :CONTACT, :DTSTART, :DTEND, :ORGANIZER, :URL]
41
+ @cardinality1[:TIMEZONE] = Set.new [:TZID, :LAST_MOD, :TZURL]
42
+ @cardinality1[:TZ] = Set.new [:DTSTART, :TZOFFSETTTO, :TZOFFSETFROM]
43
+ @cardinality1[:ALARM] = Set.new [:ACTION, :TRIGGER, :DURATION, :REPEAT, :DESCRIPTION, :SUMMARY]
44
+ @cardinality1[:VAVAILABILITY] = Set.new [:UID, :DTSTAMP, :DTSTART, :BUSYTYPE, :CLASS, :CREATED, :DESCRIPTION, :LAST_MOD,
45
+ :LOCATION, :ORGANIZER, :PRIORITY, :SEQ, :SUMMARY, :URL]
46
+ @cardinality1[:AVAILABLE] = Set.new [:DTSTAMP, :DTSTART, :UID, :CREATED, :DESCRIPTION, :LAST_MOD, :LOCATION,
47
+ :RECURID, :RRULE, :SUMMARY]
48
+ @cardinality1[:PARAM] = Set.new [:FMTTYPE, :LANGUAGE, :ALTREP, :FBTYPE, :TRANSP, :CUTYPE, :MEMBER, :ROLE, :PARTSTAT, :RSVP, :DELEGATED_TO,
49
+ :DELEGATED_FROM, :SENT_BY, :CN, :DIR, :RANGE, :RELTYPE, :RELATED, :DISPLAY, :FEATURE, :LABEL]
50
+
51
+ group = C::IANATOKEN
52
+ linegroup = group << "."
53
+ beginend = /BEGIN/i.r | /END/i.r
54
+
55
+ # parameters && parameter types
56
+ paramname = /ALTREP/i.r | /CN/i.r | /CUTYPE/i.r | /DELEGATED-FROM/i.r | /DELEGATED-TO/i.r |
57
+ /DIR/i.r | /ENCODING/i.r | /FMTTYPE/i.r | /FBTYPE/i.r | /LANGUAGE/i.r |
58
+ /MEMBER/i.r | /PARTSTAT/i.r | /RANGE/i.r | /RELATED/i.r | /RELTYPE/i.r |
59
+ /ROLE/i.r | /RSVP/i.r | /SENT-BY/i.r | /TZID/i.r | /RSCALE/i.r | /DISPLAY/i.r |
60
+ /FEATURE/i.r | /LABEL/i.r | /EMAIL/i.r
61
+ otherparamname = C::XNAME_VCAL | seq("".r ^ paramname, C::IANATOKEN)[1]
62
+ paramvalue = C::QUOTEDSTRING_VCAL.map { |s| self.class.rfc6868decode s } |
63
+ C::PTEXT_VCAL.map { |s| self.class.rfc6868decode(s) }
64
+ quotedparamvalue = C::QUOTEDSTRING_VCAL.map { |s| self.class.rfc6868decode s }
65
+ cutypevalue = /INDIVIDUAL/i.r | /GROUP/i.r | /RESOURCE/i.r | /ROOM/i.r | /UNKNOWN/i.r |
66
+ C::XNAME_VCAL | C::IANATOKEN.map
67
+ encodingvalue = /8BIT/i.r | /BASE64/i.r
68
+ fbtypevalue = /FREE/i.r | /BUSY/i.r | /BUSY-UNAVAILABLE/i.r | /BUSY-TENTATIVE/i.r |
69
+ C::XNAME_VCAL | C::IANATOKEN
70
+ partstatevent = /NEEDS-ACTION/i.r | /ACCEPTED/i.r | /DECLINED/i.r | /TENTATIVE/i.r |
71
+ /DELEGATED/i.r | C::XNAME_VCAL | C::IANATOKEN
72
+ partstattodo = /NEEDS-ACTION/i.r | /ACCEPTED/i.r | /DECLINED/i.r | /TENTATIVE/i.r |
73
+ /DELEGATED/i.r | /COMPLETED/i.r | /IN-PROCESS/i.r | C::XNAME_VCAL | C::IANATOKEN
74
+ partstatjour = /NEEDS-ACTION/i.r | /ACCEPTED/i.r | /DECLINED/i.r | C::XNAME_VCAL | C::IANATOKEN
75
+ partstatvalue = partstatevent | partstattodo | partstatjour
76
+ rangevalue = /THISANDFUTURE/i.r
77
+ relatedvalue = /START/i.r | /END/i.r
78
+ reltypevalue = /PARENT/i.r | /CHILD/i.r | /SIBLING/i.r | C::XNAME_VCAL | C::IANATOKEN
79
+ tzidvalue = seq("/".r._?, C::PTEXT_VCAL).map { |_, val| val }
80
+ valuetype = /BINARY/i.r | /BOOLEAN/i.r | /CAL-ADDRESS/i.r | /DATE-TIME/i.r | /DATE/i.r |
81
+ /DURATION/i.r | /FLOAT/i.r | /INTEGER/i.r | /PERIOD/i.r | /RECUR/i.r | /TEXT/i.r |
82
+ /TIME/i.r | /URI/i.r | /UTC-OFFSET/i.r | C::XNAME_VCAL | C::IANATOKEN
83
+ rolevalue = /CHAIR/i.r | /REQ-PARTICIPANT/i.r | /OPT-PARTICIPANT/i.r | /NON-PARTICIPANT/i.r |
84
+ C::XNAME_VCAL | C::IANATOKEN
85
+ pvalue_list = (seq(paramvalue << ",".r, lazy { pvalue_list }) & /[;:]/.r).map do |e, list|
86
+ [e.sub(Regexp.new("^\"(.+)\"$"), '\1').gsub(/\\n/, "\n"), list].flatten
87
+ end | (paramvalue & /[;:]/.r).map do |e|
88
+ [e.sub(Regexp.new("^\"(.+)\"$"), '\1').gsub(/\\n/, "\n")]
89
+ end
90
+ quoted_string_list = (seq(C::QUOTEDSTRING_VCAL << ",".r, lazy { quoted_string_list }) & /[;:]/.r).map do |e, list|
91
+ [self.class.rfc6868decode(e.sub(Regexp.new("^\"(.+)\"$"), "\1").gsub(/\\n/, "\n")), list].flatten
92
+ end | (C::QUOTEDSTRING_VCAL & /[;:]/.r).map do |e|
93
+ [self.class.rfc6868decode(e.sub(Regexp.new("^\"(.+)\"$"), "\1").gsub(/\\n/, "\n"))]
94
+ end
95
+
96
+ rfc4288regname = /[A-Za-z0-9!#$&.+^+-]{1,127}/.r
97
+ rfc4288typename = rfc4288regname
98
+ rfc4288subtypename = rfc4288regname
99
+ fmttypevalue = seq(rfc4288typename, "/", rfc4288subtypename).map(&:join)
100
+
101
+ # RFC 7986
102
+ displayval = /BADGE/i.r | /GRAPHIC/i.r | /FULLSIZE/i.r | /THUMBNAIL/i.r | C::XNAME_VCAL | C::IANATOKEN
103
+ displayvallist = seq(displayval << ",".r, lazy { displayvallist }) do |d, l|
104
+ [d, l].flatten
105
+ end | displayval.map { |d| [d] }
106
+ featureval = /AUDIO/i.r | /CHAT/i.r | /FEED/i.r | /MODERATOR/i.r | /PHONE/i.r | /SCREEN/i.r |
107
+ /VIDEO/i.r | C::XNAME_VCAL | C::IANATOKEN
108
+ featurevallist = seq(featureval << ",".r, lazy { featurevallist }) do |d, l|
109
+ [d, l].flatten
110
+ end | featureval.map { |d| [d] }
111
+
112
+ param = seq(/ALTREP/i.r, "=", quotedparamvalue) do |name, _, val|
113
+ { name.upcase.tr("-", "_").to_sym => val }
114
+ end | seq(/CN/i.r, "=", paramvalue) do |name, _, val|
115
+ { name.upcase.tr("-", "_").to_sym => val }
116
+ end | seq(/CUTYPE/i.r, "=", cutypevalue) do |name, _, val|
117
+ { name.upcase.tr("-", "_").to_sym => val.upcase }
118
+ end | seq(/DELEGATED-FROM/i.r, "=", quoted_string_list) do |name, _, val|
119
+ val = val[0] if val.length == 1
120
+ { name.upcase.tr("-", "_").to_sym => val }
121
+ end | seq(/DELEGATED-TO/i.r, "=", quoted_string_list) do |name, _, val|
122
+ val = val[0] if val.length == 1
123
+ { name.upcase.tr("-", "_").to_sym => val }
124
+ end | seq(/DIR/i.r, "=", quotedparamvalue) do |name, _, val|
125
+ { name.upcase.tr("-", "_").to_sym => val }
126
+ end | seq(/ENCODING/i.r, "=", encodingvalue) do |name, _, val|
127
+ { name.upcase.tr("-", "_").to_sym => val.upcase }
128
+ end | seq(/FMTTYPE/i.r, "=", fmttypevalue) do |name, _, val|
129
+ { name.upcase.tr("-", "_").to_sym => val.downcase }
130
+ end | seq(/FBTYPE/i.r, "=", fbtypevalue) do |name, _, val|
131
+ { name.upcase.tr("-", "_").to_sym => val.upcase }
132
+ end | seq(/LANGUAGE/i.r, "=", C::RFC5646LANGVALUE) do |name, _, val|
133
+ { name.upcase.tr("-", "_").to_sym => val.upcase }
134
+ end | seq(/MEMBER/i.r, "=", quoted_string_list) do |name, _, val|
135
+ val = val[0] if val.length == 1
136
+ { name.upcase.tr("-", "_").to_sym => val }
137
+ end | seq(/PARTSTAT/i.r, "=", partstatvalue) do |name, _, val|
138
+ { name.upcase.tr("-", "_").to_sym => val.upcase }
139
+ end | seq(/RANGE/i.r, "=", rangevalue) do |name, _, val|
140
+ { name.upcase.tr("-", "_").to_sym => val.upcase }
141
+ end | seq(/RELATED/i.r, "=", relatedvalue) do |name, _, val|
142
+ { name.upcase.tr("-", "_").to_sym => val.upcase }
143
+ end | seq(/RELTYPE/i.r, "=", reltypevalue) do |name, _, val|
144
+ { name.upcase.tr("-", "_").to_sym => val.upcase }
145
+ end | seq(/ROLE/i.r, "=", rolevalue) do |name, _, val|
146
+ { name.upcase.tr("-", "_").to_sym => val.upcase }
147
+ end | seq(/RSVP/i.r, "=", C::BOOLEAN) do |name, _, val|
148
+ { name.upcase.tr("-", "_").to_sym => val }
149
+ end | seq(/SENT-BY/i.r, "=", quotedparamvalue) do |name, _, val|
150
+ { name.upcase.tr("-", "_").to_sym => val }
151
+ end | seq(/TZID/i.r, "=", tzidvalue) do |name, _, val|
152
+ { name.upcase.tr("-", "_").to_sym => val }
153
+ end | seq(/VALUE/i.r, "=", valuetype) do |name, _, val|
154
+ { name.upcase.tr("-", "_").to_sym => val }
155
+ # RFC 7986
156
+ end | seq(/DISPLAY/i.r, "=", displayvallist) do |name, _, val|
157
+ { name.upcase.tr("-", "_").to_sym => val }
158
+ end | seq(/FEATURE/i.r, "=", featurevallist) do |name, _, val|
159
+ { name.upcase.tr("-", "_").to_sym => val }
160
+ end | seq(/EMAIL/i.r, "=", paramvalue) do |name, _, val|
161
+ { name.upcase.tr("-", "_").to_sym => val }
162
+ end | seq(/LABEL/i.r, "=", paramvalue) do |name, _, val|
163
+ { name.upcase.tr("-", "_").to_sym => val }
164
+ end | seq(otherparamname, "=", pvalue_list) do |name, _, val|
165
+ val = val[0] if val.length == 1
166
+ { name.upcase.tr("-", "_").to_sym => val }
167
+ end | seq(paramname, "=", pvalue_list) do |name, _, val|
168
+ parse_err("Violated format of parameter value #{name} = #{val}")
169
+ end
170
+
171
+ params = seq(";".r >> param & ";", lazy { params }) do |p, ps|
172
+ p.merge(ps) do |key, old, new|
173
+ if @cardinality1[:PARAM].include?(key)
174
+ parse_err("Violated cardinality of parameter #{key}")
175
+ end
176
+ [old, new].flatten
177
+ # deal with duplicate properties
178
+ end
179
+ end | seq(";".r >> param).map { |e| e[0] }
180
+
181
+ contentline = seq(linegroup._?, C::NAME_VCAL, params._? << ":".r,
182
+ C::VALUE, /(\r|\n|\r\n)/) do |g, name, p, value, _|
183
+ key = name.upcase.tr("-", "_").to_sym
184
+ hash = { key => { value: value } }
185
+ hash[key][:group] = g[0] unless g.empty?
186
+ errors << Paramcheck.paramcheck(strict, key, p.empty? ? {} : p[0], @ctx)
187
+ hash[key][:params] = p[0] unless p.empty?
188
+ hash
189
+ end
190
+
191
+ props = ("".r & beginend).map { {} } |
192
+ seq(contentline, lazy { props }) do |c, rest|
193
+ k = c.keys[0]
194
+ c[k][:value], errors1 = Typegrammars.typematch(strict, k, c[k][:params], :GENERIC, c[k][:value], @ctx)
195
+ errors << errors1
196
+ c.merge(rest) do |_, old, new|
197
+ [old, new].flatten
198
+ # deal with duplicate properties
199
+ end
200
+ end
201
+ alarmprops = ("".r & beginend).map { {} } |
202
+ seq(contentline, lazy { alarmprops }) do |c, rest|
203
+ k = c.keys[0]
204
+ c[k][:value], errors1 = Typegrammars.typematch(strict, k, c[k][:params], :ALARM, c[k][:value], @ctx)
205
+ errors << errors1
206
+ c.merge(rest) do |key, old, new|
207
+ if @cardinality1[:ALARM].include?(key.upcase)
208
+ parse_err("Violated cardinality of property #{key}")
209
+ end
210
+ [old, new].flatten
211
+ end
212
+ end
213
+ fbprops = ("".r & beginend).map { {} } |
214
+ seq(contentline, lazy { fbprops }) do |c, rest|
215
+ k = c.keys[0]
216
+ c[k][:value], errors1 = Typegrammars.typematch(strict, k, c[k][:params], :FREEBUSY, c[k][:value], @ctx)
217
+ errors << errors1
218
+ c.merge(rest) do |key, old, new|
219
+ if @cardinality1[:FREEBUSY].include?(key.upcase)
220
+ parse_err("Violated cardinality of property #{key}")
221
+ end
222
+ [old, new].flatten
223
+ end
224
+ end
225
+ journalprops = ("".r & beginend).map { {} } |
226
+ seq(contentline, lazy { journalprops }) do |c, rest|
227
+ k = c.keys[0]
228
+ c[k][:value], errors1 = Typegrammars.typematch(strict, k, c[k][:params], :JOURNAL, c[k][:value], @ctx)
229
+ errors << errors1
230
+ c.merge(rest) do |key, old, new|
231
+ if @cardinality1[:JOURNAL].include?(key.upcase)
232
+ parse_err("Violated cardinality of property #{key}")
233
+ end
234
+ [old, new].flatten
235
+ end
236
+ end
237
+ tzprops = ("".r & beginend).map { {} } |
238
+ seq(contentline, lazy { tzprops }) do |c, rest|
239
+ k = c.keys[0]
240
+ c[k][:value], errors1 = Typegrammars.typematch(strict, k, c[k][:params], :TZ, c[k][:value], @ctx)
241
+ errors << errors1
242
+ c.merge(rest) do |key, old, new|
243
+ if @cardinality1[:TZ].include?(key.upcase)
244
+ parse_err("Violated cardinality of property #{key}")
245
+ end
246
+ [old, new].flatten
247
+ end
248
+ end
249
+ standardc = seq(/BEGIN:STANDARD(\r|\n|\r\n)/i.r, tzprops, /END:STANDARD(\r|\n|\r\n)/i.r) do |_, e, _|
250
+ parse_err("Missing DTSTART property") unless e.has_key?(:DTSTART)
251
+ parse_err("Missing TZOFFSETTO property") unless e.has_key?(:TZOFFSETTO)
252
+ parse_err("Missing TZOFFSETFROM property") unless e.has_key?(:TZOFFSETFROM)
253
+ { STANDARD: { component: [e] } }
254
+ end
255
+ daylightc = seq(/BEGIN:DAYLIGHT(\r|\n|\r\n)/i.r, tzprops, /END:DAYLIGHT(\r|\n|\r\n)/i.r) do |_, e, _|
256
+ parse_err("Missing DTSTART property") unless e.has_key?(:DTSTART)
257
+ parse_err("Missing TZOFFSETTO property") unless e.has_key?(:TZOFFSETTO)
258
+ parse_err("Missing TZOFFSETFROM property") unless e.has_key?(:TZOFFSETFROM)
259
+ { DAYLIGHT: { component: [e] } }
260
+ end
261
+ timezoneprops =
262
+ seq(standardc, lazy { timezoneprops }) do |e, rest|
263
+ e.merge(rest) { |_, old, new| { component: [old[:component], new[:component]].flatten } }
264
+ end | seq(daylightc, lazy { timezoneprops }) do |e, rest|
265
+ e.merge(rest) { |_, old, new| { component: [old[:component], new[:component]].flatten } }
266
+ end | seq(contentline, lazy { timezoneprops }) do |e, rest|
267
+ k = e.keys[0]
268
+ e[k][:value], errors1 = Typegrammars.typematch(strict, k, e[k][:params], :TIMEZONE, e[k][:value], @ctx)
269
+ errors << errors1
270
+ e.merge(rest) do |key, old, new|
271
+ if @cardinality1[:TIMEZONE].include?(key.upcase)
272
+ parse_err("Violated cardinality of property #{key}")
273
+ end
274
+ [old, new].flatten
275
+ end
276
+ end |
277
+ ("".r & beginend).map { {} }
278
+ todoprops = ("".r & beginend).map { {} } |
279
+ seq(contentline, lazy { todoprops }) do |c, rest|
280
+ k = c.keys[0]
281
+ c[k][:value], errors1 = Typegrammars.typematch(strict, k, c[k][:params], :TODO, c[k][:value], @ctx)
282
+ errors << errors1
283
+ c.merge(rest) do |key, old, new|
284
+ if @cardinality1[:TODO].include?(key.upcase)
285
+ parse_err("Violated cardinality of property #{key}")
286
+ end
287
+ [old, new].flatten
288
+ end
289
+ end
290
+ eventprops = seq(contentline, lazy { eventprops }) do |c, rest|
291
+ k = c.keys[0]
292
+ c[k][:value], errors1 = Typegrammars.typematch(strict, k, c[k][:params], :EVENT, c[k][:value], @ctx)
293
+ errors << errors1
294
+ c.merge(rest) do |key, old, new|
295
+ if @cardinality1[:EVENT].include?(key.upcase)
296
+ parse_err("Violated cardinality of property #{key}")
297
+ end
298
+ [old, new].flatten
299
+ end
300
+ end |
301
+ ("".r & beginend).map { {} }
302
+ alarmc = seq(/BEGIN:VALARM(\r|\n|\r\n)/i.r, alarmprops, /END:VALARM(\r|\n|\r\n)/i.r) do |_, e, _|
303
+ parse_err("Missing ACTION property") unless e.has_key?(:ACTION)
304
+ parse_err("Missing TRIGGER property") unless e.has_key?(:TRIGGER)
305
+ if e.has_key?(:DURATION) && !e.has_key?(:REPEAT) || !e.has_key?(:DURATION) && e.has_key?(:REPEAT)
306
+ parse_err("Missing DURATION && REPEAT properties")
307
+ end
308
+ if e[:ACTION] == "AUDIO"
309
+ parse_err("Multiple ATTACH properties") if e.has_key?(:ATTACH) && e[:ATTACH].is_a?(Array)
310
+ parse_err("Invalid DESCRIPTION property") if e.has_key?(:DESCRIPTION)
311
+ parse_err("Invalid SUMMARY property") if e.has_key?(:SUMMARY)
312
+ parse_err("Invalid ATTENDEE property") if e.has_key?(:ATTENDEE)
313
+ elsif e[:ACTION] == "DISP"
314
+ parse_err("Missing DESCRIPTION property") unless e.has_key?(:DESCRIPTION)
315
+ parse_err("Invalid ATTACH property") if e.has_key?(:ATTACH)
316
+ parse_err("Invalid SUMMARY property") if e.has_key?(:SUMMARY)
317
+ parse_err("Invalid ATTENDEE property") if e.has_key?(:ATTENDEE)
318
+ elsif e[:ACTION] == "EMAIL"
319
+ parse_err("Missing DESCRIPTION property") unless e.has_key?(:DESCRIPTION)
320
+ end
321
+ { VALARM: { component: [e] } }
322
+ end
323
+ freebusyc = seq(/BEGIN:VFREEBUSY(\r|\n|\r\n)/i.r, fbprops, /END:VFREEBUSY(\r|\n|\r\n)/i.r) do |_, e, _|
324
+ parse_err("Missing DTSTAMP property") unless e.has_key?(:DTSTAMP)
325
+ parse_err("Missing UID property") unless e.has_key?(:UID)
326
+ parse_err("DTEND before DTSTART") if e.has_key?(:DTEND) && e.has_key?(:DTSTART) &&
327
+ e[:DTEND][:value] < e[:DTSTART][:value]
328
+ { VFREEBUSY: { component: [e] } }
329
+ end
330
+ journalc = seq(/BEGIN:VJOURNAL(\r|\n|\r\n)/i.r, journalprops, /END:VJOURNAL(\r|\n|\r\n)/i.r) do |_, e, _|
331
+ parse_err("Missing DTSTAMP property") unless e.has_key?(:DTSTAMP)
332
+ parse_err("Missing UID property") unless e.has_key?(:UID)
333
+ parse_err("Missing DTSTART property with RRULE property") if e.has_key?(:RRULE) && !e.has_key?(:DTSTART)
334
+ { VJOURNAL: { component: [e] } }
335
+ end
336
+ timezonec = seq(/BEGIN:VTIMEZONE(\r|\n|\r\n)/i.r, timezoneprops, /END:VTIMEZONE(\r|\n|\r\n)/i.r) do |_, e, _|
337
+ parse_err("Missing STANDARD || DAYLIGHT property") unless e.has_key?(:STANDARD) || e.has_key?(:DAYLIGHT)
338
+ { VTIMEZONE: { component: [e] } }
339
+ end
340
+ todoc = seq(/BEGIN:VTODO(\r|\n|\r\n)/i.r, todoprops, alarmc.star, /END:VTODO(\r|\n|\r\n)/i.r) do |_, e, a, _|
341
+ parse_err("Missing DTSTAMP property") unless e.has_key?(:DTSTAMP)
342
+ parse_err("Missing UID property") unless e.has_key?(:UID)
343
+ parse_err("Coocurring DUE && DURATION properties") if e.has_key?(:DUE) && e.has_key?(:DURATION)
344
+ parse_err("Missing DTSTART property with DURATION property") if e.has_key?(:DURATION) && !e.has_key?(:DTSTART)
345
+ parse_err("Missing DTSTART property with RRULE property") if e.has_key?(:RRULE) && !e.has_key?(:DTSTART)
346
+ parse_err("DUE before DTSTART") if e.has_key?(:DUE) &&
347
+ e.has_key?(:DTSTART) &&
348
+ e[:DUE][:value] < e[:DTSTART][:value]
349
+ # TODO not doing constraint that due && dtstart are both || neither local time
350
+ # TODO not doing constraint that recurrence-id && dtstart are both || neither local time
351
+ # TODO not doing constraint that recurrence-id && dtstart are both || neither date
352
+ a.each do |x|
353
+ e = e.merge(x) { |_, old, new| { component: [old[:component], new[:component]].flatten } }
354
+ end
355
+ { VTODO: { component: [e] } }
356
+ end
357
+ eventc = seq(/BEGIN:VEVENT(\r|\n|\r\n)/i.r, eventprops, alarmc.star, /END:VEVENT(\r|\n|\r\n)/i.r) do |_, e, a, _|
358
+ parse_err("Missing DTSTAMP property") unless e.has_key?(:DTSTAMP)
359
+ parse_err("Missing UID property") unless e.has_key?(:UID)
360
+ parse_err("Coocurring DTEND && DURATION properties") if e.has_key?(:DTEND) && e.has_key?(:DURATION)
361
+ parse_err("Missing DTSTART property with RRULE property") if e.has_key?(:RRULE) && !e.has_key?(:DTSTART)
362
+ parse_err("DTEND before DTSTART") if e.has_key?(:DTEND) &&
363
+ e.has_key?(:DTSTART) &&
364
+ e[:DTEND][:value] < e[:DTSTART][:value]
365
+ # TODO not doing constraint that dtend && dtstart are both || neither local time
366
+ a.each do |x|
367
+ e = e.merge(x) { |_, old, new| { component: [old[:component], new[:component]].flatten } }
368
+ end
369
+ { VEVENT: { component: [e] } }
370
+ end
371
+ xcomp = seq(/BEGIN:/i.r, C::XNAME_VCAL, /(\r|\n|\r\n)/i.r, props, /END:/i.r, C::XNAME_VCAL, /(\r|\n|\r\n)/i.r) do |_, n, _, p, _, n1, _|
372
+ n = n.upcase
373
+ n1 = n1.upcase
374
+ parse_err("Mismatch BEGIN:#{n}, END:#{n1}") if n != n1
375
+ { n1.to_sym => { component: [p] } }
376
+ end
377
+ ianacomp = seq(/BEGIN:/i.r ^ C::ICALPROPNAMES, C::IANATOKEN, /(\r|\n|\r\n)/i.r, props, /END:/i.r ^ C::ICALPROPNAMES, C::IANATOKEN, /(\r|\n|\r\n)/i.r) do |_, n, _, p, _, n1, _|
378
+ n = n.upcase
379
+ n1 = n1.upcase
380
+ parse_err("Mismatch BEGIN:#{n}, END:#{n1}") if n != n1
381
+ { n1.to_sym => { component: [p] } }
382
+ end
383
+ # RFC 7953
384
+ availableprops = seq(contentline, lazy { availableprops }) do |c, rest|
385
+ k = c.keys[0]
386
+ c[k][:value], errors1 = Typegrammars.typematch(strict, k, c[k][:params], :AVAILABLE, c[k][:value], @ctx)
387
+ errors << errors1
388
+ c.merge(rest) do |key, old, new|
389
+ if @cardinality1[:AVAILABLE].include?(key.upcase)
390
+ parse_err("Violated cardinality of property #{key}")
391
+ end
392
+ [old, new].flatten
393
+ end
394
+ end | ("".r & beginend).map { {} }
395
+ availablec = seq(/BEGIN:AVAILABLE(\r|\n|\r\n)/i.r, availableprops, /END:AVAILABLE(\r|\n|\r\n)/i.r) do |_, e, _|
396
+ # parse_err("Missing DTSTAMP property") unless e.has_key?(:DTSTAMP) # required in spec, but not in examples
397
+ parse_err("Missing DTSTART property") unless e.has_key?(:DTSTART)
398
+ parse_err("Missing UID property") unless e.has_key?(:UID)
399
+ parse_err("Coocurring DTEND && DURATION properties") if e.has_key?(:DTEND) && e.has_key?(:DURATION)
400
+ { AVAILABLE: { component: [e] } }
401
+ end
402
+ availabilityprops = seq(contentline, lazy { availabilityprops }) do |c, rest|
403
+ k = c.keys[0]
404
+ c[k][:value], errors1 = Typegrammars.typematch(strict, k, c[k][:params], :VAVAILABILITY, c[k][:value], @ctx)
405
+ errors << errors1
406
+ c.merge(rest) do |key, old, new|
407
+ if @cardinality1[:VAVAILABILITY].include?(key.upcase)
408
+ parse_err("Violated cardinality of property #{key}")
409
+ end
410
+ [old, new].flatten
411
+ end
412
+ end | ("".r & beginend).map { {} }
413
+ vavailabilityc = seq(/BEGIN:VAVAILABILITY(\r|\n|\r\n)/i.r, availabilityprops, availablec.star, /END:VAVAILABILITY(\r|\n|\r\n)/i.r) do |_, e, a, _|
414
+ parse_err("Missing DTSTAMP property") unless e.has_key?(:DTSTAMP)
415
+ parse_err("Missing UID property") unless e.has_key?(:UID)
416
+ parse_err("Coocurring DTEND && DURATION properties") if e.has_key?(:DTEND) && e.has_key?(:DURATION)
417
+ parse_err("Missing DTSTART property with DURATION property") if e.has_key?(:DURATION) && !e.has_key?(:DTSTART)
418
+ parse_err("DTEND before DTSTART") if e.has_key?(:DTEND) && e.has_key?(:DTSTART) && e[:DTEND][:value] < e[:DTSTART][:value]
419
+ # TODO not doing constraint that dtend && dtstart are both || neither local time
420
+ # TODO not doing constraint that each TZID param must have matching VTIMEZONE component
421
+ a.each do |x|
422
+ e = e.merge(x) { |_key, old, new| { component: [old[:component], new[:component]].flatten } }
423
+ end
424
+ { VAVAILABILITY: { component: [e] } }
425
+ end
426
+
427
+ component = eventc | todoc | journalc | freebusyc | timezonec | ianacomp | xcomp | vavailabilityc
428
+ components = seq(component, lazy { components }) do |c, r|
429
+ c.merge(r) do |_key, old, new|
430
+ { component: [old[:component], new[:component]].flatten }
431
+ end
432
+ end | component
433
+
434
+ calpropname = /CALSCALE/i.r | /METHOD/i.r | /PRODID/i.r | /VERSION/i.r |
435
+ /UID/i.r | /LAST-MOD/i.r | /URL/i.r | /REFRESH/i.r | /SOURCE/i.r | /COLOR/i.r | # RFC 7986
436
+ /NAME/i.r | /DESCRIPTION/i.r | /CATEGORIES/i.r | /IMAGE/i.r | # RFC 7986
437
+ C::XNAME_VCAL | C::IANATOKEN
438
+ calprop = seq(calpropname, params._? << ":".r, C::VALUE, /(\r|\n|\r\n)/) do |key, p, value, _|
439
+ key = key.upcase.tr("-", "_").to_sym
440
+ val, errors1 = Typegrammars.typematch(strict, key, p[0], :CALENDAR, value, @ctx)
441
+ errors << errors1
442
+ hash = { key => { value: val } }
443
+ errors << Paramcheck.paramcheck(strict, key, p.empty? ? {} : p[0], @ctx)
444
+ hash[key][:params] = p[0] unless p.empty?
445
+ hash
446
+ # TODO not doing constraint that each description must be in a different language
447
+ end
448
+ calprops = ("".r & beginend).map { {} } |
449
+ seq(calprop, lazy { calprops }) do |c, rest|
450
+ c.merge(rest) do |key, old, new|
451
+ if @cardinality1[:ICAL].include?(key.upcase)
452
+ parse_err("Violated cardinality of property #{key}")
453
+ end
454
+ [old, new].flatten
455
+ end
456
+ end
457
+ vobject = seq(/BEGIN:VCALENDAR(\r|\n|\r\n)/i.r, calprops, components, /END:VCALENDAR(\r|\n|\r\n)/i.r) do |_b, v, rest, _e|
458
+ parse_err("Missing PRODID attribute") unless v.has_key?(:PRODID)
459
+ parse_err("Missing VERSION attribute") unless v.has_key?(:VERSION)
460
+ rest.delete(:END)
461
+ if !v.has_key?(:METHOD) && rest.has_key?(:VEVENT)
462
+ rest[:VEVENT][:component].each do |e1|
463
+ parse_err("Missing DTSTART property from VEVENT component") if !e1.has_key?(:DTSTART)
464
+ end
465
+ end
466
+ tidyup(VCALENDAR: v.merge(rest), errors: errors.flatten)
467
+ end
468
+ vobject.eof
469
+ end
470
+
471
+ # any residual tidying of object
472
+ def tidyup(v)
473
+ # adjust any VTIMEZONE.{STANDARD|DAYLIGHT}.{DTSTART|RDATE} times from floating local to the time within the timezone component
474
+ if !v[:VCALENDAR].has_key?(:VTIMEZONE) || v[:VCALENDAR][:VTIMEZONE][:component].nil? || v[:VCALENDAR][:VTIMEZONE][:component].empty?
475
+ return v
476
+ elsif v[:VCALENDAR][:VTIMEZONE][:component].is_a?(Array)
477
+ v[:VCALENDAR][:VTIMEZONE][:component].map do |x|
478
+ timezoneadjust x
479
+ end
480
+ else
481
+ v[:VCALENDAR][:VTIMEZONE][:component] = timezoneadjust v[:VCALENDAR][:VTIMEZONE][:component]
482
+ end
483
+ v
484
+ end
485
+
486
+ def timezoneadjust(x)
487
+ if x[:TZID].nil? || x[:TZID].empty?
488
+ return x
489
+ end
490
+ # TODO deal with unregistered timezones
491
+ begin
492
+ tz = TZInfo::Timezone.get(x[:TZID][:value].value)
493
+ rescue
494
+ return x
495
+ end
496
+ [:STANDARD, :DAYLIGHT].each do |k|
497
+ next unless x.has_key?(k)
498
+ if x[k][:component].is_a?(Array)
499
+ x[k][:component].each do |y|
500
+ # subtracting an hour and a minute to avoid PeriodNotFound exceptions on the boundary between daylight saving && standard time
501
+ # if that doesn't work either, we'll rescue to floating localtime
502
+ # ... no, I will treat STANDARD times as standard, and DAYLIGHT times as daylight savings
503
+ # TODO lookup offsets applicable by parsing dates && offsets in the ical. I'd rather not.
504
+ begin
505
+ y[:DTSTART][:value].value[:time] = tz.local_to_utc(y[:DTSTART][:value].value[:time] - 3660, true) + 3660
506
+ rescue
507
+ # nop
508
+ else
509
+ y[:DTSTART][:value].value[:zone] = x[:TZID][:value].value
510
+ end
511
+ next unless y.has_key?(:RDATE)
512
+ if y[:RDATE].is_a?(Array)
513
+ y[:RDATE].each do |z|
514
+ z[:value].value.each do |w|
515
+ begin
516
+ w.value[:time] = tz.local_to_utc(w.value[:time] - 3660, true) + 3660
517
+ rescue
518
+ # nop
519
+ else
520
+ w.value[:zone] = x[:TZID][:value].value
521
+ end
522
+ end
523
+ end
524
+ else
525
+ begin
526
+ y[:RDATE][:value].value[:time] = tz.local_to_utc(y[:RDATE].value[:time] - 3660, true) + 3660
527
+ rescue
528
+ # nop
529
+ else
530
+ y[:RDATE][:value].value[:zone] = x[:TZID][:value].value
531
+ end
532
+ end
533
+ end
534
+ else
535
+ begin
536
+ x[k][:component][:DTSTART][:value].value[:time] = tz.local_to_utc(x[k][:component][:DTSTART][:value].value[:time] - 3660, true) + 3660
537
+ rescue
538
+ # nop
539
+ else
540
+ x[k][:component][:DTSTART][:value].value[:zone] = x[:TZID][:value].value
541
+ end
542
+ next unless x[k][:component].has_key?(:RDATE)
543
+ if x[k][:component][:RDATE].is_a?(Array)
544
+ x[k][:component][:RDATE].each do |z|
545
+ z[:value].value.each do |w|
546
+ begin
547
+ w.value[:time] = tz.local_to_utc(w.value[:time] - 3660, true) + 3660
548
+ rescue
549
+ # nop
550
+ else
551
+ w.value[:zone] = x[:TZID][:value].value
552
+ end
553
+ end
554
+ end
555
+ else
556
+ begin
557
+ x[k][:component][:RDATE][:value].value[:time] = tz.local_to_utc(x[k][:component][:RDATE][:value].value[:time] - 3660, true) + 3660
558
+ rescue
559
+ # nop
560
+ else
561
+ x[k][:component][:RDATE][:value].value[:zone] = x[:TZID][:value].value
562
+ end
563
+ end
564
+ end
565
+ end
566
+ x
567
+ end
568
+
569
+ def initialize(strict)
570
+ self.strict = strict
571
+ self.errors = []
572
+ end
573
+
574
+ def parse(vobject)
575
+ @ctx = Rsec::ParseContext.new self.class.unfold(vobject), "source"
576
+ ret = vobject_grammar._parse @ctx
577
+ if !ret || Rsec::INVALID[ret]
578
+ parse_err(@ctx.generate_error("source"))
579
+ ret = { VCALENDAR: nil, errors: errors.flatten }
580
+ end
581
+ Rsec::Fail.reset
582
+ ret
583
+ end
584
+
585
+ private
586
+
587
+ def parse_err(msg)
588
+ if strict
589
+ raise @ctx.report_error msg, "source"
590
+ else
591
+ errors << @ctx.report_error(msg, "source")
592
+ end
593
+ end
594
+ end
595
+ end