vobject 0.1.0 → 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 (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