3030RE_GLOB_MAGIC = re .compile (r"[*?[]" )
3131RE_REGFLEX_NAME_VALUE = re .compile (r'^"(?P<name>(?:[^"\\]|\\.)*?)"=(?P<value>.*)' )
3232
33- REG_FLEX_HEADER = "Windows Registry Editor Version 5.00"
34- _REG_LINE_LIMIT = 80
33+ REGFLEX_HEADER = "Windows Registry Editor Version 5.00"
34+ _REGFLEX_LINE_LIMIT = 80
35+
36+ SHORTNAMES = {
37+ "HKLM" : "HKEY_LOCAL_MACHINE" ,
38+ "HKCC" : "HKEY_CURRENT_CONFIG" ,
39+ "HKCU" : "HKEY_CURRENT_USER" ,
40+ "HKCR" : "HKEY_CLASSES_ROOT" ,
41+ "HKU" : "HKEY_USERS" ,
42+ }
3543
3644
3745KeyType = regf .IndexLeaf | regf .FastLeaf | regf .HashLeaf | regf .IndexRoot | regf .KeyNode
@@ -798,110 +806,11 @@ def map_definition(self, fh: TextIO) -> None:
798806
799807 vhive .map_key (vkey .path , vkey )
800808
801- def dump_definition (self , fh : TextIO , paths : list [str ] | None = None ) -> None :
802- """Dump the parsed registry hives to a .reg file format.
803-
804- This is the inverse of :meth:`map_definition`. When ``paths`` is
805- given, only the listed registry subtrees are written; otherwise every
806- hive stored in :attr:`hives` is exported in full.
807-
808- The header always contains a comment line indicating that the file was
809- generated by RegFlex. When ``paths`` is provided each path is also
810- listed as an individual comment line so that the exported file is
811- self-documenting.
812-
813- Args:
814- fh: A file-like object opened in text mode to write the registry export to.
815- paths: Optional list of full registry paths to export
816- (e.g. ``["HKEY_LOCAL_MACHINE\\ \\ SOFTWARE\\ \\ Test"]``).
817- When ``None`` all hives are exported in full.
818- """
819- fh .write (f"{ REG_FLEX_HEADER } \n \n " )
820- fh .write ("; This .reg file was generated by RegFlex (part of Dissect Target)\n " )
821- if paths :
822- fh .write ("; Registry paths exported:\n " )
823- fh .writelines (f"; { path } \n " for path in paths )
824- fh .write ("\n " )
825-
826- if not paths :
827- for hive_name , hive in self .hives .items ():
828- for subkey in hive .root ().subkeys ():
829- write_reg_key (fh , subkey , f"{ hive_name } \\ { subkey .name } " )
830- return
831-
832- for path in paths :
833- hive_name , _ , key_path = path .partition ("\\ " )
834- hive_name = hive_name .upper ()
835- hive = self .hives .get (hive_name )
836- if hive is None :
837- log .warning ("RegFlex: hive %r not found, skipping path %r" , hive_name , path )
838- continue
839- try :
840- key = hive .key (key_path ) if key_path else hive .root ()
841- except RegistryKeyNotFoundError :
842- log .warning ("RegFlex: key %r not found in hive %r, skipping" , key_path , hive_name )
843- continue
844- write_reg_key (fh , key , path )
845-
846809
847810class RegFlexHive (VirtualHive ):
848811 pass
849812
850813
851- def _escape_reg_str (value : str ) -> str :
852- """Escape special characters in a string for .reg file format.
853-
854- Backslashes are doubled and double-quote characters are backslash-escaped,
855- matching the encoding used by the Windows Registry Editor.
856-
857- Args:
858- value: The string to escape.
859-
860- Returns:
861- The escaped string, safe for use inside a quoted .reg value.
862- """
863- return value .replace ("\\ " , "\\ \\ " ).replace ('"' , '\\ "' )
864-
865-
866- def _format_hex_data (data : bytes , prefix : str ) -> str :
867- """Format binary data as hex for .reg format, wrapping long lines with continuation.
868-
869- Produces comma-separated lowercase hex byte pairs. Lines longer than
870- :data:`_REG_LINE_LIMIT` characters are split with a trailing backslash and
871- the continuation line is indented by two spaces, matching the layout
872- produced by the Windows Registry Editor.
873-
874- Args:
875- data: The bytes to encode.
876- prefix: The line prefix (e.g. ``'"name"=hex:'``). Included verbatim at
877- the start of the first line and used when computing line length.
878-
879- Returns:
880- The formatted string, potentially spanning multiple lines.
881- """
882- if not data :
883- return prefix
884-
885- hex_pairs = [f"{ b :02x} " for b in data ]
886- lines = []
887- current = prefix
888-
889- for i , pair in enumerate (hex_pairs ):
890- is_last = i == len (hex_pairs ) - 1
891- entry = pair if is_last else f"{ pair } ,"
892- # Reserve one column for the backslash continuation character on non-final segments
893- fits = len (current ) + len (entry ) + (0 if is_last else 1 ) <= _REG_LINE_LIMIT
894-
895- if not fits and i > 0 :
896- lines .append (f"{ current } \\ " )
897- current = f" { entry } "
898- else :
899- current += entry
900-
901- lines .append (current )
902- return "\n " .join (lines )
903-
904-
905814class RegFlexKey (VirtualKey ):
906815 pass
907816
@@ -977,12 +886,66 @@ def parse_flex_value(value: str) -> tuple[RegistryValueType, ValueType]:
977886 raise ValueError (f"Unsupported registry flex value type: { value !r} " )
978887
979888
980- def write_flex_value (value : RegistryValue ) -> str :
889+ def _escape_flex_str (value : str ) -> str :
890+ """Escape special characters in a string for ``.reg`` file format.
891+
892+ Backslashes are doubled and double-quote characters are backslash-escaped,
893+ matching the encoding used by the Windows Registry Editor.
894+
895+ Args:
896+ value: The string to escape.
897+
898+ Returns:
899+ The escaped string, safe for use inside a quoted ``.reg`` value.
900+ """
901+ return value .replace ("\\ " , "\\ \\ " ).replace ('"' , '\\ "' )
902+
903+
904+ def _format_flex_hex (data : bytes , prefix : str ) -> str :
905+ """Format binary data as hex for .reg format, wrapping long lines with continuation.
906+
907+ Produces comma-separated lowercase hex byte pairs. Lines longer than
908+ :data:`_REGFLEX_LINE_LIMIT` characters are split with a trailing backslash and
909+ the continuation line is indented by two spaces, matching the layout
910+ produced by the Windows Registry Editor.
911+
912+ Args:
913+ data: The bytes to encode.
914+ prefix: The line prefix (e.g. ``'"name"=hex:'``). Included verbatim at
915+ the start of the first line and used when computing line length.
916+
917+ Returns:
918+ The formatted string, potentially spanning multiple lines.
919+ """
920+ if not data :
921+ return prefix
922+
923+ hex_pairs = [f"{ b :02x} " for b in data ]
924+ lines = []
925+ current = prefix
926+
927+ for i , pair in enumerate (hex_pairs ):
928+ is_last = i == len (hex_pairs ) - 1
929+ entry = pair if is_last else f"{ pair } ,"
930+ # Reserve one column for the backslash continuation character on non-final segments
931+ fits = len (current ) + len (entry ) + (0 if is_last else 1 ) <= _REGFLEX_LINE_LIMIT
932+
933+ if not fits and i > 0 :
934+ lines .append (f"{ current } \\ " )
935+ current = f" { entry } "
936+ else :
937+ current += entry
938+
939+ lines .append (current )
940+ return "\n " .join (lines )
941+
942+
943+ def format_flex_value (value : RegistryValue ) -> str :
981944 """Format a :class:`RegistryValue` as a ``.reg`` file value line.
982945
983946 This is the inverse of :func:`parse_flex_value`.
984947
985- When the registry type is :attr:`RegistryValueType.NONE` the Python type
948+ When the value type is :attr:`RegistryValueType.NONE` the Python type
986949 of the value data is used to infer an appropriate registry type:
987950
988951 - :class:`str` → :attr:`RegistryValueType.SZ`
@@ -1001,7 +964,7 @@ def write_flex_value(value: RegistryValue) -> str:
1001964 data = value .value
1002965 vtype = RegistryValueType (value .type )
1003966
1004- name_str = "@" if name == "" else f'"{ _escape_reg_str (name )} "'
967+ name_str = "@" if name == "" else f'"{ _escape_flex_str (name )} "'
1005968
1006969 # Infer a sensible registry type from the Python value when the type is NONE.
1007970 # This covers VirtualValues which always report NONE.
@@ -1014,58 +977,79 @@ def write_flex_value(value: RegistryValue) -> str:
1014977 vtype = RegistryValueType .MULTI_SZ
1015978 elif isinstance (data , bytes ):
1016979 vtype = RegistryValueType .BINARY
980+
1017981 if vtype == RegistryValueType .SZ :
1018- return f'{ name_str } ="{ _escape_reg_str (str (data ))} "'
982+ return f'{ name_str } ="{ _escape_flex_str (str (data ))} "'
1019983
1020984 if vtype == RegistryValueType .DWORD :
1021985 return f"{ name_str } =dword:{ int (data ):08x} "
1022986
1023987 if vtype == RegistryValueType .BINARY :
1024- return _format_hex_data (data if isinstance (data , bytes ) else b"" , f"{ name_str } =hex:" )
988+ return _format_flex_hex (data if isinstance (data , bytes ) else b"" , f"{ name_str } =hex:" )
1025989
1026990 if vtype == RegistryValueType .EXPAND_SZ :
1027991 raw = data .encode ("utf-16-le" ) + b"\x00 \x00 " if isinstance (data , str ) else (data or b"" )
1028- return _format_hex_data (raw , f"{ name_str } =hex(2):" )
992+ return _format_flex_hex (raw , f"{ name_str } =hex(2):" )
1029993
1030994 if vtype == RegistryValueType .MULTI_SZ :
1031995 if isinstance (data , list ):
1032996 joined = "" .join (f"{ s } \x00 " for s in data ) + "\x00 "
1033997 raw = joined .encode ("utf-16-le" )
1034998 else :
1035999 raw = data if isinstance (data , bytes ) else b""
1036- return _format_hex_data (raw , f"{ name_str } =hex(7):" )
1000+ return _format_flex_hex (raw , f"{ name_str } =hex(7):" )
10371001
10381002 if vtype == RegistryValueType .DWORD_BIG_ENDIAN :
10391003 raw = int (data ).to_bytes (4 , "big" ) if isinstance (data , int ) else (data or b"" )
1040- return _format_hex_data (raw , f"{ name_str } =hex(5):" )
1004+ return _format_flex_hex (raw , f"{ name_str } =hex(5):" )
10411005
10421006 if vtype == RegistryValueType .QWORD :
10431007 raw = int (data ).to_bytes (8 , "little" ) if isinstance (data , int ) else (data or b"" )
1044- return _format_hex_data (raw , f"{ name_str } =hex(b):" )
1008+ return _format_flex_hex (raw , f"{ name_str } =hex(b):" )
10451009
10461010 if vtype == RegistryValueType .NONE :
10471011 raw = data if isinstance (data , bytes ) else b""
1048- return _format_hex_data (raw , f"{ name_str } =hex(0):" )
1012+ return _format_flex_hex (raw , f"{ name_str } =hex(0):" )
10491013
10501014 # Fallback for LINK, RESOURCE_LIST, FULL_RESOURCE_DESCRIPTOR, RESOURCE_REQUIREMENTS_LIST, etc.
10511015 raw = data if isinstance (data , bytes ) else b""
1052- return _format_hex_data (raw , f"{ name_str } =hex({ int (vtype ):x} ):" )
1016+ return _format_flex_hex (raw , f"{ name_str } =hex({ int (vtype ):x} ):" )
10531017
10541018
1055- def write_reg_key (fh : TextIO , key : RegistryKey , path : str ) -> None :
1056- """Recursively write a registry key and its subkeys in .reg format.
1019+ def format_flex_key (
1020+ key : RegistryKey ,
1021+ path : str | None = None ,
1022+ ) -> Iterator [str ]:
1023+ """Recursively format a registry key and its subkeys in ``.reg`` format.
10571024
10581025 Args:
1059- fh: A file-like object opened in text mode.
1060- key: The registry key to write.
1061- path: The full registry path for this key
1062- (e.g. ``HKEY_LOCAL_MACHINE\\ SOFTWARE\\ Test``).
1026+ key: The registry key to format.
1027+ path: The full registry path for this key (e.g. ``HKEY_LOCAL_MACHINE\\ SOFTWARE\\ Test``).
1028+ If ``None``, ``key.path`` is used.
10631029 """
1064- fh .write (f"[{ path } ]\n " )
1065- fh .writelines (f"{ write_flex_value (reg_value )} \n " for reg_value in key .values ())
1066- fh .write ("\n " )
1030+ yield f"[{ path or key .path } ]"
1031+ for value in key .values ():
1032+ yield f"{ format_flex_value (value )} "
1033+ yield ""
10671034 for subkey in key .subkeys ():
1068- write_reg_key (fh , subkey , f"{ path } \\ { subkey .name } " )
1035+ yield from format_flex_key (subkey , f"{ path or key .path } \\ { subkey .name } " )
1036+
1037+
1038+ def format_flex_header (keys : list [str ] | None = None ) -> Iterator [str ]:
1039+ """Format the header for a ``.reg`` file.
1040+
1041+ Args:
1042+ keys: Optional list of registry paths that will be exported in the file.
1043+ If provided, each path is included as a comment line in the header.
1044+ """
1045+ yield f"{ REGFLEX_HEADER } "
1046+ yield ""
1047+ yield "; Generated by dissect.target"
1048+ if keys :
1049+ yield "; Registry paths exported:"
1050+ for key in keys :
1051+ yield f"; { key } "
1052+ yield ""
10691053
10701054
10711055def has_glob_magic (pattern : str ) -> bool :
@@ -1080,6 +1064,33 @@ def has_glob_magic(pattern: str) -> bool:
10801064 return RE_GLOB_MAGIC .search (pattern ) is not None
10811065
10821066
1067+ def split_key_path (path : str ) -> tuple [str , str ]:
1068+ """Split a registry key path into the canonical ``HKEY_*`` hive name and the rest of the path.
1069+
1070+ Args:
1071+ path: A registry path starting with a shortname such as ``HKLM`` or ``HKCU``.
1072+
1073+ Returns:
1074+ A tuple containing the canonical ``HKEY_*`` hive name and the rest of the path.
1075+ """
1076+ hive , _ , rest = path .partition ("\\ " )
1077+ expanded = SHORTNAMES .get (hive .upper (), hive .upper ())
1078+ return expanded , rest
1079+
1080+
1081+ def expand_key_path (path : str ) -> str :
1082+ """Expand a registry shortname prefix to the canonical ``HKEY_*`` name.
1083+
1084+ Args:
1085+ path: A registry path starting with a shortname such as ``HKLM`` or ``HKCU``.
1086+
1087+ Returns:
1088+ The path with the leading component expanded to its full name.
1089+ """
1090+ hive , rest = split_key_path (path )
1091+ return f"{ hive } \\ { rest } "
1092+
1093+
10831094def glob_split (pattern : str ) -> tuple [str ]:
10841095 """Split a key path with glob patterns on the first key path part with glob patterns.
10851096
0 commit comments