require "rsec" require "set" require "uri" require "date" require "tzinfo" include Rsec::Helpers require_relative "../../c" require_relative "../../error" require_relative "./propertyparent" require "vobject" require_relative "./propertyvalue" module Vobject::Vcalendar class Typegrammars class << self # property value types, each defining their own parser def recur freq = /SECONDLY/i.r | /MINUTELY/i.r | /HOURLY/i.r | /DAILY/i.r | /WEEKLY/i.r | /MONTHLY/i.r | /YEARLY/i.r enddate = C::DATE_TIME | C::DATE seconds = /[0-9]{1,2}/.r byseclist = seq(seconds << ",".r, lazy { byseclist }) do |s, l| [s, l].flatten end | seconds.map { |s| [s] } minutes = /[0-9]{1,2}/.r byminlist = seq(minutes << ",".r, lazy { byminlist }) do |m, l| [m, l].flatten end | minutes.map { |m| [m] } hours = /[0-9]{1,2}/.r byhrlist = seq(hours << ",".r, lazy { byhrlist }) do |h, l| [h, l].flatten end | hours.map { |h| [h] } ordwk = /[0-9]{1,2}/.r weekday = /SU/i.r | /MO/i.r | /TU/i.r | /WE/i.r | /TH/i.r | /FR/i.r | /SA/i.r weekdaynum1 = seq(C::SIGN._?, ordwk) do |s, o| h = { ordwk: o } h[:sign] = s[0] unless s.empty? h end weekdaynum = seq(weekdaynum1._?, weekday) do |a, b| h = { weekday: b } h = h.merge a[0] unless a.empty? h end bywdaylist = seq(weekdaynum << ",".r, lazy { bywdaylist }) do |w, l| [w, l].flatten end | weekdaynum.map { |w| [w] } ordmoday = /[0-9]{1,2}/.r monthdaynum = seq(C::SIGN._?, ordmoday) do |s, o| h = { ordmoday: o } h[:sign] = s[0] unless s.empty? h end bymodaylist = seq(monthdaynum << ",".r, lazy { bymodaylist }) do |m, l| [m, l].flatten end | monthdaynum.map { |m| [m] } ordyrday = /[0-9]{1,3}/.r yeardaynum = seq(C::SIGN._?, ordyrday) do |s, o| h = { ordyrday: o } h[:sign] = s[0] unless s.empty? h end byyrdaylist = seq(yeardaynum << ",".r, lazy { byyrdaylist }) do |y, l| [y, l].flatten end | yeardaynum.map { |y| [y] } weeknum = seq(C::SIGN._?, ordwk) do |s, o| h = { ordwk: o } h[:sign] = s[0] unless s.empty? h end bywknolist = seq(weeknum << ",".r, lazy { bywknolist }) do |w, l| [w, l].flatten end | weeknum.map { |w| [w] } # monthnum = /[0-9]{1,2}/.r # RFC 7529 add leap month indicator monthnum = /[0-9]{1,2}L?/i.r bymolist = seq(monthnum << ",".r, lazy { bymolist }) do |m, l| [m, l].flatten end | monthnum.map { |m| [m] } setposday = yeardaynum bysplist = seq(setposday << ",".r, lazy { bysplist }) do |s, l| [s, l].flatten end | setposday.map { |s| [s] } # http://www.unicode.org/repos/cldr/tags/latest/common/bcp47/calendar.xml rscale = C::XNAME_VCAL | /buddhist/i.r | /chinese/i.r | /coptic/i.r | /dangi/i.r | /ethioaa/i.r | /ethiopic-amete-alem/i.r | /ethiopic/i.r | /gregory/i.r | /hebrew/i.r | /indian/i.r | /islamic/i.r | /islamic-umalqura/i.r | /islamic-tbla/i.r | /islamic-civil/i.r | /islamic-rgsa/i.r | /iso8601/i.r | /japanese/i.r | /persian/i.r | /roc/i.r | /islamicc/i.r | /gregorian/i.r skip = /OMIT/i.r | /BACKWARD/i.r | /FORWARD/i.r recur_rule_part = seq(/FREQ/i.r << "=".r, freq) { |_k, v| { freq: v } } | seq(/UNTIL/i.r << "=".r, enddate) { |_k, v| { until: v } } | seq(/COUNT/i.r << "=".r, /[0-9]+/i.r) { |_k, v| { count: v } } | seq(/INTERVAL/i.r << "=".r, /[0-9]+/i.r) { |_k, v| { interval: v } } | seq(/BYSECOND/i.r << "=".r, byseclist) { |_k, v| { bysecond: v } } | seq(/BYMINUTE/i.r << "=".r, byminlist) { |_k, v| { byminute: v } } | seq(/BYHOUR/i.r << "=".r, byhrlist) { |_k, v| { byhour: v } } | seq(/BYDAY/i.r << "=".r, bywdaylist) { |_k, v| { byday: v } } | seq(/BYMONTHDAY/i.r << "=".r, bymodaylist) { |_k, v| { bymonthday: v } } | seq(/BYYEARDAY/i.r << "=".r, byyrdaylist) { |_k, v| { byyearday: v } } | seq(/BYWEEKNO/i.r << "=".r, bywknolist) { |_k, v| { byweekno: v } } | seq(/BYMONTH/i.r << "=".r, bymolist) { |_k, v| { bymonth: v } } | seq(/BYSETPOS/i.r << "=".r, bysplist) { |_k, v| { bysetpos: v } } | seq(/WKST/i.r << "=".r, weekday) { |_k, v| { wkst: v } } | # RFC 7529 seq(/RSCALE/i.r << "=".r, rscale) { |_k, v| { rscale: v } } | seq(/SKIP/i.r << "=".r, skip) { |_k, v| { skip: v } } recur1 = seq(recur_rule_part, ";", lazy { recur1 }) { |h, _, r| h.merge r } | recur_rule_part recur = recur1.map { |r| PropertyValue::Recur.new r } recur.eof end def integer integer = prim(:int32).map { |i| PropertyValue::Integer.new i } integer.eof end def percent_complete integer = prim(:int32).map do |a| if a >= 0 && a <= 100 PropertyValue::PercentComplete.new a else { error: "Percentage outside of range 0..100" } end end integer.eof end def priority integer = prim(:int32).map do |a| if a >= 0 && a <= 9 PropertyValue::Priority.new a else { error: "Percentage outside of range 0..100" } end end integer.eof end def float_t float_t = prim(:double).map { |f| PropertyValue::Float.new f } float_t.eof end def time_t time_t = C::TIME.map { |t| PropertyValue::Time.new t } time_t.eof end def geovalue float = prim(:double) geovalue = seq(float << ";".r, float) do |a, b| if a <= 180.0 && a >= -180.0 && b <= 180 && b > -180 PropertyValue::Geovalue.new(lat: a, long: b) else { error: "Latitude/Longitude outside of range -180..180" } end end geovalue.eof end def calscalevalue calscalevalue = /GREGORIAN/i.r.map { PropertyValue::Calscale.new "GREGORIAN" } calscalevalue.eof end def iana_token iana_token = C::IANATOKEN.map { |x| PropertyValue::Ianatoken.new x } iana_token.eof end def versionvalue versionvalue = seq(prim(:double) << ";".r, prim(:double)) do |x, y| PropertyValue::Version.new [x, y] end | "2.0".r.map do PropertyValue::Version.new ["2.0"] end | prim(:double).map do |v| PropertyValue::Version.new v end versionvalue.eof end def binary binary = seq(/[a-zA-Z0-9+\/]*/.r, /={0,2}/.r) do |b, q| if (b.length + q.length) % 4 == 0 PropertyValue::Binary.new(b + q) else { error: "Malformed binary coding" } end end binary.eof end def uri uri = /\S+/.r.map do |s| if s =~ URI::DEFAULT_PARSER.make_regexp PropertyValue::Uri.new(s) else { error: "Invalid URI" } end end uri.eof end def text_t text_t = C::TEXT.map { |t| PropertyValue::Text.new(unescape(t)) } text_t.eof end def textlist textlist1 = seq(C::TEXT << ",".r, lazy { textlist1 }) { |a, b| [unescape(a), b].flatten } | C::TEXT.map { |t| [unescape(t)] } textlist = textlist1.map { |m| PropertyValue::Textlist.new m } textlist.eof end def request_statusvalue @req_status = Set.new %w{2.0 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 2.10 2.11 3.0 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10 3.11 3.12 3.13 3.14 4.0 5.0 5.1 5.2 5.3} extdata = seq(";".r, C::TEXT) { |_, t| t } request_statusvalue = seq(/[0-9](\.[0-9]){1,2}/.r << ";".r, C::TEXT, extdata._?) do |n, t1, t2| return { error: "Invalid request status #{n}" } unless @req_status.include?(n) # RFC 5546 hash = { statcode: n, statdesc: t1 } hash[:extdata] = t2[0] unless t2.empty? Vobject::Vcalendar::PropertyValue::Requeststatusvalue.new hash end request_statusvalue.eof end def classvalue classvalue = (/PUBLIC/i.r | /PRIVATE/i.r | /CONFIDENTIAL/i.r | C::XNAME_VCAL | C::IANATOKEN).map do |m| PropertyValue::ClassValue.new m end classvalue.eof end def eventstatus eventstatus = (/TENTATIVE/i.r | /CONFIRMED/i.r | /CANCELLED/i.r).map do |m| PropertyValue::EventStatus.new m end eventstatus.eof end def todostatus todostatus = (/NEEDS-ACTION/i.r | /COMPLETED/i.r | /IN-PROCESS/i.r | /CANCELLED/i.r).map do |m| PropertyValue::Todostatus.new m end todostatus.eof end def journalstatus journalstatus = (/DRAFT/i.r | /FINAL/i.r | /CANCELLED/i.r).map do |m| PropertyValue::Journalstatus.new m end journalstatus.eof end def date_t date_t = C::DATE date_t.eof end def datelist datelist1 = seq(C::DATE << ",".r, lazy { datelist1 }) do |d, l| [d, l].flatten end | C::DATE.map { |d| [d] } datelist = datelist1.map { |m| PropertyValue::Datelist.new m } datelist.eof end def date_time_t C::DATE_TIME.eof end def date_timelist date_timelist1 = seq(C::DATE_TIME << ",".r, lazy { date_timelist1 }) do |d, l| [d, l].flatten end | C::DATE_TIME.map { |d| [d] } date_timelist = date_timelist1.map do |m| PropertyValue::Datetimelist.new m end date_timelist.eof end def date_time_utc_t date_time_utc_t = C::DATE_TIME_UTC date_time_utc_t.eof end def date_time_utclist date_time_utclist1 = seq(C::DATE_TIME_UTC << ",".r, lazy { date_time_utclist1 }) do |d, l| [d, l].flatten end | C::DATE_TIME_UTC.map { |d| [d] } date_time_utclist = date_time_utclist1.map do |m| PropertyValue::Datetimeutclist.new m end date_time_utclist.eof end def duration_t duration = C::DURATION.map { |d| PropertyValue::Duration.new d } duration.eof end def periodlist period_explicit = seq(C::DATE_TIME << "/".r, C::DATE_TIME) do |s, e| { start: s, end: e } end period_start = seq(C::DATE_TIME << "/".r, C::DURATION) do |s, d| { start: s, duration: PropertyValue::Duration.new(d) } end period = period_explicit | period_start periodlist1 = seq(period << ",".r, lazy { periodlist1 }) do |p, l| [p, l].flatten end | period.map { |p| [p] } periodlist = periodlist1.map { |m| PropertyValue::Periodlist.new m } periodlist.eof end def transpvalue transpvalue = (/OPAQUE/i.r | /TRANSPARENT/i.r).map do |m| PropertyValue::TranspValue.new m end transpvalue.eof end def utc_offset utc_offset = seq(C::SIGN, /[0-9]{2}/.r, /[0-9]{2}/.r, /[0-9]{2}/.r._?) do |sign, h, m, sec| hash = { sign: sign, hr: h, min: m } hash[:sec] = sec[0] unless sec.empty? PropertyValue::Utcoffset.new hash end utc_offset.eof end def actionvalue actionvalue = (/AUDIO/i.r | /DISPLAY/i.r | /EMAIL/i.r | C::IANATOKEN | C::XNAME_VCAL).map { |m| PropertyValue::ActionValue.new m } actionvalue.eof end def boolean boolean = C::BOOLEAN.map { |b| PropertyValue::Boolean.new b } boolean.eof end # RFC 5546 def methodvalue methodvalue = (/PUBLISH/i.r | /REQUEST/i.r | /REPLY/i.r | /ADD/i.r | /CANCEL/i.r | /REFRESH/i.r | /COUNTER/i.r | /DECLINECOUNTER/i.r).map { |m| PropertyValue::MethodValue.new m } methodvalue.eof end # RFC 7953 def busytype busytype = (/BUSY-UNAVAILABLE/i.r | /BUSY-TENTATIVE/i.r | /BUSY/i.r | C::IANATOKEN | C::XNAME_VCAL).map { |m| PropertyValue::BusyType.new m } busytype.eof end # https://www.w3.org/TR/2011/REC-css3-color-20110607/#svg-color def color color = C::COLOR.map { |m| PropertyValue::Color.new m } color.eof end # text escapes: \\ \; \, \N \n def unescape(x) # temporarily escape \\ as \007f, which is disallowed in any text x.gsub(/\\\\/, "\u007f").gsub(/\\;/, ";").gsub(/\\,/, ","). gsub(/\\[Nn]/, "\n").tr("\u007f", "\\") end def registered_propname registered_propname = C::NAME_VCAL registered_propname.eof end def registered_propname?(x) p = registered_propname.parse(x) not(Rsec::INVALID[p]) end # Enforce type restrictions on values of particular properties. # If successful, return typed interpretation of string def typematch(strict, key, params, component, value, ctx) errors = [] errors << property_parent(strict, key, component, value, ctx) ctx1 = Rsec::ParseContext.new value, "source" case key when :CALSCALE ret = calscalevalue._parse ctx1 when :METHOD ret = methodvalue._parse ctx1 when :VERSION ret = versionvalue._parse ctx1 when :ATTACH ret = if params[:VALUE] == "BINARY" binary._parse ctx1 else uri._parse ctx1 end when :IMAGE parse_err(strict, errors, "No VALUE parameter specified for property #{key}", ctx1) if params.empty? parse_err(strict, errors, "No VALUE parameter specified for property #{key}", ctx1) unless params[:VALUE] if params[:VALUE] == "BINARY" parse_err(strict, errors, "No ENCODING parameter specified for property #{key}", ctx1) unless params[:ENCODING] parse_err(strict, errors, "Incorrect ENCODING parameter specified for property #{key}", ctx1) unless params[:ENCODING] == "BASE64" ret = binary._parse ctx1 elsif params[:VALUE] == "URI" ret = uri._parse ctx1 else parse_err(strict, errors, "Incorrect VALUE parameter specified for property #{key}", ctx1) end when :CATEGORIES, :RESOURCES ret = textlist._parse ctx1 when :CLASS ret = classvalue._parse ctx1 when :COMMENT, :DESCRIPTION, :LOCATION, :SUMMARY, :TZID, :TZNAME, :CONTACT, :RELATED_TO, :UID, :PRODID, :NAME ret = text_t._parse ctx1 when :GEO ret = geovalue._parse ctx1 when :PERCENT_COMPLETE ret = percent_complete._parse ctx1 when :PRIORITY ret = priority._parse ctx1 when :STATUS ret = case component when :EVENT eventstatus._parse ctx1 when :TODO todostatus._parse ctx1 when :JOURNAL journalstatus._parse ctx1 else text_t._parse ctx1 end when :COMPLETED, :CREATED, :DTSTAMP, :LAST_MODIFIED ret = date_time_utc_t._parse ctx1 when :DTEND, :DTSTART, :DUE, :RECURRENCE_ID if params && params[:VALUE] == "DATE" ret = date_t._parse ctx1 elsif component == :FREEBUSY ret = date_time_utc_t._parse ctx1 elsif params && params[:TZID] if [:STANDARD || :DAYLIGHT].include? component parse_err(strict, errors, "Specified TZID within property #{key} in #{component}", ctx1) end begin tz = TZInfo::Timezone.get(params[:TZID]) ret = date_time_t._parse ctx1 # note that we use the registered tz information to map to UTC, rather than look up the values witin the VTIMEZONE component ret.value[:time] = tz.local_to_utc(ret.value[:time]) ret.value[:zone] = params[:TZID] rescue # undefined timezone: default to floating local ret = date_time_t._parse ctx1 end else ret = date_time_t._parse ctx1 end when :EXDATE if params && params[:VALUE] == "DATE" ret = datelist._parse ctx1 elsif params && params[:TZID] if [:STANDARD || :DAYLIGHT].include? component parse_err(strict, errors, "Specified TZID within property #{key} in #{component}", ctx1) end tz = TZInfo::Timezone.get(params[:TZID]) ret = date_timelist._parse ctx1 ret.value.each do |x| x.value[:time] = tz.local_to_utc(x.value[:time]) x.value[:zone] = params[:TZID] end else ret = date_timelist._parse ctx1 end when :RDATE if params && params[:VALUE] == "DATE" ret = datelist._parse ctx1 elsif params && params[:VALUE] == "PERIOD" ret = periodlist._parse ctx1 elsif params && params[:TZID] if [:STANDARD || :DAYLIGHT].include? component parse_err(strict, errors, "Specified TZID within property #{key} in #{component}", ctx1) end tz = TZInfo::Timezone.get(params[:TZID]) ret = date_timelist._parse ctx1 ret.value.each do |x| x.value[:time] = tz.local_to_utc(x.value[:time]) x.value[:zone] = params[:TZID] end else ret = date_timelist._parse ctx1 end when :TRIGGER if params && params[:VALUE] == "DATE-TIME" || /^\d{8}T/.match(value) if params && params[:RELATED] parse_err(strict, errors, "Specified RELATED within property #{key} as date-time", ctx1) end ret = date_time_utc_t._parse ctx1 else ret = duration_t._parse ctx1 end when :FREEBUSY ret = periodlist._parse ctx1 when :TRANSP ret = transpvalue._parse ctx1 when :TZOFFSETFROM, :TZOFFSETTO ret = utc_offset._parse ctx1 when :TZURI, :URL, :SOURCE, :CONFERENCE if key == :CONFERENCE parse_err(strict, errors, "Missing URI VALUE parameter", ctx1) if params.empty? parse_err(strict, errors, "Missing URI VALUE parameter", ctx1) if !params[:VALUE] parse_err(strict, errors, "report_error Type mismatch of VALUE parameter #{params[:VALUE]} for property #{key}", ctx1) if params[:VALUE] != "URI" end ret = uri._parse ctx1 when :ATTENDEE, :ORGANIZER ret = uri._parse ctx1 when :RRULE ret = recur._parse ctx1 when :ACTION ret = actionvalue._parse ctx1 when :REPEAT, :SEQUENCE ret = integer._parse ctx1 when :REQUEST_STATUS ret = request_statusvalue._parse ctx1 # RFC 7953 when :BUSYTYPE ret = busytype._parse ctx1 # RFC 7986 when :REFRESH_INTERVAL parse_err(strict, errors, "Missing VALUE parameter for property #{key}", ctx1) if params.empty? parse_err(strict, errors, "Missing VALUE parameter for property #{key}", ctx1) if !params[:VALUE] parse_err(strict, errors, "Type mismatch of VALUE parameter #{params[:VALUE]} for property #{key}", ctx1) if params[:VALUE] != "DURATION" ret = duration_t._parse ctx1 # RFC 7986 when :COLOR ret = color._parse ctx1 else if params && params[:VALUE] case params[:VALUE] when "BOOLEAN" ret = boolean._parse ctx1 when "BINARY" ret = binary._parse ctx1 when "CAL-ADDRESS" ret = uri._parse ctx1 when "DATE-TIME" ret = date_time_t._parse ctx1 when "DATE" ret = date_t._parse ctx1 when "DURATION" ret = duration_t._parse ctx1 when "FLOAT" ret = float_t._parse ctx1 when "INTEGER" ret = integer._parse ctx1 when "PERIOD" ret = period._parse ctx1 when "RECUR" ret = recur._parse ctx1 when "TEXT" ret = text_t._parse ctx1 when "TIME" ret = time_t._parse ctx1 when "URI" ret = uri._parse ctx1 when "UTC-OFFSET" ret = utc_offset._parse ctx1 end else ret = text_t._parse ctx1 end end if ret.is_a?(Hash) && ret[:error] parse_err(strict, errors, "#{ret[:error]} for property #{key}, value #{value}", ctx) end if Rsec::INVALID[ret] parse_err(strict, errors, "Type mismatch for property #{key}, value #{value}", ctx) end Rsec::Fail.reset [ret, errors] end private def parse_err(strict, errors, msg, ctx) if strict raise ctx.report_error msg, "source" else errors << ctx.report_error(msg, "source") end end end end end