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 1 4 25.0
total 188 262 71.7


line stmt bran cond sub pod time code
1             package FSM::Basic;
2              
3 9     9   443432 use 5.006;
  9         108  
4 9     9   44 use strict;
  9         19  
  9         172  
5 9     9   40 use warnings;
  9         14  
  9         13445  
6              
7             =head1 NAME
8              
9             FSM::Basic - Finite state machine using HASH as state definitions
10              
11             =head1 VERSION
12              
13             Version 0.12
14              
15             =cut
16              
17             our $VERSION = '0.12';
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, and 2 specific commands:
106              
107             =over 4
108              
109             =item *
110             "cat" just reading the file content provided in parameter).
111              
112             =item *
113             "catRAND" chose randomly one of the files provided in parameter space separated
114              
115             =item *
116             "catSEQ" read sequentialy the next files provided in parameter space separated
117             if "catSEQ_idx" is defined, that file is used to keep the state. Otherwise , the state file is named used
118             all the files name from "catSEQ" concatenated with a final '.tate'. All spaces are replaced by an underscore
119              
120             =back
121              
122             =item
123              
124             "matching" to define the state when the input match the "expect" value
125              
126              
127             =item
128              
129             "final" to return a flag
130              
131              
132             =item
133              
134             "not_matching" the state if the input is not matching the "expect" value (if missing stay in the same state)
135              
136              
137             =item
138              
139             "repeat" a number of trial before the state goes to "not_matching0"
140              
141              
142             =item
143              
144             "not_matching0" the state when the number of failled matching number is reached
145              
146              
147             =item
148              
149             "not_matching_info_last" info returned as second value when the failled matching number is reached
150              
151              
152             =item
153              
154             "output" the info returned as second value
155              
156              
157             =back
158              
159             It is perfectly possible to add extra tag not used by FSM::Basic for generic purpose.
160             Check examples/fake_bash_ssh1.*
161             Take a look at timout and timer usage
162             In this example if destination IP from the SSH connection is available, the file IP.json is used as definition
163             (with fallback to fake_bash1.pl)
164              
165             =cut
166              
167             sub new {
168 8     8 1 6411 my ($class, $l, $s) = @_;
169 8         16 my $self;
170 8         23 $self->{states_list} = $l;
171 8         20 $self->{state} = $s;
172 8         17 bless($self, $class);
173 8         18 return $self;
174             }
175              
176             =back
177             =head2 run
178              
179             my ( $final, $out ) = $fsm->run( $in );
180              
181             Run the FSM with the input and return the expected output and an extra flag
182              
183              
184             =cut
185              
186             sub run {
187 40015     40015 0 316803 my ($self, $in) = @_;
188              
189 40015         46742 my $output = '';
190 40015 50       73077 if (exists $self->{states_list}) {
191 40015 100 66     128102 if ( exists $self->{states_list}{ $self->{state} }
      100        
192             && exists $self->{states_list}{ $self->{state} }{repeat}
193             && $self->{states_list}{ $self->{state} }{repeat} <= 0)
194             {
195 1         2 $self->{previous_state} = $self->{state};
196 1   33     6 $self->{state} = $self->{states_list}{ $self->{state} }{expect}{not_matching0} // $self->{states_list}{ $self->{state} }{not_matching0};
197 1 50       3 if (exists $self->{states_list}{ $self->{previous_state} }{not_matching_info_last}) {
198 1         3 $output = $self->{states_list}{ $self->{previous_state} }{not_matching_info_last};
199             }
200 1   50     5 $output .= $self->{states_list}{ $self->{state} }{output} // '';
201 1   50     5 return ($self->{states_list}{ $self->{state} }{final} // 0, $output);
202             }
203 40014 50       76785 if (exists $self->{states_list}{ $self->{state} }{expect}) {
204 40014 50       68491 if (exists $self->{states_list}{ $self->{state} }{info}) {
205             $output =
206 0         0 $self->{states_list}{ $self->{state} }{info} . $output;
207             }
208 40014 50       64843 if (exists $self->{states_list}{ $self->{state} }{info_once}) {
209 0         0 $output = delete($self->{states_list}{ $self->{state} }{info_once}) . $output;
210             }
211 40014         40573 my $state;
212 40014 50 66     97972 if ( exists $self->{previous_output}
      33        
213             && $in eq ''
214             && $self->{previous_output} =~ /\[(.+)\]/)
215             {
216 0         0 $in = $1;
217             }
218 40014 100       72998 if (exists $self->{states_list}{ $self->{state} }{expect}{$in}) {
219 8         26 $state = $self->{states_list}{ $self->{state} }{expect}{$in};
220             } else {
221 40006         43963 foreach my $key (keys %{ $self->{states_list}{ $self->{state} }{expect} }) {
  40006         98848  
222 160015 100       1110343 if ($in =~ /$key/) {
223             $state =
224 40003         88912 $self->{states_list}{ $self->{state} }{expect}{$key};
225             }
226             }
227             }
228 40014 100       91370 if (ref $state eq 'HASH') {
229 40011         56881 $self->{previous_state} = $self->{state};
230 40011   33     125268 $self->{previous_output} = $state->{output} // $self->{states_list}{ $self->{state} }{output} // '';
      50        
231 40011   66     83401 $self->{state} = $state->{matching} // $self->{state};
232 40011   66     119488 $output .= $state->{output} // $self->{states_list}{ $self->{state} }{output} // '';
      100        
233 40011 50       64397 if (exists $state->{cmd}) {
234 0         0 my $cmd_state = delete $state->{cmd};
235 0         0 $cmd_state =~ s/\$in/$in/g;
236 0         0 push(@{ $self->{cmd_stack} }, $cmd_state);
  0         0  
237             }
238 40011 50       56075 if (exists $state->{cmd_exec}) {
239 0         0 my $cmd_exec = join ' ', @{ $self->{cmd_stack} };
  0         0  
240 0         0 my $string = `$cmd_exec`;
241 0         0 $output = sprintf("%s", $string) . $output;
242 0         0 $self->{cmd_exec} = [];
243             }
244 40011 50       56310 if (exists $state->{exec}) {
245 0         0 my $old_exec = $state->{exec};
246 0         0 $state->{exec} =~ s/__IN__/$in/g;
247 0         0 my $string = `$state->{exec}`;
248 0         0 $output = sprintf("%s", $string) . $output;
249 0         0 $state->{exec} = $old_exec;
250             }
251 40011 50       55293 if (exists $state->{do}) {
252 0         0 my $old_do = $state->{do};
253 0         0 $state->{do} =~ s/__IN__/$in/g;
254 0         0 $output = (eval $state->{do}) . $output;
255 0         0 $state->{do} = $old_do;
256             }
257 40011 100       53438 if (exists $state->{cat}) {
258 1         2 my $old_cat = $state->{cat};
259 1         3 $state->{cat} =~ s/__IN__/$in/g;
260 1         2 my $string = do { local (@ARGV, $/) = $state->{cat}; <> };
  1         5  
  1         45  
261 1         6 $output = sprintf("%s", $string) . $output;
262 1         5 $state->{cat} = $old_cat;
263             }
264 40011 100       56152 if (exists $state->{catRAND}) {
265 20000         26707 my $old_cat = $state->{catRAND};
266 20000         30257 $state->{catRAND} =~ s/__IN__/$in/g;
267 20000         74885 my @files = split /\s+/, $state->{catRAND};
268 20000         47301 my $file = $files[ rand @files ];
269 20000         25032 my $string = do { local (@ARGV, $/) = $file; <> };
  20000         69914  
  20000         498307  
270 20000         68155 $output = sprintf("%s", $string) . $output;
271 20000         45172 $state->{catRAND} = $old_cat;
272             }
273 40011 100       62072 if (exists $state->{catWRAND}) {
274 20000         23464 my $old_cat = $state->{catWRAND};
275 20000         28633 $state->{catWRAND} =~ s/__IN__/$in/g;
276 20000         74283 my %files = map { split /:/} split /\s+/, $state->{catWRAND};
  40000         98389  
277 20000         34699 my $file ;
278             my $weight;
279 20000         46813 while ( my ( $p, $w ) = each %files ) {
280 40000   50     58296 $w //=1;
281 40000   50     70495 $weight += $w//1;
282 40000 100       114174 $file = $p if rand($weight) < $w;
283             }
284 20000         21747 my $string = do { local (@ARGV, $/) = $file; <> };
  20000         65163  
  20000         495836  
285 20000         63309 $output = sprintf("%s", $string) . $output;
286 20000         52105 $state->{catWRAND} = $old_cat;
287             }
288 40011 100       69346 if (exists $state->{catSEQ}) {
289 3         4 my $old_cat =$state->{catSEQ};
290 3         3 my $state_file;
291 3 50       5 if (exists $state->{catSEQ_idx}) {
292             $state_file= $state->{catSEQ_idx}
293 3         6 }else{
294 0         0 $state_file = $old_cat . '.state';
295 0         0 $state_file =~ s/\s/_/g;
296              
297             }
298 3         6 $state->{catSEQ} =~ s/__IN__/$in/g;
299 3         18 my @files = split /\s+/, $state->{catSEQ};
300 3         18 tie my $nbr => 'FSM::Basic::Modulo', scalar @files, 0;
301 3 100       57 if (-f $state_file) {
302 2         2 $nbr = do {
303 2         11 local (@ARGV, $/) = $state_file;
304 2         57 <>;
305             };
306             }
307 3         10 my $file = $files[ $nbr++ ];
308 3         4 my $string = do { local (@ARGV, $/) = $file; <> };
  3         15  
  3         80  
309 3         14 $output = sprintf("%s", $string) . $output;
310 3         4 $state->{catSEQ} = $old_cat;
311 3         9 write_file($state_file, $nbr);
312             }
313             } else {
314 3         8 $self->{previous_state} = $self->{state};
315 3   33     9 $self->{state} = $self->{states_list}{ $self->{state} }{not_matching} // $self->{state};
316             $self->{states_list}{ $self->{state} }{repeat}--
317 3 50       10 if exists $self->{states_list}{ $self->{state} }{repeat};
318 3   50     10 $output .= $self->{states_list}{ $self->{state} }{output} // '';
319 3 50       8 if (exists $self->{states_list}{ $self->{state} }{not_matching_info}) {
320 0         0 $output = $self->{states_list}{ $self->{state} }{not_matching_info} . "\n" . $output;
321             }
322 3   33     52 return ($self->{states_list}{ $self->{state} }{$in}{final} // $self->{states_list}{ $self->{state} }{final} // 0, $output);
      50        
323             }
324 40011   66     244294 return ($self->{states_list}{ $self->{state} }{$in}{final} // $self->{states_list}{ $self->{state} }{final} // 0, $output);
      100        
325             }
326             }
327             }
328              
329             sub set {
330 0     0 0 0 my ($self, $in) = @_;
331 0         0 $self->{previous_state} = $self->{state};
332 0   0     0 $self->{previous_output} = $self->{states_list}{ $self->{state} }{output} // '';
333 0 0       0 $self->{state} = $in if exists $self->{states_list}{$in};
334             }
335              
336             sub write_file {
337 3     3 0 7 my ($file, $content) = @_;
338 3 50       706 open my $fh, '>' , $file or die "Error opening file for write $file: $!\n";
339 3         33 print $fh $content;
340 3 50       237 close $fh or die "Error closing file $file: $!\n";
341             }
342              
343              
344              
345              
346             package FSM::Basic::Modulo;
347 3   50 3   15 sub TIESCALAR { bless [ $_[2] || 0, $_[1] ] => $_[0] }
348 6     6   7 sub FETCH { ${ $_[0] }[0] }
  6         20  
349 5     5   7 sub STORE { ${ $_[0] }[0] = $_[1] % ${ $_[0] }[1] }
  5         11  
  5         11  
350             1;
351              
352             =head1 EXAMPLE
353              
354              
355             use strict;
356             use feature qw( say );
357             use FSM::Basic;
358             use JSON;
359             use Term::ReadLine;
360              
361             my %states = (
362             'accept' => {
363             'expect' => {
364             'default' => {
365             'final' => 0,
366             'matching' => 'prompt'
367             }
368             },
369             'not_matching' => 'accept',
370             'not_matching0' => 'close',
371             'not_matching_info_last' => '% Bad passwords
372             ',
373             'output' => 'Password: ',
374             'repeat' => 2
375             },
376              
377             'close' => {'final' => 1},
378             'prompt' => {
379             'expect' => {
380             'not_matching' => 'prompt',
381             'exit' => {
382             'matching' => 'close',
383             'final' => 0
384             },
385             "read" => {'cat' => 'file.txt'},
386             "read_random" => {'catRAND' => 'file1.txt file2.txt file3.txt'},
387             "read_seq" => {'catSEQ' => 'file1.txt file2.txt file3.txt', 'catSEQ_idx' => 'catSEQ_status'},
388             'meminfo' => {'do' => 'do { local( @ARGV, $/ ) = "/proc/meminfo" ; <> }'},
389             'mem' => {
390             '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);"
391             },
392             'h(elp)?|\\?' => {
393             'output' => 'exit
394             read
395             read_random
396             read_seq
397             meminfo
398             mem_usage
399             mem
400             User> '
401             },
402             '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); '},
403             },
404             'not_matching_info' => '% Unknown command or computer name, or unable to find computer address',
405             'output' => 'User> '
406             }
407             );
408             my $history_file = glob( '/tmp/fsm.history' );
409             my $prompt = '> ';
410             my $line;
411             my $final = 0;
412             my $term = new Term::ReadLine 'bash';
413             my $attribs = $term->Attribs->ornaments( 0 );
414             $term->using_history();
415             $term->read_history( $history_file );
416             $term->clear_signals();
417              
418             my $fsm = FSM::Basic->new( \%states, 'accept' );
419             my $out = "Password> ";
420             while ( defined( $line = $term->readline( $out ) ) )
421             {
422             ( $final, $out ) = $fsm->run( $line );
423             $term->write_history( $history_file );
424             last if $final;
425             }
426              
427             print $out if $final;
428              
429              
430             More sample code in the examples folder.
431              
432              
433             =head1 TODO
434              
435             add "edit" to allow on the fly modification of the states definition
436              
437             add "verify_states" to check all states are reachable from a original state
438              
439              
440              
441             =head1 AUTHOR
442              
443             DULAUNOY Fabrice, C<< >>
444              
445             =head1 BUGS
446              
447             Please report any bugs or feature requests to C, or through
448             the web interface at L. I will be notified, and then you'll
449             automatically be notified of progress on your bug as I make changes.
450              
451             =head1 SUPPORT
452              
453             You can find documentation for this module with the perldoc command.
454              
455             perldoc FSM::Basic
456              
457              
458             You can also look for information at:
459              
460             =over 4
461              
462             =item * RT: CPAN's request tracker (report bugs here)
463              
464             L
465              
466             =item * AnnoCPAN: Annotated CPAN documentation
467              
468             L
469              
470             =item * CPAN Ratings
471              
472             L
473              
474             =item * Search CPAN
475              
476             L
477              
478             =back
479              
480              
481             =head1 ACKNOWLEDGEMENTS
482              
483              
484             =head1 LICENSE AND COPYRIGHT
485              
486             Copyright 2016 DULAUNOY Fabrice.
487              
488             This program is free software; you can redistribute it and/or modify it
489             under the terms of the the Artistic License (2.0). You may obtain a
490             copy of the full license at:
491              
492             L
493              
494             Any use, modification, and distribution of the Standard or Modified
495             Versions is governed by this Artistic License. By using, modifying or
496             distributing the Package, you accept this license. Do not use, modify,
497             or distribute the Package, if you do not accept this license.
498              
499             If your Modified Version has been derived from a Modified Version made
500             by someone other than you, you are nevertheless required to ensure that
501             your Modified Version complies with the requirements of this license.
502              
503             This license does not grant you the right to use any trademark, service
504             mark, tradename, or logo of the Copyright Holder.
505              
506             This license includes the non-exclusive, worldwide, free-of-charge
507             patent license to make, have made, use, offer to sell, sell, import and
508             otherwise transfer the Package with respect to any patent claims
509             licensable by the Copyright Holder that are necessarily infringed by the
510             Package. If you institute patent litigation (including a cross-claim or
511             counterclaim) against any party alleging that the Package constitutes
512             direct or contributory patent infringement, then this Artistic License
513             to you shall terminate on the date that such litigation is filed.
514              
515             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
516             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
517             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
518             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
519             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
520             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
521             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
522             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
523              
524              
525             =cut
526              
527             1; # End of FSM::Basic