File Coverage

blib/lib/FSM/Basic.pm
Criterion Covered Total %
statement 113 141 80.1
branch 35 52 67.3
condition 30 55 54.5
subroutine 9 10 90.0
pod 2 4 50.0
total 189 262 72.1


line stmt bran cond sub pod time code
1             package FSM::Basic;
2              
3 9     9   760813 use 5.010;
  9         130  
4 9     9   55 use strict;
  9         17  
  9         211  
5 9     9   51 use warnings;
  9         25  
  9         20035  
6              
7             =head1 NAME
8              
9             FSM::Basic - Finite state machine using HASH as state definitions
10              
11             =head1 VERSION
12              
13             Version 0.16
14              
15             =cut
16              
17             our $VERSION = '0.16';
18              
19             =head1 SYNOPSIS
20              
21             A small finite state machine using a HASH data as description of the states
22             Mainly used to create fake bash or fake telnet server in the purpose to mimic some CLI device interface (like SWITCH or ROUTER interface)
23             Perhaps a little code snippet.
24             The HASH is easily using a JSON file
25              
26             use FSM::Basic;
27              
28             my $fsm = FSM::Basic->new( \%states, 'accept' );
29             my $final = 0;
30             my $out;
31             foreach my $in ( @ins )
32             {
33             ( $final, $out ) = $fsm->run( $in );
34             say $out;
35             last if $final;
36             }
37              
38              
39             =head1 SUBROUTINES/METHODS
40              
41             =head2 new
42              
43             my $fsm = FSM::Basic->new( \%states, 'accept' );
44              
45             Create the FSM with the HASH ref as first paramter
46             and the initial state as second parameter
47              
48             The HASH is like this:
49              
50             my %states = (
51             'accept' => {
52             'expect' => {
53             'default' => {
54             'final' => 0,
55             'matching' => 'prompt'
56             }
57             },
58             'not_matching' => 'accept',
59             'not_matching0' => 'close',
60             'not_matching_info_last' => '% Bad passwords
61             ',
62             'output' => 'Password: ',
63             'repeat' => 2
64             },
65              
66             'close' => {'final' => 1},
67             'prompt' => {
68             'expect' => {
69             'not_matching' => 'prompt',
70             'exit' => {
71             'matching' => 'close',
72             'final' => 0
73             },
74             'meminfo' => {'do' => 'do { local( @ARGV, $/ ) = "/proc/meminfo" ; <> }'},
75             'h(elp)?|\\?' => {
76             'output' => 'exit
77             meminfo
78             mem_usage
79             User> '
80             },
81             'mem_usage' => {'do' => 'my ( $tot,$avail) = (split /\\n/ ,do { local( @ARGV, $/ ) = "/proc/meminfo" ; <> })[0,2];$tot =~ s/\\D*//g; $avail =~ s/\\D*//g; sprintf "%0.2f%%\\n",(100*($tot-$avail)/$tot); '},
82             },
83             'not_matching_info' => '% Unknown command or computer name, or unable to find computer address',
84             'output' => 'User> '
85             }
86             );
87              
88              
89              
90             The keys are the states name.
91             "expect" contain a sub HASH where the keys are word or REGEX expected as input
92              
93             =over 1
94              
95             =item In this hash, you have :
96              
97             =over 2
98              
99             =item executable code:
100              
101              
102             "do" for perl code
103              
104              
105             "exec" for system code,
106              
107             =back
108              
109             and 4 dedicated commands:
110              
111             =over 4
112              
113             =item *
114             "cat" just reading the file content provided in parameter).
115              
116             =item *
117             "catRAND" chose randomly one of the files provided in parameter space separated
118              
119             =item *
120             "catWRAND" chose randomly (weighted) one of the files provided in parameter space separatedwith a : to separate the weight
121             e.g. 'catWRAND' => './t/test_cat.txt:1 ./t/test_cat1.txt:50', in this case the file ./t/test_cat1.txt get 50 more chance to be selected than file ./t/test_cat.txt
122              
123             =item *
124             "catSEQ" read sequentialy the next files provided in parameter space separated
125             if "catSEQ_idx" is defined, that file is used to keep the state. Otherwise , the state file is named used
126             all the files name from "catSEQ" concatenated with a final '.tate'. All spaces are replaced by an underscore
127              
128             =back
129              
130             =over 4
131              
132             =item "matching"
133             to define the state when the input match the "expect" value
134              
135             =back
136              
137             =over 4
138              
139             =item "final"
140             to return a flag
141              
142             =back
143              
144             =over 4
145              
146             =item "not_matching"
147             the state if the input is not matching the "expect" value (if missing stay in the same state)
148            
149             =back
150              
151             =over 4
152              
153             =item "repeat"
154             a number of trial before the state goes to "not_matching0"
155              
156             =back
157              
158             =over 4
159              
160             =item "not_matching0"
161             the state when the number of failled matching number is reached
162              
163              
164             =back
165              
166             =over 4
167              
168             =item "not_matching_info_last"
169             info returned as second value when the failled matching number is reached
170              
171             =back
172              
173             =over 4
174              
175             =item "output"
176             the info returned as second value
177              
178              
179             =back
180              
181             It is perfectly possible to add extra tag not used by FSM::Basic for generic purpose.
182             Check examples/fake_bash_ssh1.*
183             Take a look at timout and timer usage
184             In this example if destination IP from the SSH connection is available, the file IP.json is used as definition
185             (with fallback to fake_bash1.pl)
186              
187             =cut
188              
189             sub new {
190 8     8 1 8305 my ($class, $l, $s) = @_;
191 8         20 my $self;
192 8         29 $self->{states_list} = $l;
193 8         27 $self->{state} = $s;
194 8         49 bless($self, $class);
195 8         34 return $self;
196             }
197              
198             =back
199              
200             =head2 run
201              
202             my ( $final, $out ) = $fsm->run( $in );
203              
204             Run the FSM with the input and return the expected output and an extra flag
205              
206              
207             =cut
208              
209             sub run {
210 40015     40015 1 447591 my ($self, $in) = @_;
211              
212 40015         58749 my $output = '';
213 40015 50       97751 if (exists $self->{states_list}) {
214 40015 100 66     173296 if ( exists $self->{states_list}{ $self->{state} }
      100        
215             && exists $self->{states_list}{ $self->{state} }{repeat}
216             && $self->{states_list}{ $self->{state} }{repeat} <= 0)
217             {
218 1         3 $self->{previous_state} = $self->{state};
219 1   33     8 $self->{state} = $self->{states_list}{ $self->{state} }{expect}{not_matching0} // $self->{states_list}{ $self->{state} }{not_matching0};
220 1 50       5 if (exists $self->{states_list}{ $self->{previous_state} }{not_matching_info_last}) {
221 1         9 $output = $self->{states_list}{ $self->{previous_state} }{not_matching_info_last};
222             }
223 1   50     8 $output .= $self->{states_list}{ $self->{state} }{output} // '';
224 1   50     7 return ($self->{states_list}{ $self->{state} }{final} // 0, $output);
225             }
226 40014 50       101129 if (exists $self->{states_list}{ $self->{state} }{expect}) {
227 40014 50       88981 if (exists $self->{states_list}{ $self->{state} }{info}) {
228             $output =
229 0         0 $self->{states_list}{ $self->{state} }{info} . $output;
230             }
231 40014 50       81969 if (exists $self->{states_list}{ $self->{state} }{info_once}) {
232 0         0 $output = delete($self->{states_list}{ $self->{state} }{info_once}) . $output;
233             }
234 40014         51383 my $state;
235 40014 50 66     130591 if ( exists $self->{previous_output}
      33        
236             && $in eq ''
237             && $self->{previous_output} =~ /\[(.+)\]/)
238             {
239 0         0 $in = $1;
240             }
241 40014 100       92985 if (exists $self->{states_list}{ $self->{state} }{expect}{$in}) {
242 8         48 $state = $self->{states_list}{ $self->{state} }{expect}{$in};
243             } else {
244 40006         55361 foreach my $key (keys %{ $self->{states_list}{ $self->{state} }{expect} }) {
  40006         134766  
245 160015 100       1537374 if ($in =~ /$key/) {
246             $state =
247 40003         129130 $self->{states_list}{ $self->{state} }{expect}{$key};
248             }
249             }
250             }
251 40014 100       123185 if (ref $state eq 'HASH') {
252 40011         80610 $self->{previous_state} = $self->{state};
253 40011   33     166184 $self->{previous_output} = $state->{output} // $self->{states_list}{ $self->{state} }{output} // '';
      50        
254 40011   66     111262 $self->{state} = $state->{matching} // $self->{state};
255 40011   66     160772 $output .= $state->{output} // $self->{states_list}{ $self->{state} }{output} // '';
      100        
256 40011 50       82193 if (exists $state->{cmd}) {
257 0         0 my $cmd_state = delete $state->{cmd};
258 0         0 $cmd_state =~ s/\$in/$in/g;
259 0         0 push(@{ $self->{cmd_stack} }, $cmd_state);
  0         0  
260             }
261 40011 50       72531 if (exists $state->{cmd_exec}) {
262 0         0 my $cmd_exec = join ' ', @{ $self->{cmd_stack} };
  0         0  
263 0         0 my $string = `$cmd_exec`;
264 0         0 $output = sprintf("%s", $string) . $output;
265 0         0 $self->{cmd_exec} = [];
266             }
267 40011 50       73377 if (exists $state->{exec}) {
268 0         0 my $old_exec = $state->{exec};
269 0         0 $state->{exec} =~ s/__IN__/$in/g;
270 0         0 my $string = `$state->{exec}`;
271 0         0 $output = sprintf("%s", $string) . $output;
272 0         0 $state->{exec} = $old_exec;
273             }
274 40011 50       75052 if (exists $state->{do}) {
275 0         0 my $old_do = $state->{do};
276 0         0 $state->{do} =~ s/__IN__/$in/g;
277 0         0 $output = (eval $state->{do}) . $output;
278 0         0 $state->{do} = $old_do;
279             }
280 40011 100       73476 if (exists $state->{cat}) {
281 1         2 my $old_cat = $state->{cat};
282 1         5 $state->{cat} =~ s/__IN__/$in/g;
283 1         2 my $string = do { local (@ARGV, $/) = $state->{cat}; <> };
  1         8  
  1         82  
284 1         13 $output = sprintf("%s", $string) . $output;
285 1         5 $state->{cat} = $old_cat;
286             }
287 40011 100       70683 if (exists $state->{catRAND}) {
288 20000         30001 my $old_cat = $state->{catRAND};
289 20000         42511 $state->{catRAND} =~ s/__IN__/$in/g;
290 20000         100805 my @files = split /\s+/, $state->{catRAND};
291 20000         62984 my $file = $files[ rand @files ];
292 20000         33739 my $string = do { local (@ARGV, $/) = $file; <> };
  20000         93475  
  20000         1209886  
293 20000         97330 $output = sprintf("%s", $string) . $output;
294 20000         63769 $state->{catRAND} = $old_cat;
295             }
296 40011 100       83925 if (exists $state->{catWRAND}) {
297 20000         34085 my $old_cat = $state->{catWRAND};
298 20000         41464 $state->{catWRAND} =~ s/__IN__/$in/g;
299 20000         117290 my %files = map { split /:/} split /\s+/, $state->{catWRAND};
  40000         143322  
300 20000         47418 my $file ;
301             my $weight;
302 20000         64709 while ( my ( $p, $w ) = each %files ) {
303 40000   50     78345 $w //=1;
304 40000   50     90282 $weight += $w//1;
305 40000 100       158959 $file = $p if rand($weight) < $w;
306             }
307 20000         28941 my $string = do { local (@ARGV, $/) = $file; <> };
  20000         93604  
  20000         1275391  
308 20000         103209 $output = sprintf("%s", $string) . $output;
309 20000         79257 $state->{catWRAND} = $old_cat;
310             }
311 40011 100       95964 if (exists $state->{catSEQ}) {
312 3         5 my $old_cat =$state->{catSEQ};
313 3         4 my $state_file;
314 3 50       7 if (exists $state->{catSEQ_idx}) {
315             $state_file= $state->{catSEQ_idx}
316 3         4 }else{
317 0         0 $state_file = $old_cat . '.state';
318 0         0 $state_file =~ s/\s/_/g;
319              
320             }
321 3         9 $state->{catSEQ} =~ s/__IN__/$in/g;
322 3         18 my @files = split /\s+/, $state->{catSEQ};
323 3         19 tie my $nbr => 'FSM::Basic::Modulo', scalar @files, 0;
324 3 100       66 if (-f $state_file) {
325 2         7 $nbr = do {
326 2         12 local (@ARGV, $/) = $state_file;
327 2         128 <>;
328             };
329             }
330 3         11 my $file = $files[ $nbr++ ];
331 3         3 my $string = do { local (@ARGV, $/) = $file; <> };
  3         11  
  3         158  
332 3         13 $output = sprintf("%s", $string) . $output;
333 3         23 $state->{catSEQ} = $old_cat;
334 3         8 write_file($state_file, $nbr);
335             }
336             } else {
337 3         8 $self->{previous_state} = $self->{state};
338 3   33     13 $self->{state} = $self->{states_list}{ $self->{state} }{not_matching} // $self->{state};
339             $self->{states_list}{ $self->{state} }{repeat}--
340 3 50       20 if exists $self->{states_list}{ $self->{state} }{repeat};
341 3   50     14 $output .= $self->{states_list}{ $self->{state} }{output} // '';
342 3 50       11 if (exists $self->{states_list}{ $self->{state} }{not_matching_info}) {
343 0         0 $output = $self->{states_list}{ $self->{state} }{not_matching_info} . "\n" . $output;
344             }
345 3   33     82 return ($self->{states_list}{ $self->{state} }{$in}{final} // $self->{states_list}{ $self->{state} }{final} // 0, $output);
      50        
346             }
347 40011   66     348315 return ($self->{states_list}{ $self->{state} }{$in}{final} // $self->{states_list}{ $self->{state} }{final} // 0, $output);
      100        
348             }
349             }
350             }
351              
352             sub set {
353 0     0 0 0 my ($self, $in) = @_;
354 0         0 $self->{previous_state} = $self->{state};
355 0   0     0 $self->{previous_output} = $self->{states_list}{ $self->{state} }{output} // '';
356 0 0       0 $self->{state} = $in if exists $self->{states_list}{$in};
357             }
358              
359             sub write_file {
360 3     3 0 9 my ($file, $content) = @_;
361 3 50       437 open my $fh, '>' , $file or die "Error opening file for write $file: $!\n";
362 3         32 print $fh $content;
363 3 50       195 close $fh or die "Error closing file $file: $!\n";
364             }
365              
366              
367              
368              
369             package FSM::Basic::Modulo;
370 3   50 3   18 sub TIESCALAR { bless [ $_[2] || 0, $_[1] ] => $_[0] }
371 6     6   6 sub FETCH { ${ $_[0] }[0] }
  6         24  
372 5     5   9 sub STORE { ${ $_[0] }[0] = $_[1] % ${ $_[0] }[1] }
  5         11  
  5         9  
373             1;
374              
375             =head1 EXAMPLE
376              
377              
378             use strict;
379             use feature qw( say );
380             use FSM::Basic;
381             use JSON;
382             use Term::ReadLine;
383              
384             my %states = (
385             'accept' => {
386             'expect' => {
387             'default' => {
388             'final' => 0,
389             'matching' => 'prompt'
390             }
391             },
392             'not_matching' => 'accept',
393             'not_matching0' => 'close',
394             'not_matching_info_last' => '% Bad passwords
395             ',
396             'output' => 'Password: ',
397             'repeat' => 2
398             },
399              
400             'close' => {'final' => 1},
401             'prompt' => {
402             'expect' => {
403             'not_matching' => 'prompt',
404             'exit' => {
405             'matching' => 'close',
406             'final' => 0
407             },
408             "read" => {'cat' => 'file.txt'},
409             "read_random" => {'catRAND' => 'file1.txt file2.txt file3.txt'},
410             "read_seq" => {'catSEQ' => 'file1.txt file2.txt file3.txt', 'catSEQ_idx' => 'catSEQ_status'},
411             'meminfo' => {'do' => 'do { local( @ARGV, $/ ) = "/proc/meminfo" ; <> }'},
412             'mem' => {
413             'do' => "my ( $tot,$avail) = (split /\n/ ,do { local( @ARGV, $/ ) = \"/proc/meminfo\" ; <> })[0,2];$tot =~ s/\\D*//g; $avail =~ s/\\D*//g; sprintf \"%0.2f%%\\n\",(100*($tot-$avail)/$tot);"
414             },
415             'h(elp)?|\\?' => {
416             'output' => 'exit
417             read
418             read_random
419             read_seq
420             meminfo
421             mem_usage
422             mem
423             User> '
424             },
425             'mem_usage' => {'do' => 'my ( $tot,$avail) = (split /\\n/ ,do { local( @ARGV, $/ ) = "/proc/meminfo" ; <> })[0,2];$tot =~ s/\\D*//g; $avail =~ s/\\D*//g; sprintf "%0.2f%%\\n",(100*($tot-$avail)/$tot); '},
426             },
427             'not_matching_info' => '% Unknown command or computer name, or unable to find computer address',
428             'output' => 'User> '
429             }
430             );
431             my $history_file = glob( '/tmp/fsm.history' );
432             my $prompt = '> ';
433             my $line;
434             my $final = 0;
435             my $term = new Term::ReadLine 'bash';
436             my $attribs = $term->Attribs->ornaments( 0 );
437             $term->using_history();
438             $term->read_history( $history_file );
439             $term->clear_signals();
440              
441             my $fsm = FSM::Basic->new( \%states, 'accept' );
442             my $out = "Password> ";
443             while ( defined( $line = $term->readline( $out ) ) )
444             {
445             ( $final, $out ) = $fsm->run( $line );
446             $term->write_history( $history_file );
447             last if $final;
448             }
449              
450             print $out if $final;
451              
452              
453             More sample code in the examples folder.
454              
455              
456             =head1 TODO
457              
458             add "edit" to allow on the fly modification of the states definition
459              
460             add "verify_states" to check all states are reachable from a original state
461              
462              
463              
464             =head1 AUTHOR
465              
466             DULAUNOY Fabrice, C<< >>
467              
468             =head1 BUGS
469              
470             Please report any bugs or feature requests to C, or through
471             the web interface at L. I will be notified, and then you'll
472             automatically be notified of progress on your bug as I make changes.
473              
474             =head1 SUPPORT
475              
476             You can find documentation for this module with the perldoc command.
477              
478             perldoc FSM::Basic
479              
480              
481             You can also look for information at:
482              
483             =over 4
484              
485             =item * RT: CPAN's request tracker (report bugs here)
486              
487             L
488              
489             =item * AnnoCPAN: Annotated CPAN documentation
490              
491             L
492              
493             =item * CPAN Ratings
494              
495             L
496              
497             =item * Search CPAN
498              
499             L
500              
501             =back
502              
503              
504             =head1 ACKNOWLEDGEMENTS
505              
506              
507             =head1 LICENSE AND COPYRIGHT
508              
509             Copyright 2016 DULAUNOY Fabrice.
510              
511             This program is free software; you can redistribute it and/or modify it
512             under the terms of the the Artistic License (2.0). You may obtain a
513             copy of the full license at:
514              
515             L
516              
517             Any use, modification, and distribution of the Standard or Modified
518             Versions is governed by this Artistic License. By using, modifying or
519             distributing the Package, you accept this license. Do not use, modify,
520             or distribute the Package, if you do not accept this license.
521              
522             If your Modified Version has been derived from a Modified Version made
523             by someone other than you, you are nevertheless required to ensure that
524             your Modified Version complies with the requirements of this license.
525              
526             This license does not grant you the right to use any trademark, service
527             mark, tradename, or logo of the Copyright Holder.
528              
529             This license includes the non-exclusive, worldwide, free-of-charge
530             patent license to make, have made, use, offer to sell, sell, import and
531             otherwise transfer the Package with respect to any patent claims
532             licensable by the Copyright Holder that are necessarily infringed by the
533             Package. If you institute patent litigation (including a cross-claim or
534             counterclaim) against any party alleging that the Package constitutes
535             direct or contributory patent infringement, then this Artistic License
536             to you shall terminate on the date that such litigation is filed.
537              
538             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
539             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
540             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
541             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
542             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
543             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
544             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
545             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
546              
547              
548             =cut
549              
550             1; # End of FSM::Basic