unit IDE.Wizard.WizardFormRegistryHelper; { Inno Setup Copyright (C) 1997-2024 Jordan Russell Portions by Martijn Laan For conditions of distribution and use, see LICENSE.TXT. Helper to avoid duplicate code between WizardForm and RegistryDesignerForm } interface uses Forms, StdCtrls, ExtCtrls; type TPrivilegesRequired = (prAdmin, prLowest, prDynamic); TWizardFormRegistryHelper = class private FForm: TForm; FFileEdit: TEdit; FUninsDeleteKeyCheck, FUninsDeleteKeyIfEmptyCheck, FUninsDeleteValueCheck, FMinVerCheck: TCheckBox; FMinVerEdit: TEdit; FMinVerDocImage: TImage; FPrivilegesRequired: TPrivilegesRequired; procedure SetPrivilegesRequired(const Value: TPrivilegesRequired); procedure UpdateImages; procedure AfterMonitorDpiChanged(Sender: TObject; OldDPI: Integer; NewDPI: Integer); procedure FileButtonClick(Sender: TObject); procedure UninsDeleteKeyIfEmptyCheckClick(Sender: TObject); procedure MinVerCheckClick(Sender: TObject); procedure MinVerDocImageClick(Sender: TObject); public constructor Create(const Form: TForm; const FileEdit: TEdit; const FileButton: TButton; const UninsDeleteKeyCheck, UninsDeleteKeyIfEmptyCheck, UninsDeleteValueCheck, MinVerCheck: TCheckBox; const MinVerEdit: TEdit; const MinVerDocImage: TImage); procedure AddScript(var Registry: String; const AllowException: Boolean); property PrivilegesRequired: TPrivilegesRequired write SetPrivilegesRequired; end; implementation uses Windows, Classes, SysUtils, StrUtils, TypInfo, Graphics, UITypes, ComCtrls, BrowseFunc, IDE.MainForm, IDE.ImagesModule, IDE.HelperFunc, IDE.Messages, Shared.CommonFunc, IDE.HtmlHelpFunc; { TWizardFormRegistryHelper } procedure TWizardFormRegistryHelper.SetPrivilegesRequired( const Value: TPrivilegesRequired); begin FPrivilegesRequired := Value; end; procedure TWizardFormRegistryHelper.UpdateImages; function GetImage(const Button: TToolButton; const WH: Integer): TWICImage; begin Result := ImagesModule.LightToolBarImageCollection.GetSourceImage(Button.ImageIndex, WH, WH) end; begin { After a DPI change the button's Width and Height isn't yet updated, so calculate it ourselves } var WH := MulDiv(16, FForm.CurrentPPI, 96); FMinVerDocImage.Picture.Graphic:= GetImage(MainForm.HelpButton, WH); end; constructor TWizardFormRegistryHelper.Create(const Form: TForm; const FileEdit: TEdit; const FileButton: TButton; const UninsDeleteKeyCheck, UninsDeleteKeyIfEmptyCheck, UninsDeleteValueCheck, MinVerCheck: TCheckBox; const MinVerEdit: TEdit; const MinVerDocImage: TImage); begin FForm := Form; FFileEdit := FileEdit; FUninsDeleteKeyCheck := UninsDeleteKeyCheck; FUninsDeleteKeyIfEmptyCheck := UninsDeleteKeyIfEmptyCheck; FUninsDeleteValueCheck := UninsDeleteValueCheck; FMinVerCheck := MinVerCheck; FMinVerEdit := MinVerEdit; FMinVerDocImage := MinVerDocImage; FileButton.OnClick := FileButtonClick; UninsDeleteKeyIfEmptyCheck.OnClick := UninsDeleteKeyIfEmptyCheckClick; MinVerCheck.OnClick := MinVerCheckClick; MinVerCheck.OnClick(nil); MinVerDocImage.OnClick := MinVerDocImageClick; MinVerDocImage.Cursor := crHandPoint; TryEnableAutoCompleteFileSystem(FileEdit.Handle); Form.OnAfterMonitorDpiChanged := AfterMonitorDpiChanged; UpdateImages; end; procedure TWizardFormRegistryHelper.AfterMonitorDpiChanged(Sender: TObject; OldDPI: Integer; NewDPI: Integer); begin UpdateImages; end; procedure TWizardFormRegistryHelper.FileButtonClick(Sender: TObject); begin var FileName: String := FFileEdit.Text; if NewGetOpenFileName('', FileName, '', SWizardAppRegFilter, SWizardAppRegDefaultExt, FForm.Handle) then FFileEdit.Text := FileName; end; procedure TWizardFormRegistryHelper.UninsDeleteKeyIfEmptyCheckClick(Sender: TObject); begin FUninsDeleteKeyCheck.Enabled := FUninsDeleteKeyIfEmptyCheck.Checked; if not FUninsDeleteKeyCheck.Enabled then FUninsDeleteKeyCheck.Checked := False; end; procedure TWizardFormRegistryHelper.MinVerCheckClick(Sender: TObject); begin FMinVerEdit.Enabled := FMinVerCheck.Checked; FMinVerDocImage.Visible := FMinVerCheck.Checked; if FMinVerEdit.Enabled then FForm.ActiveControl := FMinVerEdit; end; procedure TWizardFormRegistryHelper.MinVerDocImageClick(Sender: TObject); begin if Assigned(HtmlHelp) then HtmlHelp(GetDesktopWindow, PChar(GetHelpFile), HH_DISPLAY_TOPIC, Cardinal(PChar('topic_winvernotes.htm'))); end; procedure TWizardFormRegistryHelper.AddScript(var Registry: String; const AllowException: Boolean); function NextLine(const Lines: TStrings; var LineIndex: Integer): String; begin Inc(LineIndex); if LineIndex < Lines.Count then Result := Lines[LineIndex] else Result := ''; { Official .reg files must end with a blank line so should never get here but we support ones without } end; function CutStrBeginEnd(S: String; CharCount: Integer): String; begin Result := Copy(S, CharCount + 1, S.Length - 2 * CharCount); end; function StrRootRename(S: String): String; type TStrings = (HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, HKEY_CLASSES_ROOT, HKEY_USERS, HKEY_CURRENT_CONFIG); begin var ARoot := TStrings(GetEnumValue(TypeInfo(TStrings), S)); case ARoot of HKEY_CURRENT_USER: Result := 'HKCU'; HKEY_LOCAL_MACHINE: Result := 'HKLM'; HKEY_CLASSES_ROOT: Result := 'HKCR'; HKEY_USERS: Result := 'HKU'; HKEY_CURRENT_CONFIG: Result := 'HKCC'; else raise Exception.CreateFmt('Unknown root %s', [S]); end; end; function UTF16LEHexStrToStr(HexStr: String): String; begin if HexStr.Length mod 4 <> 0 then HexStr := HexStr + '00'; { RegEdit does this as well on import } var UTF16LEBytes: TBytes; SetLength(UTF16LEBytes, HexStr.Length div 2); var i := 1; var idx := 0; while i <= HexStr.Length do begin UTF16LEBytes[idx] := StrToInt('$' + HexStr[i] + HexStr[i + 1]); i := i + 2; idx := idx + 1; end; Result := TEncoding.Unicode.GetString(UTF16LEBytes); end; type TValueType = (vtSz, vtSzAsList, vtExpandSz, vtMultiSz, vtBinary, vtDWord, vtDWordAsList, vtQWord, vtNone, vtDelete, vtUnsupported); function GetValueType(AStr: String): TValueType; { See https://en.wikipedia.org/wiki/Windows_Registry#.REG_files Value formats: (we don't support I/K/L and just ignore those) "Value A"="" "Value B"=hex: "Value C"=dword: "Value D"=hex(0): "Value E"=hex(1): "Value F"=hex(2): "Value G"=hex(3): ; equal to "Value B" "Value H"=hex(4): "Value I"=hex(5): "Value J"=hex(7): "Value K"=hex(8): "Value L"=hex(a): "Value M"=hex(b): Other notes from the article: To remove a key (and all subkeys, values and data), the key name must be preceded by a minus sign ("-") To remove a value (and its data), the values to be removed must have a minus sign ("-") after the equal sign ("=") The Default Value of a key can be edited by using "@" instead of "Value Name" Lines beginning with a semicolon are considered comments BTW: Missing from the article is a note about multiline lists, these use "\" to continue } begin if Pos('"', AStr) <> 0 then Result := vtSz //Value A else if (Pos('hex:', AStr) <> 0) or (Pos('hex(3):', AStr) <> 0) then Result := vtBinary //Value B or G else if Pos('dword:', AStr) <> 0 then Result := vtDWord //Value C else if Pos('hex(0):', AStr) <> 0 then Result := vtNone //Value D else if Pos('hex(1):', AStr) <> 0 then Result := vtSzAsList //Value E else if Pos('hex(2):', AStr) <> 0 then Result := vtExpandSz //Value F else if Pos('hex(4):', AStr) <> 0 then Result := vtDWordAsList //Value H else if Pos('hex(7):', AStr) <> 0 then Result := vtMultiSz //Value J else if Pos('hex(b):', AStr) <> 0 then Result := vtQWord //Value M else if AStr.StartsWith('-') then Result := vtDelete else Result := vtUnsupported; end; type TRegistryEntry = record Root, Subkey, ValueName, ValueData, ValueType: String; end; function RequiresAdminInstallMode(AEntry: TRegistryEntry): Boolean; begin Result := (AEntry.Root = 'HKLM') or (AEntry.Root = 'HKCC') or ((AEntry.Root = 'HKU') and SameText(AEntry.Subkey, '.Default')); end; function RequiresNotAdminInstallMode(AEntry: TRegistryEntry): Boolean; begin Result := (AEntry.Root = 'HKCU'); end; function TextCommon(AEntry: TRegistryEntry): String; begin Result := ''; if FMinVerCheck.Checked and (FMinVerEdit.Text <> '') then Result := Result + '; MinVersion: ' + FMinVerEdit.Text; if (FPrivilegesRequired <> prAdmin) and RequiresAdminInstallMode(AEntry) then Result := Result + '; Check: IsAdminInstallMode' else if (FPrivilegesRequired <> prLowest) and RequiresNotAdminInstallMode(AEntry) then Result := Result + '; Check: not IsAdminInstallMode'; end; function TextKeyEntry(AEntry: TRegistryEntry; ADeleteKey: Boolean): String; begin Result := 'Root: ' + AEntry.Root + '; Subkey: ' + AEntry.Subkey; if ADeleteKey then Result := Result + '; ValueType: none' + '; Flags: deletekey' else begin if FUninsDeleteKeyCheck.Checked then Result := Result + '; Flags: uninsdeletekey' else if FUninsDeleteKeyIfEmptyCheck.Checked then Result := Result + '; Flags: uninsdeletekeyifempty'; end; Result := Result + TextCommon(AEntry); end; function TextValueEntry(AEntry: TRegistryEntry; AValueType: TValueType): String; begin Result := 'Root: ' + AEntry.Root + '; Subkey: ' + AEntry.Subkey + '; ValueType: ' + AEntry.ValueType + '; ValueName: ' + AEntry.ValueName; if AValueType = vtDelete then Result := Result + '; Flags: deletevalue' else begin if AValueType <> vtNone then Result := Result + '; ValueData: ' + AEntry.ValueData; if FUninsDeleteValueCheck.Checked then Result := Result + '; Flags: uninsdeletevalue'; end; Result := Result + TextCommon(AEntry); end; function TextHeader: String; begin Result := ';Registry data from file ' + ExtractFileName(FFileEdit.Text); end; function TextBadHeader: String; begin Result := ';COULD NOT IMPORT ' + ExtractFileName(FFileEdit.Text); end; function TextFooter(const HadFilteredKeys, HadUnsupportedValueTypes: Boolean): String; begin Result := ';End of registry data from file ' + ExtractFileName(FFileEdit.Text); if HadFilteredKeys then Result := Result + SNewLine + ';SOME KEYS FILTERED DUE TO PRIVILEGESREQUIRED SETTINGS!'; if HadUnsupportedValueTypes then Result := Result + SNewLine + ';SOME VALUES WITH UNSUPPORTED TYPES SKIPPED!' end; begin if FFileEdit.Text = '' then Exit; var Lines := TStringList.Create; var OutLines := TStringList.Create; try Lines.LoadFromFile(FFileEdit.Text); { Official .reg files must have blank lines as second and last lines but we don't require that so we just check for the header on the first line } const Header = 'Windows Registry Editor Version 5.00'; { don't localize } if (Lines.Count = 0) or (Lines[0] <> Header) then begin if AllowException then raise Exception.Create('Invalid file format.') else begin Registry := Registry + TextBadHeader + SNewLine; Exit; end; end; var LineIndex := 1; var HadFilteredKeys := False; var HadUnsupportedValueTypes := False; while LineIndex <= Lines.Count-1 do begin var Line := Lines[LineIndex]; if (Length(Line) > 2) and (Line[1] = '[') and (Line[Line.Length] = ']') then begin { Got a new section, first handle the key } Line := CutStrBeginEnd(Line, 1); var DeleteKey := Line.StartsWith('-'); if DeleteKey then Delete(Line, 1, 1); var P := Pos('\', Line); var Entry: TRegistryEntry; Entry.Root := StrRootRename(Copy(Line, 1, P - 1)); Entry.Subkey := Copy(Line, P + 1, MaxInt); if Entry.Root = 'HKCR' then begin Entry.Root := 'HKA'; Entry.Subkey := 'Software\Classes\' + Entry.Subkey; end; Entry.Subkey := Entry.Subkey.Replace('\WOW6432Node', '') .Replace('{', '{{') .QuotedString('"'); var FilterKey := ((FPrivilegesRequired = prAdmin) and RequiresNotAdminInstallMode(Entry)) or ((FPrivilegesRequired = prLowest) and RequiresAdminInstallMode(Entry)); if not FilterKey then OutLines.Add(TextKeyEntry(Entry, DeleteKey)) else HadFilteredKeys := True; { Key done, handle values } Line := NextLine(Lines, LineIndex); while Line <> '' do begin if not FilterKey and not DeleteKey and (Line[1] <> ';') then begin P := Pos('=', Line); if (P = 2) and (Line[1] = '@') then Entry.ValueName := '""' else begin Entry.ValueName := CutStrBeginEnd(Copy(Line, 1, P - 1), 1); Entry.ValueName := Entry.ValueName.Replace('\\', '\') .Replace('{', '{{') .QuotedString('"'); end; var ValueTypeAndData := Copy(Line, P + 1, MaxInt); var ValueType := GetValueType(ValueTypeAndData); case ValueType of vtSz: begin Entry.ValueData := CutStrBeginEnd(ValueTypeAndData, 1); Entry.ValueData := Entry.ValueData.Replace('\\', '\') .Replace('{', '{{') .QuotedString('"'); Entry.ValueType := 'string'; end; vtSzAsList, vtExpandSz, vtMultiSz, vtBinary: begin P := Pos(':', ValueTypeAndData); var ValueData := Copy(ValueTypeAndData, P + 1, MaxInt); var HasMoreLines := ValueData[ValueData.Length] = '\'; if HasMoreLines then Delete(ValueData, ValueData.Length, 1); Entry.ValueData := ValueData; while HasMoreLines do begin ValueData := NextLine(Lines, LineIndex).TrimLeft; HasMoreLines := ValueData[ValueData.Length] = '\'; if HasMoreLines then Delete(ValueData, ValueData.Length, 1); Entry.ValueData := Entry.ValueData + ValueData; end; Entry.ValueData := Entry.ValueData.Replace(',', ' '); if ValueType <> vtBinary then begin Entry.ValueData := Entry.ValueData.Replace(' ', ''); Entry.ValueData := UTF16LEHexStrToStr(Entry.ValueData); end; if ValueType in [vtSzAsList, vtExpandSz] then begin Entry.ValueData := Entry.ValueData.Replace(#0, ''); Entry.ValueType := IfThen(ValueType = vtSzAsList, 'string', 'expandsz'); end else if ValueType = vtMultiSz then begin Entry.ValueData := Entry.ValueData.Replace(#0, '{break}'); Entry.ValueType := 'multisz'; end else Entry.ValueType := 'binary'; Entry.ValueData := Entry.ValueData.QuotedString('"'); end; vtDWord, vtDWordAsList, vtQWord: begin P := Pos(':', ValueTypeAndData); Entry.ValueData := Copy(ValueTypeAndData, P + 1, MaxInt); if ValueType in [vtDWordAsList, vtQWord] then begin { ValueData is in reverse order, fix this } var ReverseValueData := Entry.ValueData.Replace(',', ''); Entry.ValueData := ''; for var I := 0 to ReverseValueData.Length div 2 do Entry.ValueData := Copy(ReverseValueData, (I * 2) + 1, 2) + Entry.ValueData; Entry.ValueType := IfThen(ValueType = vtDWordAsList, 'dword', 'qword'); end else Entry.ValueType := 'dword'; Entry.ValueData := '$' + Entry.ValueData; end; vtNone, vtDelete: begin Entry.ValueType := 'none'; Entry.ValueData := ''; { value doesn't matter } end; end; if ValueType <> vtUnsupported then OutLines.Add(TextValueEntry(Entry, ValueType)) else HadUnsupportedValueTypes := True; end; Line := NextLine(Lines, LineIndex); { Go to the next line - should be the next value or a comment } end; { Out of values } end; Inc(LineIndex); { Go to the next line - should be the next key section or a comment } end; OutLines.Insert(0, TextHeader); OutLines.Add(TextFooter(HadFilteredKeys, HadUnsupportedValueTypes)); Registry := Registry + OutLines.Text; finally Lines.Free; OutLines.Free; end; end; end.