commit 8a0fd0b6a5df5ec969fcf1d6301d089f3766727b
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date: Wed, 14 Aug 2024 11:06:03 +0800
reg-dump: quickly search the Windows Registry
Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Diffstat:
| A | LICENSE | | | 61 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | README.md | | | 19 | +++++++++++++++++++ |
| A | go.mod | | | 10 | ++++++++++ |
| A | go.sum | | | 6 | ++++++ |
| A | main.go | | | 276 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
5 files changed, 372 insertions(+), 0 deletions(-)
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,61 @@
+This project is provided under the terms of the UNLICENSE or
+the MIT license denoted by the following SPDX identifier:
+
+SPDX-License-Identifier: Unlicense OR MIT
+
+You may use the project under the terms of either license.
+
+Both licenses are reproduced below.
+
+----
+The MIT License (MIT)
+
+Copyright (c) 2024 Jack Mordaunt
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+---
+
+---
+The UNLICENSE
+
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to <https://unlicense.org/>
+---
diff --git a/README.md b/README.md
@@ -0,0 +1,19 @@
+# `reg-dump`
+
+This utility provides fast searching within the Windows registry.
+
+## Install
+
+`go install git.sr.ht/~jackmordaunt/reg-dump@latest`
+
+## Usage
+
+`reg-dump search`
+
+Supply a regex to match paths and values.
+
+`reg-dump search -pattern "*.exe"`
+
+Combine with `fzf` to fuzzy search the result.
+
+`reg-dump search -pattern "*.exe" -log log.txt | fzf --preview "reg-dump show {}"`
diff --git a/go.mod b/go.mod
@@ -0,0 +1,10 @@
+module git.sr.ht/~jackmordaunt/reg-dump
+
+go 1.22.3
+
+require (
+ github.com/carlmjohnson/flowmatic v0.23.4
+ golang.org/x/sys v0.24.0
+)
+
+require github.com/carlmjohnson/deque v0.23.1 // indirect
diff --git a/go.sum b/go.sum
@@ -0,0 +1,6 @@
+github.com/carlmjohnson/deque v0.23.1 h1:X2HOJM9xcglY03deMZ0oZ1V2xtbqYV7dJDnZiSZN4Ak=
+github.com/carlmjohnson/deque v0.23.1/go.mod h1:LF5NJjICBrEOPx84pxPL4nCimy5n9NQjxKi5cXkh+8U=
+github.com/carlmjohnson/flowmatic v0.23.4 h1:SfK6f+zKUlw4aga1ph+7/csqVeUAWnBxfqKN5gvQzzs=
+github.com/carlmjohnson/flowmatic v0.23.4/go.mod h1:Jpvyl591Dvkt9chYpnVupjxlKvqkZ9CtCmqL4wfQD7U=
+golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
+golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
diff --git a/main.go b/main.go
@@ -0,0 +1,276 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "hash/maphash"
+ "io"
+ "log/slog"
+ "os"
+ "path/filepath"
+ "regexp"
+ "slices"
+ "strconv"
+ "strings"
+ "sync"
+
+ "github.com/carlmjohnson/flowmatic"
+ "golang.org/x/sys/windows/registry"
+
+ _ "net/http/pprof"
+)
+
+func main() {
+ args := os.Args[1:]
+
+ cmd := "search"
+
+ if v, ok := take(&args); ok {
+ cmd = v
+ }
+
+ switch cmd {
+ case "search":
+ Search(args)
+ case "show":
+ Show(args)
+ }
+}
+
+// take returns the first element.
+func take[S ~[]E, E any](s *S) (e E, ok bool) {
+ if len(*s) == 0 {
+ return e, false
+ }
+ defer func() { *s = (*s)[1:] }()
+ return (*s)[0], true
+}
+
+// Show outputs the given registry key values.
+func Show(args []string) {
+ var path string
+
+ flags := flag.NewFlagSet("show", flag.ExitOnError)
+ flags.StringVar(&path, "path", "", "path to show values for")
+ flags.Parse(args)
+
+ positional := flags.Args()
+
+ if path == "" && len(positional) > 0 {
+ path, _ = take(&positional)
+ }
+
+ show(path)
+}
+
+// Search the registry outputting each entry that contains the given pattern.
+// If the pattern is empty search will output all entries.
+func Search(args []string) {
+ var (
+ log string
+ pattern string
+ root string
+ )
+
+ flags := flag.NewFlagSet("search", flag.ExitOnError)
+ flags.StringVar(&log, "log", "", "log file")
+ flags.StringVar(&pattern, "pattern", "", "regex pattern to match against")
+ flags.StringVar(&root, "path", "", "root search path")
+ flags.Parse(args)
+
+ var exp *regexp.Regexp
+
+ if pattern != "" {
+ exp = regexp.MustCompile(pattern)
+ }
+
+ var output io.Writer
+
+ if log != "" {
+ f, err := os.OpenFile(log, os.O_CREATE|os.O_WRONLY, 0o644)
+ if err != nil {
+ panic(fmt.Errorf("opening log file: %w", err))
+ }
+ defer f.Close()
+ output = f
+ } else {
+ output = os.Stderr
+ }
+
+ manager := Manager{
+ Logger: slog.New(slog.NewTextHandler(output, nil)),
+ Pattern: exp,
+ }
+
+ flowmatic.ManageTasks(-1, walk, manager.Manage, root)
+}
+
+type Manager struct {
+ *slog.Logger
+ Pattern *regexp.Regexp
+
+ seedInit sync.Once
+ seed maphash.Seed
+
+ seen []uint64
+}
+
+func (m *Manager) Manage(path string, subkeys []string, err error) ([]string, bool) {
+ if err != nil {
+ m.Error("task", path, err)
+ }
+
+ if m.redundant(path) {
+ m.Warn("skipping", "path", path)
+ return nil, true
+ }
+
+ if m.match(path) {
+ if err == nil {
+ fmt.Println(path)
+ } else {
+ fmt.Printf("%s (%s)", path, err.Error())
+ }
+ }
+
+ for ii, subkey := range subkeys {
+ subkeys[ii] = filepath.Join(path, subkey)
+ }
+
+ return subkeys, true
+}
+
+// redundant is true if the path has already been seen.
+//
+// The check is done using a binary search over sorted hashes to avoid retaining
+// many heap allocated strings. The registry has a lot of entries and can easily
+// consume gigabytes of memory if we heap allocate each path.
+func (m *Manager) redundant(path string) bool {
+ m.seedInit.Do(func() {
+ m.seed = maphash.MakeSeed()
+ })
+
+ hash := maphash.String(m.seed, path)
+
+ if _, ok := slices.BinarySearch(m.seen, hash); ok {
+ return true
+ }
+
+ m.seen = append(m.seen, hash)
+ slices.Sort(m.seen)
+
+ return false
+}
+
+// match the path and its values against the pattern.
+func (m *Manager) match(path string) bool {
+ if m.Pattern == nil {
+ return true
+ }
+
+ if m.Pattern.MatchString(path) {
+ return true
+ }
+
+ key, err := registry.OpenKey(registry.CURRENT_USER, path, registry.READ|registry.ENUMERATE_SUB_KEYS|registry.QUERY_VALUE)
+ if err != nil {
+ return false
+ }
+
+ defer key.Close()
+
+ return !forEachValue(key, func(name, value string, err error) bool {
+ if err != nil {
+ m.Error("matching", fmt.Sprintf("%s.%s", path, name), err)
+ return true
+ }
+ if m.Pattern.MatchString(name) || m.Pattern.MatchString(value) {
+ return false
+ }
+ return true
+ })
+}
+
+func walk(path string) ([]string, error) {
+ key, err := registry.OpenKey(registry.CURRENT_USER, path, registry.READ|registry.ENUMERATE_SUB_KEYS|registry.QUERY_VALUE)
+ if err != nil {
+ return nil, fmt.Errorf("opening key: %w", err)
+ }
+
+ defer key.Close()
+
+ subkeys, err := key.ReadSubKeyNames(-1)
+ if err != nil {
+ return nil, fmt.Errorf("reading subkeys: %w", err)
+ }
+
+ return subkeys, nil
+}
+
+func show(path string) {
+ key, err := registry.OpenKey(registry.CURRENT_USER, path, registry.READ|registry.ENUMERATE_SUB_KEYS|registry.QUERY_VALUE)
+ if err != nil {
+ panic(err)
+ }
+
+ defer key.Close()
+
+ forEachValue(key, func(name, value string, err error) bool {
+ if name == "" {
+ name = "Default"
+ }
+ if err != nil {
+ fmt.Fprintf(os.Stdout, "%v: %q (%v)\n", name, value, err.Error())
+ } else {
+ fmt.Fprintf(os.Stdout, "%v: %q\n", name, value)
+ }
+ return true
+ })
+}
+
+// forEachValue invokes [fn] on each value [key], returning true if all values were processed.
+func forEachValue(key registry.Key, fn func(name string, value string, err error) bool) bool {
+ names, err := key.ReadValueNames(-1)
+ if err != nil {
+ panic(fmt.Errorf("reading value names: %w", err))
+ }
+
+ for _, name := range names {
+ _, t, err := key.GetValue(name, nil)
+ if err != nil {
+ panic(err)
+ }
+
+ var value string
+
+ switch t {
+ case registry.SZ, registry.EXPAND_SZ:
+ value, _, err = key.GetStringValue(name)
+ if err != nil {
+ fn(name, value, err)
+ }
+
+ case registry.DWORD, registry.QWORD:
+ n, _, err := key.GetIntegerValue(name)
+ if err != nil {
+ fn(name, value, err)
+ }
+ value = strconv.Itoa(int(n))
+
+ case registry.MULTI_SZ:
+ values, _, err := key.GetStringsValue(name)
+ if err != nil {
+ fn(name, value, err)
+ }
+ value = strings.Join(values, " | ")
+ }
+
+ if value != "" {
+ if !fn(name, value, err) {
+ return false
+ }
+ }
+ }
+
+ return true
+}