main.go (8026B)
1 package main 2 3 import ( 4 "flag" 5 "fmt" 6 "hash/maphash" 7 "io" 8 "log/slog" 9 "os" 10 "path/filepath" 11 "regexp" 12 "slices" 13 "strconv" 14 "strings" 15 "sync" 16 17 "github.com/carlmjohnson/flowmatic" 18 "golang.org/x/sys/windows/registry" 19 20 _ "net/http/pprof" 21 ) 22 23 func main() { 24 args := os.Args[1:] 25 26 cmd := "search" 27 28 if v, ok := take(&args); ok { 29 cmd = v 30 } 31 32 switch cmd { 33 case "search": 34 Search(args) 35 case "show": 36 Show(args) 37 case "purge": 38 Purge(args) 39 } 40 } 41 42 // take returns the first element. 43 func take[S ~[]E, E any](s *S) (e E, ok bool) { 44 if len(*s) == 0 { 45 return e, false 46 } 47 defer func() { *s = (*s)[1:] }() 48 return (*s)[0], true 49 } 50 51 // Show outputs the given registry key values. 52 func Show(args []string) { 53 var path string 54 55 flags := flag.NewFlagSet("show", flag.ExitOnError) 56 flags.StringVar(&path, "path", "", "path to show values for") 57 flags.Parse(args) 58 59 positional := flags.Args() 60 61 if path == "" && len(positional) > 0 { 62 path, _ = take(&positional) 63 } 64 65 show(path) 66 } 67 68 // Search the registry outputting each entry that contains the given pattern. 69 // If the pattern is empty search will output all entries. 70 func Search(args []string) { 71 var ( 72 log string 73 pattern string 74 root string 75 ) 76 77 flags := flag.NewFlagSet("search", flag.ExitOnError) 78 flags.StringVar(&log, "log", "", "log file") 79 flags.StringVar(&pattern, "pattern", "", "regex pattern to match against") 80 flags.StringVar(&root, "path", "", "root search path") 81 flags.Parse(args) 82 83 var exp *regexp.Regexp 84 85 if pattern != "" { 86 exp = regexp.MustCompile(pattern) 87 } 88 89 var output io.Writer 90 91 if log != "" { 92 f, err := os.OpenFile(log, os.O_CREATE|os.O_WRONLY, 0o644) 93 if err != nil { 94 panic(fmt.Errorf("opening log file: %w", err)) 95 } 96 defer f.Close() 97 output = f 98 } else { 99 output = os.Stderr 100 } 101 102 manager := Manager{ 103 Logger: slog.New(slog.NewTextHandler(output, nil)), 104 Pattern: exp, 105 } 106 107 flowmatic.ManageTasks(-1, walk, manager.Search, root) 108 } 109 110 // Purge the registry of all entries that match the pattern. 111 func Purge(args []string) { 112 var ( 113 log string 114 pattern string 115 root string 116 ) 117 118 flags := flag.NewFlagSet("search", flag.ExitOnError) 119 flags.StringVar(&log, "log", "", "log file") 120 flags.StringVar(&pattern, "pattern", "", "regex pattern to match against") 121 flags.StringVar(&root, "path", "", "root search path") 122 flags.Parse(args) 123 124 if pattern == "" { 125 fmt.Println("-pattern is required") 126 os.Exit(1) 127 } 128 129 var exp *regexp.Regexp 130 131 if pattern != "" { 132 exp = regexp.MustCompile(pattern) 133 } 134 135 var output io.Writer 136 137 if log != "" { 138 f, err := os.OpenFile(log, os.O_CREATE|os.O_WRONLY, 0o644) 139 if err != nil { 140 panic(fmt.Errorf("opening log file: %w", err)) 141 } 142 defer f.Close() 143 output = f 144 } else { 145 output = os.Stderr 146 } 147 148 manager := Manager{ 149 Logger: slog.New(slog.NewTextHandler(output, nil)), 150 Pattern: exp, 151 } 152 153 flowmatic.ManageTasks(-1, walk, manager.Purge, root) 154 } 155 156 type Manager struct { 157 *slog.Logger 158 Pattern *regexp.Regexp 159 160 seedInit sync.Once 161 seed maphash.Seed 162 163 seen []uint64 164 } 165 166 // Search outputs matched registry entries. 167 func (m *Manager) Search(path string, subkeys []string, err error) ([]string, bool) { 168 if err != nil { 169 m.Error("task", path, err) 170 } 171 172 if m.redundant(path) { 173 m.Warn("skipping", "path", path) 174 return nil, true 175 } 176 177 if m.match(path) { 178 if err == nil { 179 fmt.Println(path) 180 } else { 181 fmt.Printf("%s (%s)", path, err.Error()) 182 } 183 } 184 185 for ii, subkey := range subkeys { 186 subkeys[ii] = filepath.Join(path, subkey) 187 } 188 189 return subkeys, true 190 } 191 192 // Purge deletes matched registry entries. 193 func (m *Manager) Purge(path string, subkeys []string, err error) ([]string, bool) { 194 if err != nil { 195 m.Error("task", path, err) 196 } 197 198 if m.redundant(path) { 199 m.Warn("skipping", "path", path) 200 return nil, true 201 } 202 203 if m.match(path) && err == nil { 204 if err := m.purge(path); err != nil { 205 m.Error("purge", path, err) 206 } 207 return nil, true 208 } 209 210 for ii, subkey := range subkeys { 211 subkeys[ii] = filepath.Join(path, subkey) 212 } 213 214 return subkeys, true 215 } 216 217 // purge the given path. 218 // If the path contains the pattern, delete the key. 219 // Else if the key's values contain the pattern in name or value, delete them. 220 func (m *Manager) purge(path string) error { 221 if m.Pattern.MatchString(path) { 222 if err := registry.DeleteKey(registry.CURRENT_USER, path); err != nil { 223 return fmt.Errorf("deleting key: %w", err) 224 } 225 return nil 226 } 227 228 key, err := registry.OpenKey(registry.CURRENT_USER, path, registry.ALL_ACCESS) 229 if err != nil { 230 return fmt.Errorf("opening key with write permissions: %w", err) 231 } 232 233 defer key.Close() 234 235 forEachValue(key, func(name, value string, err error) bool { 236 if err != nil { 237 m.Error("iterating", fmt.Sprintf("%s.%s", path, name), err) 238 } 239 if m.Pattern.MatchString(name) || m.Pattern.MatchString(value) { 240 if err := key.DeleteValue(name); err != nil { 241 m.Error("deleting", fmt.Sprintf("%s.%s", path, name), err) 242 } 243 } 244 return true 245 }) 246 247 return nil 248 } 249 250 // redundant is true if the path has already been seen. 251 // 252 // The check is done using a binary search over sorted hashes to avoid retaining 253 // many heap allocated strings. The registry has a lot of entries and can easily 254 // consume gigabytes of memory if we heap allocate each path. 255 func (m *Manager) redundant(path string) bool { 256 m.seedInit.Do(func() { 257 m.seed = maphash.MakeSeed() 258 }) 259 260 hash := maphash.String(m.seed, path) 261 262 if _, ok := slices.BinarySearch(m.seen, hash); ok { 263 return true 264 } 265 266 m.seen = append(m.seen, hash) 267 slices.Sort(m.seen) 268 269 return false 270 } 271 272 // match the path and its values against the pattern. 273 func (m *Manager) match(path string) bool { 274 if m.Pattern == nil { 275 return true 276 } 277 278 if m.Pattern.MatchString(path) { 279 return true 280 } 281 282 key, err := registry.OpenKey(registry.CURRENT_USER, path, registry.READ|registry.ENUMERATE_SUB_KEYS|registry.QUERY_VALUE) 283 if err != nil { 284 return false 285 } 286 287 defer key.Close() 288 289 return !forEachValue(key, func(name, value string, err error) bool { 290 if err != nil { 291 m.Error("matching", fmt.Sprintf("%s.%s", path, name), err) 292 return true 293 } 294 if m.Pattern.MatchString(name) || m.Pattern.MatchString(value) { 295 return false 296 } 297 return true 298 }) 299 } 300 301 func walk(path string) ([]string, error) { 302 key, err := registry.OpenKey(registry.CURRENT_USER, path, registry.READ|registry.ENUMERATE_SUB_KEYS|registry.QUERY_VALUE) 303 if err != nil { 304 return nil, fmt.Errorf("opening key: %w", err) 305 } 306 307 defer key.Close() 308 309 subkeys, err := key.ReadSubKeyNames(-1) 310 if err != nil { 311 return nil, fmt.Errorf("reading subkeys: %w", err) 312 } 313 314 return subkeys, nil 315 } 316 317 func show(path string) { 318 key, err := registry.OpenKey(registry.CURRENT_USER, path, registry.READ|registry.ENUMERATE_SUB_KEYS|registry.QUERY_VALUE) 319 if err != nil { 320 panic(err) 321 } 322 323 defer key.Close() 324 325 forEachValue(key, func(name, value string, err error) bool { 326 if name == "" { 327 name = "Default" 328 } 329 if err != nil { 330 fmt.Fprintf(os.Stdout, "%v: %q (%v)\n", name, value, err.Error()) 331 } else { 332 fmt.Fprintf(os.Stdout, "%v: %q\n", name, value) 333 } 334 return true 335 }) 336 } 337 338 // forEachValue invokes [fn] on each value [key], returning true if all values were processed. 339 func forEachValue(key registry.Key, fn func(name string, value string, err error) bool) bool { 340 names, err := key.ReadValueNames(-1) 341 if err != nil { 342 panic(fmt.Errorf("reading value names: %w", err)) 343 } 344 345 for _, name := range names { 346 _, t, err := key.GetValue(name, nil) 347 if err != nil { 348 panic(err) 349 } 350 351 var value string 352 353 switch t { 354 case registry.SZ, registry.EXPAND_SZ: 355 value, _, err = key.GetStringValue(name) 356 if err != nil { 357 fn(name, value, err) 358 } 359 360 case registry.DWORD, registry.QWORD: 361 n, _, err := key.GetIntegerValue(name) 362 if err != nil { 363 fn(name, value, err) 364 } 365 value = strconv.Itoa(int(n)) 366 367 case registry.MULTI_SZ: 368 values, _, err := key.GetStringsValue(name) 369 if err != nil { 370 fn(name, value, err) 371 } 372 value = strings.Join(values, " | ") 373 } 374 375 if value != "" { 376 if !fn(name, value, err) { 377 return false 378 } 379 } 380 } 381 382 return true 383 }