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   84377 use strict;
  5         7  
  5         115  
4 5     5   14 use warnings;
  5         5  
  5         115  
5              
6 5     5   1908 use Encode qw(decode_utf8 encode_utf8);
  5         29610  
  5         327  
7 5     5   1900 use MIME::Base64 qw(decode_base64 encode_base64);
  5         2388  
  5         9327  
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.08';
33              
34             require XSLoader;
35             XSLoader::load('Text::VCardFast', $VERSION);
36              
37             # public API
38              
39 5     5 1 1854 sub vcard2hash { &vcard2hash_c }
40 15     15 1 109383 sub hash2vcard { &hash2vcard_pp }
41              
42             # Implementation
43              
44             sub vcard2hash_c {
45 31     31 0 2975 my $vcard = shift;
46 31         60 my %params = @_;
47 31 100       74 if (utf8::is_utf8($vcard)) {
48 24         33 utf8::encode($vcard);
49 24         30 $params{is_utf8} = 1;
50             }
51 31         1236 my $hash = Text::VCardFast::_vcard2hash($vcard, \%params);
52 25         63 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 52626 my $vcard = shift;
71 12         35 my %params = @_;
72 12         221 return vcardlines2hash_pp(\%params, (split /\r?\n/, $vcard));
73             }
74              
75             sub vcardlines2hash_pp {
76 12     12 0 12 my $args = shift;
77 12         14 local $_;
78              
79 12         13 my %MultiFieldMap;
80             my %MultiParamMap;
81 12 50       31 if ($args->{multival}) {
82 12         12 %MultiFieldMap = map { $_ => 1 } @{$args->{multival}};
  36         59  
  12         25  
83             }
84 12 50       27 if ($args->{multiparam}) {
85 12         7 %MultiParamMap = map { $_ => 1 } @{$args->{multiparam}};
  12         25  
  12         17  
86             }
87              
88             # rfc2425, rfc2426, rfc6350, rfc6868
89              
90 12         14 my @Path;
91             my $Current;
92 12         28 while ($_ = shift @_) {
93             # Strip EOL
94 190         223 s/\r?\n$//;
95              
96             # 5.8.1 - Unfold lines if next line starts with space or tab
97 190 100 100     680 if (@_ && $_[0] =~ s/^[ \t]//) {
98 21         28 $_ .= shift @_;
99 21         20 redo;
100             }
101              
102             # Ignore empty lines
103 169 50       314 next if /^\s*$/;
104              
105 169 100       257 if (/^BEGIN:(.*)/i) {
106 13         16 push @Path, $Current;
107 13         49 $Current = { type => lc $1 };
108 13         13 push @{ $Path[-1]{objects} }, $Current;
  13         30  
109 13         28 next;
110             }
111 156 100       253 if (/^END:(.*)/i) {
112             die "END $1 in $Current->{type}"
113 13 50       34 unless $Current->{type} eq lc $1;
114 13         14 $Current = pop @Path;
115 13 50 33     22 return $Current if ($args->{only_one} and not @Path);
116 13         27 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         324 my ($Name) = /^([^;:]*)/gc;
123 143         370 my @Params = /\G;(?:([\w\-]+)=)?("[^"]*"|[^";:=]*)/gc;
124 143         252 my ($Value) = /\G:(.*)$/g;
125              
126             # 5.8.2 - Type names and parameter names are case insensitive
127 143         157 my $LName = lc $Name;
128              
129 143         98 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       261 if ($LName =~ s/^(.+)\.(.*?)$/$2/) {
134 18         25 $Props{group} = $1;
135             }
136              
137 143         183 $Props{name} = $LName;
138              
139             # Parse out parameters
140 143         100 my %Params;
141 143         182 while (@Params) {
142             # Parsed into param => param-value pairs
143 100         121 my ($PName, $PValue) = splice @Params, 0, 2;
144 100 100       137 if (not defined $PName) {
145 7 50       10 if ($args->{barekeys}) {
146 0         0 $PName = $PValue;
147 0         0 $PValue = undef;
148             }
149             else {
150 7         7 $PName = 'type';
151             }
152             }
153              
154             # 5.8.2 - parameter names are case insensitive
155 100         85 my $LPName = lc $PName;
156              
157 100         91 my @PValue = (undef);
158 100 50       128 if (defined $PValue) {
159 100         127 $PValue =~ s/^"(.*)"$/$1/;
160             # \n needed for label, but assume any \; is meant to be ; as well
161 100   33     93 $PValue =~ s#\\(.)#$UnescapeMap{$1} // $1#ge;
  6         30  
162             # And RFC6868 recoding
163 100         78 $PValue =~ s/\^([n^'])/$RFC6868Map{$1}/g;
164 100 100       135 if ($MultiParamMap{$LPName}) {
165 70         115 @PValue = split /,/, $PValue;
166             }
167             else {
168 30         39 @PValue = ($PValue);
169             }
170             }
171              
172 100 100       110 if (exists $Params{$LPName}) {
173 15         10 push @{$Params{$LPName}}, @PValue;
  15         36  
174             } else {
175 85         198 $Params{$LPName} = \@PValue;
176             }
177             }
178 143 100       235 $Props{params} = \%Params if keys %Params;
179              
180 143         117 my $Encoding = $Params{encoding};
181              
182 143 100 66     404 if ($MultiFieldMap{$LName}) {
    50          
183             # use negative 'limit' to force trailing fields
184 26         148 $Value = [ split /(?
185 26   0     88 s#\\(.)#$UnescapeMap{$1} // $1#ge for @$Value;
  0         0  
186 26         33 $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     129 $Value =~ s#\\(.)#$UnescapeMap{$1} // $1#ge;
  21         66  
193 117         113 $Props{value} = $Value;
194             }
195              
196 143         98 push @{$Current->{properties}->{$LName}}, \%Props;
  143         580  
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       28 die "BEGIN $Current->{type} without matching END"
202             if @Path;
203              
204 12         45 return $Current;
205             }
206              
207             sub hash2vcard_pp {
208 15   50 15 0 29 return join "", map { $_ . ($_[1] // "\n") } hash2vcardlines_pp($_[0]);
  223         563  
209             }
210              
211             sub hash2vcardlines_pp {
212 31   100 31 0 95 my $Objects = shift->{objects} // [];
213              
214 31         33 my @Lines;
215 31         40 for my $Card (@$Objects) {
216             # We group properties in the same group together, track if we've
217             # already output a property
218 16         12 my %DoneProps;
219              
220 16         14 my $Props = $Card->{properties};
221              
222             # Order the properties
223             my @PropKeys = sort {
224 16 50 100     65 ($PropOutputOrder{$a} // 1000) <=> ($PropOutputOrder{$b} // 1000)
  298   100     795  
225             || $a cmp $b
226             } keys %$Props;
227              
228             # Make sure items in the same group are output together
229 16   33     40 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         13 my @OutputProps;
239 16         18 for my $PropKey (@PropKeys) {
240 125         78 my @PropVals = @{$Props->{$PropKey}};
  125         124  
241 125         94 for my $PropVal (@PropVals) {
242 146 100       275 next if $DoneProps{"$PropVal"}++;
243              
244 137         94 push @OutputProps, $PropVal;
245              
246             # If it has a group, output all values in that group together
247 137 100       215 if (my $Group = $PropVal->{group}) {
248 9         5 push @OutputProps, grep { !$DoneProps{"$_"}++ } @{$Groups->{$Group}};
  18         34  
  9         12  
249             }
250             }
251             }
252              
253 16         29 my $Type = uc $Card->{type};
254 16         25 push @Lines, ("BEGIN:" . $Type);
255              
256 16         20 for (@OutputProps) {
257             # Skip deleted or synthetic properties
258 146 50 33     452 next if $_->{deleted} || $_->{name} eq 'online';
259              
260 146         110 my $Binary = $_->{binary};
261 146 50       161 if ($Binary) {
262 0   0     0 my $Encoding = ($_->{params}->{encoding} //= []);
263 0 0       0 push @$Encoding, "b" if !@$Encoding;
264             }
265              
266 146         112 my $LName = $_->{name};
267 146         94 my $Group = $_->{group};
268              
269             # rfc6350 3.3 - it is RECOMMENDED that property and parameter names be upper-case on output.
270 146 100       242 my $Line = ($Group ? (uc $Group . ".") : "") . uc $LName;
271              
272 146   100     96 while (my ($Param, $ParamVals) = each %{$_->{params} // {}}) {
  231         719  
273 85 50       104 if (!defined $ParamVals) {
274 0         0 $Line .= ";" . uc($Param);
275             }
276 85 50       137 for (ref($ParamVals) ? @$ParamVals : $ParamVals) {
277 114   50     133 my $PV = $_ // next; # Modify copy
278 114 100       135 $PV =~ s/\n/\\N/g if $Param eq 'label';
279 114         120 $PV =~ s/([\n^"])/'^' . $RFC6868RevMap{$1}/ge;
  5         13  
280 114 100       162 $PV = '"' . $PV . '"' if $PV =~ /\W/;
281 114         229 $Line .= ";" . uc($Param) . "=" . $PV;
282             }
283             }
284 146         178 $Line .= ":";
285              
286 146   66     266 my $Value = $_->{values} || $_->{value};
287              
288 146 50       150 if ($_->{binary}) {
289 0         0 $Value = encode_base64($Value, '');
290              
291             } else {
292             my @Values = map {
293 146 50       192 my $V = ref($_) ? $$_ : $_; # Modify copy
  253 100       253  
294 253   50     290 $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         306 $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         223 $V =~ s/\n/\\n/g;
307 253         328 $V;
308             } ref $Value ? @$Value : $Value;
309              
310 146         204 $Value = join ";", @Values;
311              
312             # Stripped v4 proto prefix, add it back
313 146 50       243 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         125 $Line .= $Value;
324              
325 146         187 push @Lines, foldline($Line);
326             }
327              
328 16         40 push @Lines, hash2vcardlines_pp($Card);
329              
330 16         61 push @Lines, "END:" . $Type;
331             }
332              
333 31         58 return @Lines;
334             }
335              
336             sub foldline {
337 146     146 0 122 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         92 my @Out;
343 146   100     917 while (/\G(.{0,75}?\\n)/gc || /\G(.{60,75})(?<=[^\n\t ])(?=[\n\t ])/gc || /\G(.{0,74}[^\\])(?![\x80-\xbf])/gc) {
      100        
344 191 100       1064 push @Out, (@Out ? " " . $1 : $1);
345             }
346 146 50       310 push @Out, " " . substr($_, pos($_)) if pos $_ != length $_;
347              
348 146         274 return @Out;
349             }
350              
351             # }}}
352              
353             1;
354              
355             1;
356             __END__