vobject 0.1.0 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +5 -5
  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/.hound.yml +3 -0
  6. data/.rubocop.tb.yml +650 -0
  7. data/.rubocop.yml +1077 -0
  8. data/Gemfile +1 -1
  9. data/LICENSE.txt +21 -17
  10. data/README.adoc +151 -0
  11. data/Rakefile +1 -1
  12. data/lib/c.rb +173 -0
  13. data/lib/error.rb +19 -0
  14. data/lib/vcalendar.rb +77 -0
  15. data/lib/vcard.rb +67 -0
  16. data/lib/vobject.rb +13 -170
  17. data/lib/vobject/component.rb +87 -36
  18. data/lib/vobject/parameter.rb +116 -0
  19. data/lib/vobject/parametervalue.rb +26 -0
  20. data/lib/vobject/property.rb +134 -55
  21. data/lib/vobject/propertyvalue.rb +46 -0
  22. data/lib/vobject/vcalendar/component.rb +106 -0
  23. data/lib/vobject/vcalendar/grammar.rb +595 -0
  24. data/lib/vobject/vcalendar/paramcheck.rb +259 -0
  25. data/lib/vobject/vcalendar/propertyparent.rb +98 -0
  26. data/lib/vobject/vcalendar/propertyvalue.rb +606 -0
  27. data/lib/vobject/vcalendar/typegrammars.rb +605 -0
  28. data/lib/vobject/vcard/v3_0/component.rb +40 -0
  29. data/lib/vobject/vcard/v3_0/grammar.rb +175 -0
  30. data/lib/vobject/vcard/v3_0/paramcheck.rb +110 -0
  31. data/lib/vobject/vcard/v3_0/parameter.rb +17 -0
  32. data/lib/vobject/vcard/v3_0/property.rb +18 -0
  33. data/lib/vobject/vcard/v3_0/propertyvalue.rb +401 -0
  34. data/lib/vobject/vcard/v3_0/typegrammars.rb +425 -0
  35. data/lib/vobject/vcard/v4_0/component.rb +40 -0
  36. data/lib/vobject/vcard/v4_0/grammar.rb +224 -0
  37. data/lib/vobject/vcard/v4_0/paramcheck.rb +269 -0
  38. data/lib/vobject/vcard/v4_0/parameter.rb +18 -0
  39. data/lib/vobject/vcard/v4_0/property.rb +63 -0
  40. data/lib/vobject/vcard/v4_0/propertyvalue.rb +404 -0
  41. data/lib/vobject/vcard/v4_0/typegrammars.rb +539 -0
  42. data/lib/vobject/version.rb +1 -1
  43. data/vobject.gemspec +19 -16
  44. metadata +81 -26
  45. data/.travis.yml +0 -5
  46. data/README.md +0 -94
@@ -0,0 +1,425 @@
1
+ require "rsec"
2
+ require "set"
3
+ require "uri"
4
+ require "date"
5
+ include Rsec::Helpers
6
+ require "vobject"
7
+ require_relative "./propertyvalue"
8
+
9
+ module Vcard::V3_0
10
+ class Typegrammars
11
+ class << self
12
+ # property value types, each defining their own parser
13
+
14
+ def binary
15
+ binary = seq(/[a-zA-Z0-9+\/]*/.r, /={0,2}/.r) do |b, q|
16
+ if (b.length + q.length) % 4 == 0
17
+ PropertyValue::Binary.new(b + q)
18
+ else
19
+ { error: "Malformed binary coding" }
20
+ end
21
+ end
22
+ binary.eof
23
+ end
24
+
25
+ def phone_number
26
+ # This is on the lax side; there should be up to 15 digits
27
+ # Will allow letters
28
+ phone_number = /[0-9() +A-Z-]+/i.r.map { |p| PropertyValue::Phonenumber.new p }
29
+ phone_number.eof
30
+ end
31
+
32
+ def geovalue
33
+ float = prim(:double)
34
+ geovalue = seq(float << ";".r, float) do |a, b|
35
+ if a <= 180.0 && a >= -180.0 && b <= 180 && b > -180
36
+ PropertyValue::Geovalue.new(lat: a, long: b)
37
+ else
38
+ { error: "Latitude/Longitude outside of range -180..180" }
39
+ end
40
+ end
41
+ geovalue.eof
42
+ end
43
+
44
+ def classvalue
45
+ iana_token = /[a-zA-Z\d\-]+/.r
46
+ xname = seq(/[xX]-/, /[a-zA-Z0-9-]+/.r).map(&:join)
47
+ classvalue = (/PUBLIC/i.r | /PRIVATE/i.r | /CONFIDENTIAL/i.r | iana_token | xname).map do |m|
48
+ PropertyValue::ClassValue.new m
49
+ end
50
+ classvalue.eof
51
+ end
52
+
53
+ def integer
54
+ integer = prim(:int32).map { |i| PropertyValue::Integer.new i }
55
+ integer.eof
56
+ end
57
+
58
+ def float_t
59
+ float_t = prim(:double).map { |f| PropertyValue::Float.new f }
60
+ float_t.eof
61
+ end
62
+
63
+ def iana_token
64
+ iana_token = /[a-zA-Z\d\-]+/.r.map { |x| PropertyValue::Ianatoken.new x }
65
+ iana_token.eof
66
+ end
67
+
68
+ def versionvalue
69
+ versionvalue = "3.0".r.map { |v| PropertyValue::Version.new v }
70
+ versionvalue.eof
71
+ end
72
+
73
+ def profilevalue
74
+ profilevalue = /VCARD/i.r.map { |v| PropertyValue::Profilevalue.new v }
75
+ profilevalue.eof
76
+ end
77
+
78
+ def uri
79
+ uri = /\S+/.r.map do |s|
80
+ if s =~ URI::DEFAULT_PARSER.make_regexp
81
+ PropertyValue::Uri.new(s)
82
+ else
83
+ { error: "Invalid URI" }
84
+ end
85
+ end
86
+ uri.eof
87
+ end
88
+
89
+ def text_t
90
+ text_t = C::TEXT3.map { |t| PropertyValue::Text.new(unescape(t)) }
91
+ text_t.eof
92
+ end
93
+
94
+ def textlist
95
+ text = C::TEXT3
96
+ textlist1 =
97
+ seq(text << ",".r, lazy { textlist1 }) { |a, b| [unescape(a), b].flatten } |
98
+ text.map { |t| [unescape(t)] }
99
+ textlist = textlist1.map { |m| PropertyValue::Textlist.new m }
100
+ textlist.eof
101
+ end
102
+
103
+ def org
104
+ text = C::TEXT3
105
+ org1 =
106
+ seq(text << ";".r, lazy { org1 }) { |a, b| [unescape(a), b].flatten } |
107
+ text.map { |t| [unescape(t)] }
108
+ org = org1.map { |o| PropertyValue::Org.new o }
109
+ org.eof
110
+ end
111
+
112
+ def date_t
113
+ date_t = seq(/[0-9]{4}/.r, /-/.r._? >> /[0-9]{2}/.r, /-/.r._? >> /[0-9]{2}/.r) do |yy, mm, dd|
114
+ PropertyValue::Date.new(year: yy, month: mm, day: dd)
115
+ end
116
+ date_t.eof
117
+ end
118
+
119
+ def time_t
120
+ utc_offset = seq(C::SIGN, /[0-9]{2}/.r << /:/.r._?, /[0-9]{2}/.r) do |s, h, m|
121
+ { sign: s, hour: h, min: m }
122
+ end
123
+ zone = utc_offset.map { |u| u } |
124
+ /Z/i.r.map { "Z" }
125
+ hour = /[0-9]{2}/.r
126
+ minute = /[0-9]{2}/.r
127
+ second = /[0-9]{2}/.r
128
+ secfrac = seq(",".r >> /[0-9]+/)
129
+ time_t = seq(hour << /:/._?, minute << /:/._?, second, secfrac._?, zone._?) do |h, m, s, f, z|
130
+ h = { hour: h, min: m, sec: s }
131
+ h[:zone] = z[0] unless z.empty?
132
+ h[:secfrac] = f[0] unless f.empty?
133
+ PropertyValue::Time.new(h)
134
+ end
135
+ time_t.eof
136
+ end
137
+
138
+ def date_time
139
+ utc_offset = seq(C::SIGN, /[0-9]{2}/.r << /:/.r._?, /[0-9]{2}/.r) do |s, h, m|
140
+ { sign: s, hour: h, min: m }
141
+ end
142
+ zone = utc_offset.map { |u| u } |
143
+ /Z/i.r.map { "Z" }
144
+ hour = /[0-9]{2}/.r
145
+ minute = /[0-9]{2}/.r
146
+ second = /[0-9]{2}/.r
147
+ secfrac = seq(",".r >> /[0-9]+/)
148
+ date = seq(/[0-9]{4}/.r, /-/.r._?, /[0-9]{2}/.r, /-/.r._?, /[0-9]{2}/.r) do |yy, _, mm, _, dd|
149
+ { year: yy, month: mm, day: dd }
150
+ end
151
+ time = seq(hour << /:/.r._?, minute << /:/.r._?, second, secfrac._?, zone._?) do |h, m, s, f, z|
152
+ h = { hour: h, min: m, sec: s }
153
+ h[:zone] = if z.empty?
154
+ ""
155
+ else
156
+ z[0]
157
+ end
158
+ h[:secfrac] = f[0] unless f.empty?
159
+ h
160
+ end
161
+ date_time = seq(date << "T".r, time) do |d, t|
162
+ PropertyValue::DateTimeLocal.new(d.merge(t))
163
+ end
164
+ date_time.eof
165
+ end
166
+
167
+ def date_or_date_time
168
+ utc_offset = seq(C::SIGN, /[0-9]{2}/.r << /:/.r._?, /[0-9]{2}/.r) do |s, h, m|
169
+ { sign: s, hour: h, min: m }
170
+ end
171
+ zone = utc_offset.map { |u| u } |
172
+ /Z/i.r.map { "Z" }
173
+ hour = /[0-9]{2}/.r
174
+ minute = /[0-9]{2}/.r
175
+ second = /[0-9]{2}/.r
176
+ secfrac = seq(",".r >> /[0-9]+/)
177
+ date = seq(/[0-9]{4}/.r << /-/.r._?, /[0-9]{2}/.r << /-/.r._?, /[0-9]{2}/.r) do |yy, mm, dd|
178
+ { year: yy, month: mm, day: dd }
179
+ end
180
+ time = seq(hour << /:/.r._?, minute << /:/.r._?, second, secfrac._?, zone._?) do |h, m, s, f, z|
181
+ h = { hour: h, min: m, sec: s }
182
+ h[:zone] = z[0] unless z.empty?
183
+ h[:secfrac] = f[0] unless f.empty?
184
+ h
185
+ end
186
+ date_or_date_time = seq(date << "T".r, time) do |d, t|
187
+ PropertyValue::DateTimeLocal.new(d.merge(t))
188
+ end | date.map { |d| PropertyValue::Date.new(d) }
189
+ date_or_date_time.eof
190
+ end
191
+
192
+ def utc_offset
193
+ utc_offset = seq(C::SIGN, /[0-9]{2}/.r, /:/.r._?, /[0-9]{2}/.r) do |s, h, _, m|
194
+ PropertyValue::Utcoffset.new(sign: s, hour: h, min: m)
195
+ end
196
+ utc_offset.eof
197
+ end
198
+
199
+ def kindvalue
200
+ iana_token = /[a-zA-Z\d\-]+/.r
201
+ xname = seq(/[xX]-/, /[a-zA-Z0-9-]+/.r).map(&:join)
202
+ kindvalue = (/individual/i.r | /group/i.r | /org/i.r | /location/i.r |
203
+ iana_token | xname).map do |k|
204
+ PropertyValue::Kindvalue.new(k)
205
+ end
206
+ kindvalue.eof
207
+ end
208
+
209
+ def fivepartname
210
+ text = C::TEXT3
211
+ component = seq(text << ",".r, lazy { component }) do |a, b|
212
+ [unescape(a), b].flatten
213
+ end | text.map { |t| [unescape(t)] }
214
+ fivepartname1 = seq(component << ";".r, component << ";".r, component << ";".r,
215
+ component << ";".r, component) do |a, b, c, d, e|
216
+ a = a[0] if a.length == 1
217
+ b = b[0] if b.length == 1
218
+ c = c[0] if c.length == 1
219
+ d = d[0] if d.length == 1
220
+ e = e[0] if e.length == 1
221
+ { surname: a, givenname: b, middlename: c, honprefix: d, honsuffix: e }
222
+ end | seq(component << ";".r, component << ";".r, component << ";".r, component) do |a, b, c, d|
223
+ a = a[0] if a.length == 1
224
+ b = b[0] if b.length == 1
225
+ c = c[0] if c.length == 1
226
+ d = d[0] if d.length == 1
227
+ { surname: a, givenname: b, middlename: c, honprefix: d, honsuffix: "" }
228
+ end | seq(component << ";".r, component << ";".r, component) do |a, b, c|
229
+ a = a[0] if a.length == 1
230
+ b = b[0] if b.length == 1
231
+ c = c[0] if c.length == 1
232
+ { surname: a, givenname: b, middlename: c, honprefix: "", honsuffix: "" }
233
+ end | seq(component << ";".r, component) do |a, b|
234
+ a = a[0] if a.length == 1
235
+ b = b[0] if b.length == 1
236
+ { surname: a, givenname: b, middlename: "", honprefix: "", honsuffix: "" }
237
+ end | component.map do |a|
238
+ a = a[0] if a.length == 1
239
+ { surname: a, givenname: "", middlename: "", honprefix: "", honsuffix: "" }
240
+ end
241
+ fivepartname = fivepartname1.map { |n| PropertyValue::Fivepartname.new(n) }
242
+ fivepartname.eof
243
+ end
244
+
245
+ def address
246
+ text = C::TEXT3
247
+ component = seq(text << ",".r, lazy { component }) do |a, b|
248
+ [unescape(a), b].flatten
249
+ end | text.map { |t| [unescape(t)] }
250
+ address1 = seq(component << ";".r, component << ";".r, component << ";".r, component << ";".r,
251
+ component << ";".r, component << ";".r, component) do |a, b, c, d, e, f, g|
252
+ a = a[0] if a.length == 1
253
+ b = b[0] if b.length == 1
254
+ c = c[0] if c.length == 1
255
+ d = d[0] if d.length == 1
256
+ e = e[0] if e.length == 1
257
+ f = f[0] if f.length == 1
258
+ g = g[0] if g.length == 1
259
+ { pobox: a, ext: b, street: c,
260
+ locality: d, region: e, code: f, country: g }
261
+ end | seq(component << ";".r, component << ";".r, component << ";".r, component << ";".r,
262
+ component << ";".r, component) do |a, b, c, d, e, f|
263
+ a = a[0] if a.length == 1
264
+ b = b[0] if b.length == 1
265
+ c = c[0] if c.length == 1
266
+ d = d[0] if d.length == 1
267
+ e = e[0] if e.length == 1
268
+ f = f[0] if f.length == 1
269
+ { pobox: a, ext: b, street: c,
270
+ locality: d, region: e, code: f, country: "" }
271
+ end | seq(component << ";".r, component << ";".r, component << ";".r,
272
+ component << ";".r, component) do |a, b, c, d, e|
273
+ a = a[0] if a.length == 1
274
+ b = b[0] if b.length == 1
275
+ c = c[0] if c.length == 1
276
+ d = d[0] if d.length == 1
277
+ e = e[0] if e.length == 1
278
+ { pobox: a, ext: b, street: c,
279
+ locality: d, region: e, code: "", country: "" }
280
+ end | seq(component << ";".r, component << ";".r, component << ";".r, component) do |a, b, c, d|
281
+ a = a[0] if a.length == 1
282
+ b = b[0] if b.length == 1
283
+ c = c[0] if c.length == 1
284
+ d = d[0] if d.length == 1
285
+ { pobox: a, ext: b, street: c,
286
+ locality: d, region: "", code: "", country: "" }
287
+ end | seq(component << ";".r, component << ";".r, component) do |a, b, c|
288
+ a = a[0] if a.length == 1
289
+ b = b[0] if b.length == 1
290
+ c = c[0] if c.length == 1
291
+ { pobox: a, ext: b, street: c,
292
+ locality: "", region: "", code: "", country: "" }
293
+ end | seq(component << ";".r, component) do |a, b|
294
+ a = a[0] if a.length == 1
295
+ b = b[0] if b.length == 1
296
+ { pobox: a, ext: b, street: "",
297
+ locality: "", region: "", code: "", country: "" }
298
+ end | component.map do |a|
299
+ a = a[0] if a.length == 1
300
+ { pobox: a, ext: "", street: "",
301
+ locality: "", region: "", code: "", country: "" }
302
+ end
303
+ address = address1.map { |n| PropertyValue::Address.new(n) }
304
+ address.eof
305
+ end
306
+
307
+ def registered_propname
308
+ registered_propname = C::NAME_VCARD
309
+ registered_propname.eof
310
+ end
311
+
312
+ def registered_propname?(x)
313
+ p = registered_propname.parse(x)
314
+ not(Rsec::INVALID[p])
315
+ end
316
+
317
+ # text escapes: \\ \; \, \N \n
318
+ def unescape(x)
319
+ # temporarily escape \\ as \007f, which is disallowed in any text
320
+ x.gsub(/\\\\/, "\u007f").gsub(/\\;/, ";").gsub(/\\,/, ",").gsub(/\\[Nn]/, "\n").tr("\u007f", "\\")
321
+ end
322
+
323
+ # Enforce type restrictions on values of particular properties.
324
+ # If successful, return typed interpretation of string
325
+ def typematch(strict, key, params, _component, value, ctx)
326
+ errors = []
327
+ params[:VALUE] = params[:VALUE].downcase if params && params[:VALUE]
328
+ ctx1 = Rsec::ParseContext.new value, "source"
329
+ case key
330
+ when :VERSION
331
+ ret = versionvalue._parse ctx1
332
+ when :SOURCE, :URL, :IMPP, :FBURL, :CALURI, :CALADRURI, :CAPURI
333
+ ret = uri._parse ctx1
334
+ # not imposing filename restrictions on calendar URIs
335
+ when :NAME, :FN, :LABEL, :EMAIL, :MAILER, :TITLE, :ROLE, :NOTE, :PRODID, :SORT_STRING, :UID
336
+ ret = text_t._parse ctx1
337
+ when :CLASS
338
+ ret = classvalue._parse ctx1
339
+ when :CATEGORIES, :NICKNAME
340
+ ret = textlist._parse ctx1
341
+ when :ORG
342
+ ret = org._parse ctx1
343
+ when :PROFILE
344
+ ret = profilevalue._parse ctx1
345
+ when :N
346
+ ret = fivepartname._parse ctx1
347
+ when :PHOTO, :LOGO, :SOUND
348
+ ret = if params && params[:VALUE] == "uri"
349
+ uri._parse ctx1
350
+ else
351
+ binary._parse ctx1
352
+ end
353
+ when :KEY
354
+ ret = if params && params[:ENCODING] == "b"
355
+ binary._parse ctx1
356
+ else
357
+ text_t._parse ctx1
358
+ end
359
+ when :BDAY
360
+ ret = if params && params[:VALUE] == "date-time"
361
+ date_time._parse ctx1
362
+ elsif params && params[:VALUE] == "date"
363
+ date_t._parse ctx1
364
+ else
365
+ # unlike VCARD 4, can have either date || date_time without explicit value switch
366
+ date_or_date_time._parse ctx1
367
+ end
368
+ when :REV
369
+ ret = if params && params[:VALUE] == "date"
370
+ date_t._parse ctx1
371
+ elsif params && params[:VALUE] == "date-time"
372
+ date_time._parse ctx1
373
+ else
374
+ # unlike VCARD 4, can have either date || date_time without explicit value switch
375
+ ret = date_or_date_time._parse ctx1
376
+ end
377
+ when :ADR
378
+ ret = address._parse ctx1
379
+ when :TEL
380
+ ret = phone_number._parse ctx1
381
+ when :TZ
382
+ ret = if params && params[:VALUE] == "text"
383
+ text_t._parse ctx1
384
+ else
385
+ utc_offset._parse ctx1
386
+ end
387
+ when :GEO
388
+ ret = geovalue._parse ctx1
389
+ when :AGENT
390
+ if params && params[:VALUE] == "uri"
391
+ ret = uri._parse ctx1
392
+ else
393
+ # unescape
394
+ value = value.gsub(/\\n/, "\n").gsub(/\\;/, ";").gsub(/\\,/, ",").gsub(/\\:/, ":")
395
+ # spec says that colons need to be escaped, but none of the examples do so
396
+ value = value.gsub(/BEGIN:VCARD\n/, "BEGIN:VCARD\nVERSION:3.0\n") unless value =~ /\nVERSION:3\.0/
397
+ ctx1 = Rsec::ParseContext.new value, "source"
398
+ ret = PropertyValue::Agent.new(Grammar.new(strict).vobject_grammar._parse(ctx1))
399
+ # TODO same strictness as grammar
400
+ end
401
+ else
402
+ ret = text_t._parse ctx1
403
+ end
404
+ if ret.is_a?(Hash) && ret[:error]
405
+ parse_err(strict, errors, "#{ret[:error]} for property #{key}, value #{value}", ctx)
406
+ end
407
+ if Rsec::INVALID[ret]
408
+ parse_err(strict, errors, "Type mismatch for property #{key}, value #{value}", ctx)
409
+ end
410
+ Rsec::Fail.reset
411
+ [ret, errors]
412
+ end
413
+
414
+ private
415
+
416
+ def parse_err(strict, errors, msg, ctx)
417
+ if strict
418
+ raise ctx.report_error msg, "source"
419
+ else
420
+ errors << ctx.report_error(msg, "source")
421
+ end
422
+ end
423
+ end
424
+ end
425
+ end
@@ -0,0 +1,40 @@
1
+ require "vobject/component"
2
+ require "vobject/vcard/v4_0/property"
3
+ require "vobject/vcard/v4_0/grammar"
4
+ require "pp"
5
+
6
+ module Vcard::V4_0
7
+ class Component < Vobject::Component
8
+ class << self
9
+ def parse(vcf, strict)
10
+ hash = Vcard::V4_0::Grammar.new(strict).parse(vcf)
11
+ comp_name = hash.keys.first
12
+ new comp_name, hash[comp_name], hash[:errors]
13
+ end
14
+
15
+ private
16
+
17
+ def raise_invalid_parsing
18
+ raise "vCard parse failed"
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def property_base_class
25
+ version_class.const_get(:Property)
26
+ end
27
+
28
+ def component_base_class
29
+ version_class.const_get(:Component)
30
+ end
31
+
32
+ def parameter_base_class
33
+ version_class.const_get(:Parameter)
34
+ end
35
+
36
+ def version_class
37
+ Vcard::V4_0
38
+ end
39
+ end
40
+ end