virginity 0.3.31

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/lib/virginity.rb +6 -0
  3. data/lib/virginity/api_extensions.rb +87 -0
  4. data/lib/virginity/api_extensions/fields_to_json.rb +82 -0
  5. data/lib/virginity/api_extensions/fields_to_xml.rb +151 -0
  6. data/lib/virginity/bnf.rb +84 -0
  7. data/lib/virginity/dir_info.rb +93 -0
  8. data/lib/virginity/dir_info/content_line.rb +146 -0
  9. data/lib/virginity/dir_info/line_folding.rb +60 -0
  10. data/lib/virginity/dir_info/param.rb +208 -0
  11. data/lib/virginity/dir_info/query.rb +144 -0
  12. data/lib/virginity/encoding_decoding.rb +177 -0
  13. data/lib/virginity/encodings.rb +36 -0
  14. data/lib/virginity/fixes.rb +230 -0
  15. data/lib/virginity/vcard.rb +244 -0
  16. data/lib/virginity/vcard/base_field.rb +126 -0
  17. data/lib/virginity/vcard/categories.rb +57 -0
  18. data/lib/virginity/vcard/cleaning.rb +364 -0
  19. data/lib/virginity/vcard/field.rb +22 -0
  20. data/lib/virginity/vcard/field/params.rb +93 -0
  21. data/lib/virginity/vcard/field_values.rb +10 -0
  22. data/lib/virginity/vcard/field_values/binary.rb +22 -0
  23. data/lib/virginity/vcard/field_values/boolean.rb +14 -0
  24. data/lib/virginity/vcard/field_values/case_insensitive_value.rb +13 -0
  25. data/lib/virginity/vcard/field_values/date.rb +16 -0
  26. data/lib/virginity/vcard/field_values/integer.rb +15 -0
  27. data/lib/virginity/vcard/field_values/optional_structured_text.rb +35 -0
  28. data/lib/virginity/vcard/field_values/separated_text.rb +59 -0
  29. data/lib/virginity/vcard/field_values/structured_text.rb +71 -0
  30. data/lib/virginity/vcard/field_values/text.rb +23 -0
  31. data/lib/virginity/vcard/field_values/uri.rb +15 -0
  32. data/lib/virginity/vcard/fields.rb +284 -0
  33. data/lib/virginity/vcard/fields_osx.rb +95 -0
  34. data/lib/virginity/vcard/fields_soocial.rb +45 -0
  35. data/lib/virginity/vcard/name_handler.rb +151 -0
  36. data/lib/virginity/vcard/patching.rb +262 -0
  37. data/lib/virginity/vcard21.rb +2 -0
  38. data/lib/virginity/vcard21/base.rb +30 -0
  39. data/lib/virginity/vcard21/parser.rb +359 -0
  40. data/lib/virginity/vcard21/reader.rb +103 -0
  41. data/lib/virginity/vcard21/writer.rb +139 -0
  42. metadata +111 -0
@@ -0,0 +1,177 @@
1
+ module Virginity
2
+
3
+ class Error < StandardError; end
4
+ class InvalidEncoding < Error; end
5
+
6
+ module EncodingDecoding
7
+ extend Encodings
8
+
9
+ # VALUE-CHAR = WSP / VCHAR / NON-ASCII
10
+ WSP = [0x20, 0x09] # WSP = SP / HTAB
11
+ VCHAR = 0x21..0x7E # VCHAR = %x21-7E ; visible (printing) characters
12
+ NONASCII = 0x80..0xFF # NON-ASCII = %x80-FF
13
+
14
+ CR_AND_LF = /\r\n/
15
+ CR = "\r"
16
+ LF = "\n"
17
+ def self.decode_quoted_printable(text)
18
+ text.gsub(CR_AND_LF, LF).gsub(/\=([0-9a-fA-F])?\n\s+([0-9a-fA-F])/, "=\\1\\2").unpack('M*').first
19
+ end
20
+
21
+ QP_ALSO_ENCODE = "\x0A\x20"
22
+ def self.encode_quoted_printable(text, options = {})
23
+ options[:also_encode] ||= QP_ALSO_ENCODE
24
+ # special_chars = /[\t ](?:[\v\t ]|$)|[=\x00-\x08\x0B-\x1F\x7F-\xFF#{options[:also_encode]}]/
25
+ special_chars = /[=\x00-\x08\x0B-\x1F\x7F-\xFF#{options[:also_encode]}]/n
26
+ encoded = to_binary(text).gsub(special_chars) do |char|
27
+ char[0 ... -1] + "=%02X" % char[-1].ord
28
+ end
29
+ fold_quoted_printable(encoded, options[:width] || 76, options[:initial_position])
30
+ end
31
+
32
+ QPCHAR = /[^=]|=[\dABCDEF]{2}/
33
+ QPFOLD = "=\r\n" # only vCard 2.1 uses encode_quoted_printable so we always use windows line endings
34
+ def self.fold_quoted_printable(qp_text, width = 76, initial_position = 0)
35
+ return qp_text unless width > 5
36
+ pos = initial_position.to_i
37
+ scanner = StringScanner.new(qp_text)
38
+ folded = ""
39
+ while !scanner.eos?
40
+ char = scanner.scan(QPCHAR)
41
+ charsize = char.size
42
+ if pos + charsize > width - 3
43
+ folded << QPFOLD
44
+ pos = 0
45
+ end
46
+ folded << char
47
+ pos += charsize
48
+ end
49
+ folded
50
+ end
51
+
52
+
53
+ def self.normalize_newlines!(text)
54
+ text.gsub!(/\r?\n|\r/, "\n")
55
+ end
56
+
57
+ # "text": The "text" value type should be used to identify values that
58
+ # contain human-readable text. The character set and language in which
59
+ # the text is represented is controlled by the charset content-header
60
+ # and the language type parameter and content-header.
61
+ #
62
+ # A formatted text line break in a text value type MUST be represented
63
+ # as the character sequence backslash (ASCII decimal 92) followed by a
64
+ # Latin small letter n (ASCII decimal 110) or a Latin capital letter N
65
+ # (ASCII decimal 78), that is "\n" or "\N".
66
+ #
67
+ # TODO options for saving to ascii (convert to quoted printable) or storing plain utf-8
68
+ ENCODED_LF = "\\n"
69
+ CRLF = CR + LF
70
+ BACKSLASH = "\\"
71
+ COMMA = ","
72
+ SEMICOLON = ";"
73
+ STUFF_TO_ENCODE = /[\n\\\,\;]/
74
+ STUFF_NOT_TO_ENCODE = %r{[^\n\\\,\;]*}
75
+ def self.encode_text(text)
76
+ raise "#{text.inspect} must be a String" unless text.is_a? String
77
+ normalize_newlines!(text)
78
+ encoded = ""
79
+ s = StringScanner.new(text)
80
+ while !s.eos?
81
+ encoded << s.scan(STUFF_NOT_TO_ENCODE)
82
+ # 5.8.4 Backslashes, newlines, and commas must be encoded.
83
+ case x = s.scan(STUFF_TO_ENCODE)
84
+ when LF
85
+ encoded << ENCODED_LF
86
+ when BACKSLASH, COMMA, SEMICOLON
87
+ # RFC2426 tells us to encode ":" too, which is needed for structured text fields
88
+ encoded << BACKSLASH << x
89
+ end
90
+ end
91
+ encoded
92
+ end
93
+
94
+ def self.decode_text(text)
95
+ text.gsub(/\\(.)/) { $1.casecmp('n') == 0 ? LF : $1 }
96
+ end
97
+
98
+ def self.encode_text_list(list, separator = COMMA)
99
+ list.map { |value| encode_text(value) }.join(separator)
100
+ end
101
+
102
+ # # TODO: port to C someday
103
+ # This is the old simple implementation that is easy to port to another language
104
+ # this can be a lot faster if we don't create a new string object for each char
105
+ # def self.decode_text_list(text_list, separator = COMMA)
106
+ # strings = []
107
+ # state = :normal # there are two states: :normal and :escaped
108
+ # string = ""
109
+ # text_list.each_char do |char|
110
+ # if state == :escaped
111
+ # string << (%w(n N).include?(char) ? LF : char)
112
+ # state = :normal
113
+ # else
114
+ # case char
115
+ # when BACKSLASH
116
+ # state = :escaped
117
+ # when separator
118
+ # strings << string
119
+ # string = ""
120
+ # else
121
+ # string << char
122
+ # end
123
+ # end
124
+ # end
125
+ # strings << string
126
+ # strings
127
+ # end
128
+
129
+
130
+ NON_ESCAPE_OR_SEPARATOR_REGEXP = {}
131
+ def self.non_escape_or_separator_regexp(separator)
132
+ NON_ESCAPE_OR_SEPARATOR_REGEXP[separator] ||= %r{[^\\#{separator}\\\\]*}
133
+ end
134
+
135
+ def self.decode_text_list(text_list, separator = COMMA)
136
+ not_special = non_escape_or_separator_regexp(separator)
137
+ list = []
138
+ text = ""
139
+ s = StringScanner.new(text_list)
140
+ while !s.eos?
141
+ text << s.scan(not_special)
142
+ break if s.eos?
143
+ case s.getch
144
+ when BACKSLASH
145
+ char = s.getch
146
+ # what do I do when char is nil? ignore the backslash too? I don't know...
147
+ raise InvalidEncoding, "text list \"#{text_list}\" ends after escape char" if char.nil?
148
+ text << (char.casecmp('n') == 0 ? LF : char)
149
+ when separator
150
+ list << text
151
+ text = ""
152
+ else
153
+ raise InvalidEncoding, "read #{s.matched.inspect} at #{s.pos} in #{s.string.inspect} (#{s.string.size}) using #{not_special.inspect}"
154
+ end
155
+ end
156
+ list << text
157
+ list
158
+ end
159
+
160
+ # Compound type values are delimited by a field delimiter, specified by the SEMI-COLON character (ASCII decimal 59). A SEMI-COLON in a component of a compound property value MUST be escaped with a BACKSLASH character (ASCII decimal 92).
161
+ #
162
+ # Lists of values are delimited by a list delimiter, specified by the COMMA character (ASCII decimal 44). A COMMA character in a value MUST be escaped with a BACKSLASH character (ASCII decimal 92).
163
+ #
164
+ # This profile supports the type grouping mechanism defined in [MIME-DIR]. Grouping of related types is a useful technique to communicate common semantics concerning the properties of a vCard.
165
+ def self.decode_structured_text(value, size, separator = SEMICOLON)
166
+ list = decode_text_list(value, separator)
167
+ list << "" while list.size < size
168
+ list.pop while list.size > size
169
+ list
170
+ end
171
+
172
+ def self.encode_structured_text(list, separator = SEMICOLON)
173
+ encode_text_list(list, separator)
174
+ end
175
+
176
+ end
177
+ end
@@ -0,0 +1,36 @@
1
+ module Virginity
2
+ module Encodings
3
+
4
+ def binary?(s)
5
+ s.encoding == Encoding::BINARY
6
+ end
7
+
8
+ def to_binary(s)
9
+ s.dup.force_encoding(Encoding::BINARY)
10
+ end
11
+
12
+ def to_ascii(s)
13
+ s.dup.force_encoding(Encoding::ASCII)
14
+ end
15
+
16
+ def to_default(s)
17
+ s.encode
18
+ end
19
+
20
+ def to_default!(s)
21
+ s.encode!
22
+ end
23
+
24
+ def verify_utf8ness(string)
25
+ if string.encoding == Encoding::UTF_8 || string.encoding == Encoding::US_ASCII
26
+ unless string.valid_encoding?
27
+ # puts "*"*100, "incorrectly encoded String", string, "*"*100
28
+ raise InvalidEncoding, "incorrectly encoded String"
29
+ end
30
+ else
31
+ raise InvalidEncoding, "expected UTF-8 or ASCII"
32
+ end
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,230 @@
1
+ module Virginity
2
+ module Fixes
3
+ def unfold_faulty_qp_lines(faulty_lines)
4
+ lines = faulty_lines.dup
5
+ loop do # unfold line that do not begin with " " but are encoded as QP
6
+ changed = false
7
+ lines.each_with_index do |line, i|
8
+ # if line is QP, ends with equals and is not the last line
9
+ if line =~ /ENCODING=QUOTED-PRINTABLE/i and line =~ /=$/ and lines.length > i+1
10
+ lines[i] = lines[i].chomp('=') # remove the soft-line break, the last character, (=)...
11
+ lines[i] += lines.delete_at(i+1) # ...and add the next line to the failing line.
12
+ changed = true
13
+ end
14
+ end
15
+ break unless changed
16
+ end
17
+ lines
18
+ end
19
+
20
+ def non_empty_lines(faulty_lines) # (needed for simon3.vcf)
21
+ faulty_lines.reject {|line| line.empty? }
22
+ end
23
+
24
+ # for osx
25
+ # more info on this Base64-line: http://www.imc.org/imc-vcard/mail-archive/msg00555.html
26
+ def remove_spaces_from_base64_lines(faulty_lines)
27
+ faulty_lines.map do |line|
28
+ if line =~ /BASE64/ # FIXME, this will break if the value contains this string. --> so let's do this in the field-class?
29
+ line.gsub(/\s/, "")
30
+ else
31
+ line
32
+ end
33
+ end
34
+ end
35
+
36
+ def self.line_parts(line)
37
+ ContentLine::line_parts(line)
38
+ rescue InvalidEncoding
39
+ # so, it's invalid 3.0 encoded, could it be a 2.1-encoding?
40
+ DirectoryInformation::line21_parts(line)
41
+ end
42
+
43
+ # FIXME --> width can't be greater than width in the normal folding method... but it is now :-(
44
+ def self.photo_folding_like_apple(value, options = {})
45
+ width = options[:width] || 78
46
+ line_ending = (options[:windows_line_endings] ? "\r\n" : "\n")
47
+ s = line_ending + " " + value.gsub(/.{#{width-2},#{width-2}}/) {|x| x + line_ending + " "}
48
+ s.sub(/#{line_ending} $/,"") # remove the last line ending if if it so happens to be that the last line is 'empty'
49
+ end
50
+
51
+ def self.sane_line_endings(s)
52
+ s.gsub(LineFolding::LINE_ENDING, "\n")
53
+ end
54
+
55
+ def self.should_be_folded?(line)
56
+ return false if line =~ /\A $/ # TODO: clarify this, it was line.first, which takes the first string.
57
+ if line.include?(":") # probably a field
58
+ unless line[0] == ":" or line.split(":").first.match(/[\(\)]/)
59
+ # it is a name then, so it's a new field
60
+ return false
61
+ end
62
+ end
63
+ true
64
+ end
65
+
66
+ def self.unfold_wrongly_folded_lines(s)
67
+ x = ""
68
+ sane_line_endings(s).split("\n").each do |line|
69
+ if should_be_folded?(line.dup)
70
+ x << " " + line
71
+ else
72
+ x << line
73
+ end
74
+ x << "\n"
75
+ end
76
+ x
77
+ end
78
+
79
+ def self.remove_ascii_ctl_chars(s)
80
+ ctl = (0..31).to_a + [127]
81
+ v = ""
82
+ s.each_byte do |c|
83
+ v << c unless ctl.include? c
84
+ end
85
+ v
86
+ end
87
+
88
+ def self.reencode_qp(qp, type=:value)
89
+ decoded = EncodingDecoding::decode_quoted_printable(qp)
90
+ case type
91
+ when nil
92
+ @value = EncodingDecoding::encode_text decoded
93
+ when :separated
94
+ @value = EncodingDecoding::encode_text_list [decoded]
95
+ when :structured
96
+ @value = EncodingDecoding::encode_structured_text [decoded]
97
+ else
98
+ raise TypeError, type
99
+ end
100
+ end
101
+
102
+ EQUALS_SIGN_WITHOUT_HEX = /=(([^0-9^A-F])|(.[^0-9^A-F]))/
103
+
104
+ def self.fix_faulty_qp_chars(s)
105
+ x = Virginity::LineFolding::unfold_and_split(s)
106
+ y = x.map do |l|
107
+ if l =~ /QUOTED\-PRINTABLE/
108
+ l =~ /(.*):(.*)/
109
+ prevalue, value = $1, $2
110
+ value = value.gsub(EQUALS_SIGN_WITHOUT_HEX) { |s| " #{$1}" }
111
+ "#{prevalue}:#{value}"
112
+ else
113
+ l
114
+ end
115
+ end
116
+ y.join("\n")
117
+ end
118
+
119
+ def self.guess_latin1(s)
120
+ x = Vcard.new(s).super_clean!
121
+ x.fields.each do |f|
122
+ begin
123
+ f.to_s.encode('UTF-8//TRANSLIT', Encoding::UTF_8)
124
+ f.to_s.force_encoding(LATIN1).encode
125
+ rescue EncodingError
126
+ print "\tguessing Latin1 for #{f.to_s.inspect}"
127
+ f.params << Param.new("CHARSET", "Latin1")
128
+ f.clean_charsets!
129
+ raise "GAAAAAAAAH!" unless f.to_s.is_utf8?
130
+ puts "\t-->\t" + f.to_s.inspect
131
+ end
132
+ end
133
+ x.super_clean!.to_s
134
+ end
135
+
136
+ # def faulty?(lns=lines)
137
+ # lns.any? { |line| not line =~ %r{#{Bnf::LINE}}i }
138
+ # end
139
+
140
+ # def fix!(lines)
141
+ # ls = lines.dup
142
+ # ls = unfold_faulty_qp_lines(ls)
143
+ # ls = non_empty_lines(ls)
144
+ # raise InvalidEncoding.new("I do not know how to repair this vcard:\n" + inspect) if faulty?(ls)
145
+ # ls
146
+ # end
147
+
148
+ # def fixed!
149
+ # original = lines.join("\n")
150
+ # ls = lines.dup
151
+ # before = original.dup
152
+ # after = ""
153
+ # while before != after and faulty_lines?(lines)
154
+ # before = ls.join("\n")
155
+ # ls = unfold_faulty_qp_lines(ls)
156
+ # ls = non_empty_lines(ls)
157
+ # after = ls.join("\n")
158
+ # end
159
+ # raise InvalidEncoding.new("I do not know how to repair this vcard:\n" + original) if faulty_lines?(lines)
160
+ # lines
161
+ # end
162
+
163
+ end
164
+ end
165
+
166
+
167
+ class Virginity::Vcard < Virginity::DirectoryInformation
168
+
169
+ def self.fields_from_broken_vcard(vcard_as_string)
170
+ lines = Virginity::LineFolding::unfold_and_split(vcard_as_string.lstrip).map do |line|
171
+ begin
172
+ # if it is not valid 3.0, could it be a 2.1-line?
173
+ Virginity::Field.parse(line)
174
+ rescue
175
+ group, name, params, value = DirectoryInformation::line21_parts(line)
176
+ Virginity::Field[name].new(name, value, params, group, :no_deep_copy => true)
177
+ end
178
+ end
179
+ end
180
+
181
+ def self.valid_utf8?(v)
182
+ if v.to_s.dup.force_encoding(Encoding::UTF_8).valid_encoding?
183
+ true
184
+ else
185
+ false
186
+ end
187
+ rescue EncodingError
188
+ return false
189
+ end
190
+
191
+ # NB. this only works for vCard 3.0, not for 2.1!
192
+ def self.fix_and_clean(vcard_as_string)
193
+ fixes ||= []
194
+ # puts vcard_as_string
195
+ # puts "fixes => #{fixes.inspect}"
196
+ lines = fields_from_broken_vcard(vcard_as_string)
197
+ v = Virginity::Vcard.new(lines)
198
+ v.super_clean!
199
+ valid_utf8?(v)
200
+ v
201
+ rescue EncodingError
202
+ #puts e.class, e
203
+ if !fixes.include?(:faulty_qp)
204
+ newcard = Fixes::fix_faulty_qp_chars(vcard_as_string)
205
+ fixes << :faulty_qp
206
+ elsif !fixes.include?(:guess_latin1)
207
+ newcard = Fixes::guess_latin1(lines)
208
+ fixes << :guess_latin1
209
+ else
210
+ File.open("illegal_sequence_#{vcard_as_string.hash}.vcf", "wb") do |f|
211
+ f.puts Virginity::Vcard.new(vcard_as_string).super_clean!
212
+ end
213
+ raise
214
+ end
215
+ vcard_as_string = newcard
216
+ retry
217
+ rescue Virginity::InvalidEncoding, Virginity::Vcard21::ParseError => e
218
+ if !fixes.include?(:fix_folding)
219
+ vcard_as_string = Fixes::unfold_wrongly_folded_lines(vcard_as_string)
220
+ fixes << :fix_folding
221
+ retry
222
+ elsif !fixes.include?(:weird_mac_16bit_encoding_that_is_not_utf16) && vcard_as_string.include?("\000")
223
+ vcard_as_string.gsub!("\000", "")
224
+ fixes << :weird_mac_16bit_encoding_that_is_not_utf16
225
+ retry
226
+ else
227
+ raise
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,244 @@
1
+ require 'digest'
2
+ require 'virginity/encodings'
3
+ require 'virginity/dir_info'
4
+ require 'virginity/vcard/cleaning'
5
+ require 'virginity/vcard/categories'
6
+ require 'virginity/vcard/name_handler'
7
+ require 'virginity/vcard/fields'
8
+ require 'virginity/vcard/fields_osx'
9
+ require 'virginity/vcard/fields_soocial'
10
+ require 'virginity/vcard/patching'
11
+
12
+ module Virginity
13
+
14
+ class InvalidVcard < Error
15
+ attr_reader :original
16
+
17
+ def initialize(msg, original = $!)
18
+ super(msg)
19
+ @original = original
20
+ end
21
+ end
22
+
23
+ # rfc 2426, vCard MIME Directory Profile
24
+ class Vcard < DirectoryInformation
25
+ include VcardCleaning
26
+ include VcardCategories
27
+ include Patching
28
+ include Encodings
29
+
30
+ def self.diff(old, new)
31
+ Patching::diff(old, new)
32
+ end
33
+
34
+ EMPTY = "BEGIN:VCARD\nN:;;;;\nVERSION:3.0\nEND:VCARD\n"
35
+
36
+ VCARD_REGEX = /^[\ \t]*VCARD[\ \t]*$/i
37
+ def initialize(lines = EMPTY, options = {})
38
+ if lines.is_a? Array
39
+ @lines = lines.map {|line| line.to_field }
40
+ else # it's expected to be a String
41
+ verify_utf8ness lines
42
+ @lines = LineFolding::unfold_and_split(lines.lstrip).map { |line| Field.parse(line) }
43
+ end
44
+ raise InvalidVcard, "missing BEGIN:VCARD" unless @lines.first.name =~ Field::BEGIN_REGEX
45
+ raise InvalidVcard, "missing END:VCARD" unless @lines.last.name =~ Field::END_REGEX
46
+ raise InvalidVcard, "missing BEGIN:VCARD" unless @lines.first.raw_value =~ VCARD_REGEX
47
+ raise InvalidVcard, "missing END:VCARD" unless @lines.last.raw_value =~ VCARD_REGEX
48
+ rescue => error
49
+ raise InvalidVcard, error.message
50
+ end
51
+
52
+ # the empty vcard
53
+ def self.empty
54
+ new(LineFolding::unfold_and_split(EMPTY.lstrip).map { |line| Field.parse(line) })
55
+ end
56
+
57
+ # import vcard21 and convert it to 3.0
58
+ def self.from_vcard21(vcf, options = {})
59
+ verify_utf8ness vcf
60
+ vcard = new(lines_from_vcard21(vcf, options), options)
61
+ vcard.clean_version!
62
+ rescue => error
63
+ raise InvalidVcard, error.message
64
+ end
65
+
66
+ VERSION21 = /^VERSION\:2\.1(\s*)$/i
67
+ def self.parse(vcf, options = {})
68
+ vcf = vcf.to_s
69
+ verify_utf8ness vcf
70
+ if vcf =~ VERSION21
71
+ from_vcard21(vcf, options)
72
+ else
73
+ new(vcf, options)
74
+ end
75
+ end
76
+
77
+ def self.from_vcard(vcf, options = {})
78
+ parse(vcf, options)
79
+ end
80
+
81
+ def self.load_all_from(filename, options = {})
82
+ list(File.read(filename), options)
83
+ end
84
+
85
+ END_VCARD = /^END\:VCARD\s*$/i
86
+ # split a given string of concatenated vcards to an array containing one vcard per element
87
+ def self.vcards_in_list(vcf)
88
+ s = StringScanner.new(vcf)
89
+ array = []
90
+ while !s.eos?
91
+ if v = s.scan_until(END_VCARD)
92
+ v.lstrip!
93
+ array << v
94
+ else
95
+ puts s.peek(100)
96
+ break
97
+ end
98
+ end
99
+ array
100
+ end
101
+
102
+ # returns an array of Vcards for a string of concatenated vcards
103
+ def self.list(vcf, options = {})
104
+ vcards_in_list(vcf).map do |v|
105
+ from_vcard(v, options)
106
+ end
107
+ end
108
+
109
+ def inspect
110
+ super.chomp(">") + " name=" + name.to_s.inspect + ">"
111
+ end
112
+
113
+ def dir_info
114
+ DirectoryInformation.new(to_s)
115
+ end
116
+
117
+ def deep_copy
118
+ Marshal::load(Marshal::dump(self))
119
+ end
120
+
121
+ alias_method :fields, :lines
122
+ alias_method :delete_field, :delete_content_line
123
+
124
+ # add a field and return it
125
+ # if a block is given, then the new field is yielded so it can be changed
126
+ def add_field(line)
127
+ end_vcard = @lines.pop
128
+ raise InvalidVcard, "there is no last line? ('END:VCARD')" if end_vcard.nil?
129
+ @lines << (field = Field.parse(line))
130
+ @lines << end_vcard
131
+ yield field if block_given?
132
+ field
133
+ end
134
+
135
+ # add a field, returns the vcard
136
+ def <<(line)
137
+ add_field(line)
138
+ self
139
+ end
140
+ alias_method :push, :<<
141
+
142
+ # a vCard 2.1 string representation of this vCard
143
+ # if :windows_line_endings => true then lines end with \r\n otherwise with \n
144
+ CRLF = "\r\n"
145
+ def to_vcard21(options = {})
146
+ line_ending = options[:windows_line_endings] ? CRLF : LF
147
+ fields.map { |field| field.encode21(options) }.join(line_ending)
148
+ end
149
+
150
+ # are all fields also present or supersets in other?
151
+ IGNORE_IN_SUBSET_COMPARISON = %w(BEGIN END FN)
152
+ def subset_of?(other)
153
+ fields.all? do |field|
154
+ if IGNORE_IN_SUBSET_COMPARISON.include?(field.name)
155
+ true
156
+ else
157
+ # puts "-----\n"
158
+ # puts "#{field} in #{other}?\n"
159
+ other.lines_with_name(field.name).any? do |f|
160
+ begin
161
+ # puts "considering #{f} ==> #{f.raw_value == field.raw_value or field.subset_of?(f)}"
162
+ f.raw_value == field.raw_value or field.subset_of?(f)
163
+ rescue NoMethodError
164
+ false
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
170
+
171
+ SINGLETON_FIELDS = %w(N FN BEGIN END VERSION)
172
+ # import all fields except N, FN, and VERSION from other (another Vcard)
173
+ # duplicate fields are deduped
174
+ def assimilate_fields_from!(other)
175
+ other.fields.each do |field|
176
+ next if SINGLETON_FIELDS.include? field.name.upcase
177
+ push(field)
178
+ end
179
+ clean_same_value_fields!
180
+ self
181
+ end
182
+
183
+ def add(name)
184
+ add_field(name + ":") do |field|
185
+ yield field if block_given?
186
+ end
187
+ end
188
+
189
+ def add_email(address = nil)
190
+ add(EMAIL) do |email|
191
+ email.address = address.to_s
192
+ yield email if block_given?
193
+ end
194
+ end
195
+
196
+ def add_telephone(number = nil)
197
+ add(TEL) do |tel|
198
+ tel.number = number.to_s
199
+ yield tel if block_given?
200
+ end
201
+ end
202
+
203
+ def name
204
+ @name ||= NameHandler.new(self)
205
+ end
206
+
207
+ ADR = "ADR"
208
+ BDAY = "BDAY"
209
+ CATEGORIES = "CATEGORIES"
210
+ EMAIL = "EMAIL"
211
+ IMPP = "IMPP"
212
+ LOGO = "LOGO"
213
+ NICKNAME = "NICKNAME"
214
+ TEL = "TEL"
215
+ NOTE = "NOTE"
216
+ ORG = "ORG"
217
+ PHOTO = "PHOTO"
218
+ TITLE = "TITLE"
219
+ URL = "URL"
220
+ def addresses; lines_with_name(ADR); end
221
+ def birthdays; lines_with_name(BDAY); end
222
+ def categories; lines_with_name(CATEGORIES); end
223
+ def emails; lines_with_name(EMAIL); end
224
+ def impps; lines_with_name(IMPP); end
225
+ def logos; lines_with_name(LOGO); end
226
+ def nicknames; lines_with_name(NICKNAME); end
227
+ def telephones; lines_with_name(TEL); end
228
+ def notes; lines_with_name(NOTE); end
229
+ def organizations; lines_with_name(ORG); end
230
+ alias_method :organisations, :organizations # Britania rules the waves!
231
+ def photos; lines_with_name(PHOTO); end
232
+ def titles; lines_with_name(TITLE); end
233
+ def urls; lines_with_name(URL); end
234
+ def custom_im_fields; @lines.select{|line| line.is_a? Virginity::Vcard::CustomImField}; end
235
+
236
+ XABRELATEDNAMES = "X-ABRELATEDNAMES"
237
+ def related_names; lines_with_name(XABRELATEDNAMES); end
238
+ XABDATE = "X-ABDATE"
239
+ def dates; lines_with_name(XABDATE); end
240
+ XANNIVERSARY = "X-ANNIVERSARY"
241
+ def anniversaries; lines_with_name(XANNIVERSARY); end
242
+ end
243
+
244
+ end