File Coverage

blib/lib/Device/OUI.pm
Criterion Covered Total %
statement 208 233 89.2
branch 82 106 77.3
condition 16 20 80.0
subroutine 33 33 100.0
pod 24 24 100.0
total 363 416 87.2


line stmt bran cond sub pod time code
1             package Device::OUI;
2 9     9   418808 use strict; use warnings;
  9     9   21  
  9         372  
  9         51  
  9         17  
  9         383  
3             our $VERSION = '1.04';
4 9     9   8490 use IO::File ();
  9         2147267  
  9         279  
5 9     9   81 use Carp qw( croak carp );
  9         17  
  9         484  
6 9     9   9665 use AnyDBM_File;
  9         68074  
  9         936  
7             use overload (
8 9         81 '<=>' => 'overload_cmp',
9             'cmp' => 'overload_cmp',
10             '""' => 'normalized',
11             fallback => 1,
12 9     9   5547 );
  9         3509  
13 9     9   1094 use base qw( Class::Accessor::Grouped );
  9         20  
  9         20621  
14 9         126 use Sub::Exporter -setup => {
15             exports => [qw( normalize_oui oui_cmp parse_oui_entry oui_to_integers )],
16 9     9   1678681 };
  9         145527  
17              
18             __PACKAGE__->mk_group_accessors( inherited => qw(
19             cache_db cache_file
20             file_url search_url
21             ) );
22             my $cache = $^O eq 'MSWin32' ? 'C:\device_oui' : '/var/cache/device_oui';
23             __PACKAGE__->cache_db( $cache );
24             __PACKAGE__->cache_file( $cache . '.txt' );
25             __PACKAGE__->search_url( 'http://standards.ieee.org/cgi-bin/ouisearch?%s' );
26             __PACKAGE__->file_url( 'http://standards.ieee.org/regauth/oui/oui.txt' );
27              
28             sub cache_handle {
29 89     89 1 2597 my $self = shift;
30              
31 89 100       206 if ( @_ ) { $self->set_inherited( 'cache_handle', @_ ) }
  2         11  
32 89         365 my $handle = $self->get_inherited( 'cache_handle' );
33 89 100       1909 if ( $handle ) { return $handle }
  72         256  
34 17         44 $handle = {};
35 17 100       566 if ( my $db = $self->cache_db ) {
36 8         182 my $opts = Fcntl::O_RDWR | Fcntl::O_CREAT;
37 8         14 my %cache;
38 8 100       1910 if ( tie( %cache, 'AnyDBM_File', $db, $opts, 0666 ) ) { ## no critic
39 7         17 $handle = \%cache;
40             }
41             }
42 17         492 $self->set_inherited( 'cache_handle', $handle );
43 17         291 return $handle;
44             }
45              
46             sub cache {
47 77     77 1 6582 my $self = shift;
48 77 100 100     428 my $oui = ( @_ && not ref $_[0] ) ? shift : $self->norm;
49            
50 77         186 my $handle = $self->cache_handle;
51              
52 77 100       355 if ( @_ ) {
    100          
53 53         62 my %hash = %{ shift() };
  53         324  
54 53         156 for my $x ( keys %hash ) {
55 186 100       522 if ( not defined $hash{ $x } ) { $hash{ $x } = '' }
  1         3  
56             }
57 53         1310 return $handle->{ $oui } = join( "\0", %hash );
58             } elsif ( my $x = $handle->{ $oui } ) {
59 13         224 return { split( "\0", $x ) };
60             }
61 11         43 return;
62             }
63              
64             sub new {
65 98     98 1 49984 my $self = bless( {}, shift );
66 98 100       799 $self->oui( shift ) if @_;
67 97         399 return $self;
68             }
69              
70             sub oui {
71 102     102 1 2634 my $self = shift;
72              
73 102 100       335 if ( @_ ) {
74 96         120 my $oui = shift;
75 96 100       196 if ( my $norm = normalize_oui( $oui ) ) {
76 95         817 $self->{ 'oui' } = $oui;
77 95         159 $self->{ 'oui_norm' } = $norm;
78 95         192 delete $self->{ 'lookup' };
79             } else {
80 1         39 croak "Invalid OUI format: $oui";
81             }
82             }
83              
84 101 100       383 if ( not $self->{ 'oui' } ) { croak "Object does not have an OUI" }
  1         21  
85              
86 100         193 return $self->{ 'oui' };
87             }
88              
89             sub oui_to_integers {
90 5159   100 5159 1 14226 my $oui = shift || return;
91              
92 5153 100       11605 if ( ref $oui ) { return map { hex } split( '-', $oui->norm ) }
  292         764  
  876         1783  
93              
94             # 00-06-2A or 0:6:2a, etc. any non-hex delimiter will do
95             {
96 4861         5445 my @parts = grep { length } split( /[^a-f0-9]+/i, $oui );
  4861         31583  
  13141         45532  
97 4861 100       13632 if ( @parts == 3 ) { return map { hex } @parts }
  4140         8851  
  12420         42258  
98             }
99              
100             # 00062a, requires exactly 6 hex characters
101             {
102 721         1109 my @parts = ( $oui =~ /([a-f0-9])/ig );
  721         6437  
103 721 100       3061 if ( @parts == 6 ) {
104             return(
105 719         4622 hex( $parts[0].$parts[1] ),
106             hex( $parts[2].$parts[3] ),
107             hex( $parts[4].$parts[5] ),
108             );
109             }
110             }
111 2         22 return ();
112             }
113              
114             sub normalize_oui {
115 3847     3847 1 2328077 my $oui = shift;
116 3847 100       12364 my @int = oui_to_integers( $oui ) or return;
117 3839         38313 return sprintf( '%02X-%02X-%02X', @int );
118             }
119              
120 1223     1223 1 3550 sub normalized { return shift->{ 'oui_norm' } }
121             *norm = \&normalized;
122              
123 18     18 1 2913 sub organization { return shift->lookup->{ 'organization' } }
124 5     5 1 3154 sub company_id { return shift->lookup->{ 'company_id' } }
125 5     5 1 2681 sub address { return shift->lookup->{ 'address' } }
126              
127             sub is_private {
128 5     5 1 2747 my $self = shift;
129              
130 5 100       17 return $self->organization eq 'PRIVATE' ? 1 : 0;
131             }
132              
133             sub lookup {
134 28     28 1 46 my $self = shift;
135              
136 28         32 my $x;
137 28 100       82 if ( $x = $self->{ 'lookup' } ) { return $x }
  15         88  
138              
139 13 100       50 if ( $x = $self->cache ) { return $self->{ 'lookup' } = $x }
  2         19  
140 11 50       38 if ( $x = $self->update_from_file ) { return $self->{ 'lookup' } = $x }
  11         77  
141 0 0       0 if ( $x = $self->update_from_web ) { return $self->{ 'lookup' } = $x }
  0         0  
142 0 0       0 if ( $self->mirror_file ) {
143 0 0       0 if ( $x = $self->update_from_file ) {
144 0         0 return $self->{ 'lookup' } = $x;
145             }
146             }
147              
148 0         0 return $self->{ 'lookup' } = {};
149             }
150              
151             sub update_from_file {
152 13     13 1 19 my $self = shift;
153 13         30 my $oui = $self->norm;
154              
155 13         404 my $cf = $self->cache_file;
156 13 100       323 if ( ! $cf ) { return }
  1         5  
157 12         121 my $fh = IO::File->new( $cf, 'r' );
158 12 100       1454 if ( ! $fh ) { return }
  1         5  
159              
160 11         53 local $/ = "";
161            
162 11         309 while ( my $entry = $fh->getline ) {
163 46 100       2546 if ( substr( $entry, 0, 8 ) eq $oui ) {
164 11         40 my $data = $self->parse_oui_entry( $entry );
165 11         30 $self->cache( $data );
166 11         239 return $data;
167             }
168             }
169 0         0 return;
170             }
171              
172             {
173             my $HAVE_LWP_SIMPLE;
174             sub have_lwp_simple {
175 17     17 1 28 my $self = shift;
176 17 100       50 if ( defined $HAVE_LWP_SIMPLE ) { return $HAVE_LWP_SIMPLE }
  15         48  
177 2         110970 eval "require LWP::Simple"; ## no critic
178 2 50       18 if ( $@ ) {
179 0         0 carp "Unable to load LWP::Simple, network access not available\n";
180 0         0 $HAVE_LWP_SIMPLE = 0;
181             } else {
182 2         18 $HAVE_LWP_SIMPLE = 1;
183             }
184             }
185             }
186              
187             sub mirror_file {
188 14     14 1 6984 my $self = shift;
189 14   100     291 my $url = shift || $self->file_url;
190 14 100       118 if ( ! $url ) { return }
  2         11  
191 12   100     101 my $file = shift || $self->cache_file;
192 12 100       96 if ( ! $file ) { return }
  1         12  
193 11 50       75 if ( ! $self->have_lwp_simple ) { return }
  0         0  
194              
195 11         56 my $res = LWP::Simple::mirror( $url, $file );
196 11 100       2365326 if ( $res == LWP::Simple::RC_NOT_MODIFIED() ) { return 0 }
  4         21  
197 7 50       45 if ( ! LWP::Simple::is_success( $res ) ) {
198 0         0 carp "Failed to mirror $url to $file ($res)";
199 0         0 return;
200             }
201 7         84 return 1;
202             }
203              
204             sub get_url {
205 8     8 1 1184 my $self = shift;
206 8         10 my $url = shift;
207 8 100       98 if ( ! $url ) { return }
  1         5  
208              
209 7         131 return LWP::Simple::get( $url );
210             }
211              
212             sub load_cache_from_web {
213 3     3 1 364189 my $self = shift;
214 3   33     14 my $url = shift || $self->file_url;
215 3 50       11 if ( ! $url ) { return }
  0         0  
216 3   66     89 my $file = shift || $self->cache_file;
217 3 50       50 if ( ! $file ) { return }
  0         0  
218              
219 3 50       12 if ( $self->mirror_file( $url, $file ) ) {
220 3         23 return $self->load_cache_from_file( $file );
221             }
222 0         0 return;
223             }
224              
225             sub load_cache_from_file {
226 5     5 1 694 my $self = shift;
227 5         8 my $file = shift;
228 5 100       18 if ( ! $file ) { $file = $self->cache_file }
  1         32  
229 5 50       32 if ( ! $file ) { return }
  0         0  
230              
231 5         36 my $fh = IO::File->new( $file );
232 5         527 local $/ = "";
233 5         196 $fh->getline; # dump the header
234 5         231 my $counter = 0;
235 5         198 while ( my $entry = $fh->getline ) {
236 25         820 my $data = $self->parse_oui_entry( $entry );
237 25         76 $self->cache( $data->{ 'oui' } => $data );
238 25         754 $counter++;
239             }
240 5         248 return $counter;
241             }
242              
243             sub search_url_for {
244 8     8 1 779 my $self = shift;
245 8         22 my $oui = normalize_oui( shift );
246 8 100       26 if ( ! $oui ) { $oui = $self->norm }
  6         14  
247              
248 8         270 my $url_format = $self->search_url;
249 8 50       379 if ( ! $url_format ) { return }
  0         0  
250              
251 8 50       32 if ( $url_format =~ /%s/ ) {
252 8         43 return sprintf( $url_format, $oui );
253             } else {
254 0         0 return $url_format.$oui;
255             }
256             }
257              
258             sub update_from_web {
259 6     6 1 27 my $self = shift;
260              
261 6 50       15 if ( not ref $self ) { return }
  0         0  
262 6 50       15 if ( not $self->have_lwp_simple ) { return }
  0         0  
263              
264 6         14 my $url = $self->search_url_for;
265 6 50       15 if ( ! $url ) { return }
  0         0  
266              
267 6 50       17 if ( my $page = $self->get_url( $url ) ) {
268 6 50       216 if ( $page =~ /listing contains no match/ ) { return }
  0         0  
269 6         52 my @entries = ( $page =~ m{
(.*?)
}gs );
270 6 50       19 if ( @entries > 1 ) { croak "Too many entries returned from $url\n" }
  0         0  
271 6         18 my $data = $self->parse_oui_entry( shift( @entries ) );
272 6         20 $self->cache( $data );
273 6         31 return $data;
274             }
275 0         0 return;
276             }
277              
278             sub parse_oui_entry {
279 52     52 1 8492 local $_ = pop( @_ ); # pop in case we get called as a class method
280 9     9   31364 use Carp qw( confess );
  9         30  
  9         7536  
281 52 50       153 if ( ! $_ ) { confess "eh?" }
  0         0  
282 52         162 s{}{}g;
283 52         93 s/\r//g;
284              
285 52         305 s/\s*\(hex\)\s*/\n/gm;
286 52         396 s/\s*\(base 16\).*$//gm;
287 52         1868 s/^\s*|\s*$//gsm;
288              
289 52         105 my %data = ();
290 52         366 @data{ qw( oui organization company_id address ) } = split( "\n", $_, 4 );
291 52 100       164 delete $data{ 'address' } unless $data{ 'address' };
292 52         165 return \%data;
293             }
294              
295 146 50   146 1 881 sub overload_cmp { return oui_cmp( pop( @_ ) ? reverse @_ : @_ ) }
296             sub oui_cmp {
297 656     656 1 157823 my @l = oui_to_integers( shift );
298 656         1639 my @r = oui_to_integers( shift );
299              
300 656   66     5433 return ( $l[0] <=> $r[0] || $l[1] <=> $r[1] || $l[2] <=> $r[2] );
301             }
302              
303             sub dump_cache {
304 5     5 1 9 my $self = shift;
305              
306 5         17 my @lines = (
307             "\n",
308             "OUI\t\t\t\tOrganization\n",
309             "company_id\t\t\tOrganization\n",
310             "\t\t\t\tAddress\n",
311             "\n", "\n",
312             );
313              
314 5         11 my $db = $self->cache_handle;
315              
316 5         8 foreach my $oui ( sort { $a cmp $b } keys %{ $db } ) {
  40         60  
  5         194  
317 25         382 my $d = { split( "\0", $db->{ $oui } ) };
318 25         69 my $org = $d->{ 'organization' };
319              
320 25 100       150 push( @lines,
321             sprintf( "%-10s (hex)\t\t%s\n", $d->{ 'oui' }, $org ),
322             sprintf(
323             "%-10s (base 16)\t\t%s\n",
324             $d->{ 'company_id' },
325             $org eq 'PRIVATE' ? '' : $org,
326             ),
327             );
328 25 100       48 if ( my $a = $d->{ 'address' } ) {
329 20         55 for my $x ( split( "\n", $d->{ 'address' } ) ) {
330 65         128 push( @lines, "\t\t\t\t$x\n" );
331             }
332 20         64 push( @lines, "\n" );
333             } else {
334 5         18 push( @lines, "\t\t\t\t\n" );
335             }
336             }
337              
338 5         92 return join( "", @lines );
339             }
340              
341             1;
342              
343             __END__