require "rsec" require "set" require "uri" require "date" include Rsec::Helpers require "vobject" require "vobject/component" require "vobject/vcard/v4_0/paramcheck" require "vobject/vcard/v4_0/typegrammars" require_relative "../../../c" require_relative "../../../error" module Vcard::V4_0 class Grammar attr_accessor :strict, :errors class << self def unfold(str) str.gsub(/[\n\r]+[ \t]/, "") end end # RFC 6868 def rfc6868decode(x) x.gsub(/\^n/, "\n").gsub(/\^\^/, "^").gsub(/\^'/, '"') end def vobject_grammar # properties with value cardinality 1 @cardinality1 = {} @cardinality1[:PARAM] = Set.new [:VALUE] @cardinality1[:PROP] = Set.new [:KIND, :N, :BDAY, :ANNIVERSARY, :GENDER, :PRODID, :REV, :UID, :BIRTHPLACE, :DEATHPLACE, :DEATHDATE] group = C::IANATOKEN linegroup = group << "." beginend = /BEGIN/i.r | /END/i.r # parameters && parameter types paramname = /LANGUAGE/i.r | /VALUE/i.r | /PREF/i.r | /ALTID/i.r | /PID/i.r | /TYPE/i.r | /MEDIATYPE/i.r | /CALSCALE/i.r | /SORT-AS/i.r | /GEO/i.r | /TZ/i.r | /LABEL/i.r | /INDEX/i.r | /LEVEL/i.r otherparamname = C::NAME_VCARD ^ paramname paramvalue = C::QUOTEDSTRING_VCARD.map { |s| rfc6868decode s } | C::PTEXT_VCARD.map { |s| rfc6868decode(s).upcase } # tzidvalue = seq("/".r._?, C::PTEXT_VCARD).map { |_, val| val } calscalevalue = /GREGORIAN/i.r | C::IANATOKEN | C::XNAME_VCARD prefvalue = /[0-9]{1,2}/i.r | "100".r pidvalue = /[0-9]+(\.[0-9]+)?/.r pidvaluelist = seq(pidvalue, ",", lazy { pidvaluelist }) do |a, _, b| [a, b].flatten end | (pidvalue ^ ",".r).map { |z| [z] } typeparamtel1 = /TEXT/i.r | /VOICE/i.r | /FAX/i.r | /CELL/i.r | /VIDEO/i.r | /PAGER/i.r | /TEXTPHONE/i.r typeparamtel = typeparamtel1 | C::IANATOKEN | C::XNAME_VCARD typeparamrelated = /CONTACT/i.r | /ACQUAINTANCE/i.r | /FRIEND/i.r | /MET/i.r | /CO-WORKER/i.r | /COLLEAGUE/i.r | /CO-RESIDENT/i.r | /NEIGHBOR/i.r | /CHILD/i.r | /PARENT/i.r | /SIBLING/i.r | /SPOUSE/i.r | /KIN/i.r | /MUSE/i.r | /CRUSH/i.r | /DATE/i.r | /SWEETHEART/i.r | /ME/i.r | /AGENT/i.r | /EMERGENCY/i.r typevalue = /WORK/i.r | /HOME/i.r | typeparamtel1 | typeparamrelated | C::IANATOKEN | C::XNAME_VCARD typevaluelist = seq(typevalue << ",".r, lazy { typevaluelist }) do |a, b| [a.upcase, b].flatten end | typevalue.map { |t| [t.upcase] } typeparamtel1list = seq(typeparamtel << ",".r, lazy { typeparamtel1list }) do |a, b| [a.upcase, b].flatten end | typeparamtel.map { |t| [t.upcase] } geourlvalue = seq('"'.r >> C::TEXT4 << '"'.r) do |s| parse_err("geo value not a URI") unless s =~ URI::DEFAULT_PARSER.make_regexp s end tzvalue = paramvalue | geourlvalue valuetype = /TEXT/i.r | /URI/i.r | /TIMESTAMP/i.r | /TIME/i.r | /DATE-TIME/i.r | /DATE/i.r | /DATE-AND-OR-TIME/i.r | /BOOLEAN/i.r | /INTEGER/i.r | /FLOAT/i.r | /UTC-OFFSET/i.r | /LANGUAGE-TAG/i.r | C::IANATOKEN | C::XNAME_VCARD mediaattr = /[!\"#$%&'*+.^A-Z0-9a-z_`i{}|~-]+/.r mediavalue = mediaattr | C::QUOTEDSTRING_VCARD mediatail = seq(";".r >> mediaattr << "=".r, mediavalue).map do |a, v| ";#{a}=#{v}" end rfc4288regname = /[A-Za-z0-9!#$&.+^+-]{1,127}/.r rfc4288typename = rfc4288regname rfc4288subtypename = rfc4288regname mediavalue = seq(rfc4288typename << "/".r, rfc4288subtypename, mediatail.star).map do |t, s, tail| ret = "#{t}/#{s}" ret = ret . tail[0] unless tail.empty? ret end pvalue_list = (seq(paramvalue << ",".r, lazy { pvalue_list }) & /[;:]/.r).map do |e, list| [e.sub(Regexp.new("^\"(.+)\"$"), '\1').gsub(/\\n/, "\n"), list].flatten end | (paramvalue & /[;:]/.r).map do |e| [e.sub(Regexp.new("^\"(.+)\"$"), '\1').gsub(/\\n/, "\n")] end quoted_string_list = (seq(C::QUOTEDSTRING_VCARD << ",".r, lazy { quoted_string_list }) & /[;:]/.r).map do |e, list| [e.sub(Regexp.new("^\"(.+)\"$"), '\1').gsub(/\\n/, "\n"), list].flatten end | (C::QUOTEDSTRING_VCARD & /[;:]/.r).map do |e| [e.sub(Regexp.new("^\"(.+)\"$"), '\1').gsub(/\\n/, "\n")] end # fmttypevalue = seq(rfc4288typename, "/", rfc4288subtypename).map(&:join) levelvalue = /beginner/i.r | /average/i.r | /expert/i.r | /high/i.r | /medium/i.r | /low/i.r param = seq(/ALTID/i.r, "=", paramvalue) do |name, _, val| { name.upcase.tr("-", "_").to_sym => val } end | seq(/LANGUAGE/i.r, "=", C::RFC5646LANGVALUE) do |name, _, val| { name.upcase.tr("-", "_").to_sym => val.upcase } end | seq(/PREF/i.r, "=", prefvalue) do |name, _, val| { name.upcase.tr("-", "_").to_sym => val.upcase } end | seq(/TYPE/i.r, "=", "\"".r >> typevaluelist << "\"".r) do |name, _, val| # not in spec but in examples. Errata ID 3488, "Held for Document Update": acknwoledged as error requiring an updated spec. With this included, TYPE="x,y,z" is a list of values; the proper ABNF behaviour is that "x,y,z" is interpreted as a single value { name.upcase.tr("-", "_").to_sym => val } end | seq(/TYPE/i.r, "=", typevaluelist) do |name, _, val| { name.upcase.tr("-", "_").to_sym => val } end | seq(/MEDIATYPE/i.r, "=", mediavalue) do |name, _, val| { name.upcase.tr("-", "_").to_sym => val } end | seq(/CALSCALE/i.r, "=", calscalevalue) do |name, _, val| { name.upcase.tr("-", "_").to_sym => val } end | seq(/SORT-AS/i.r, "=", pvalue_list) do |name, _, val| { name.upcase.tr("-", "_").to_sym => val } end | seq(/TZ/i.r, "=", tzvalue) do |name, _, val| { name.upcase.tr("-", "_").to_sym => val } end | seq(/GEO/i.r, "=", geourlvalue) do |name, _, val| { name.upcase.tr("-", "_").to_sym => val } end | seq(/VALUE/i.r, "=", valuetype) do |name, _, val| { name.upcase.tr("-", "_").to_sym => val } end | seq(/PID/i.r, "=", pidvaluelist) do |name, _, val| { name.upcase.tr("-", "_").to_sym => val } end | seq(/INDEX/i.r, "=", prim(:int32)) do |name, _, val| { name.upcase.tr("-", "_").to_sym => val } end | seq(/LEVEL/i.r, "=", levelvalue) do |name, _, val| { name.upcase.tr("-", "_").to_sym => val.upcase } end | seq(otherparamname, "=", pvalue_list) do |name, _, val| val = val[0] if val.length == 1 { name.upcase.tr("-", "_").to_sym => val } end | seq(paramname, "=", pvalue_list) do |name, _, val| parse_err("Violated format of parameter value #{name} = #{val}") end params = seq(";".r >> param, lazy { params }) do |p, ps| p.merge(ps) do |key, old, new| if @cardinality1[:PARAM].include?(key) parse_err("Violated cardinality of parameter #{key}") end [old, new].flatten # deal with duplicate properties end end | seq(";".r >> param ^ ";".r).map { |e| e[0] } contentline = seq(linegroup._?, C::NAME_VCARD, params._? << ":".r, C::VALUE, /[\r\n]/) do |l, name, p, value, _| key = name.upcase.tr("-", "_").to_sym hash = { key => {} } errors << Paramcheck.paramcheck(strict, key, p.empty? ? {} : p[0], @ctx) hash[key][:value], errors1 = Typegrammars.typematch(strict, key, p[0], :GENERIC, value) errors << errors1 hash[key][:group] = l[0] unless l.empty? hash[key][:params] = p[0] unless p.empty? hash end props = seq(contentline, lazy { props }) do |c, rest| c.merge(rest) do |key, old, new| if @cardinality1[:PROP].include?(key.upcase) && !(new.is_a?(Array) && new[0].key?(:params) && new[0][:params].key?(:ALTID) && old.key?(:params) && old[:params].key?(:ALTID) && old[:params][:ALTID] == new[0][:params][:ALTID]) && !(new.is_a?(Hash) && old.key?(:params) && old[:params].key?(:ALTID) && new.key?(:params) && new[:params].key?(:ALTID) && old[:params][:ALTID] == new[:params][:ALTID]) parse_err("Violated cardinality of property #{key}") end [old, new].flatten # deal with duplicate properties end end | ("".r & beginend).map { {} } calpropname = /VERSION/i.r calprop = seq(calpropname << ":".r, C::VALUE, /[\r\n]/.r) do |key, value| key = key.upcase.tr("-", "_").to_sym hash = { key => {} } hash[key][:value], errors1 = Typegrammars.typematch(strict, key, nil, :VCARD, value) errors << errors1 hash end vobject = seq(/BEGIN:VCARD[\r\n]/i.r >> calprop, props << /END:VCARD[\r\n]/i.r) do |v, rest| parse_err("Missing VERSION attribute") unless v.has_key?(:VERSION) parse_err("Missing FN attribute") unless rest.has_key?(:FN) rest.delete(:END) { VCARD: v.merge(rest), errors: errors.flatten } end vobject.eof end def initialize(strict) self.strict = strict self.errors = [] end def parse(vobject) @ctx = Rsec::ParseContext.new self.class.unfold(vobject), "source" ret = vobject_grammar._parse @ctx if !ret || Rsec::INVALID[ret] if strict raise @ctx.generate_error "source" else errors << @ctx.generate_error("source") ret = { VCARD: nil, errors: errors.flatten } end end Rsec::Fail.reset ret end private def parse_err(msg) if strict raise @ctx.report_error msg, "source" end errors << @ctx.report_error(msg, "source") end end end