
use strict;
use diagnostics;
#perl2exe_include C:\Perl\5.00502\lib\pod\perldiag.pod
# the preceding line is for perl2exe to include
# diagnostic messages in the .exe
use Carp;
use Cwd;

use Win32::Registry;
use Tk;
# need to list the following Tk modules separately
# so that perl2exe can find them when using "Scrolled" 
use Tk::Label;
use Tk::HList;
use Tk::Text;
use Tk::DialogBox;
use Tk::FileSelect;
use Tk::Menubutton;
use Tk::Checkbutton;
use Tk::Frame;

my $VERSION = "1.14";

my $HlpData = init_HlpData();
my ($List, @SrchRes, @Txt, $related_values, $to_find, $sr_num, @TopKeys);
$related_values = 0;

my $cmd_line = scalar @ARGV;
if (@ARGV) { 
    print "Sorry, Command Line use not implemented yet.\n"; exit(1); 
    # someday I'll get around to this.
    if ( $cmd_line > 1) {
        do_search(@ARGV);
        exit(0);
    }
    elsif ( $cmd_line == 1 ) {
        do_search(@ARGV, @TopKeys);
    }
}


my $out_txt;
my %reg_type = (
'0' => 'REG_NONE',
'1' => 'REG_SZ',
'2' => 'REG_EXPAND_SZ',
'3' => 'REG_BINARY',
'4' => 'REG_DWORD',
'7' => 'REG_MULTI_SZ'
);

@TopKeys = ();
for ( sort (keys %main::)) {
   if ( /^HKEY_[A-Z_]*/ ) {
       push(@TopKeys,$_);
    }
}
# find the HKEY_ vars which Registry.pm puts into
# the main:: namespace

$| = 1;

my $mw = MainWindow->new(-title=>"REG v$VERSION");

my $HlpBttn = $mw->Button(-text=>"HELP",
                       -command=> sub { make_help(); }
                   );
$HlpBttn->pack();

sub make_help {
    my $HlpWin = $mw->Toplevel(-title=>'Help for REG',
                               -height=>10, -width=>50);
    $HlpWin->Label(-text=>'Help for Reg')->pack();
    my $HlpTxt= $HlpWin->Scrolled('Text',-scrollbars=>'e', -wrap=>'word');
    $HlpTxt->pack(-fill=>'both', -expand=>1);
    $HlpTxt->insert('end', $HlpData);
}

my $hlist = $mw->Scrolled('HList',-scrollbars=>'e',
                    -indent     => 10,
                -highlightcolor => 'green',
                    -height     => 15,
                    -width      => 70,
                    -itemtype   => 'text',
                    -separator  => '\\',
                    -selectmode => 'single',
                    -indicator  => 1,
                 -indicatorcmd  => sub { my $File = shift;
                                         my $S = shift;
                                         if ( $S eq "<Arm>" ) {
                                              drill($File);
                                         }
                                   },
                    -command    => sub { my $File = shift; make_list($File); },
                  );

TOPKEY:
for (@TopKeys) {
    my $Dir = '';
    my (@SubKeys, %data, );
    my $NewObj = undef;

    # check that the top level key is valid.  To be valid, the operation
    # must return a true (not 0 and not '') value.
    {
    no strict 'refs';
    ${$_}->Open($Dir, $NewObj) or next TOPKEY;
    # this is $HKEY_WHATEVER->Open($Dir, $NewObj);
    }

    $hlist->add($_, -text=>$_);
    $hlist->indicator('create', $_, -text=>'+');
}
$hlist->pack(-fill=>'both', -expand=>1);

my $frame1 = $mw->Frame(-relief=>'raised',
                       -borderwidth=>3)
                       ->pack(-fill=>'x');

my $frame2 = $mw->Frame(-relief=>'raised',
                       -borderwidth=>3)
                       ->pack(-fill=>'x');

my $label4 = $frame1->Label(-width=>0, -text=>"Search from roots:");
$label4->pack(-side=>'left');

$List = $frame1->Listbox(
                 -height=>0,
                 -width=>0,
                 -selectmode=>'extended',
             );
$List->pack(-side=>'left');

my $Bttn = $frame1->Button(-text=>"remove",
                    -command=> sub {
                                   my @Selected = $List->curselection;
                                   for (reverse @Selected) {
                                       $List->delete($_);
                                   }
                               }
                 );
$Bttn->pack(-side=>'left');

my $label2 = $frame2->Label(-width=>0, -text=>"Search Regex:");
$label2->pack(-side=>'left');

my $SrchEnt=  $frame2->Entry(-width=>30);
$SrchEnt->pack(-side=>'left');

my $Bttn2 = $frame2->Button(-text=>"search", 
                     -command=> sub {
                                    my @Roots = $List->get(0, 'end');
                                    my $SrchStr = $SrchEnt->get;
                                    if ( length($SrchStr) == 0 ) {
                                        $SrchStr = '.';
                                    }
                                    # if they ask for nothing, give them everything

                                    %main::ThisSeen = ();
                                    # Clear this hash so that multiple
                                    # searches per run work right.
                                    do_search($SrchStr, @Roots);
                                }
                  );
$Bttn2->pack(-side=>'left');
my $CkBttn = $frame2->Checkbutton(-text => 'Show Related Values',
                   -variable => \$main::related_values,
                   -onvalue  => 1,
                   -offvalue => 0);
$CkBttn->pack(-side=>'bottom');
$CkBttn->select;

MainLoop();

sub drill {
    # show subkeys in the main window.
    my $Dir = $_[0];
    my ($NewObj, @SubKeys, $OldDir, $RootDir, $DoReg, $PrevDir);
    if ( $hlist->info("children",$Dir) ) {
        $hlist->delete("offspring", $Dir);
        # if already 'drilled', undrill and reset indicator
        $hlist->indicator('create', $Dir, -text=>'+');
        return;
    }
    $hlist->indicator('create', $Dir, -text=>'-');
    $Dir =~ s/(^\\?HKEY[_A-Z]*)\\?//;
    #remove HKEY_whatever\
    $RootDir = $1;
    #save HKEY_whatever
    {
    no strict 'refs';
    ${$RootDir}->Open($Dir, $NewObj) or return;
    }      
    $NewObj->GetKeys(\@SubKeys); 
    $OldDir = ( $Dir  ? "$RootDir\\$Dir" : "$RootDir" );
    # add root and cur dir - avoid trailing '\' if
    # $Dir is null

    @SubKeys = map  { $_->[1] }
               sort { $a->[0] cmp $b->[0] }
               map  { [uc($_), $_] } @SubKeys;
    # sort sub-keys regardless of case, like regedt32,
    # but preserve case in display (like regedt32)
 
    for (@SubKeys) {
        $hlist->add("$OldDir\\$_", -text=>"$_");
        $hlist->indicator('create', "$OldDir\\$_", -text=>'+');
    }
}

sub make_list {
    # add to the list of "Search from roots"
    my $File = shift;
    $List->insert('end',$File);
}

sub do_search {
    # set up the search results window, then cycle through
    # the search routine starting at each entry in the "search from roots" list.
    $to_find = $_[0];
    $sr_num++;
    my @to_search = @_[1..$#_];
    $SrchRes[$sr_num] = $mw->Toplevel(-title=>"$sr_num: Search Results for '$to_find'",
                                -height=>10, -width=>50);

    my $menubar = $SrchRes[$sr_num]->Frame(-relief  => "raised",
                                    -borderwidth => 2
                                  )->pack (-anchor => "nw", -fill => "x");

    my $i = $sr_num; # create a closure so that each window has its index
                     # for file_save and destroy_window_SrchRes functions

    my $file_menu = $menubar->Menubutton(-text  => "File",
                                     -underline => 1,
                                     -menuitems => [
                    [ Button => "Save",-command => sub { file_save($i) }  ] ,
                                     -underline => 1,
      [ Button => "Close This Window", -command => sub { destroy_window_SrchRes($i) } ] ,
                                     -underline => 1,
      [ Button => "EXIT REG ENTIRELY", -command => \&file_exit ] 
                                                   ] 
                              )->pack(-side => "left");

    my $srch_info = "searching for '$to_find' in\n" 
                     . join("\n", @to_search) 
                     . "\non " .  (scalar localtime(time)) 
                     . ( $main::related_values
                         ? " with related values"
                         : ' WithOUT related values')
                     . "\n";
    
    my $label3 = $SrchRes[$sr_num]->Label(-text=>$srch_info);
    $label3->pack;
    
    $Txt[$sr_num] = $SrchRes[$sr_num]->Scrolled('Text',-width=>120, -height=>30, 
                              -scrollbars=>'ose', -wrap=>'word');
    $Txt[$sr_num]->pack(-fill=>'both', -expand=>1);

    $out_txt = '';
    $srch_info =~ s/\n/   /g;
    $srch_info .= "\n\n";
    out_add($srch_info);

    for (@to_search) {
        actual_search($to_find, $_, $Txt[$sr_num]);
    }
    out_put($sr_num);
}

sub actual_search {
    my ($to_find, $Dir, $Txt ) = @_;
    my $OldDir = $Dir;
    my ( $thing, $RootDir, $DoReg, $NewObj, @SubKeys, %data, $NewDir, 
         $Val, $ThisKey, $Tmp, $ShouldUse, $allval);

    $Dir =~ s/(^\\?HKEY[_A-Z]*)\\?//;
    # remove HKEY_WHATEVER\
    $RootDir = $1;
    # save HKEY_WHATEVER
    {
        no strict 'refs';
        if ( ${$RootDir}->Open($Dir, $NewObj) ) {
            $NewObj->GetKeys(\@SubKeys); 
            $NewObj->GetValues(\%data);
        }
        # only retrieve keys if return code from
        # from ->Open of key is true.  Proceed w/ rest of subroutine
        # so that we find out if the key matches.
    }
    
    @SubKeys = sort @SubKeys;
    
    $Val = "";     # all values which match the search regex.
    $allval = "";  # all values (used for 'related values'.
    # each value is actually a list of [name, type, value]
    foreach $thing ( sort keys %data){
        $Tmp = "";
        $ShouldUse = 0;
        my $val_type;
        for (0..2) {
            my $Xx = ${$data{$thing}}[$_];
 
            if ( /0/ ) { 
                if ( not $Xx )  { $Tmp .= "(Default) ";}
                else            { $Tmp .= "$Xx "; }
            }
            # if the name (1st element) is null, use "(Default)" ala regedt

            elsif ( /1/ ) {
                $val_type = $Xx;
                $Tmp .=  ( exists $reg_type{$val_type}
                           ? "$reg_type{$val_type} "
                           : "REG_UNKNOWN:($val_type) ");
            }

            elsif ( /2/ ) {
                if ( $Xx =~ /^ +$/ ) {
                    $Tmp .= '"' . $Xx . '" ' ;
                    # if the element is all space, wrap quotes around it
                }

                else {
                    if ( $val_type == 1 or $val_type == 2 ) { 
                        $Xx =~ s/\0/ /g;
                        $Tmp .=  "$Xx";
                    }    # REG_SZ or REG_EXPAND_SZ

                    elsif ( $val_type == 3 ) { 
                        $Tmp .= 
                        join " ",
                        map { sprintf("%02x" , $_) }
                        unpack("C*", $Xx);
                    }    # REG_BINARY; 

                    elsif ( $val_type == 4 ) {
                        $Tmp .= sprintf("0x%08x (%d)", $Xx, $Xx);
                    }    # REG_DWORD

                    elsif ( $val_type == 7 ) {
                        $Xx =~ s/\0$//;
                        $Xx =~ s/\0/"\n   "/eg; 
                        $Tmp .= $Xx;
                    }   # REG_MULTI_SZ - list indented

                    elsif ( $val_type == 0 ) {
                    # REG_NONE - no action.
                    }

                    else {
                        $Tmp .= 
                        join " ",
                        map { sprintf("%02x" , $_) }
                        unpack("C*", $Xx);
                    }
                }
            }
        }

        if  ( $Tmp =~ /$to_find/i )  {
            $Val .= "$Tmp\n";
        }

        # save for if 'show related values' on
        if ( $main::related_values ) { 
            $allval .= "$Tmp\n";
        }
    }


    if ( $Val ) {
        if ($main::related_values) {
            out_add("$OldDir:\n$allval\n");
        }
        else {
            out_add("$OldDir:\n$Val\n");
        }
        $main::ThisSeen{$OldDir}++;
    }
    # if any of the values contained the regex, print the key
    # and the values that matched.  If 'show related values' checked,
    # print all the siblings too. 
    # also, remember the key, so that if it also matches,
    # we won't print it again later.

    foreach (@SubKeys) {
        $NewDir = $OldDir . "\\$_";
        # construct the key for the next recursion, 

        actual_search($to_find, $NewDir, $Txt);
        # follow it down another level
    }

    if (  $OldDir =~ /$to_find/i 
          and (not defined $main::ThisSeen{$OldDir}) ) {
        if ( $main::related_values ) {
            out_add("$OldDir:\n$allval\n");
        }

        else {
            out_add("$OldDir:\n\n");
        }
    # if the key matches the regex, and if we haven't already
    # seen it while printing values, print it.  If we're
    # printing 'related values' do so.
    }
    $NewObj->Close;
}

sub file_save {
    my $sr_num = $_[0];
    my $save_file = "";
    my $start_dir = cwd;
    my $open_mode= "";
    GET_FILE_NAME:
    while (1) {
        my $FSref = $SrchRes[$sr_num]->FileSelect(-directory => $start_dir);
        $save_file = $open_mode = '';
        $save_file = $FSref->Show;
      
        if ( (defined $save_file) 
             and $save_file 
             and not -f $save_file) {
             $open_mode = '>'; last GET_FILE_NAME;
        }
        # user chose a non-null filename which doesn't exist
        # get outta here and use it.

        last GET_FILE_NAME if ( not (defined $save_file) or !$save_file);
         # user cancelled out of SelectFile -
         # get outta here and do nothing


        # user chose file which already exists
        my $button = "";
        my $dialog = $SrchRes[$sr_num]->Dialog( -title => "$save_file exists!",
                            -text => 'The file you chose Already Exists.
Use the mouse (not the keyboard)
to choose an option.',
                            -default_button => 'Choose another',
                            -buttons =>[ "Overwrite", "Append", "Choose another", "Cancel" ] );
        $button = $dialog->Show();

        if ( $button eq "Overwrite") {
            $open_mode = '>';  last;
        }
        if ( $button eq "Append") {
            $open_mode = '>>';  last;
        }
        if ( $button eq "Cancel") {
            $save_file = $open_mode = '' ; last;
        }
    }
    
    if ( $open_mode and $save_file) {
        my $what = $Txt[$sr_num]->get('1.0', 'end');
        open OUTPUT, "$open_mode$save_file" or die "can't open $save_file: $!\n";
        print OUTPUT "$what\n";
        close OUTPUT;
    }
}

sub file_exit {
    exit();
}

sub destroy_window_SrchRes {
    my $sr_num = $_[0];
   $SrchRes[$sr_num]->destroy;
}


sub out_add {
    $out_txt .= $_[0];
}

sub out_put {
    my $sr_num = $_[0];
    my @lines = split("\n", $out_txt);
    my $line = 1;
    my $found = "found";
    my @hits = ();
    my $size;
    $Txt[$sr_num]->tagConfigure($found, -foreground => 'red');

    for (@lines) {
        $Txt[$sr_num]->insert('end', "$_\n");
        while ( m/($to_find)/ig ) {
            $size = length($1);;
            push @hits, [join(".", $line, pos($_) - $size), $size];
        }
        $line++;
    }

    for (@hits) {
        $Txt[$sr_num]->tagAdd($found, $_->[0], "$_->[0] + $_->[1] chars");
    }            
}                                                                       

sub init_HlpData {
my $HelpString = "CLICK INSIDE THIS WINDOW, THEN USE ARROW KEYS AND/OR PgUp/PgDn to navigate, 
or use the scrollbar. 

This is the help file for the 'reg' program.  

The reg program does a recursive search of the registry, starting at one or more 
points, looking for all keys or values (note that regedt32 only does keys) that match 
a string entered by the user.

The match is according to Perl's rules for 'regular expressions', 
so searches can be done for things that can't be expressed only as a literal string. 
For example, entering 'excel [45]' would match all keys and values containing 
'excel 4' or 'excel 5' but not 'excel 3' or excel 7'.  For more information 
on regular expressions, check your nearest Unix machine and/or geek. 


MAIN WINDOW:

The initial menu of Reg shows the toplevel Registry keys.  Each key or subkey 
in the window has a '+' or '-' sign, called an 'indicator', in front of it.  Single 
clicking the indicator of a key or subkey will open the next level of subkeys 
down ('drill down').  If the subkeys were already visible, 
they become hidden again. 


'SEARCH FROM ROOTS:' list:

When in the Main Window, double-clicking a key or subkey (or pressing enter while one is 
highlighted) copies it into the 'Search from roots: ' listbox below the main window. 
This is the list of startpoint(s) for recursive searches of the registry.  Multiple entries 
in this list causes the recursive search to be started from each in turn. 


'REMOVE' Button:

If you got something into the 'Search from roots:' list accidentally, highlight it 
then click the 'remove' button. 


'SEARCH REGEX' entry:

Once you've selected the search roots, you need only enter a string to search 
for and click the 'Search' button.  

The string for which to search is entered into the entry box labeled 'Search regex?'.  
This is called 
a regex because the string entered here will be used as a 'regular expresion' 
when matched against subkeys and values.  This means that a bewildering array 
of wildcards and other magical matching can be done.  If your only familiarity 
is with DOS wildcards, let me say that the regex equivalent of DOS's * is .* and 
the regex equivalent of DOS's ? is . which is a dot.  If you want to match a dot 
or an asterisk literally, precede the character with a backslash as in \\*.  This applies 
to most other punctuation characters as well. 

If you enter nothing into the 'Search Regex' entry field, the program will 
act as if you entered a period, which matches everything.


'SHOW RELATED VALUES' checkbox:

The search/match behavior is modified by the 'Show Related Values' check-box.  

If the 'show related values is not checked, the following happens:

When a value matches, the subkey containing that value will also be printed out, 
whether it matched or not.  If a subkey matches, it will be printed even if none of 
it's values matched. 

If the 'show related values is checked:

When a key matches, 
it *and* all it's values will be printed.  Additionally, when a value 
matches, all of its 'sibling' values (those under the same key) will 
be printed.


'SEARCH' button:

Pressing this starts the search.  The search is performed recursivley 
starting at each of the points in the 'Search From Roots' list.  
When the search is done the results will be shown in the 'Search Results Window'. 
Note that multiple searches can be performed - when the search button is 
clicked, it will start a new search based on the contents of the 
'search from roots' list and the 'search regex' entry, and put the results 
into a new numbered 'Search Results' window.


'SEARCH RESULTS' window:

The results of the search are shown here.  Text matched by the search regex will 
be highlighted in red.

The FILE menu of this window offers 3 options: 

SAVE, which allows the user to choose 
a file in whcih to save the contents of the window.  If th user chooses a file which 
already exists, s/he will be prompted as to whether to overwrite the file, append to 
the file, choose another file or cancel the save operation.  The contents of 
the 'search results' window may be editted.

CLOSE THIS WINDOW, which closes the 'Search Results' window from 
which it is invoked.

QUIT REG ENTIRELY which exits the reg program, closing all windows immeidately.


Notes:

All searching is done CASE INSENSITIVELY. (Making this an option may 
be a feature in the future.)

The 'Search Results' window won't appear until the search is done, in 
spite of much effort on my part.  I've also had no luck at making any other 
indicator of activity, so you'll just have to take it on faith that something 
is happening.  Be aware that searches from near the root of the tree can take several 
minutes. 

The strings matched by the regular expression will be highlighted in the 
search results window.  The results of this are sometimes surprising - 
it is quite possible to make a regex which will not match itself, while 
still matching other things.

You can copy and paste out of the search results window by highlighting 
text with the mouse (or by holding down the shift key while moving the cursor) then  
typing ctrl-C to copy the text to the clipboard, 
then going to other applications and pasting.

Other items to note: the program may complain about 'use of uninitialized variable 
in ...Registry.pm' when first run - this appears to be harmless.  It is due to
a bug in Registry.pm for which there is a patch. (The .exe version of this program 
should not exhibit this behaviour.)  It appears to be harmless.

If you decide that this software has a bug (translation: 'is cheesy garbage'), 
please email me at bobn\@interaccess.com with a complete rant, uh, bug report.  
Ditto on suggestions for added features.

I may or may not actually do anything about it, but you'll feel better.

- Bob Niederman, 28 Oct 1998

BUGS: (or, as some companies would have it, 'features'):

The highlighting of the matched strings in the Search Results window will 
sometimes start being on the wrong text part way through the window. 
This appears to be caused by non-printable characters existing in strings. 
Attempts have been made to fix this, remdying this on the machines I've 
tried this on, but that's not a very big sample.
";

$HelpString =~ s/([^\n])\n([^\n])/$1$2/gm; 
# remove newlines which ain't new paragraphs - let Tk::Text handle the wrap.

return $HelpString;
}

=head1 NAME

reg.pl - a script to aid in searching the Win32 Registry.

=head1 DESCRIPTION

The reg program does a recursive search of the registry, starting at one or more 
user-selected points, looking for all keys or values that match 
a string entered by the user.

The user interface is a GUI, through use of Tk.

The match is according to Perl's rules for 'regular expressions', 
allowing great versatility in how searches are done.

This application was created under the ActiveSate perl 5.00502.

For more information, run the script and click on 'HELP'.


=head1 Runs on the following OSs:

WinNT 4.0, Win95

=head1 Author

Bob Niederman, bobn@interaccess.com

=head1 Acknowledgements

I couldn't list them all, but I am particularly indebted to 
the authors of the Win32::Registry, the Tk modules as well as the 
FileSelect module.

=head1 Copyright

This is free software, redistributable under the same terms as Perl.

=cut

=head1 CPAN Scripts Miscellanea:

=head1 SCRIPT CATEGORIES

C<Win32/Utilities>

=head1 PREREQUISITES

This script requires the following modules: 
C<strict>
C<diagnostics>
C<Carp>
C<Cwd>
C<Win32::Registry>
C<Tk>
C<Tk::Label>
C<Tk::HList>
C<Tk::Text>
C<Tk::DialogBox>
C<Tk::FileSelect>
C<Tk::Menubutton>
C<Tk::Checkbutton>
C<Tk::Frame>

=head1 OSNAMES

C<WinNT>
C<Win95>

=head1 README

The reg program does a recursive search of the registry, starting at one or more 
user-selected points, looking for all keys or values that match 
a string entered by the user.

The match is according to Perl's rules for 'regular expressions', 
allowing great versatility in how searches are done.

The user interface is a GUI, through use of Tk.

This application was created under the ActiveSate perl 5.00502.

For more information, run the script and click on 'HELP'.

=cut
__END__
