File Coverage

blib/lib/Text/VCardFast.pm
Criterion Covered Total %
statement 159 169 94.0
branch 55 76 72.3
condition 30 49 61.2
subroutine 12 12 100.0
pod 2 8 25.0
total 258 314 82.1


line stmt bran cond sub pod time code
1             package Text::VCardFast;
2              
3 5     5   176940 use strict;
  5         16  
  5         277  
4 5     5   40 use warnings;
  5         11  
  5         266  
5              
6 5     5   3641 use Encode qw(decode_utf8 encode_utf8);
  5         53490  
  5         628  
7 5     5   3244 use MIME::Base64 qw(decode_base64 encode_base64);
  5         3799  
  5         13731  
8              
9             require Exporter;
10              
11             our @ISA = qw(Exporter);
12              
13             # Items to export into callers namespace by default. Note: do not export
14             # names by default without a very good reason. Use EXPORT_OK instead.
15             # Do not simply export all your public functions/methods/constants.
16              
17             # This allows declaration use Text::VCardFast ':all';
18             # If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
19             # will save memory.
20             our %EXPORT_TAGS = ( 'all' => [ qw(
21             vcard2hash
22             hash2vcard
23             ) ] );
24              
25             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
26              
27             our @EXPORT = qw(
28             vcard2hash
29             hash2vcard
30             );
31              
32             our $VERSION = '0.11';
33              
34             require XSLoader;
35             XSLoader::load('Text::VCardFast', $VERSION);
36              
37             # public API
38              
39 5     5 1 3485 sub vcard2hash { &vcard2hash_c }
40 15     15 1 156677 sub hash2vcard { &hash2vcard_pp }
41              
42             # Implementation
43              
44             sub vcard2hash_c {
45 31     31 0 4239 my $vcard = shift;
46 31         131 my %params = @_;
47 31 100       125 if (utf8::is_utf8($vcard)) {
48 24         64 utf8::encode($vcard);
49 24         65 $params{is_utf8} = 1;
50             }
51 31 100       164 unless ($vcard =~ m/\n\S/) {
52             # cruddy card with \r as line separator?
53 1         3 $vcard =~ tr/\r/\n/;
54             }
55 31         2128 my $hash = Text::VCardFast::_vcard2hash($vcard, \%params);
56 25         107 return $hash;
57             }
58              
59             # pureperl version
60              
61             # VCard parsing and formatting {{{
62              
63             my %RFC6868Map = ("n" => "\n", "^" => "^", "'" => "\"");
64             my %RFC6868RevMap = reverse %RFC6868Map;
65             my %UnescapeMap = ("n" => "\n", "N" => "\n");
66              
67             my $Pos = 1;
68             my @PropOutputOrder = qw(version fn n nickname lang gender org title role bday anniversary email tel adr url impp);
69             my %PropOutputOrder = map { $_ => $Pos++ } @PropOutputOrder;
70             my @ParamOutputOrder = qw(type pref);
71             my %ParamOutputOrder = map { $_ => $Pos++ } @ParamOutputOrder;
72              
73             sub vcard2hash_pp {
74 12     12 0 75672 my $vcard = shift;
75 12 50       105 unless ($vcard =~ m/\n\S/) {
76             # cruddy card with \r as line separator?
77 0         0 $vcard =~ tr/\r/\n/;
78             }
79 12         58 my %params = @_;
80 12         303 return vcardlines2hash_pp(\%params, (split /\r?\n/, $vcard));
81             }
82              
83             sub vcardlines2hash_pp {
84 12     12 0 24 my $args = shift;
85 12         20 local $_;
86              
87 12         19 my %MultiFieldMap;
88             my %MultiParamMap;
89 12 50       41 if ($args->{multival}) {
90 12         21 %MultiFieldMap = map { $_ => 1 } @{$args->{multival}};
  36         100  
  12         35  
91             }
92 12 50       41 if ($args->{multiparam}) {
93 12         18 %MultiParamMap = map { $_ => 1 } @{$args->{multiparam}};
  12         47  
  12         25  
94             }
95              
96             # rfc2425, rfc2426, rfc6350, rfc6868
97              
98 12         22 my @Path;
99             my $Current;
100 12         57 while ($_ = shift @_) {
101             # Strip EOL
102 190         285 s/\r?\n$//;
103              
104             # 5.8.1 - Unfold lines if next line starts with space or tab
105 190 100 100     896 if (@_ && $_[0] =~ s/^[ \t]//) {
106 21         39 $_ .= shift @_;
107 21         27 redo;
108             }
109              
110             # Ignore empty lines
111 169 50       405 next if /^\s*$/;
112              
113 169 100       368 if (/^BEGIN:(.*)/i) {
114 13         20 push @Path, $Current;
115 13         121 $Current = { type => lc $1 };
116 13         23 push @{ $Path[-1]{objects} }, $Current;
  13         52  
117 13         39 next;
118             }
119 156 100       303 if (/^END:(.*)/i) {
120             die "END $1 in $Current->{type}"
121 13 50       74 unless $Current->{type} eq lc $1;
122 13         20 $Current = pop @Path;
123 13 50 33     42 return $Current if ($args->{only_one} and not @Path);
124 13         40 next;
125             }
126              
127             # 5.8.2 - Parse '[group "."] name *(";" param) ":" value'
128             # In v2.1, params may not have "=value" part
129             # In v4, "," is allowed in non-quoted param value
130 143         444 my ($Name) = /^([^;:]*)/gc;
131 143         491 my @Params = /\G;(?:([\w\-]+)=)?("[^"]*"|[^";:=]*)/gc;
132 143         364 my ($Value) = /\G:(.*)$/g;
133              
134             # 5.8.2 - Type names and parameter names are case insensitive
135 143         219 my $LName = lc $Name;
136              
137 143         103 my %Props;
138              
139             # Remove group from each property name and add as attribute
140             # (in v4, group names are case insensitive as well)
141 143 100       365 if ($LName =~ s/^(.+)\.(.*?)$/$2/) {
142 18         45 $Props{group} = $1;
143             }
144              
145 143         245 $Props{name} = $LName;
146              
147             # Parse out parameters
148 143         112 my %Params;
149 143         327 while (@Params) {
150             # Parsed into param => param-value pairs
151 100         191 my ($PName, $PValue) = splice @Params, 0, 2;
152 100 100       167 if (not defined $PName) {
153 7 50       11 if ($args->{barekeys}) {
154 0         0 $PName = $PValue;
155 0         0 $PValue = undef;
156             }
157             else {
158 7         7 $PName = 'type';
159             }
160             }
161              
162             # 5.8.2 - parameter names are case insensitive
163 100         116 my $LPName = lc $PName;
164              
165 100         138 my @PValue = (undef);
166 100 50       167 if (defined $PValue) {
167 100         156 $PValue =~ s/^"(.*)"$/$1/;
168             # \n needed for label, but assume any \; is meant to be ; as well
169 100   33     114 $PValue =~ s#\\(.)#$UnescapeMap{$1} // $1#ge;
  6         28  
170             # And RFC6868 recoding
171 100         131 $PValue =~ s/\^([n^'])/$RFC6868Map{$1}/g;
172 100 100       225 if ($MultiParamMap{$LPName}) {
173 70         153 @PValue = split /,/, $PValue;
174             }
175             else {
176 30         53 @PValue = ($PValue);
177             }
178             }
179              
180 100 100       155 if (exists $Params{$LPName}) {
181 15         11 push @{$Params{$LPName}}, @PValue;
  15         53  
182             } else {
183 85         289 $Params{$LPName} = \@PValue;
184             }
185             }
186 143 100       310 $Props{params} = \%Params if keys %Params;
187              
188 143         155 my $Encoding = $Params{encoding};
189              
190 143 100 66     395 if ($MultiFieldMap{$LName}) {
    50          
191             # use negative 'limit' to force trailing fields
192 26         179 $Value = [ split /(?
193 26   0     115 s#\\(.)#$UnescapeMap{$1} // $1#ge for @$Value;
  0         0  
194 26         40 $Props{values} = $Value;
195             } elsif ($Encoding && lc $Encoding eq 'b') {
196             # Don't bother unescaping base64 value
197              
198 0         0 $Props{value} = $Value;
199             } else {
200 117   66     175 $Value =~ s#\\(.)#$UnescapeMap{$1} // $1#ge;
  21         106  
201 117         218 $Props{value} = $Value;
202             }
203              
204 143         123 push @{$Current->{properties}->{$LName}}, \%Props;
  143         768  
205             }
206              
207             # something did a BEGIN but no END - TODO, unwind this nicely as
208             # it may be more than one level
209 12 50       26 die "BEGIN $Current->{type} without matching END"
210             if @Path;
211              
212 12         86 return $Current;
213             }
214              
215             sub hash2vcard_pp {
216 15   50 15 0 84 return join "", map { $_ . ($_[1] // "\n") } hash2vcardlines_pp($_[0]);
  223         904  
217             }
218              
219             sub hash2vcardlines_pp {
220 31   100 31 0 167 my $Objects = shift->{objects} // [];
221              
222 31         50 my @Lines;
223 31         83 for my $Card (@$Objects) {
224             # We group properties in the same group together, track if we've
225             # already output a property
226 16         31 my %DoneProps;
227              
228 16         34 my $Props = $Card->{properties};
229              
230             # Order the properties
231             my @PropKeys = sort {
232 16 50 100     153 ($PropOutputOrder{$a} // 1000) <=> ($PropOutputOrder{$b} // 1000)
  293   100     1139  
233             || $a cmp $b
234             } keys %$Props;
235              
236             # Make sure items in the same group are output together
237 16   33     119 my $Groups = $Card->{groups} || do {
238             my %Groups;
239             for (map { @$_ } values %$Props) {
240             push @{$Groups{$_->{group}}}, $_ if $_->{group};
241             }
242             \%Groups;
243             };
244              
245             # Generate output list
246 16         25 my @OutputProps;
247 16         37 for my $PropKey (@PropKeys) {
248 125         104 my @PropVals = @{$Props->{$PropKey}};
  125         204  
249 125         144 for my $PropVal (@PropVals) {
250 146 100       450 next if $DoneProps{"$PropVal"}++;
251              
252 137         142 push @OutputProps, $PropVal;
253              
254             # If it has a group, output all values in that group together
255 137 100       328 if (my $Group = $PropVal->{group}) {
256 9         10 push @OutputProps, grep { !$DoneProps{"$_"}++ } @{$Groups->{$Group}};
  18         62  
  9         20  
257             }
258             }
259             }
260              
261 16         60 my $Type = uc $Card->{type};
262 16         45 push @Lines, ("BEGIN:" . $Type);
263              
264 16         55 for (@OutputProps) {
265             # Skip deleted or synthetic properties
266 146 50 33     700 next if $_->{deleted} || $_->{name} eq 'online';
267              
268 146         166 my $Binary = $_->{binary};
269 146 50       227 if ($Binary) {
270 0   0     0 my $Encoding = ($_->{params}->{encoding} //= []);
271 0 0       0 push @$Encoding, "b" if !@$Encoding;
272             }
273              
274 146         157 my $LName = $_->{name};
275 146         146 my $Group = $_->{group};
276              
277             # rfc6350 3.3 - it is RECOMMENDED that property and parameter names be upper-case on output.
278 146 100       343 my $Line = ($Group ? (uc $Group . ".") : "") . uc $LName;
279              
280 146   100     169 while (my ($Param, $ParamVals) = each %{$_->{params} // {}}) {
  231         1011  
281 85 50       143 if (!defined $ParamVals) {
282 0         0 $Line .= ";" . uc($Param);
283             }
284 85 50       197 for (ref($ParamVals) ? @$ParamVals : $ParamVals) {
285 114   50     205 my $PV = $_ // next; # Modify copy
286 114 100       233 $PV =~ s/\n/\\N/g if $Param eq 'label';
287 114         177 $PV =~ s/([\n^"])/'^' . $RFC6868RevMap{$1}/ge;
  5         18  
288 114 100       236 $PV = '"' . $PV . '"' if $PV =~ /\W/;
289 114         300 $Line .= ";" . uc($Param) . "=" . $PV;
290             }
291             }
292 146         258 $Line .= ":";
293              
294 146   66     402 my $Value = $_->{values} || $_->{value};
295              
296 146 50       206 if ($_->{binary}) {
297 0         0 $Value = encode_base64($Value, '');
298              
299             } else {
300             my @Values = map {
301 146 50       275 my $V = ref($_) ? $$_ : $_; # Modify copy
  253 100       348  
302 253   50     400 $V //= '';
303             # rfc6350 3.4 (v4, assume clarifies many v3 semantics)
304             # - a SEMICOLON in a field of such a "compound" property MUST be
305             # escaped with a BACKSLASH character
306             # - a COMMA character in one of a field's values MUST be escaped
307             # with a BACKSLASH character
308             # - BACKSLASH characters in values MUST be escaped with a BACKSLASH
309             # character.
310 253         508 $V =~ s/([\,\;\\])/\\$1/g;
311             # - NEWLINE (U+000A) characters in values MUST be encoded
312             # by two characters: a BACKSLASH followed by either an 'n' (U+006E)
313             # or an 'N' (U+004E).
314 253         299 $V =~ s/\n/\\n/g;
315 253         516 $V;
316             } ref $Value ? @$Value : $Value;
317              
318 146         293 $Value = join ";", @Values;
319              
320             # Stripped v4 proto prefix, add it back
321 146 50       338 if (my $ProtoStrip = $_->{proto_strip}) {
322 0         0 $Value = $ProtoStrip . $Value;
323             }
324              
325             # If it's a perl unicode string, make it utf-8 bytes
326             #if (utf8::is_utf8($Value)) {
327             #$Value = encode_utf8($Value);
328             #}
329             }
330              
331 146         194 $Line .= $Value;
332              
333 146         247 push @Lines, foldline($Line);
334             }
335              
336 16         95 push @Lines, hash2vcardlines_pp($Card);
337              
338 16         103 push @Lines, "END:" . $Type;
339             }
340              
341 31         191 return @Lines;
342             }
343              
344             sub foldline {
345 146     146 0 182 local $_ = shift;
346              
347             # Fold at every \n, regardless of position
348             # Try folding on at whitespace boundaries after 60 chars first
349             # Otherwise fold to 75 chars, but don't split utf-8 unicode char or end with a \
350 146         120 my @Out;
351 146   100     1355 while (/\G(.{0,75}?\\n)/gc || /\G(.{60,75})(?<=[^\n\t ])(?=[\n\t ])/gc || /\G(.{0,74}[^\\])(?![\x80-\xbf])/gc) {
      100        
352 191 100       1583 push @Out, (@Out ? " " . $1 : $1);
353             }
354 146 50       469 push @Out, " " . substr($_, pos($_)) if pos $_ != length $_;
355              
356 146         512 return @Out;
357             }
358              
359             # }}}
360              
361             1;
362              
363             1;
364             __END__