icalendar 2.10.3 → 2.11.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.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +1 -0
- data/CHANGELOG.md +11 -0
- data/icalendar.gemspec +2 -0
- data/lib/icalendar/offset/active_support_exact.rb +23 -0
- data/lib/icalendar/offset/active_support_partial.rb +27 -0
- data/lib/icalendar/offset/null.rb +17 -0
- data/lib/icalendar/offset/time_zone_store.rb +29 -0
- data/lib/icalendar/offset.rb +42 -0
- data/lib/icalendar/parser.rb +7 -2
- data/lib/icalendar/values/helpers/time_with_zone.rb +12 -34
- data/lib/icalendar/version.rb +1 -1
- data/spec/fixtures/single_event_bad_location.ics +23 -0
- data/spec/fixtures/two_time_events.ics +1 -1
- data/spec/parser_spec.rb +10 -0
- metadata +37 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0d1b88e3b63c6a6c5865093d45c296bfc4f0d807922b79bbd071355f5bdf72a2
|
4
|
+
data.tar.gz: 2b0e5e61d9dbab1ec76aab2a97bf3055da0ee42bfcb294867e1cba5d60841d17
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 070602bed48d0b45c04dc6801944b0f2d8916a67ecc39194ff93a5bc6619aca5eafad06843cb39df72910f9babbe010310dcada5bce1a27d046297586a0d1042
|
7
|
+
data.tar.gz: f4e43c98d659fa97b4cffadb0c79ba34ef0ba077d181272747eaf0065301d1b540a3fa5169dcd8d1701a60076f1f748ec8d28a44d1aefaf179197437ecab2958
|
data/.github/workflows/main.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,16 @@
|
|
1
1
|
## Unreleased
|
2
2
|
|
3
|
+
## 2.11.2 - 2025-06-21
|
4
|
+
- Deal with more bad value parameter values by falling back to the property default type
|
5
|
+
|
6
|
+
## 2.11.1 - 2025-06-06
|
7
|
+
- Gracefully deal with malformed ics files that use spaces in the value parameter instead of hyphens
|
8
|
+
|
9
|
+
## 2.11.0 - 2025-04-12
|
10
|
+
- Require gems for ruby 3.4 that used to be in stdlib - Go Sueyoshi
|
11
|
+
- Refactor how timezone offsets are calculated - Pat Allan
|
12
|
+
- 'tzid' param is always returned as an array when accessing `ical_params` directly, rather than matching whatever the input parsed. ICS output remains unchanged
|
13
|
+
|
3
14
|
## 2.10.3 - 2024-09-21
|
4
15
|
- Override Icalendar::Value.== so that value objects can be compared to each other.
|
5
16
|
- Correctly load activesupport before activesupport/time
|
data/icalendar.gemspec
CHANGED
@@ -31,7 +31,9 @@ ActiveSupport is required for TimeWithZone support, but not required for general
|
|
31
31
|
|
32
32
|
s.required_ruby_version = '>= 2.4.0'
|
33
33
|
|
34
|
+
s.add_dependency 'base64'
|
34
35
|
s.add_dependency 'ice_cube', '~> 0.16'
|
36
|
+
s.add_dependency 'logger'
|
35
37
|
s.add_dependency 'ostruct'
|
36
38
|
|
37
39
|
s.add_development_dependency 'rake', '~> 13.0'
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Icalendar
|
4
|
+
class Offset
|
5
|
+
class ActiveSupportExact < Icalendar::Offset
|
6
|
+
def valid?
|
7
|
+
support_classes_defined? && tz
|
8
|
+
end
|
9
|
+
|
10
|
+
def normalized_value
|
11
|
+
Icalendar.logger.debug("Plan a - parsing #{value}/#{tzid} as ActiveSupport::TimeWithZone")
|
12
|
+
# plan a - use ActiveSupport::TimeWithZone
|
13
|
+
Icalendar::Values::Helpers::ActiveSupportTimeWithZoneAdapter.new(nil, tz, value)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def tz
|
19
|
+
@tz ||= ActiveSupport::TimeZone[tzid]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Icalendar
|
4
|
+
class Offset
|
5
|
+
class ActiveSupportPartial < Offset
|
6
|
+
def valid?
|
7
|
+
support_classes_defined? && tz
|
8
|
+
end
|
9
|
+
|
10
|
+
def normalized_value
|
11
|
+
# plan c - try to find an ActiveSupport::TimeWithZone based on the first word of the tzid
|
12
|
+
Icalendar.logger.debug("Plan c - parsing #{value}/#{tz.tzinfo.name} as ActiveSupport::TimeWithZone")
|
13
|
+
Icalendar::Values::Helpers::ActiveSupportTimeWithZoneAdapter.new(nil, tz, value)
|
14
|
+
end
|
15
|
+
|
16
|
+
def normalized_tzid
|
17
|
+
[tz.tzinfo.name]
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def tz
|
23
|
+
@tz ||= ActiveSupport::TimeZone[tzid.split.first]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Icalendar
|
4
|
+
class Offset
|
5
|
+
class Null < Offset
|
6
|
+
def valid?
|
7
|
+
true
|
8
|
+
end
|
9
|
+
|
10
|
+
def normalized_value
|
11
|
+
# plan d - just ignore the tzid
|
12
|
+
Icalendar.logger.info("Ignoring timezone #{tzid} for time #{value}")
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Icalendar
|
4
|
+
class Offset
|
5
|
+
class TimeZoneStore < Offset
|
6
|
+
def valid?
|
7
|
+
timezone_store && tz_info
|
8
|
+
end
|
9
|
+
|
10
|
+
def normalized_value
|
11
|
+
# plan b - use definition from provided `VTIMEZONE`
|
12
|
+
offset = tz_info.offset_for_local(value).to_s
|
13
|
+
|
14
|
+
Icalendar.logger.debug("Plan b - parsing #{value} with offset: #{offset}")
|
15
|
+
if value.respond_to?(:change)
|
16
|
+
value.change offset: offset
|
17
|
+
else
|
18
|
+
::Time.new value.year, value.month, value.day, value.hour, value.min, value.sec, offset
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def tz_info
|
25
|
+
@tz_info ||= timezone_store.retrieve(tzid)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Icalendar
|
4
|
+
class Offset
|
5
|
+
def self.build(value, params, timezone_store)
|
6
|
+
return nil if params.nil? || params['tzid'].nil?
|
7
|
+
|
8
|
+
tzid = Array(params['tzid']).first
|
9
|
+
|
10
|
+
[
|
11
|
+
Icalendar::Offset::ActiveSupportExact,
|
12
|
+
Icalendar::Offset::TimeZoneStore,
|
13
|
+
Icalendar::Offset::ActiveSupportPartial,
|
14
|
+
Icalendar::Offset::Null
|
15
|
+
].lazy.map { |klass| klass.new(tzid, value, timezone_store) }.detect(&:valid?)
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(tzid, value, timezone_store)
|
19
|
+
@tzid = tzid
|
20
|
+
@value = value
|
21
|
+
@timezone_store = timezone_store
|
22
|
+
end
|
23
|
+
|
24
|
+
def normalized_tzid
|
25
|
+
Array(tzid)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :tzid, :value, :timezone_store
|
31
|
+
|
32
|
+
def support_classes_defined?
|
33
|
+
defined?(ActiveSupport::TimeZone) &&
|
34
|
+
defined?(Icalendar::Values::Helpers::ActiveSupportTimeWithZoneAdapter)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
require_relative "offset/active_support_exact"
|
40
|
+
require_relative "offset/active_support_partial"
|
41
|
+
require_relative "offset/null"
|
42
|
+
require_relative "offset/time_zone_store"
|
data/lib/icalendar/parser.rb
CHANGED
@@ -105,7 +105,7 @@ module Icalendar
|
|
105
105
|
((multi_property && value =~ WRAP_IN_ARRAY_REGEX_1) || value =~ WRAP_IN_ARRAY_REGEX_2)
|
106
106
|
end
|
107
107
|
|
108
|
-
GET_WRAPPER_CLASS_GSUB_REGEX = /(?:\A
|
108
|
+
GET_WRAPPER_CLASS_GSUB_REGEX = /(?:\A|-|\s+)(.)/.freeze
|
109
109
|
|
110
110
|
def get_wrapper_class(component, fields)
|
111
111
|
klass = component.class.default_property_types[fields[:name]]
|
@@ -113,7 +113,12 @@ module Icalendar
|
|
113
113
|
klass_name = fields[:params].delete('value').first
|
114
114
|
unless klass_name.upcase == klass.value_type
|
115
115
|
klass_name = "Icalendar::Values::#{klass_name.downcase.gsub(GET_WRAPPER_CLASS_GSUB_REGEX) { |m| m[-1].upcase }}"
|
116
|
-
|
116
|
+
begin
|
117
|
+
klass = Object.const_get klass_name if Object.const_defined?(klass_name)
|
118
|
+
rescue NameError => e
|
119
|
+
Icalendar.logger.error "NameError trying to find value type for #{component.name} | #{fields[:name]}: #{e.message}"
|
120
|
+
raise e if strict?
|
121
|
+
end
|
117
122
|
end
|
118
123
|
end
|
119
124
|
klass
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "icalendar/offset"
|
4
|
+
|
3
5
|
begin
|
4
6
|
require 'active_support'
|
5
7
|
require 'active_support/time'
|
@@ -21,7 +23,13 @@ module Icalendar
|
|
21
23
|
params = Icalendar::DowncasedHash(params)
|
22
24
|
@tz_utc = params['tzid'] == 'UTC'
|
23
25
|
@timezone_store = params.delete 'x-tz-store'
|
24
|
-
|
26
|
+
|
27
|
+
offset = Icalendar::Offset.build(value, params, timezone_store)
|
28
|
+
|
29
|
+
@offset_value = offset&.normalized_value
|
30
|
+
params['tzid'] = offset.normalized_tzid if offset
|
31
|
+
|
32
|
+
super (@offset_value || value), params
|
25
33
|
end
|
26
34
|
|
27
35
|
def __getobj__
|
@@ -29,9 +37,9 @@ module Icalendar
|
|
29
37
|
if set_offset?
|
30
38
|
orig_value
|
31
39
|
else
|
32
|
-
|
33
|
-
__setobj__(
|
34
|
-
|
40
|
+
new_value = Icalendar::Offset.build(orig_value, ical_params, timezone_store)&.normalized_value
|
41
|
+
__setobj__(new_value) unless new_value.nil?
|
42
|
+
new_value || orig_value
|
35
43
|
end
|
36
44
|
end
|
37
45
|
|
@@ -42,36 +50,6 @@ module Icalendar
|
|
42
50
|
|
43
51
|
private
|
44
52
|
|
45
|
-
def offset_value(value, params)
|
46
|
-
@offset_value = unless params.nil? || params['tzid'].nil?
|
47
|
-
tzid = params['tzid'].is_a?(::Array) ? params['tzid'].first : params['tzid']
|
48
|
-
support_classes_defined = defined?(ActiveSupport::TimeZone) && defined?(ActiveSupportTimeWithZoneAdapter)
|
49
|
-
if support_classes_defined && (tz = ActiveSupport::TimeZone[tzid])
|
50
|
-
Icalendar.logger.debug("Plan a - parsing #{value}/#{tzid} as ActiveSupport::TimeWithZone")
|
51
|
-
# plan a - use ActiveSupport::TimeWithZone
|
52
|
-
ActiveSupportTimeWithZoneAdapter.new(nil, tz, value)
|
53
|
-
elsif !timezone_store.nil? && !(x_tz_info = timezone_store.retrieve(tzid)).nil?
|
54
|
-
# plan b - use definition from provided `VTIMEZONE`
|
55
|
-
offset = x_tz_info.offset_for_local(value).to_s
|
56
|
-
Icalendar.logger.debug("Plan b - parsing #{value} with offset: #{offset}")
|
57
|
-
if value.respond_to?(:change)
|
58
|
-
value.change offset: offset
|
59
|
-
else
|
60
|
-
::Time.new value.year, value.month, value.day, value.hour, value.min, value.sec, offset
|
61
|
-
end
|
62
|
-
elsif support_classes_defined && (tz = ActiveSupport::TimeZone[tzid.split.first])
|
63
|
-
# plan c - try to find an ActiveSupport::TimeWithZone based on the first word of the tzid
|
64
|
-
Icalendar.logger.debug("Plan c - parsing #{value}/#{tz.tzinfo.name} as ActiveSupport::TimeWithZone")
|
65
|
-
params['tzid'] = [tz.tzinfo.name]
|
66
|
-
ActiveSupportTimeWithZoneAdapter.new(nil, tz, value)
|
67
|
-
else
|
68
|
-
# plan d - just ignore the tzid
|
69
|
-
Icalendar.logger.info("Ignoring timezone #{tzid} for time #{value}")
|
70
|
-
nil
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
53
|
def set_offset?
|
76
54
|
!!@offset_value
|
77
55
|
end
|
data/lib/icalendar/version.rb
CHANGED
@@ -0,0 +1,23 @@
|
|
1
|
+
BEGIN:VCALENDAR
|
2
|
+
VERSION:2.0
|
3
|
+
PRODID:bsprodidfortestabc123
|
4
|
+
CALSCALE:GREGORIAN
|
5
|
+
BEGIN:VEVENT
|
6
|
+
DTSTAMP:20050118T211523Z
|
7
|
+
UID:bsuidfortestabc123
|
8
|
+
DTSTART;TZID=US-Mountain:20050120T170000
|
9
|
+
DTEND;TZID=US-Mountain:20050120T184500
|
10
|
+
CLASS:PRIVATE
|
11
|
+
GEO:37.386013;-122.0829322
|
12
|
+
ORGANIZER;CN="Joe Bob: Magician":mailto:joebob@random.net
|
13
|
+
LOCATION;VALUE=1000 MAIN ST EXAMPLE, STATE 12345:1000 Main St Example\, State 12345
|
14
|
+
PRIORITY:2
|
15
|
+
SUMMARY:This is a really long summary to test the method of unfolding lines
|
16
|
+
\, so I'm just going to make it a whole bunch of lines. With a twist: a "
|
17
|
+
ö" takes up multiple bytes\, and should be wrapped to the next line.
|
18
|
+
ATTACH:http://bush.sucks.org/impeach/him.rhtml
|
19
|
+
ATTACH:http://corporations-dominate.existence.net/why.rhtml
|
20
|
+
RDATE;TZID=US-Mountain:20050121T170000,20050122T170000
|
21
|
+
X-TEST-COMPONENT;QTEST="Hello, World":Shouldn't double double quotes
|
22
|
+
END:VEVENT
|
23
|
+
END:VCALENDAR
|
data/spec/parser_spec.rb
CHANGED
@@ -116,4 +116,14 @@ describe Icalendar::Parser do
|
|
116
116
|
expect(event.dtstart).to be_kind_of Icalendar::Values::Date
|
117
117
|
end
|
118
118
|
end
|
119
|
+
|
120
|
+
describe 'completely bad location value' do
|
121
|
+
let(:fn) { 'single_event_bad_location.ics' }
|
122
|
+
|
123
|
+
it 'falls back to string type for location' do
|
124
|
+
event = subject.parse.first.events.first
|
125
|
+
expect(event.location).to be_kind_of Icalendar::Values::Text
|
126
|
+
expect(event.location.value).to eq "1000 Main St Example, State 12345"
|
127
|
+
end
|
128
|
+
end
|
119
129
|
end
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: icalendar
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.11.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Ahearn
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-06-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: base64
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: ice_cube
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -24,6 +38,20 @@ dependencies:
|
|
24
38
|
- - "~>"
|
25
39
|
- !ruby/object:Gem::Version
|
26
40
|
version: '0.16'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: logger
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
27
55
|
- !ruby/object:Gem::Dependency
|
28
56
|
name: ostruct
|
29
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -195,6 +223,11 @@ files:
|
|
195
223
|
- lib/icalendar/journal.rb
|
196
224
|
- lib/icalendar/logger.rb
|
197
225
|
- lib/icalendar/marshable.rb
|
226
|
+
- lib/icalendar/offset.rb
|
227
|
+
- lib/icalendar/offset/active_support_exact.rb
|
228
|
+
- lib/icalendar/offset/active_support_partial.rb
|
229
|
+
- lib/icalendar/offset/null.rb
|
230
|
+
- lib/icalendar/offset/time_zone_store.rb
|
198
231
|
- lib/icalendar/parser.rb
|
199
232
|
- lib/icalendar/timezone.rb
|
200
233
|
- lib/icalendar/timezone_store.rb
|
@@ -234,6 +267,7 @@ files:
|
|
234
267
|
- spec/fixtures/single_event.ics
|
235
268
|
- spec/fixtures/single_event_bad_dtstart.ics
|
236
269
|
- spec/fixtures/single_event_bad_line.ics
|
270
|
+
- spec/fixtures/single_event_bad_location.ics
|
237
271
|
- spec/fixtures/single_event_bad_organizer.ics
|
238
272
|
- spec/fixtures/single_event_organizer_parsed.ics
|
239
273
|
- spec/fixtures/timezone.ics
|
@@ -302,6 +336,7 @@ test_files:
|
|
302
336
|
- spec/fixtures/single_event.ics
|
303
337
|
- spec/fixtures/single_event_bad_dtstart.ics
|
304
338
|
- spec/fixtures/single_event_bad_line.ics
|
339
|
+
- spec/fixtures/single_event_bad_location.ics
|
305
340
|
- spec/fixtures/single_event_bad_organizer.ics
|
306
341
|
- spec/fixtures/single_event_organizer_parsed.ics
|
307
342
|
- spec/fixtures/timezone.ics
|