File Coverage

blib/lib/XML/Debian/ENetInterfaces.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package XML::Debian::ENetInterfaces;
2              
3 1     1   21358 use warnings;
  1         3  
  1         39  
4 1     1   7 use strict;
  1         2  
  1         75  
5             require 5.10.0;
6              
7             =head1 NAME
8              
9             XML::Debian::ENetInterfaces - Work with Debian's /etc/network/interfaces in XML.
10              
11             =head1 VERSION
12              
13             Version 0.07
14              
15             =cut
16              
17             our $VERSION = '0.07';
18              
19 1     1   7 use feature 'switch';
  1         6  
  1         103  
20             #use Data::Dumper;
21 1     1   481 use XML::LibXML;
  0            
  0            
22             use XML::Parser::PerlSAX;
23             use Fcntl qw(O_RDONLY O_WRONLY O_CREAT O_APPEND :flock);
24             use Carp;
25              
26             sub read();
27             sub write;
28             sub lock;
29             sub relock;
30             sub unlock();
31             my @locked;
32             my $S;
33             my $dom;
34              
35             =head1 SYNOPSIS
36              
37             Import/Export Debian /etc/network/interfaces from/to XML.
38              
39             use XML::Debian::ENetInterfaces;
40             XML::Debian::ENetInterfaces::lock(); # Optionally takes a Fcntl/flock
41             # constant like LOCK_SH
42             my $xmlstr = XML::Debian::ENetInterfaces::read();
43             XML::Debian::ENetInterfaces::write('XML String'||
44             IO::Handle->new('file','r'));
45             XML::Debian::ENetInterfaces::unlock();
46              
47             =head1 SUBROUTINES/METHODS
48              
49             =head2 new
50              
51             Just returns an object.
52              
53             =cut
54              
55             sub new {
56             my ($class) = @_;
57             return bless {}, $class;
58             }
59              
60             =head2 lock
61              
62             By default, no arguments, creates an exclusive semaphoric lock on at least the two files written to by this application. Can be used to create a shared semaphoric lock, like so:
63              
64             use Fcntl qw(:flock);
65             XML::Debian::ENetInterfaces::lock(LOCK_SH);
66            
67             =cut
68              
69             sub lock {
70             my ($lvl)=@_;
71             $lvl||=LOCK_EX;
72             relock($lvl) if ( @locked );
73             my $file=$ENV{INTERFACES}||'/etc/network/interfaces';
74             my $SEMAPHORE=$file.'.lck';
75             sysopen($S,$SEMAPHORE,
76             ($lvl==LOCK_SH?O_RDONLY:O_WRONLY)|O_CREAT|O_APPEND) or
77             die "$SEMAPHORE: $!";
78             flock($S,$lvl) or die "flock() failed for $SEMAPHORE: $!";
79             @locked=($file,$lvl);
80             }
81              
82             =head2 relock
83              
84             Used internally to detect in proper Round Trip locking. May also be useful to you. Takes the same arguments as lock above.
85              
86             =cut
87              
88             sub relock {
89             carp "Re-locking, sounds like an issue with Round Trip.";
90             my ($lvl)=@_;
91             $lvl||=LOCK_EX;
92             my $file = $ENV{INTERFACES}||'/etc/network/interfaces';
93             carp "Re-locking wrong file: $file" unless ( $file eq $locked[0] );
94             croak "Semaphore not open." unless (defined $S);
95             flock($S, $lvl) or die "flock() failed for $file.lck: $!";
96             $locked[1] = $lvl;
97             }
98              
99             =head2 unlock
100              
101             Close the existing lock.
102              
103             =cut
104              
105             sub unlock() {
106             my $file = $ENV{INTERFACES}||'/etc/network/interfaces';
107             carp "Unlocking wrong file: $file" unless ( $file eq $locked[0] );
108             croak "Semaphore not open." unless (defined $S);
109             close $S;
110             $S=undef;
111             @locked=();
112             }
113              
114             sub __identattr
115             {
116             my ($attr, $str)= @_;
117             $attr = $dom->createAttribute($attr); # Scary.
118             while ($str =~ m/(.)/g) {
119             given ($1){
120             when (" ") { $attr->appendChild($dom->createEntityReference('#32')); }
121             when ("\t") { $attr->appendChild($dom->createEntityReference('#9')); }
122             default { warn $1; $attr->appendChild($dom->createTextNode($1)); }
123             }
124             }
125             return $attr;
126             }
127              
128             =head2 read
129              
130             Takes no arguments and returns a string containing XML.
131              
132             =cut
133              
134             sub read() {
135             my $RootName='etc_network_interfaces';
136             # NOTE: The example file uses 8 space indents, the DTD specifies
137             # 4 spaces as the default.
138             # This cheat is used because DTD creation is not yet implemented.
139             $dom = XML::LibXML->load_xml(string => <");
140            
141            
142            
143            
144            
145            
146             _indent CDATA ""
147             _alias CDATA #IMPLIED
148             >
149            
150            
151             name CDATA #REQUIRED
152             opts CDATA #REQUIRED
153             _indent CDATA ""
154             _childindent CDATA " "
155             _alias CDATA #IMPLIED
156             address CDATA #IMPLIED
157             network CDATA #IMPLIED
158             broadcast CDATA #IMPLIED
159             gateway CDATA #IMPLIED
160             dns-nameservers CDATA #IMPLIED
161             netmask CDATA #IMPLIED
162             ttl CDATA #IMPLIED
163             local CDATA #IMPLIED
164             remote CDATA #IMPLIED
165             mtu CDATA #IMPLIED
166             endpoint CDATA #IMPLIED
167             >
168            
169            
170             name CDATA #REQUIRED
171             opts CDATA #REQUIRED
172             script CDATA #REQUIRED
173             _indent CDATA ""
174             _alias CDATA #IMPLIED
175             _childindent CDATA " "
176             >
177            
178            
179             _indent CDATA " "
180             >
181            
182            
183             _indent CDATA " "
184             _alias CDATA #IMPLIED
185             >
186            
187            
188             _indent CDATA " "
189             >
190            
191            
192             _indent CDATA " "
193             _alias CDATA #IMPLIED
194             >
195            
196            
197             _indent CDATA " "
198             >
199            
200             ]>
201             END
202              
203             my $root = $dom->documentElement();
204              
205             # Any lock will do.
206             my $waslocked = @locked;
207             lock(LOCK_SH) unless ($waslocked);
208             my $file = $ENV{INTERFACES}||'/etc/network/interfaces';
209             open (my $INTER, '<', $file) or croak "Can't read $file: $!";
210              
211             my $domptr;
212             LINE: while (my $ln=<$INTER>) {
213             chomp $ln;
214             # A line may be extended across multiple lines by making the
215             # last character a backslash.
216             while ($ln =~ /\\$/) {
217             # Removes/joins extended lines.
218             # TODO: Fix case where result has no whitespace.
219             chop $ln; chomp($ln.=<$INTER>); last if (eof $INTER);
220             }
221             # warn $ln;
222             # White space around comments is written out.
223             if ($ln =~ /^\s*#/ ) {
224             my $element = $dom->createElement('COMMENT');
225             $element->appendChild($dom->createTextNode($ln));
226             if (defined $domptr) {
227             $domptr->appendChild($element);
228             } else {
229             $root->appendChild($element);
230             }
231             next LINE;
232             }
233             # Removes white space on blank lines.
234             if ($ln eq '' or $ln =~ /^\s*$/) {
235             if (defined $domptr) {
236             $domptr->appendChild($dom->createElement('br'));
237             } else {
238             $root->appendChild($dom->createElement('br'));
239             }
240             next LINE;
241             }
242             # This loop could be done within the regex,
243             # though I originally wrote it this way... no particular reason.
244             foreach my $rx (qr(auto),qr(allow-auto),qr(allow-[^ ]*),
245             qr(mapping),qr(iface)) {
246             if ($ln =~ /^(\s*)($rx)\s+(\S*)\s*(.*)$/) {
247             my ($ind,$ele,$nam,$opt) = ($1,$2,$3,$4);
248             $domptr = undef;
249             my $element = $dom->createElement($ele);
250             given ($ele){
251             when('mapping') {
252             $element->addChild($dom->createAttribute('name', $nam));
253             $element->addChild($dom->createAttribute('opts', $opt));
254             $element->addChild(__identattr('_indent',$ind));
255             $domptr = $element;
256             }
257             when('iface') {
258             $element->addChild($dom->createAttribute('name', $nam));
259             $element->addChild($dom->createAttribute('opts', $opt));
260             $element->addChild(__identattr('_indent',$ind));
261             $domptr = $element;
262             }
263             when('allow-auto') {
264             $element = $dom->createElement('auto');
265             $element->addChild(__identattr('_alias',$ele));
266             $element->addChild(__identattr('_indent',$ind));
267             $element->appendChild($dom->createTextNode(join ' ', grep /./, $nam, $opt));
268             }
269             default {
270             $element->addChild(__identattr('_indent',$ind));
271             $element->appendChild($dom->createTextNode(join ' ', grep /./, $nam, $opt));
272             }
273             }
274             $root->appendChild($element);
275             next LINE;
276             }
277             }
278             if ($ln =~ /^(\s*)(\S*)\s(.*)$/) {
279             my ($ind,$ele,$dat) = ($1,$2,$3);
280             if (defined $domptr) {
281             given ($ele){
282             when(['post-up', 'pre-down']) {
283             carp unless ($domptr->tagName eq 'iface');
284             my $aele = $ele;
285             $aele=~s/^.*(up|down)/$1/;
286             my $element = $dom->createElement($aele);
287             $element->addChild(__identattr('_alias',$ele));
288             $element->addChild(__identattr('_indent',$ind));
289             $element->appendChild($dom->createTextNode($dat));
290             $domptr->appendChild($element);
291             }
292             when('map') {
293             carp unless ($domptr->tagName eq 'mapping');
294             my $element = $dom->createElement($ele);
295             $element->addChild(__identattr('_indent',$ind));
296             $element->appendChild($dom->createTextNode($dat));
297             $domptr->appendChild($element);
298             }
299             when(['up','down','pre-up','post-down']) {
300             carp unless ($domptr->tagName eq 'iface');
301             my $element = $dom->createElement($ele);
302             $element->addChild(__identattr('_indent',$ind));
303             $element->appendChild($dom->createTextNode($dat));
304             $domptr->appendChild($element);
305             }
306             default {
307             $domptr->addChild(__identattr('_childindent',$ind))
308             unless (defined $domptr->getAttributeNode('_childindent') );
309             $domptr->addChild($dom->createAttribute($ele,$dat));
310             }
311             }
312             next LINE;
313             }
314             warn $ln;
315             next LINE;
316             }
317             warn $ln;
318             }
319             close $INTER;
320             # Should not hurt and is useful for Round Trip detection.
321             # unlock() unless ($waslocked);
322              
323             my $out = $dom->toString(1);
324             $dom=undef;
325             return $out;
326             }
327              
328             =head2 write
329              
330             Takes either a string or a file handle, per IO::Handle understanding of what a file handle is, and passes this to XML::Parser::PerlSAX as a string. Current versions of XML::Parser::PerlSAX treat this identically to an IO::Handle, though I guess one couldn't count on that continually being the case.
331              
332             Passed to XML-Parser-2.41(XML::Parser->parse($parse_options->{Source}{String})), libxml-perl-0.08(XML::Parser::PerlSAX->new({Source=>{String=>$_[0]}}))'s back-end.
333              
334             =cut
335              
336             sub write {
337             my ($inp) = @_;
338             # Make sure the lock is exclusive.
339             my $waslocked = (@locked and $locked[1]||-1==LOCK_EX);
340             lock(LOCK_EX) unless ($waslocked);
341             my $file = $ENV{INTERFACES}||'/etc/network/interfaces';
342             open (my $INTER, '>', "$file.tmp") or die "Can't write $file.tmp: $!";
343             my $handler = XML::Debian::ENetInterfaces::Handler->new($INTER);
344             my $parser = XML::Parser::PerlSAX->new(
345             Handler=>$handler,
346             UseAttributeOrder=>1,
347             Source=>{String=>$inp}
348             );
349             $parser->parse();
350              
351             close $INTER;
352             rename("$file.tmp", $file);
353             unlock() unless ($waslocked);
354              
355             }
356              
357             1;
358             package XML::Debian::ENetInterfaces::Handler;
359             use warnings;
360             use strict;
361             require 5.10.0;
362             use feature 'switch';
363             #use Data::Dumper;
364              
365             sub new {
366             my ($class,$outp) = @_;
367             return bless {INTER=>$outp}, $class;
368             }
369              
370             my $last_element;
371             my $last_alias;
372             my $childindent;
373             my $indent;
374             sub start_element {
375             my ($self, $element) = @_;
376             my $fh = $self->{INTER};
377             $last_element=$element->{Name};
378             $last_alias=$element->{Attributes}->{_alias}||$last_element;
379             given ($last_element){
380             when(undef) { warn; }
381             when('br') { print $fh "\n"; }
382             when(['iface','mapping']) {
383             print $fh join(' ', grep /./,
384             "$element->{Attributes}->{_indent}$last_alias",
385             $element->{Attributes}->{name},
386             $element->{Attributes}->{opts}),"\n";
387             $childindent=$element->{Attributes}->{'_childindent'};
388             delete $element->{Attributes}->{name};
389             delete $element->{Attributes}->{opts};
390             delete $element->{Attributes}->{_alias};
391             delete $element->{Attributes}->{_indent};
392             delete $element->{Attributes}->{_childindent};
393             for (@{$element->{AttributeOrder}}) {
394             my $tmp = $_;
395             print $fh "$childindent$tmp $element->{Attributes}->{$tmp}\n"
396             unless ($tmp =~ /^_/ or !defined $element->{Attributes}->{$tmp});
397             }
398             }
399             default { $indent=$element->{Attributes}->{'_indent'}; }
400             }
401             }
402              
403             sub end_element {
404             my ($self, $element) = @_;
405             given ($last_element||'nomatch'){
406             when(['iface','mapping']) {
407             $childindent=undef;
408             continue;
409             }
410             when($element->{Name}) {
411             $last_element=undef;
412             $last_alias=undef;
413             $indent=undef;
414             }
415             }
416             }
417              
418             sub characters {
419             my ($self, $characters) = @_;
420             my $fh = $self->{INTER};
421             # warn Dumper(\$characters);
422             my $hack='__NEVERMATCH';
423             given ($last_element){
424             when(undef) {}
425             when(['etc_network_interfaces','iface','mapping']) {}
426             when('COMMENT') { print $fh "$characters->{Data}\n"; }
427             when(/allow-[^ ]*/) { $hack=$last_element; continue; }
428             when(['up','down','post-up','pre-down','auto',$hack]) {
429             print $fh "$indent$last_alias $characters->{Data}\n"; }
430             default { print $fh "$indent$last_alias $characters->{Data}\n"; }
431             }
432             }
433              
434             1;
435             __END__