File Coverage

blib/lib/Device/Gsm/Pdu.pm
Criterion Covered Total %
statement 63 98 64.2
branch 12 26 46.1
condition 2 3 66.6
subroutine 10 15 66.6
pod 5 12 41.6
total 92 154 59.7


line stmt bran cond sub pod time code
1             # Device::Gsm::Pdu - PDU encoding/decoding functions for Device::Gsm class
2             # Copyright (C) 2002-2015 Cosimo Streppone, cosimo@cpan.org
3             # Copyright (C) 2006-2015 Grzegorz Wozniak, wozniakg@gmail.com
4             #
5             # This program is free software; you can redistribute it and/or modify
6             # it only under the terms of Perl itself.
7             #
8             # This program is distributed in the hope that it will be useful,
9             # but WITHOUT ANY WARRANTY; without even the implied warranty of
10             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11             # Perl licensing terms for details.
12             #
13             # Commercial support is available. Write me if you are
14             # interested in new features or software support.
15              
16             # TODO document decode_text8()
17              
18             package Device::Gsm::Pdu;
19              
20 11     11   53151 use strict;
  11         12  
  11         259  
21 11     11   4523 use Device::Gsm::Charset;
  11         17  
  11         254  
22 11     11   5104 use Device::Gsm::Sms::Token::UDH;
  11         16  
  11         9430  
23              
24             # decode a pdu encoded phone number into human readable format
25             sub decode_address {
26 20 50   20 1 520 my $address = shift or return;
27              
28 20         19 my $number;
29 20         64 my ($length, $type, $bcd_digits) = unpack('A2 A2 A*', $address);
30              
31             # XXX DEBUG
32             #print STDERR "len=$length type=$type bcd=$bcd_digits\n";
33              
34             # Manage alphabetical addresses (as per TS 03.38 specs)
35             # Alphabetical addresses begin with 'D0'.
36             # Check also http://smslink.sourceforge.net/pdu.html
37             #
38 20 100       44 if ($type eq 'D0') {
39 4         20 $number = decode_text7($length . $bcd_digits);
40 4         17 return $number;
41             }
42              
43             # Reverse each pair of bcd digits
44 16         25 while ($bcd_digits) {
45 91         84 $number .= reverse substr($bcd_digits, 0, 2);
46 91         116 $bcd_digits = substr $bcd_digits, 2;
47             }
48              
49             #print STDERR "num=$number - ";
50              
51             # Truncate last `F' if found (XXX ???)
52             #$number = substr( $number, 0, hex($length) );
53 16 100       27 chop $number if substr($number, -1) eq 'F';
54              
55             # Decode special characters for GPRS dialing
56 16         22 $number =~ s/A/\*/;
57 16         15 $number =~ s/B/#/;
58              
59             # If number is international, put a '+' sign before
60 16 100 66     68 if ($type == 91 && $number !~ /^\s*\+/) {
61 11         15 $number = '+' . $number;
62             }
63              
64 16         37 return $number;
65             }
66              
67             sub decode_text7 {
68 30     30 0 786 pack '(b*)*',
69             unpack 'C/(a7)',
70             pack 'C a*',
71             unpack 'C b*',
72             pack 'H*', $_[0];
73             }
74              
75             #remains for comatibility reasons with my production scripts :)
76             sub decode_text7_udh1 {
77 0     0 0 0 my $unpacked = join '',
78             unpack 'C/(a7)',
79             pack 'C a*',
80             unpack 'C b*',
81             pack 'H*', $_[0];
82              
83             #remove bit of padding from message
84 0         0 $unpacked = substr($unpacked, 1, length($unpacked));
85 0         0 pack '(b*)*', ($unpacked =~ m/([01]{1,7})/gs);
86             }
87              
88             #decode text with padding
89             sub decode_text7_udh {
90 0     0 0 0 my ($encoded, $padding) = @_;
91 0 0       0 $padding = 0 unless ($padding);
92 0         0 my $unpacked = join '',
93             unpack 'C/(a7)',
94             pack 'C a*',
95             unpack 'C b*',
96             pack 'H*', $encoded;
97              
98             #remove bits of padding from message
99 0         0 $unpacked = substr($unpacked, $padding, length($unpacked));
100 0         0 pack '(b*)*', ($unpacked =~ m/([01]{7})/gs);
101              
102             }
103              
104             # decode 8-bit encoded text
105             sub decode_text8($) {
106              
107 0     0 0 0 my $text8 = shift();
108 0 0       0 return unless $text8;
109              
110 0         0 my $str;
111 0         0 while ($text8) {
112 0         0 $str .= chr(hex(substr $text8, 0, 2));
113 0 0       0 if (length($text8) > 2) {
114 0         0 $text8 = substr($text8, 2);
115             }
116             else {
117 0         0 $text8 = '';
118             }
119             }
120 0         0 return $str;
121             }
122              
123             sub encode_address {
124 5     5 1 14 my $num = shift;
125 5         7 my $type = '';
126 5         5 my $len = 0;
127 5         8 my $encoded = '';
128              
129 5         10 $num =~ s/\s+//g;
130              
131             #warn('encode_address('.$num.')');
132              
133             # Check for alphabetical addresses (TS 03.38)
134 5 50       12 if ($num =~ /[A-Z][a-z]/) {
135              
136             # Encode clear text in gsm0338 7-bit
137 0         0 $type = 'D0';
138 0         0 $encoded = encode_text7($num);
139 0         0 $len = unpack 'H2' => chr(length $encoded);
140             }
141             else {
142 5 100       14 $type = index($num, '+') == 0 ? 91 : 81;
143              
144             # Remove all non-numbers. Beware to GPRS dialing chars.
145 5         20 $num =~ s/[^\d\*#]//g;
146 5         7 $num =~ s/\*/A/g; # "*" maps to A
147 5         6 $num =~ s/#/B/g; # "#" maps to B
148              
149 5         18 $len = unpack 'H2' => chr(length $num);
150 5         7 $num .= 'F';
151 5         24 my @digit = split // => $num;
152              
153 5         14 while (@digit > 1) {
154 28         66 $encoded .= join '', reverse splice @digit, 0, 2;
155             }
156             }
157              
158             #warn(' [' . (uc $len . $type . $encoded ) . ']' );
159              
160 5         26 return (uc $len . $type . $encoded);
161             }
162              
163             sub decode_text_UCS2 {
164 5     5 0 12 my $encoded = shift;
165 5 50       9 return undef unless $encoded;
166              
167 5         10 my $len = hex substr($encoded, 0, 2);
168 5         5 $encoded = substr $encoded, 2;
169              
170 5         3 my $decoded = "";
171 5         9 while ($encoded) {
172 5         19 $decoded .= pack("C0U", hex(substr($encoded, 0, 4)));
173 5         9 $encoded = substr($encoded, 4);
174             }
175 5         15 return $decoded;
176             }
177              
178             sub encode_text7 {
179             uc
180 12     12 1 3578 unpack 'H*',
181             pack 'C b*',
182             length $_[0],
183             join '',
184             unpack '(b7)*', $_[0];
185             }
186              
187             #
188             #return complete ud with udh
189             #remains for comatibility reasons with my production scripts :)
190             #
191             sub encode_text7_udh1 {
192 0     0 0 0 my $decoded = shift;
193 0         0 my $udh1 = shift;
194 0         0 my $decoded_length = length($decoded);
195 0         0 $decoded = Device::Gsm::Charset::iso8859_to_gsm0338($decoded);
196 0         0 my $pdu_msg = uc
197             unpack 'H*',
198             pack 'b*',
199              
200             #add one bit of padding to align septet boundary
201             '0' . join '', unpack '(b7)*', $decoded;
202              
203             #below add 7 septets length for udh1
204             return
205 0         0 sprintf("%02X", $decoded_length + Sms::Token::UDH::UDH1_LENGTH)
206             . $udh1
207             . $pdu_msg;
208             }
209              
210             #
211             #encode text with padding
212             #
213             sub encode_text7_udh {
214 0     0 0 0 my $decoded = shift;
215 0         0 my $padding = shift;
216 0 0       0 $padding = 0 unless ($padding);
217 0         0 my $decoded_length = length($decoded);
218 0         0 $decoded = Device::Gsm::Charset::iso8859_to_gsm0338($decoded);
219 0         0 my $pdu_msg = uc
220             unpack 'H*',
221             pack 'b*',
222              
223             #add bits of padding to align septet boundary
224             '0' x $padding . join '', unpack '(b7)*', $decoded;
225              
226             #below add septets length of text
227 0         0 my $len_hex = sprintf("%02X", $decoded_length);
228             return
229             wantarray
230 0 0       0 ? ($len_hex, $pdu_msg, $len_hex . $pdu_msg)
231             : $len_hex . $pdu_msg;
232             }
233              
234             sub pdu_to_latin1 {
235              
236             # Reattach a length octet.
237 1     1 1 7 my $s = shift;
238 1         2 my $len = length $s;
239              
240             #arn "len=$len, len/2=", $len/2, "\n";
241 1         14 my $l = uc unpack("H*", pack("C", int(length($s) / 2 * 8 / 7)));
242 1 50       3 if (length($l) % 2 == 1) { $l = '0' . $l }
  0         0  
243 1         3 my $pdu = $l . $s;
244              
245             #arn "l=$l, pdu=$pdu\n";
246 1         3 my $decoded = Device::Gsm::Pdu::decode_text7($pdu);
247              
248             #arn "decoded_text7=$decoded\n";
249 1         7 my $latin1 = Device::Gsm::Charset::gsm0338_to_iso8859($decoded);
250              
251             #arn "latin1=$latin1\n";
252 1         7 return $latin1;
253             }
254              
255             sub latin1_to_pdu {
256 1     1 1 1 my $latin1_text = $_[0];
257              
258             #arn "latin1=$latin1_text\n";
259 1         4 my $gsm0338 = Device::Gsm::Charset::iso8859_to_gsm0338($latin1_text);
260              
261             #arn "gsm0338=$gsm0338\n";
262 1         3 my $fullpdu = Device::Gsm::Pdu::encode_text7($gsm0338);
263              
264             #arn "pdu=$fullpdu\n";
265 1         8 return substr($fullpdu, 2); # strip off the length octet
266             }
267              
268             1;
269              
270             =head1 NAME
271              
272             Device::Gsm::Pdu - library to manage PDU encoded data for GSM messaging
273              
274             =head1 WARNING
275              
276             This is C software, still needs extensive testing and
277             support for custom GSM commands, so use it at your own risk,
278             and without C warranty! Have fun.
279              
280             =head1 NOTICE
281              
282             This module is meant to be used internally by C class,
283             so you probably do not want to use it directly.
284              
285             =head1 SYNOPSIS
286              
287             use Device::Gsm::Pdu;
288              
289             # DA is destination address
290             $DA = Device::Gsm::Pdu::encode_address('+39347101010');
291             $number = Device::Gsm::Pdu::decode_address( $DA );
292              
293             # Encode 7 bit text to send messages
294             $text = Device::Gsm::Pdu::encode_text7('hello');
295              
296             =head1 DESCRIPTION
297              
298             C module includes a few basic functions to deal with SMS in PDU mode,
299             such as encoding GSM addresses (phone numbers) and, for now only, 7 bit text.
300              
301             =head1 FUNCTIONS
302              
303             =head2 decode_address( pdu_encoded_address )
304              
305             Takes a PDU encoded address and decodes into human-readable mobile number.
306             If number type is international, result will be prepended with a `+' sign.
307              
308             Clearly, it is intended as an internal function.
309              
310             =head3 Example
311              
312             print Device::Gsm::Pdu::decode_address( '0B919343171010F0' );
313             # prints `+39347101010';
314              
315             =head2 encode_address( mobile_number )
316              
317             Takes a mobile number and encodes it as DA (destination address).
318             If it begins with a `+', as in `+39328101010', it is treated as an international
319             number.
320              
321             =head3 Example
322              
323             print Device::Gsm::Pdu::encode_address( '+39347101010' );
324             # prints `0B919343171010F0'
325              
326             =head2 encode_text7( text_string )
327              
328             Encodes some text ASCII string in 7 bits PDU format, including a header byte
329             which tells the length is septets. This is the only 100% supported mode to
330             encode text.
331              
332             =head3 Example
333              
334             print Device::Gsm::Pdu::encode_text7( 'hellohello' );
335             # prints `0AE832...'
336              
337             =head2 pdu_to_latin1($pdu)
338              
339             Converts a PDU (without the initial length octet) into a latin1 string.
340              
341             =head3 Example
342              
343             my $pdu = 'CAFA9C0E0ABBDF7474590E8296E56C103A3C5E97E5';
344             print Device::Gsm::Pdu::pdu_to_latin1($pdu);
345             # prints `Just another Perl hacker'
346              
347             =head2 latin1_to_pdu($text)
348              
349             Converts a text string in latin1 encoding (ISO-8859-1) into a PDU string.
350              
351             =head3 Example
352              
353             my $text = "Just another Perl hacker";
354             print Device::Gsm::Pdu::latin1_to_pdu($text);
355             # prints `CAFA9C0E0ABBDF7474590E8296E56C103A3C5E97E5'
356              
357             =head1 AUTHOR
358              
359             Cosimo Streppone, cosimo@cpan.org
360              
361             =head1 COPYRIGHT
362              
363             This library is free software; you can redistribute it and/or modify
364             it only under the terms of Perl itself.
365              
366             =head1 SEE ALSO
367              
368             Device::Gsm(3), perl(1)
369