File Coverage

blib/lib/Convert/DUDE.pm
Criterion Covered Total %
statement 57 71 80.2
branch 9 14 64.2
condition 2 3 66.6
subroutine 7 11 63.6
pod 5 5 100.0
total 80 104 76.9


line stmt bran cond sub pod time code
1             package Convert::DUDE;
2              
3 1     1   7063 use strict;
  1         3  
  1         50  
4 1     1   5 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
  1         2  
  1         108  
5             $VERSION = '0.02';
6              
7 1     1   1044 use Unicode::String qw(utf8);
  1         6347  
  1         109  
8              
9             BEGIN {
10 1     1   7 require Exporter;
11 1         12 @ISA = qw(Exporter);
12 1         2 @EXPORT = qw(to_dude from_dude);
13 1         1 @EXPORT_OK = qw(dude_encode dude_decode);
14 1         81 %EXPORT_TAGS = (
15             all => [ @EXPORT, @EXPORT_OK ],
16             encode => [ @EXPORT_OK ],
17             );
18             }
19              
20             {
21             my $prefix = 'dq--'; # default
22             sub prefix {
23 0     0 1 0 shift;
24 0 0       0 $prefix = shift if @_;
25 0         0 $prefix;
26             }
27             }
28              
29 0     0   0 sub _die { require Carp; Carp::croak @_; }
  0         0  
30              
31             # XXX don't use Convert::Base32
32             # XXX because Base32 tables in RACE / DUDE are different ...
33 1     1   4 use vars qw(%bits2char %char2bits);
  1         1  
  1         670  
34              
35             %bits2char = qw@
36             00000 a
37             00001 b
38             00010 c
39             00011 d
40             00100 e
41             00101 f
42             00110 g
43             00111 h
44             01000 i
45             01001 j
46             01010 k
47             01011 m
48             01100 n
49             01101 p
50             01110 q
51             01111 r
52             10000 s
53             10001 t
54             10010 u
55             10011 v
56             10100 w
57             10101 x
58             10110 y
59             10111 z
60             11000 2
61             11001 3
62             11010 4
63             11011 5
64             11100 6
65             11101 7
66             11110 8
67             11111 9
68             @; # End of qw
69              
70             %char2bits = reverse %bits2char;
71              
72             =begin algorithm
73              
74             let prev = 0x60
75             for each input integer n (in order) do begin
76             if n == 0x2D then output hyphen-minus
77             else begin
78             let diff = prev XOR n
79             represent diff in base 16 as a sequence of quartets,
80             as few as are sufficient (but at least one)
81             prepend 0 to the last quartet and 1 to each of the others
82             output a base-32 character corresponding to each quintet
83             let prev = n
84             end
85             end
86              
87             =end algorithm
88              
89             =cut
90              
91             sub dude_encode ($) {
92 10     10 1 527 my $input = utf8(shift);
93              
94 10         14 my $output;
95 10         12 my $prev = 0x60;
96 10         31 for my $i (0 .. $input->length-1) {
97 122         385 my $n = $input->substr($i, 1)->ord;
98 122 100       2909 if ($n == 0x2d) {
99 6         8 $output .= '-';
100 6         12 next;
101             }
102              
103 116         152 my $diff = $prev ^ $n;
104              
105 116         833 my @quartets = unpack('B*', pack('n*', $diff)) =~ m/(.{4})/gs;
106 116   66     1350 shift @quartets while (@quartets && $quartets[0] eq '0000');
107              
108 116         276 my @fb_quartets = ((map { '1' . $_ } @quartets[0..$#quartets - 1]),
  168         410  
109             '0' . $quartets[-1]);
110 116         462 $output .= $bits2char{$_} for (@fb_quartets);
111 116         276 $prev = $n;
112             }
113 10         43 return $output;
114             }
115              
116             sub to_dude($) {
117 0     0 1 0 my $domain = shift;
118 0         0 return __PACKAGE__->prefix . dude_encode($domain);
119             }
120              
121             =begin algorithm
122              
123             let prev = 0x60
124             while the input string is not exhausted do begin
125             if the next character is hyphen-minus
126             then consume it and output 0x2D
127             else begin
128             consume characters and convert them to quintets until
129             encountering a quintet whose first bit is 0
130             fail upon encountering a non-base-32 character or end-of-input
131             strip the first bit of each quintet
132             concatenate the resulting quartets to form diff
133             let prev = prev XOR diff
134             output prev
135             end
136             end
137             encode the output sequence and compare it to the input string
138             fail if they do not match (case-insensitively)
139              
140             =end algorithm
141              
142             =cut
143              
144             sub dude_decode ($) {
145 5     5 1 12 my $input = lc shift;
146              
147 5         7 my $prev = 0x60;
148 5         83 my @input = split //, $input;
149              
150 5         22 my $output = Unicode::String->new;
151 5         42 while (@input) {
152 61 100       1271 if ($input[0] eq '-') {
153 3         10 $output->append(Unicode::String::uchr(0x2d));
154 3         56 shift @input;
155 3         16 next;
156             }
157              
158 58         57 my @quintets;
159 58         53 CONSUME: while (1) {
160 142 50       264 unless (exists $char2bits{$input[0]}) {
161 0         0 _die "encountered non-base-32 character: $input[0]";
162             }
163 142 50       227 unless (@input) {
164 0         0 _die "reached end-of-input.";
165             }
166              
167 142         195 my $quintet = $char2bits{shift @input};
168 142         253 push @quintets, $quintet;
169 142 100       306 last CONSUME if substr($quintet, 0, 1) eq '0';
170             }
171              
172 58         57 my $diff = 0;
173 58         58 my $order = 0;
174 58         76 for my $quintet (reverse @quintets) {
175 142         378 $diff += ord(pack('B*', '0000' . substr($quintet, 1))) * (16 ** $order++);
176             }
177 58         79 $prev = $prev ^ $diff;
178 58         133 $output->append(Unicode::String::uchr($prev));
179             }
180              
181 5 50       122 unless (dude_encode($output->utf8) eq $input) {
182 0         0 _die "uniqueness check (paranoia) failed.";
183             }
184              
185 5         39 return $output->utf8;
186             }
187              
188             sub from_dude ($) {
189 0     0 1   my $dude = shift;
190 0           my $prefix = __PACKAGE__->prefix;
191 0           $dude =~ s/^$prefix//o;
192 0           return dude_decode($dude);
193             }
194              
195              
196             1;
197              
198             __END__