File Coverage

blib/lib/Convert/RACE.pm
Criterion Covered Total %
statement 85 99 85.8
branch 28 42 66.6
condition 17 24 70.8
subroutine 13 14 92.8
pod 3 3 100.0
total 146 182 80.2


line stmt bran cond sub pod time code
1             package Convert::RACE;
2              
3 2     2   30315 use strict;
  2         5  
  2         122  
4 2     2   12 use vars qw($VERSION @ISA @EXPORT);
  2         5  
  2         221  
5              
6             BEGIN {
7 2     2   10 require Exporter;
8 2         35 @ISA = qw(Exporter);
9 2         6 @EXPORT = qw(to_race from_race);
10              
11 2         30 $VERSION = '0.07';
12             }
13              
14 2     2   11 use Carp ();
  2         3  
  2         56  
15 2     2   4106 use Convert::Base32 qw(encode_base32 decode_base32);
  2         9942  
  2         1554  
16              
17 2     2   23 use constant COMPRESS_EXCEPTION => 'Invalid encoding to compress';
  2         5  
  2         456  
18 2     2   12 use constant DECOMPRESS_EXCEPTION => 'Invalid format to decompress';
  2         5  
  2         3118  
19              
20             my $_prefix_tag = 'bq--';
21              
22             sub prefix_tag {
23 0     0 1 0 my $class = shift;
24 0 0       0 $_prefix_tag = $_[0] if (@_);
25 0         0 return $_prefix_tag;
26             }
27              
28             sub to_race($) {
29 9     9 1 377 my $str = shift;
30              
31             # 2.2.1 Check the input string for disallowed names
32 9 50       20 unless (_include_disallowed_names($str)) {
33 0         0 Carp::croak('String includes no internationalized characters');
34             }
35              
36             # 2.2.2 Compress the pre-converted string
37 9         22 my $compressed = _compress($str);
38              
39             # 2.2.3 Check the length of the compressed string
40 9 50       26 if (length($compressed) > 36) {
41 0         0 Carp::croak('String too long');
42             }
43              
44             # 2.2.4 Encode the compressed string with Base32
45 9         29 my $encoded = encode_base32($compressed);
46              
47             # 2.2.5 Prepend "bq--" to the encoded string and finish
48 9         3110 return $_prefix_tag . $encoded;
49             }
50              
51             sub from_race($) {
52 9     9 1 18 my $str = lc(shift);
53              
54             # 2.3.1 Strip the "bq--"
55 9 50       78 $str =~ s/^$_prefix_tag// or Carp::croak("String not begin with $_prefix_tag");
56              
57             # 2.3.2 Decode the stripped string with Base32
58 9         28 my $decoded = decode_base32($str);
59              
60             # 2.3.3 Decompress the decoded string
61 9         489 my $decompressed = _decompress($decoded);
62              
63             # 2.3.4 Check the internationalized string for disallowed names
64 9 50       22 unless (_include_disallowed_names($decompressed)) {
65 0         0 Carp::croak('Decoded string includes no internationalized characters');
66             }
67              
68 9         40 return $decompressed;
69             }
70              
71              
72             sub _compress($) {
73 9     9   12 my $str = shift;
74              
75 9         18 my @unique_upper_octet = _make_uniq_upper_octet($str);
76 9 100 100     50 if (@unique_upper_octet > 2 ||
  6   66     22  
77             (@unique_upper_octet == 2 &&
78             ! grep { $_ eq "\x00" } @unique_upper_octet)) {
79             # from more than 2 rows
80             # or from 2 rows neither of with is 0
81 2         177 return "\xD8" . $str;
82             }
83              
84 4         11 my $u1 = @unique_upper_octet == 1
85 7 100       19 ? $unique_upper_octet[0] : (grep { $_ ne "\x00" } @unique_upper_octet)[0];
86 7 50       23 if ($u1 =~ /^[\xd8-\xdc]{1}$/) {
87 0         0 Carp::croak(COMPRESS_EXCEPTION);
88             }
89              
90 7         9 my $res = $u1;
91              
92 7         27 while ($str =~ m/(.)(.)/gs) {
93 23         126 my ($u2, $n1) = ($1, $2);
94 23 50 66     379 if ($u2 eq "\x00" and $n1 eq "\x99") {
    100 100        
    100 66        
95 0         0 Carp::croak(COMPRESS_EXCEPTION);
96             } elsif ($u2 eq $u1 and $n1 ne "\xff") {
97 20         74 $res .= $n1;
98             } elsif ($u2 eq $u1 and $n1 eq "\xff") {
99 1         5 $res .= "\xff\x99";
100             } else {
101 2         10 $res .= "\xff$n1";
102             }
103             }
104              
105 7         105 return $res;
106             }
107              
108              
109             sub _decompress($) {
110 9     9   12 my $str = shift;
111              
112             # 1)
113 9         22 my ($u1, $rest) = (substr($str,0,1), substr($str,1));
114 9 50       23 if (length($str) == 1) {
115 0         0 Carp::croak(DECOMPRESS_EXCEPTION);
116             }
117              
118 9 100       18 if ($u1 eq "\xd8") {
119             # 8)
120 2         4 my $lcheck = $rest;
121 2 50       7 if (length($lcheck) % 2) {
122 0         0 Carp::croak(DECOMPRESS_EXCEPTION);
123             }
124             # 9)
125 2         4 my @unique_upper_octet = _make_uniq_upper_octet($lcheck);
126 2 50 66     24 if (@unique_upper_octet == 1 ||
  2   33     7  
127             (@unique_upper_octet == 2 &&
128             grep { $_ eq "\x00" } @unique_upper_octet)) {
129 0         0 Carp::croak(DECOMPRESS_EXCEPTION);
130             }
131             # 10)
132 2         7 return $lcheck;
133             }
134              
135 7         9 my $buffer = '';
136 7         10 my $pos = 0;
137             # 2)
138 7         29 while (1) {
139 30 100       59 if ($pos == length($rest)) {
140             # 11)
141 7 50       18 if (length($buffer) % 2) {
142 0         0 Carp::croak(DECOMPRESS_EXCEPTION);
143             }
144 7         17 return $buffer;
145             }
146            
147 23         34 my $n1 = substr($rest, $pos, 1);
148 23 100 66     224 if ($n1 eq "\xff") {
    50          
149             # 5)
150 3 50       8 if ($pos == length($rest)-1) {
151 0         0 Carp::croak(DECOMPRESS_EXCEPTION);
152             }
153             # 6)
154 3         5 $pos++;
155 3         5 $n1 = substr($rest, $pos, 1);
156 3 100       7 if ($n1 eq "\x99") {
157 1         3 $buffer .= $u1 . "\xff";
158 1         84 next;
159             }
160             # 7)
161 2         4 $buffer .= "\x00" . $n1;
162 2         4 next;
163             } elsif ($u1 eq "\x00" and $n1 eq "\x99") {
164             # 3)
165 0         0 Carp::croak(DECOMPRESS_EXCEPTION);
166             }
167             # 4)
168 20         28 $buffer .= $u1 . $n1;
169 20         24 next;
170 23         26 } continue { $pos++; }
171             }
172              
173              
174             sub _make_uniq_upper_octet($) {
175 11     11   15 my $str = shift;
176              
177 11         13 my %seen;
178 11         40 while ($str =~ m/(.)./gs) {
179 39         226 $seen{$1}++;
180             }
181 11         47 return keys %seen;
182             }
183              
184             sub _include_disallowed_names($) {
185             # RFC 1035: letter, digit, hyphen
186 18     18   92 return $_[0] !~ /^(?:\x00[\x30-\x39\x41-\x5a\x61-\x7a\x2d])*$/;
187             }
188              
189              
190             1;
191             __END__