File Coverage

blib/lib/Net/DAAP/DMAP.pm
Criterion Covered Total %
statement 70 162 43.2
branch 19 56 33.9
condition 4 15 26.6
subroutine 9 18 50.0
pod 0 12 0.0
total 102 263 38.7


line stmt bran cond sub pod time code
1             package Net::DAAP::DMAP;
2 1     1   24871 use strict;
  1         2  
  1         111  
3             our $NOISY = 0;
4             our $VERSION = '1.27';
5              
6             =pod
7              
8             =head1 NAME
9              
10             Net::DAAP::DMAP - Perl module for reading and writing DAAP structures
11              
12             =head2 SYNOPSIS
13              
14             use Net::DAAP::DMAP qw(:all);
15              
16             $hash_ref = dmap_to_hash_ref($dmap); # crude
17             $array_ref = dmap_to_array_ref($dmap); # crude
18              
19             $array_ref = dmap_unpack($dmap); # knows about data types
20             $node = dmap_seek($array_ref, $path);
21              
22             $flattened = dmap_flatten($array_ref); # convert to path = data formta
23             $flat_list = dmap_flat_list($array_ref); # convert to [ path, data ] format
24             $xml = dmap_to_xml($dmap); # convert to XML fragment
25             $dmap = dmap_pack($dmap); # convert to DMAP packet
26             update_content_codes($unpacked_content_codes_response);
27              
28             =head1 DESCRIPTION
29              
30             =head2 WARNING!
31              
32             Until 2.0, I reserve the right to change the interface. In
33             particular, I think C, C, and
34             C are likely to disappear. And I suspect the hive
35             brain of Perl can come up with a better data structure than I have.
36              
37             =head2 Back to the Description
38              
39             A DMAP structure is a binary record used in Apple's DAAP protocol. A
40             DMAP structure may contain other DMAP structures. Fields in a DMAP
41             structure are identified by a short name ("msdc"). The short name is
42             what's in the binary record, but a content codes list gives a long
43             name ("dmap.databasescount") and a data type for the record (32-bit
44             integer).
45              
46             A parsed DMAP structure is built out of arrays. For example:
47              
48             [
49             [
50             'dmap.loginresponse',
51             [
52             [
53             'dmap.status',
54             200
55             ],
56             [
57             'dmap.sessionid',
58             2393
59             ]
60             ]
61             ]
62             ]
63              
64             (C returns this kind of structure)
65              
66             There are two rules here: a field is wrapped in an array, and
67             a container's values are wrapped in an array. So the structure
68             is programmatically built as:
69              
70             $status_field = [ 'dmap.status', 200 ];
71             $session_id_field = [ 'dmap.sessionid', 2393 ];
72             $response_value = [ $status_field, $session_id_field ];
73             $login_response_field = [ 'dmap.loginresponse', $response_value ];
74             $entire_response = [ $login_response_field ];
75              
76             The outer array is necessary because not every response has only one
77             top-level container as this does.
78              
79             In XML you'd write the response as:
80              
81            
82             200
83             2393
84            
85              
86             This is what C returns.
87              
88             A much more convenient structure for representing this data would
89             be:
90              
91             {
92             'dmap.loginresponse' => {
93             { 'dmap.status' => 200,
94             'dmap.sessionid' => 2393,
95             },
96             }
97              
98             This is the output of C, but beware! This isn't
99             suitable for every response. The hash is indexed by field name and a
100             structure may contain many elements of the same name. For example,
101             requesting the content codes list gives you a list of records that
102             have the field name C.
103              
104             The array structure returned by C is complex, but
105             the C function makes it easier. This takes a structure and
106             a path expressed as a slash-separated list of field names:
107              
108             dmap.loginresponse/dmap.sessionid
109              
110             The return value is the the value of the first C found
111             in the first C structure. In the case of the
112             sample record above, it would be 2393.
113              
114             Another way to handle these complex arrays is to C them.
115             This returns an array of "I = value" lines, where I is
116             a slash-separated path. For example:
117              
118             [
119             '/dmap.loginresponse/dmap.status = 200',
120             '/dmap.loginresponse/dmap.sessionid = 2393'
121             ]
122              
123             You can use C and regexps to find data if that's the way your
124             mind works.
125              
126             C has a similar looking cousin called C,
127             which returns an array of "I => I" pairs. For example:
128              
129              
130             [
131             '/dmap.loginresponse/dmap.status' => 200,
132             '/dmap.loginresponse/dmap.sessionid' => 2393,
133             ]
134              
135             You can then turn this into a hash (which may of course lose you the
136             first elements), or iterate over it in pairs, if that's easier.
137              
138             You can, but don't have to, update the tables of field names ("content
139             codes") and data types. DAAP offers a request that returns a packet
140             of content codes. Feed that packet to C.
141              
142             =head2 Implementation Details
143              
144             It's all implementation details. Here are the various data types.
145              
146             1, 3, 5, 7 = ints, size 8,16,32,64 bit
147             9 = string, 10 = time_t-style time
148             11 = version (two 16-bit ints, I think)
149             12 = container
150              
151             This uses Math::BigInt for 64-bit quantities, as not every platform
152             has 64-bit int support available.
153              
154             There's no support for types 2, 4, 6, 8 yet because nobody'd found
155             examples of them in the field: are they endian changes, or signedness
156             changes. The assumption is that all numbers are unsigned (why allow
157             the possibility of a negative number of songs?).
158              
159             =head1 AUTHOR
160              
161             Nathan Torkington, . For support, join the
162             DAAP developers mailing list by sending mail to
163             develooper.com>.
164              
165             Richard Clamp is the current maintainer, and
166             took over in July 2004.
167              
168             =cut
169              
170 1     1   6 use Exporter;
  1         2  
  1         46  
171 1     1   1730 use Math::BigInt;
  1         28103  
  1         6  
172 1     1   24781 use Carp;
  1         2  
  1         2134  
173              
174             our @ISA = qw(Exporter);
175             our @EXPORT_OK = qw(dmap_to_hash_ref dmap_to_array_ref update_content_codes
176             dmap_unpack dmap_to_xml dmap_seek dmap_flatten dmap_flat_list dmap_pack );
177             our %EXPORT_TAGS = ( 'all' => \@EXPORT_OK );
178              
179             our $Types;
180             my %Type_To_Unpack;
181             my $Container_Type;
182              
183             # initialize the types and their unpack() equivalents
184             init();
185              
186             sub init {
187 1     1 0 5 local $/;
188 1         1071 $Types = eval ;
189              
190 1         6 $Container_Type = 12;
191              
192 1         23 %Type_To_Unpack = (
193             1 => 'c',
194             3 => 'n',
195             5 => 'N',
196             7 => 'Q',
197             9 => 'a*', # utf-8 encoded
198             10 => 'N',
199             11 => 'nn',
200             42 => 'a*', # this is a local invention - 9 is
201             # getting handled as utf-8, but for
202             # dpap.picturedata that would be
203             # bad m'kay
204             );
205             }
206              
207             sub dmap_to_hash_ref {
208 0     0 0 0 my $buf = shift;
209 0         0 my %tags;
210              
211 0         0 while (length $buf) {
212 0         0 my ($tag, $len) = unpack("a4N", $buf);
213 0 0 0     0 if (!defined($len) or length $buf < 8+$len) {
214 0         0 return;
215             }
216 0         0 my $data = substr($buf, 8, $len);
217             # try to unpack--if we can, assume it was a container
218 0         0 my $data2 = dmap_to_hash_ref($data);
219 0 0       0 $tags{$tag} = $data2 ? $data2 : $data;
220 0         0 substr($buf, 0, 8+$len) = '';
221             }
222 0         0 return \%tags;
223             }
224              
225             sub dmap_flatten {
226 0     0 0 0 my $struct = shift;
227 0         0 my $arrayref = [];
228              
229 0         0 flatten_traverse($arrayref, "", $struct);
230 0         0 return $arrayref;
231             }
232              
233             sub flatten_traverse {
234 0     0 0 0 my ($array_ref, $prefix, $struct) = @_;
235              
236 0         0 foreach my $ref (@$struct) {
237 0         0 for (my $i=0; $i < @$ref; $i+=2) {
238 0         0 my ($tag, $data) = ($ref->[$i], $ref->[$i+1]);
239              
240 0 0       0 if (ref $data eq 'ARRAY') {
241 0         0 flatten_traverse($array_ref, "$prefix/$tag", $data);
242             } else {
243 0         0 push @$array_ref, "$prefix/$tag = $data";
244             }
245             }
246             }
247             }
248              
249             sub dmap_flat_list {
250 0     0 0 0 return @{ flat_list_traverse([], "", shift) };
  0         0  
251             }
252              
253             sub flat_list_traverse {
254 0     0 0 0 my ($list, $prefix, $struct) = @_;
255 0         0 foreach my $ref (@$struct) {
256 0         0 for (my $i=0; $i < @$ref; $i+=2) {
257 0         0 my ($tag, $data) = ($ref->[$i], $ref->[$i+1]);
258              
259 0 0       0 if (ref $data eq 'ARRAY') {
260 0         0 flat_list_traverse($list, "$prefix/$tag", $data);
261             } else {
262 0         0 push @$list, "$prefix/$tag", $data;
263             }
264             }
265             }
266 0         0 return $list;
267             }
268              
269              
270             sub dmap_unpack {
271 120     120 0 15704 my $buf = shift;
272 120         146 my @tags;
273              
274 120         254 while (length $buf) {
275 487         1413 my ($tag, $len) = unpack("a4N", $buf);
276 487         1030 my $data = substr($buf, 8, $len);
277 487         733 substr($buf, 0, 8+$len) = '';
278 487         904 my $type = $Types->{$tag}{TYPE};
279 487 100       1137 unless ($type) {
280 1 50       5 carp "'$tag' unknown, can't unpack" if $NOISY;
281 1         4 next;
282             }
283              
284 486 100       964 if ($type == 12) {
    100          
285 113         212 $data = dmap_unpack($data);
286             } elsif ($type == 7) {
287 9         22 my ($n1, $n2) = unpack("N2", $data);
288 9         50 $data = new Math::BigInt(new Math::BigInt($n1)->blsft(32));
289 9         2877 $data += $n2;
290 9         1743 $data = "$data";
291             } else {
292 364         948 $data = unpack($Type_To_Unpack{$type}, $data);
293             }
294             # type 9 is really utf-8 encoded, so if we can, show that it is
295 486 100 66     2276 if ($type == 9 && eval { require Encode; 1 }) {
  207         2194  
  207         15213  
296 207         517 $data = Encode::decode('utf-8', $data);
297             }
298 486         10597 push @tags, [ $Types->{$tag}{NAME}, $data ];
299             }
300              
301 120         463 return \@tags;
302             }
303              
304             sub dmap_to_xml {
305 0     0 0 0 my $buf = shift;
306 0         0 my $xml = '';
307              
308 0         0 while (length $buf) {
309 0         0 my ($tag, $len) = unpack("a4N", $buf);
310 0         0 my $data = substr($buf, 8, $len);
311 0         0 my $type = $Types->{$tag}{TYPE};
312              
313 0 0       0 if ($type == 12) {
314 0         0 $data = dmap_to_xml($data);
315             } else {
316 0         0 $data = unpack($Type_To_Unpack{$type}, $data);
317             }
318 0         0 $xml .= sprintf("<%s>\n %s\n\n", $tag, $data, $tag);
319 0         0 substr($buf, 0, 8+$len) = '';
320             }
321 0         0 return $xml;
322             }
323              
324             sub dmap_to_array_ref {
325 0     0 0 0 my $buf = shift;
326 0         0 my @tags;
327              
328 0         0 while (length $buf) {
329 0         0 my ($tag, $len) = unpack("a4N", $buf);
330 0 0 0     0 if (!defined($len) or length $buf < 8+$len) {
331 0         0 return;
332             }
333 0         0 my $data = substr($buf, 8, $len);
334             # try to unpack, assume it was a container if it succeeded
335 0         0 my $data2 = dmap_to_array_ref($data);
336 0 0       0 push @tags, [ $tag, $data2 ? $data2 : $data ];
337 0         0 substr($buf, 0, 8+$len) = '';
338             }
339 0         0 return \@tags;
340             }
341              
342             sub dmap_seek {
343 0     0 0 0 my($struct, $to_find) = @_;
344              
345 0   0     0 CHUNK: while (defined($to_find) && length($to_find)) {
346 0         0 my $top;
347 0         0 ($top, $to_find) = split m{/}, $to_find, 2;
348              
349 0         0 ELEMENT: foreach my $elt (@$struct) {
350              
351 0 0       0 if ($elt->[0] eq $top) {
352 0         0 $struct = $elt->[1];
353 0         0 next CHUNK;
354             }
355             }
356 0         0 return; # NOT FOUND
357             }
358 0         0 return $struct;
359             }
360              
361             sub update_content_codes {
362 0     0 0 0 my $array = shift;
363 0         0 my $short;
364              
365 0         0 my $mccr = dmap_seek($array, "dmap.contentcodesresponse");
366 0 0       0 die "Couldn't find mccr" unless defined $mccr;
367              
368 0         0 foreach my $mdcl_rec (@$mccr) {
369 0 0       0 next unless $mdcl_rec->[0] eq 'dmap.dictionary';
370 0         0 my @fields = @{$mdcl_rec->[1]};
  0         0  
371 0         0 my ($name, $id, $type);
372 0         0 foreach my $f (@fields) {
373 0 0       0 if ($f->[0] eq 'dmap.contentcodesnumber') { $id = $f->[1] }
  0         0  
374 0 0       0 if ($f->[0] eq 'dmap.contentcodesname') { $name = $f->[1] }
  0         0  
375 0 0       0 if ($f->[0] eq 'dmap.contentcodestype') { $type = $f->[1] }
  0         0  
376             }
377 0 0       0 if ($id eq 'mcnm') { $type = 9 } # string names please
  0         0  
378 0 0       0 if ($id eq 'pfdt') { $type = 42 } # and straight binary pictures
  0         0  
379 0         0 my $record = { NAME => $name, ID => $id, TYPE => $type };
380 0         0 $short->{$id} = $record;
381             }
382              
383 0         0 $Types = $short;
384             }
385              
386             sub dmap_pack {
387 120     120 0 171 my $struct = shift;
388 120         130 my $out = '';
389              
390 120 100       768 my %by_name = map { %{$_} ? ( $_->{NAME} => $_ ) : () } values %$Types;
  10684         14916  
  10684         39103  
391 120         1060 for my $pair (@$struct) {
392 486         897 my ($name, $value) = @$pair;
393             # dmap_unpack doesn't populate the name when its decoded
394             # something it doesn't know the content-code of, like aeSV
395             # which is new to 4.5
396 486 50       858 unless ($name) {
397 0 0       0 carp "element without a name - skipping" if $NOISY;
398 0         0 next;
399             }
400             # or, it may be we don't know what kind of thing this is
401 486 50       1036 unless ($by_name{ $name }) {
402 0 0       0 carp "$name has unknown type - skipping" if $NOISY;
403 0         0 next;
404             }
405              
406 486         897 my $tag = $by_name{ $name }{ID};
407 486         711 my $type = $by_name{ $name }{TYPE};
408             #print "$name => $tag $type $Type_To_Unpack{$type}\n";
409             #$SIG{__WARN__} = sub { die @_ };
410 486 100 66     1643 if ($type == 9 && eval { require Encode; 1 }) {
  207         1627  
  207         920  
411 207         1086 $value = Encode::encode('utf-8', $value);
412             }
413 486 100       7863 if ($type == 12) { # container
    100          
414 113         198 $value = dmap_pack( $value );
415             }
416             elsif ($type == 7) { # 64-bit
417 9         40 my $high = Math::BigInt->new( $value )->brsft(32)."";
418 9         3718 my $low = Math::BigInt->new( $value )->band(0xFFFFFFFF)."";
419 9         3192 $value = pack( "N2", $high, $low );
420             }
421             else {
422 1     1   8 no warnings 'uninitialized';
  1         2  
  1         70  
423 364         1022 $value = pack( $Type_To_Unpack{$type}, $value );
424             }
425 1     1   1153 my $length = do { use bytes; length $value };
  1         10  
  1         7  
  486         669  
  486         1984  
426 486         2134 $out .= $tag . pack("N", $length) . $value;
427             }
428 120         1635 return $out;
429             }
430              
431             1;
432              
433             __DATA__