#!/usr/bin/perl use strict; use warnings; binmode STDOUT, ':utf8'; use Parse::Win32Registry 0.51; package EntryTreeCtrl; use Parse::Win32Registry qw(make_multiple_subkey_iterator make_multiple_value_iterator compare_multiple_keys compare_multiple_values); use Wx qw(:everything); use Wx::ArtProvider qw(:artid :clientid); use Wx::Event qw(:everything); use base qw(Wx::TreeCtrl); sub new { my ($class, $parent) = @_; my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize, wxTR_DEFAULT_STYLE|wxBORDER_SUNKEN); bless $self, $class; EVT_TREE_ITEM_EXPANDING($self, $self, \&OnTreeItemExpanding); my $imagelist = Wx::ImageList->new(16, 16, 1); $imagelist->Add(Wx::ArtProvider::GetIcon(wxART_FOLDER, wxART_MENU, [16, 16])); $imagelist->Add(Wx::ArtProvider::GetIcon(wxART_NORMAL_FILE, wxART_MENU, [16, 16])); $self->AssignImageList($imagelist); return $self; } sub Clear { my ($self) = @_; $self->DeleteAllItems; } sub SetRootKeys { my ($self, $root_keys) = @_; return if !defined $root_keys || @$root_keys == 0; my $any_root_key = (grep { defined } @$root_keys)[0]; my $name = $any_root_key->get_name; $name =~ s/\0/[NUL]/g; $name =~ s/\n/[LF]/g; $name =~ s/\r/[CR]/g; my @changes = compare_multiple_keys(@$root_keys); my $num_changes = grep { $_ } @changes; $name .= " ($num_changes)" if $num_changes > 0; my $root_item = $self->AddRoot($name, 0, -1); $self->SetItemBold($root_item, $num_changes); # bold if $num_changes > 0 $self->SetPlData($root_item, [\@changes, $root_keys]); $self->AddChildren($root_item, $root_keys); } sub AddChildren { my ($self, $item, $keys) = @_; my $any_key = (grep { defined } @$keys)[0]; my $subkey_count = 0; my $subkeys_iter = make_multiple_subkey_iterator(@$keys); while (defined(my $subkeys = $subkeys_iter->get_next)) { my $any_subkey = (grep { defined } @$subkeys)[0]; my $name = $any_subkey->get_name; $name =~ s/\0/[NUL]/g; $name =~ s/\n/[LF]/g; $name =~ s/\r/[CR]/g; my @changes = compare_multiple_keys(@$subkeys); my $num_changes = grep { $_ } @changes; $name .= " ($num_changes)" if $num_changes > 0; my $child_item = $self->AppendItem($item, $name, 0, -1); $self->SetPlData($child_item, [\@changes, $subkeys]); $self->SetItemBold($child_item, $num_changes); $self->SetItemHasChildren($child_item, 1); $subkey_count++; } my $value_count = 0; my $values_iter = make_multiple_value_iterator(@$keys); while (defined(my $values = $values_iter->get_next)) { my $any_value = (grep { defined } @$values)[0]; my $name = $any_value->get_name; $name = "(Default)" if $name eq ''; $name =~ s/\0/[NUL]/g; $name =~ s/\n/[LF]/g; $name =~ s/\r/[CR]/g; my @changes = compare_multiple_values(@$values); my $num_changes = grep { $_ } @changes; $name .= " ($num_changes)" if $num_changes > 0; my $child_item = $self->AppendItem($item, $name, 1, -1); $self->SetPlData($child_item, [\@changes, $keys, $values]); $self->SetItemBold($child_item, $num_changes); $value_count++; } return $subkey_count + $value_count; } sub OnTreeItemExpanding { my ($self, $event) = @_; my $item = $event->GetItem; my ($child_item, $cookie) = $self->GetFirstChild($item); if ($child_item->IsOk) { return; } my ($changes, $keys) = @{$self->GetPlData($item)}; if (!$self->AddChildren($item, $keys)) { $self->SetItemHasChildren($item, 0); } } sub FindMatchingKey { my ($self, $item, $key_name) = @_; return if !$self->ItemHasChildren($item); # Make any virtual children real before proceeding my ($child_item, $cookie) = $self->GetFirstChild($item); if (!$child_item->IsOk) { # children still virtual my $data = $self->GetPlData($item); my ($changes, $keys, $values) = @$data; if (!$self->AddChildren($item, $keys)) { $self->SetItemHasChildren($item, 0); } } # Look through the children for a match ($child_item, $cookie) = $self->GetFirstChild($item); while ($child_item->IsOk) { my $data = $self->GetPlData($child_item); my ($changes, $keys, $values) = @$data; if (!defined $values) { # only keys my $any_key = (grep { defined } @$keys)[0]; if ($key_name eq $any_key->get_name) { return $child_item; # found a match } } ($child_item, $cookie) = $self->GetNextChild($item, $cookie); } return; # no match } sub FindMatchingValue { my ($self, $item, $value_name) = @_; return if !$self->ItemHasChildren($item); # Make any virtual children real before proceeding my ($child_item, $cookie) = $self->GetFirstChild($item); if (!$child_item->IsOk) { # children still virtual my $data = $self->GetPlData($item); my ($changes, $keys, $values) = @$data; if (!$self->AddChildren($item, $keys)) { $self->SetItemHasChildren($item, 0); } } # Look through the children for a match ($child_item, $cookie) = $self->GetFirstChild($item); while ($child_item->IsOk) { my $data = $self->GetPlData($child_item); my ($changes, $keys, $values) = @$data; if (defined $values) { # only values my $any_value = (grep { defined } @$values)[0]; if ($value_name eq $any_value->get_name) { return $child_item; # found a match } } ($child_item, $cookie) = $self->GetNextChild($item, $cookie); } return; # no match } sub GoToEntry { my ($self, $subkey_path, $value_name) = @_; my $item = $self->GetRootItem; if (defined $subkey_path) { my @key_names = split(/\\/, $subkey_path); while (@key_names) { my $key_name = shift @key_names; $item = $self->FindMatchingKey($item, $key_name); if (!defined $item) { return; # no match found } } } if (defined $value_name) { $item = $self->FindMatchingValue($item, $value_name); if (!defined $item) { return; # no match found } } $self->EnsureVisible($item); $self->SelectItem($item); } sub GetSelectedEntry { my ($self) = @_; my $item = $self->GetSelection; if ($item->IsOk) { my $data = $self->GetPlData($item); my ($changes, $keys, $values) = @$data; return ($changes, $keys, $values); } return; } package EntryListCtrl; use Wx qw(:everything); use base qw(Wx::ListCtrl); # Colors used for highlighting changes: use constant COLOR_ADDED => Wx::Colour->new('#b0ffb0'); # green use constant COLOR_CHANGED => Wx::Colour->new('#ffffb0'); # yellow use constant COLOR_DELETED => Wx::Colour->new('#ffb0b0'); # red sub new { my ($class, $parent) = @_; my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize, wxLC_REPORT|wxLC_SINGLE_SEL|wxBORDER_SUNKEN); bless $self, $class; $self->InsertColumn(0, '', wxLIST_FORMAT_LEFT); $self->InsertColumn(1, 'Change', wxLIST_FORMAT_LEFT); $self->InsertColumn(2, 'Timestamp/Type', wxLIST_FORMAT_LEFT); $self->InsertColumn(3, 'Class/Data', wxLIST_FORMAT_LEFT); $self->SetColumnWidth(0, 40); $self->SetColumnWidth(1, 100); $self->SetColumnWidth(2, 200); $self->SetColumnWidth(3, 200); return $self; } sub SetEntries { my ($self, $changes, $keys, $values) = @_; if (defined $values) { # change first column title my $column = $self->GetColumn(2); $column->SetText('Type'); $column->SetWidth($column->GetWidth); $column->SetImage(-1); $self->SetColumn(2, $column); # change second column title $column = $self->GetColumn(3); $column->SetText('Data'); $column->SetWidth($self->GetColumnWidth(3)); $column->SetImage(-1); $self->SetColumn(3, $column); } else { # change first column title my $column = $self->GetColumn(2); $column->SetText('Timestamp'); $column->SetWidth($column->GetWidth); $column->SetImage(-1); $self->SetColumn(2, $column); # change second column title $column = $self->GetColumn(3); $column->SetText('Class Name'); $column->SetWidth($self->GetColumnWidth(3)); $column->SetImage(-1); $self->SetColumn(3, $column); } $self->DeleteAllItems; my $index = 0; for (my $i = 0; $i < @$changes; $i++) { my $change = $changes->[$i]; my $key = $keys->[$i]; my $column1 = ''; my $column2 = ''; if (defined $values) { my $value = $values->[$i]; if (defined $value) { $column1 = $value->get_type_as_string; $column2 = substr($value->get_data_as_string, 0, 200); } } else { if (defined $key) { if (defined $key->get_timestamp) { $column1 = $key->get_timestamp_as_string; } if (defined $key->get_class_name) { $column2 = $key->get_class_name; } } } # Only the data or the class name needs checking $column2 =~ s/\0/[NUL]/g; $column2 =~ s/\n/[LF]/g; $column2 =~ s/\r/[CR]/g; $index = $self->InsertStringItem($index+1, "[$i]"); $self->SetItem($index, 1, $change); $self->SetItem($index, 2, $column1); $self->SetItem($index, 3, $column2); # Color item if ($change eq 'NEWER' || $change eq 'ADDED') { $self->SetItemBackgroundColour($index, COLOR_ADDED); } elsif ($change eq 'CHANGED') { $self->SetItemBackgroundColour($index, COLOR_CHANGED); } elsif ($change eq 'OLDER' || $change eq 'DELETED') { $self->SetItemBackgroundColour($index, COLOR_DELETED); } } $self->{_changes} = $changes; $self->{_keys} = $keys; $self->{_values} = $values; } sub GetEntry { my ($self, $index) = @_; my $change = $self->{_changes}[$index]; my $key = $self->{_keys}[$index]; if (defined $self->{_values}) { my $value = $self->{_values}[$index]; if (defined $value) { return ($change, $key, $value); } else { # No $key is returned when there is no $value to make # the recipient realise that this is a list of $value changes. # This does require that the recipient anticipates receiving # neither a $key nor a $value. return ($change); } } else { return ($change, $key); } } package CompareFrame; use File::Basename; use FindBin; use Parse::Win32Registry qw(make_multiple_subtree_iterator compare_multiple_keys compare_multiple_values hexdump); use Wx qw(:everything); use Wx::DND; # required for copying to clipboard use Wx::Event qw(:everything); use base qw(Wx::Frame); use constant ID_DUMP_ENTRIES => Wx::NewId; use constant ID_FIND_NEXT => Wx::NewId; use constant ID_FIND_CHANGE => Wx::NewId; use constant ID_FIND_NEXT_CHANGE => Wx::NewId; use constant ID_SELECT_FONT => Wx::NewId; sub new { my ($class, $parent) = @_; my $self = $class->SUPER::new($parent, -1, "Registry Compare", wxDefaultPosition, [600, 400]); bless $self, $class; $self->SetMinSize([600, 400]); my $menu1 = Wx::Menu->new; $menu1->Append(wxID_OPEN, "&Select Files...\tCtrl+O"); $menu1->Append(wxID_CLOSE, "&Close Files\tCtrl+W"); $menu1->AppendSeparator; $menu1->Append(wxID_EXIT, "E&xit\tAlt+F4"); my $menu2 = Wx::Menu->new; $menu2->Append(wxID_COPY, "&Copy Path\tCtrl+C"); my $menu3 = Wx::Menu->new; $menu3->Append(wxID_FIND, "&Find...\tCtrl+F"); $menu3->Append(ID_FIND_NEXT, "Find &Next...\tF3"); $menu3->AppendSeparator; $menu3->Append(wxID_REPLACE, "Find &Change...\tCtrl+N"); $menu3->Append(ID_FIND_NEXT_CHANGE, "Find N&ext Change...\tF4"); my $menu4 = Wx::Menu->new; $menu4->Append(ID_SELECT_FONT, "Select &Font..."); my $menu5 = Wx::Menu->new; $menu5->Append(wxID_ABOUT, "&About..."); my $menubar = Wx::MenuBar->new; $menubar->Append($menu1, "&File"); $menubar->Append($menu2, "&Edit"); $menubar->Append($menu3, "&Search"); $menubar->Append($menu4, "&View"); $menubar->Append($menu5, "&Help"); $self->SetMenuBar($menubar); my $statusbar = Wx::StatusBar->new($self, -1); $self->SetStatusBar($statusbar); EVT_MENU($self, wxID_OPEN, \&OnOpenFiles); EVT_MENU($self, wxID_CLOSE, \&OnCloseFiles); EVT_MENU($self, wxID_EXIT, \&OnQuit); EVT_MENU($self, wxID_COPY, \&OnCopy); EVT_MENU($self, wxID_FIND, \&OnFind); EVT_MENU($self, ID_FIND_NEXT, \&FindNext); EVT_MENU($self, wxID_REPLACE, \&OnFindChange); EVT_MENU($self, ID_FIND_NEXT_CHANGE, \&FindNextChange); EVT_MENU($self, ID_SELECT_FONT, \&OnSelectFont); EVT_MENU($self, wxID_ABOUT, \&OnAbout); my $hsplitter = Wx::SplitterWindow->new($self, -1, wxDefaultPosition, wxDefaultSize, wxSP_NOBORDER); my $tree = EntryTreeCtrl->new($hsplitter); my $vsplitter = Wx::SplitterWindow->new($hsplitter, -1, wxDefaultPosition, wxDefaultSize, wxSP_NOBORDER); $hsplitter->SplitVertically($tree, $vsplitter); $hsplitter->SetMinimumPaneSize(10); my $list = EntryListCtrl->new($vsplitter); my $text = Wx::TextCtrl->new($vsplitter, -1, '', wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE|wxTE_DONTWRAP|wxTE_READONLY); $text->SetFont(Wx::Font->new(10, wxMODERN, wxNORMAL, wxNORMAL)); $vsplitter->SplitHorizontally($list, $text); $vsplitter->SetMinimumPaneSize(10); $self->{_tree} = $tree; $self->{_list} = $list; $self->{_text} = $text; $self->{_statusbar} = $statusbar; EVT_SPLITTER_DCLICK($self, $hsplitter, \&OnSplitterDClick); EVT_SPLITTER_DCLICK($self, $vsplitter, \&OnSplitterDClick); EVT_TREE_SEL_CHANGED($self, $tree, \&OnEntryTreeSelChanged); EVT_LIST_ITEM_SELECTED($self, $list, \&OnEntryListItemSelected); $self->SetIcon(Wx::GetWxPerlIcon()); my $accelerators = Wx::AcceleratorTable->new( [wxACCEL_CTRL, ord('Q'), wxID_EXIT], ); $self->SetAcceleratorTable($accelerators); if (@ARGV) { $self->LoadFiles(@ARGV); } else { $self->{_registries} = []; } return $self; } sub OnSelectFont { my ($self, $event) = @_; my $text = $self->{_text}; my $font = $text->GetFont; $font = Wx::GetFontFromUser($self, $font); if ($font->IsOk) { $text->SetFont($font); } } sub OnCopy { my ($self, $event) = @_; my ($changes, $keys, $values) = $self->{_tree}->GetSelectedEntry; my $clip = ''; if (defined $keys) { my $any_key = (grep { defined } @$keys)[0]; if (defined $values) { my $any_value = (grep { defined } @$values)[0]; $clip = $any_key->get_path . ", " . $any_value->get_name; } else { $clip = $any_key->get_path; } } wxTheClipboard->Open; wxTheClipboard->SetData(Wx::TextDataObject->new($clip)); wxTheClipboard->Close; } sub OnSplitterDClick { my ($self, $event) = @_; $event->Veto; } sub OnEntryTreeSelChanged { my ($self, $event) = @_; my $item = $event->GetItem; my ($changes, $keys, $values) = @{$self->{_tree}->GetPlData($item)}; my $any_key = (grep { defined } @$keys)[0]; my $key_path = $any_key->get_path; $key_path =~ s/\0/[NUL]/g; $key_path =~ s/\n/[LF]/g; $key_path =~ s/\r/[CR]/g; # find currently selected item in entry list $item = $self->{_list}->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); $item = 0 if $item == -1; if (defined $values) { my $any_value = (grep { defined } @$values)[0]; my $name = $any_value->get_name; $name = "(Default)" if $name eq ''; $name =~ s/\0/[NUL]/g; $name =~ s/\n/[LF]/g; $name =~ s/\r/[CR]/g; $self->{_list}->SetEntries($changes, $keys, $values); $self->{_statusbar}->SetStatusText("$key_path, $name"); } else { $self->{_list}->SetEntries($changes, $keys); $self->{_statusbar}->SetStatusText($key_path); } $self->{_text}->ChangeValue(''); $self->{_list}->SetItemState($item, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED); } sub OnEntryListItemSelected { my ($self, $event) = @_; my ($change, $key, $value) = $self->{_list}->GetEntry($event->GetIndex); my $details = ''; if (defined $value) { $details = hexdump($value->get_raw_data); } elsif (defined $key) { if (defined $key->get_timestamp) { $details .= "Timestamp: " . $key->get_timestamp_as_string . "\n"; } my $class_name = $key->get_class_name; if (defined $class_name) { $class_name =~ s/\0/[NUL]/g; $class_name =~ s/\n/[NL]/g; $class_name =~ s/\r/[CR]/g; $details .= "Class Name: $class_name\n"; } my $security = $key->get_security; if (defined $security) { my $sd = $security->get_security_descriptor; $details .= $sd->as_stanza; } } $self->{_text}->ChangeValue($details); } sub OnAbout { my ($self, $event) = @_; my $info = Wx::AboutDialogInfo->new; $info->SetName($FindBin::Script); $info->SetVersion($Parse::Win32Registry::VERSION); $info->SetCopyright("Copyright (c) 2010-2012 James Macfarlane"); $info->SetDescription("wxWidgets Registry Compare for the Parse::Win32Registry module"); Wx::AboutBox($info); } sub FindNext { my ($self) = @_; my $find_param = $self->{_find_param}; my $find_iter = $self->{_find_iter}; my $search_keys = $self->{_search_keys}; my $search_values = $self->{_search_values}; return if !defined $find_param || $find_param eq ''; return if !defined $find_iter; my $start = time; my $max = 0; my $progress_dialog; my $iter_finished = 1; while (my ($keys, $values) = $find_iter->get_next) { my $any_key = (grep { defined } @$keys)[0]; my $key_name = $any_key->get_name; my $key_path = $any_key->get_path; # strip root key name from path to get subkey path my $subkey_path = (split(/\\/, $key_path, 2))[1]; if (defined $values) { if ($search_values) { # check value for match my $any_value = (grep { defined } @$values)[0]; my $value_name = $any_value->get_name; if (index(lc $value_name, lc $find_param) >= 0) { $self->{_tree}->GoToEntry($subkey_path, $value_name); $iter_finished = 0; last; } } } elsif ($search_keys) { # check key for match if (index(lc $key_name, lc $find_param) >= 0) { $self->{_tree}->GoToEntry($subkey_path); $iter_finished = 0; last; } } if (defined $progress_dialog) { if (!$progress_dialog->Update) { # Cancelled! $iter_finished = 0; last; } } else { # display progress dialog if search is slow if (time - $start >= 1) { $progress_dialog = Wx::ProgressDialog->new('Find', 'Searching registry...', $max, $self, wxPD_CAN_ABORT|wxPD_AUTO_HIDE); } } } if (defined $progress_dialog) { $progress_dialog->Destroy; } if ($iter_finished) { my $dialog = Wx::MessageDialog->new($self, 'Finished searching', 'Find', wxICON_EXCLAMATION|wxOK); $dialog->ShowModal; $dialog->Destroy; } $self->{_tree}->SetFocus; $self->SetFocus; } sub OnFind { my ($self, $event) = @_; my $root_keys = $self->{_root_keys}; return if !defined $root_keys || @$root_keys == 0; my $dialog = FindDialog->new($self); $dialog->SetText($self->{_find_param}); $dialog->SetSearchKeys($self->{_search_keys}); $dialog->SetSearchValues($self->{_search_values}); $dialog->SetSearchSelected($self->{_search_selected}); if ($dialog->ShowModal == wxID_OK) { $self->{_find_param} = $dialog->GetText; $self->{_search_keys} = $dialog->GetSearchKeys; $self->{_search_values} = $dialog->GetSearchValues; if (!$self->{_search_keys} && !$self->{_search_values}) { $self->{_search_keys} = $self->{_search_values} = 1; } my ($changes, $keys, $values) = $self->{_tree}->GetSelectedEntry; my $search_selected = $self->{_search_selected} = $dialog->GetSearchSelected; $self->{_find_iter} = $search_selected ? make_multiple_subtree_iterator(@$keys) : make_multiple_subtree_iterator(@$root_keys); $self->FindNext; } $dialog->Destroy; } sub FindNextChange { my ($self) = @_; my $change_iter = $self->{_change_iter}; my $search_keys = $self->{_search_keys}; my $search_values = $self->{_search_values}; return if !defined $change_iter; my $start = time; my $max = 0; my $progress_dialog; my $iter_finished = 1; while (my ($keys, $values) = $change_iter->get_next) { my $any_key = (grep { defined } @$keys)[0]; my $key_name = $any_key->get_name; my $key_path = $any_key->get_path; # strip root key name from path to get subkey path my $subkey_path = (split(/\\/, $key_path, 2))[1]; if (defined $values) { if ($search_values) { # check value for match my $any_value = (grep { defined } @$values)[0]; my $value_name = $any_value->get_name; my @changes = compare_multiple_values(@$values); my $num_changes = grep { $_ } @changes; if ($num_changes > 0) { $self->{_tree}->GoToEntry($subkey_path, $value_name); $iter_finished = 0; last; } } } elsif ($search_keys) { # check key for match my @changes = compare_multiple_keys(@$keys); my $num_changes = grep { $_ } @changes; if ($num_changes > 0) { $self->{_tree}->GoToEntry($subkey_path); $iter_finished = 0; last; } } if (defined $progress_dialog) { if (!$progress_dialog->Update) { # Cancelled! $iter_finished = 0; last; } } else { # display progress dialog if search is slow if (time - $start >= 1) { $progress_dialog = Wx::ProgressDialog->new('Find', 'Searching registry...', $max, $self, wxPD_CAN_ABORT|wxPD_AUTO_HIDE); } } } if (defined $progress_dialog) { $progress_dialog->Destroy; } if ($iter_finished) { my $dialog = Wx::MessageDialog->new($self, 'Finished searching', 'Find', wxICON_EXCLAMATION|wxOK); $dialog->ShowModal; $dialog->Destroy; } $self->{_tree}->SetFocus; $self->SetFocus; } sub OnFindChange { my ($self, $event) = @_; my $root_keys = $self->{_root_keys}; return if !defined $root_keys || @$root_keys == 0; my $dialog = FindChangeDialog->new($self); $dialog->SetSearchKeys($self->{_search_keys}); $dialog->SetSearchValues($self->{_search_values}); $dialog->SetSearchSelected($self->{_search_selected}); if ($dialog->ShowModal == wxID_OK) { $self->{_search_keys} = $dialog->GetSearchKeys; $self->{_search_values} = $dialog->GetSearchValues; if (!$self->{_search_keys} && !$self->{_search_values}) { $self->{_search_keys} = $self->{_search_values} = 1; } my ($changes, $keys, $values) = $self->{_tree}->GetSelectedEntry; my $search_selected = $self->{_search_selected} = $dialog->GetSearchSelected; $self->{_change_iter} = $search_selected ? make_multiple_subtree_iterator(@$keys) : make_multiple_subtree_iterator(@$root_keys); $self->{_change_iter}->get_next; # skip the starting key $self->FindNextChange; } $dialog->Destroy; } sub LoadFiles { my ($self, @filenames) = @_; my @registries; my @root_keys; foreach my $filename (@filenames) { if (!-r $filename) { my $dialog = Wx::MessageDialog->new($self, "'$filename' cannot be read", 'Error', wxICON_ERROR|wxOK); $dialog->ShowModal; $dialog->Destroy; next; } my $basename = basename($filename); my $registry = Parse::Win32Registry->new($filename); if (!defined $registry) { my $dialog = Wx::MessageDialog->new($self, "'$basename' is not a registry file", 'Error', wxICON_ERROR|wxOK); $dialog->ShowModal; $dialog->Destroy; next; } my $root_key = $registry->get_root_key; if (!defined $registry) { my $dialog = Wx::MessageDialog->new($self, "'$basename' has no root key", 'Error', wxICON_ERROR|wxOK); $dialog->ShowModal; $dialog->Destroy; next; } push @registries, $registry; push @root_keys, $root_key; } $self->LoadRegistries(\@registries); } sub LoadRegistries { my ($self, $registries) = @_; my @root_keys = map { $_->get_root_key } @$registries; $self->{_registries} = $registries; $self->{_tree}->SetRootKeys(\@root_keys); $self->{_tree}->SetFocus; $self->{_root_keys} = \@root_keys; if (@$registries) { my $filename = $registries->[0]->get_filename; $filename = basename($filename); if (@$registries > 1) { $filename .= " (+" . (@$registries - 1) . ")"; } $self->SetTitle("$filename - Registry Compare"); } else { $self->SetTitle("Registry Compare"); } } sub OnOpenFiles { my ($self, $event) = @_; my $dialog = $self->{_files_dialog}; if (!defined $dialog) { $dialog = $self->{_files_dialog} = FilesDialog->new($self); } # The original list of registries is not passed by reference # (as any changes would immediately affect the original). my @registries = @{$self->{_registries}}; $dialog->SetRegistries(\@registries); my $result = $dialog->ShowModal; $dialog->Hide; return if $result != wxID_OK; my $registries = $dialog->GetRegistries; # clear $self->OnCloseFiles; # set up $self->LoadRegistries($registries); } sub OnCloseFiles { my ($self, $event) = @_; $self->{_tree}->Clear; $self->{_list}->SetEntries([]); $self->{_text}->Clear; $self->{_statusbar}->SetStatusText(''); # $self->{_registries} is not cleared to retain currently selected files $self->{_root_keys} = undef; $self->{_find_iter} = undef; $self->{_changed_entries} = undef; $self->SetTitle("Registry Compare"); if (defined $self->{_change_list_dialog}) { $self->{_change_list_dialog}->SetChangedEntries([]); $self->{_change_list_dialog}->Hide; } } sub OnQuit { my ($self) = @_; $self->Close; } package FindDialog; use Wx qw(:everything); use Wx::Event qw(:everything); use base qw(Wx::Dialog); sub new { my ($class, $parent) = @_; my $self = $class->SUPER::new($parent, -1, "Find", wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE); bless $self, $class; my $static = Wx::StaticText->new($self, -1, 'Enter text to &search for:'); my $text = Wx::TextCtrl->new($self, -1, ''); my $check1 = Wx::CheckBox->new($self, -1, 'Search &keys'); my $check2 = Wx::CheckBox->new($self, -1, 'Search &values'); my $radio = Wx::RadioBox->new($self, -1, 'Start searching', wxDefaultPosition, wxDefaultSize, ['from root key', 'from current key'], 1); my $sizer = Wx::BoxSizer->new(wxVERTICAL); $sizer->Add($static, 0, wxEXPAND|wxALL, 5); $sizer->Add($text, 0, wxEXPAND|wxALL, 5); $sizer->Add($check1, 0, wxALL, 5); $sizer->Add($check2, 0, wxALL, 5); $sizer->Add($radio, 0, wxALL, 5); my $hsizer = Wx::BoxSizer->new(wxHORIZONTAL); my $button_sizer = $self->CreateSeparatedButtonSizer(wxOK|wxCANCEL); $sizer->Add($button_sizer, 0, wxEXPAND|wxALL, 5); $self->SetSizer($sizer); $self->{_text} = $text; $self->{_check1} = $check1; $self->{_check2} = $check2; $self->{_radio} = $radio; $self->Fit; # resize dialog to best fit child windows $self->{_text}->SetFocus; $self->SetFocus; EVT_CHECKBOX($self, $check1, sub { if (!$check1->GetValue && !$check2->GetValue) { $check2->SetValue(1); } }); EVT_CHECKBOX($self, $check2, sub { if (!$check1->GetValue && !$check2->GetValue) { $check1->SetValue(1); } }); return $self; } sub GetSearchKeys { my ($self) = @_; return $self->{_check1}->GetValue; } sub GetSearchValues { my ($self) = @_; return $self->{_check2}->GetValue; } sub GetText { my ($self) = @_; return $self->{_text}->GetValue; } sub GetSearchSelected { my ($self) = @_; return $self->{_radio}->GetSelection; } sub SetSearchKeys { my ($self, $state) = @_; $state = 1 if !defined $state; $self->{_check1}->SetValue($state); } sub SetSearchValues { my ($self, $state) = @_; $state = 1 if !defined $state; $self->{_check2}->SetValue($state); } sub SetText { my ($self, $value) = @_; $value = '' if !defined $value; $self->{_text}->ChangeValue($value); $self->{_text}->SetSelection(-1, -1); } sub SetSearchSelected { my ($self, $n) = @_; $n = 0 if !defined $n; $self->{_radio}->SetSelection($n); } package FindChangeDialog; use Wx qw(:everything); use Wx::Event qw(:everything); use base qw(Wx::Dialog); sub new { my ($class, $parent) = @_; my $self = $class->SUPER::new($parent, -1, "Find", wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE); bless $self, $class; my $static = Wx::StaticText->new($self, -1, 'Search for a change:'); my $check1 = Wx::CheckBox->new($self, -1, 'Search &keys'); my $check2 = Wx::CheckBox->new($self, -1, 'Search &values'); my $radio = Wx::RadioBox->new($self, -1, 'Start searching', wxDefaultPosition, wxDefaultSize, ['from root key', 'from current key'], 1); my $sizer = Wx::BoxSizer->new(wxVERTICAL); $sizer->Add($static, 0, wxEXPAND|wxALL, 5); $sizer->Add($check1, 0, wxALL, 5); $sizer->Add($check2, 0, wxALL, 5); $sizer->Add($radio, 0, wxALL, 5); my $hsizer = Wx::BoxSizer->new(wxHORIZONTAL); my $button_sizer = $self->CreateSeparatedButtonSizer(wxOK|wxCANCEL); $sizer->Add($button_sizer, 0, wxEXPAND|wxALL, 5); $self->SetSizer($sizer); $self->{_check1} = $check1; $self->{_check2} = $check2; $self->{_radio} = $radio; $self->Fit; # resize dialog to best fit child windows $self->{_radio}->SetFocus; $self->SetFocus; EVT_CHECKBOX($self, $check1, sub { if (!$check1->GetValue && !$check2->GetValue) { $check2->SetValue(1); } }); EVT_CHECKBOX($self, $check2, sub { if (!$check1->GetValue && !$check2->GetValue) { $check1->SetValue(1); } }); return $self; } sub GetSearchKeys { my ($self) = @_; return $self->{_check1}->GetValue; } sub GetSearchValues { my ($self) = @_; return $self->{_check2}->GetValue; } sub GetSearchSelected { my ($self) = @_; return $self->{_radio}->GetSelection; } sub SetSearchKeys { my ($self, $state) = @_; $state = 1 if !defined $state; $self->{_check1}->SetValue($state); } sub SetSearchValues { my ($self, $state) = @_; $state = 1 if !defined $state; $self->{_check2}->SetValue($state); } sub SetSearchSelected { my ($self, $n) = @_; $n = 0 if !defined $n; $self->{_radio}->SetSelection($n); } package FileListCtrl; use File::Basename; use Parse::Win32Registry qw(iso8601); use Wx qw(:everything); use Wx::ArtProvider qw(:artid :clientid); use Wx::Event qw(:everything); use base qw(Wx::ListCtrl); sub new { my ($class, $parent) = @_; my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize, wxLC_REPORT|wxLC_VIRTUAL|wxBORDER_SUNKEN); bless $self, $class; $self->InsertColumn(0, "Filename", wxLIST_FORMAT_LEFT); $self->InsertColumn(1, "Embedded Filename", wxLIST_FORMAT_LEFT); $self->InsertColumn(2, "Embedded Timestamp", wxLIST_FORMAT_LEFT); $self->InsertColumn(3, "Directory", wxLIST_FORMAT_LEFT); $self->SetColumnWidth(0, 200); $self->SetColumnWidth(1, 200); $self->SetColumnWidth(2, 200); $self->SetColumnWidth(3, 200); my $imagelist = Wx::ImageList->new(16, 16, 1); $imagelist->Add(Wx::ArtProvider::GetIcon(wxART_NORMAL_FILE, wxART_MENU, [16, 16])); $self->AssignImageList($imagelist, wxIMAGE_LIST_SMALL); $self->{_registries} = []; return $self; } sub MoveItems { my ($self, $source_items, $target_item) = @_; # build list of items to move my @items; foreach my $source_item (@$source_items) { push @items, @{$self->{_registries}}[$source_item]; } # delete originals foreach my $source_item (reverse @$source_items) { if ($source_item < $target_item) { $target_item--; } splice @{$self->{_registries}}, $source_item, 1; } # insert moved items if ($target_item == -1 || $target_item > @{$self->{_registries}}) { push @{$self->{_registries}}, @items; } else { splice @{$self->{_registries}}, $target_item, 0, @items; } $self->Refresh; # deselect my $item = -1; while (1) { $item = $self->GetNextItem($item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); last if $item == -1; $self->SetItemState($item, 0, wxLIST_STATE_SELECTED); } # reselect if ($target_item == -1 || $target_item > (@{$self->{_registries}} - @items)) { $target_item = @{$self->{_registries}} - @items; } for (my $item = 0; $item < @items; $item++) { $self->SetItemState($target_item + $item, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED); } } sub MoveSelectedItemsToTop { my ($self) = @_; # iterate selected items in list: my @items; my $item = -1; while (1) { $item = $self->GetNextItem($item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); last if $item == -1; push @items, $item; } return if @items == 0; $self->MoveItems(\@items, 0); $self->EnsureVisible(0); } sub MoveSelectedItemsToBottom { my ($self) = @_; # iterate selected items in list: my @items; my $item = -1; while (1) { $item = $self->GetNextItem($item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); last if $item == -1; push @items, $item; } return if @items == 0; $self->MoveItems(\@items, -1); $self->EnsureVisible(scalar @{$self->{_registries}} - 1); } sub MoveSelectedItemsUp { my ($self) = @_; # iterate selected items in list: my @items; my $item = -1; while (1) { $item = $self->GetNextItem($item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); last if $item == -1; push @items, $item; } return if @items == 0; my $target_item = $items[0] - 1; $target_item = 0 if $target_item < 0; $self->MoveItems(\@items, $target_item); $self->EnsureVisible($target_item); } sub MoveSelectedItemsDown { my ($self) = @_; # iterate selected items in list: my @items; my $item = -1; while (1) { $item = $self->GetNextItem($item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); last if $item == -1; push @items, $item; } return if @items == 0; my $target_item = $items[-1] + 2; $self->MoveItems(\@items, $target_item); $self->EnsureVisible($target_item - 1); } sub GetRegistries { my ($self) = @_; return $self->{_registries}; } sub SetRegistries { my ($self, $registries) = @_; $self->{_registries} = $registries; $self->SetItemCount(scalar @$registries); $self->Refresh; } sub AddRegistries { my ($self, $registries) = @_; push @{$self->{_registries}}, @$registries; $self->SetItemCount(scalar @{$self->{_registries}}); $self->Refresh; } sub RemoveSelectedRegistries { my ($self) = @_; # iterate items in list: my @items; my $item = -1; while (1) { $item = $self->GetNextItem($item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); last if $item == -1; push @items, $item; } # delete items foreach my $item (reverse @items) { splice @{$self->{_registries}}, $item, 1; } $self->SetItemCount(scalar @{$self->{_registries}}); $self->Refresh; # deselect $item = -1; while (1) { $item = $self->GetNextItem($item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); last if $item == -1; $self->SetItemState($item, 0, wxLIST_STATE_SELECTED); } } sub OnGetItemText { my ($self, $index, $column) = @_; my $registry = $self->{_registries}[$index]; if ($column == 0) { return basename $registry->get_filename; } elsif ($column == 1) { my $embedded_filename = $registry->get_embedded_filename; return defined $embedded_filename ? $embedded_filename : ''; } elsif ($column == 2) { my $embedded_timestamp = $registry->get_timestamp; return defined $embedded_timestamp ? iso8601($embedded_timestamp) : ''; } elsif ($column == 3) { return dirname $registry->get_filename; } else { return '?'; } } sub OnGetItemImage { return 0; } package FilesDialog; use File::Basename; use Wx qw(:everything); use Wx::Event qw(:everything); use Wx::ArtProvider qw(:artid :clientid); use base qw(Wx::Dialog); sub new { my ($class, $parent) = @_; my $self = $class->SUPER::new($parent, -1, "Select Registry Files", wxDefaultPosition, [600, 400], wxDEFAULT_FRAME_STYLE); bless $self, $class; $self->SetMinSize([600, 400]); my $static1 = Wx::StaticText->new($self, -1, 'Select files to compare:'); my $button1 = Wx::Button->new($self, wxID_CLEAR, 'Clear'); my $button2 = Wx::Button->new($self, wxID_ADD, 'Add'); my $button3 = Wx::Button->new($self, wxID_REMOVE, 'Remove'); my $button4 = Wx::Button->new($self, -1, 'Move &Up'); my $button5 = Wx::Button->new($self, -1, 'Move &Down'); my $button6 = Wx::Button->new($self, -1, 'Move To &Top'); my $button7 = Wx::Button->new($self, -1, 'Move To &Bottom'); my $list = FileListCtrl->new($self); my $hsizer1 = Wx::BoxSizer->new(wxHORIZONTAL); $hsizer1->Add($button1, 0, wxEXPAND|wxALL, 5); $hsizer1->Add($button2, 0, wxEXPAND|wxALL, 5); $hsizer1->Add($button3, 0, wxEXPAND|wxALL, 5); my $hsizer2 = Wx::BoxSizer->new(wxHORIZONTAL); $hsizer2->Add($button4, 0, wxEXPAND|wxALL, 5); $hsizer2->Add($button5, 0, wxEXPAND|wxALL, 5); $hsizer2->Add($button6, 0, wxEXPAND|wxALL, 5); $hsizer2->Add($button7, 0, wxEXPAND|wxALL, 5); my $sizer = Wx::BoxSizer->new(wxVERTICAL); $sizer->Add($static1, 0, wxEXPAND|wxALL, 5); $sizer->Add($hsizer1, 0, wxEXPAND); $sizer->Add($list, 1, wxEXPAND); $sizer->Add($hsizer2, 0, wxEXPAND); my $button_sizer = $self->CreateButtonSizer(wxOK|wxCANCEL); $sizer->Add($button_sizer, 0, wxEXPAND|wxALL, 5); EVT_CLOSE($self, \&OnClose); $self->SetSizer($sizer); $self->{_list} = $list; EVT_BUTTON($self, $button1, \&OnClear); EVT_BUTTON($self, $button2, \&OnAdd); EVT_BUTTON($self, $button3, \&OnRemove); EVT_BUTTON($self, $button4, sub { $list->MoveSelectedItemsUp; }); EVT_BUTTON($self, $button5, sub { $list->MoveSelectedItemsDown; }); EVT_BUTTON($self, $button6, sub { $list->MoveSelectedItemsToTop; }); EVT_BUTTON($self, $button7, sub { $list->MoveSelectedItemsToBottom; }); $self->SetFocus; return $self; } sub OnAdd { my ($self, $event) = @_; my $dialog = Wx::FileDialog->new($self, "Select Registry File(s)", $self->{_directory} || '', '', '*', wxFD_OPEN|wxFD_MULTIPLE); if ($dialog->ShowModal != wxID_OK) { return; } $self->{_directory} = $dialog->GetDirectory; my @registries = (); foreach my $filename ($dialog->GetPaths) { if (!-r $filename) { my $dialog = Wx::MessageDialog->new($self, "'$filename' cannot be read", 'Error', wxICON_ERROR|wxOK); $dialog->ShowModal; $dialog->Destroy; next; } my $basename = basename($filename); my $registry = Parse::Win32Registry->new($filename); if (!defined $registry) { my $dialog = Wx::MessageDialog->new($self, "'$basename' is not a registry file", 'Error', wxICON_ERROR|wxOK); $dialog->ShowModal; $dialog->Destroy; next; } my $root_key = $registry->get_root_key; if (!defined $registry) { my $dialog = Wx::MessageDialog->new($self, "'$basename' has no root key", 'Error', wxICON_ERROR|wxOK); $dialog->ShowModal; $dialog->Destroy; next; } push @registries, $registry; } $self->{_list}->AddRegistries(\@registries); } sub OnRemove { my ($self, $event) = @_; $self->{_list}->RemoveSelectedRegistries; } sub OnClear { my ($self) = @_; $self->{_list}->SetRegistries([]); } sub SetRegistries { my ($self, $registries) = @_; $registries = [] if !defined $registries; $self->{_list}->SetRegistries($registries); } sub OnClose { my ($self, $event) = @_; $self->EndModal(wxID_CANCEL); } sub GetRegistries { my ($self) = @_; return $self->{_list}->GetRegistries; } package CompareApp; use Wx qw(:everything); use base qw(Wx::App); sub OnInit { my $self = shift; my $frame = CompareFrame->new(undef); $frame->Show; return 1; } package main; my $app = CompareApp->new; $app->MainLoop;