File Coverage

blib/lib/Text/VCardFast.pm
Criterion Covered Total %
statement 156 165 94.5
branch 52 72 72.2
condition 30 49 61.2
subroutine 12 12 100.0
pod 2 8 25.0
total 252 306 82.3


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