winreg

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 12, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

README

ferry/windows/winreg

Load configuration from the Windows registry into a Go struct, and write it back.

Experimental. Neither the Go API nor the way values are stored is settled yet, and either may change in a release that is not a new major version of this module.

go get github.com/onhotpath/ferry/driver/windows

Addresses are subkeys and values

The registry keeps two namespaces under every key: values, which hold data, and subkeys, which hold more of both. A field's address maps onto that exactly.

the schema says the registry holds
Host string tagged host the value host under the driver's key
DB struct{...} tagged db, with Host tagged host the value host under the subkey db
Tags []string tagged tags the subkey tags, holding the values 0, 1, 2
Envs map[string]struct{...} tagged envs the subkey envs, holding one subkey per key
a schema whose root is a single value the key's own unnamed value, which regedit shows as (Default)

A value host and a subkey host under one key are two different objects, and both are legal.

The registry does not care about case, so this driver does

Setting Host and then host leaves one value, named Host, holding the second write's data. No error is raised at any point, and that is the loss this module exists to stop.

So every part of an address is folded to lower case before two of them are compared, and a schema naming two addresses that fold together is refused when the load starts, naming both:

ferry: /Host: winreg gives this and /host the same name, "host", so one of the two would be lost

The case you wrote is what gets stored, because the registry keeps whichever spelling wrote a name first. The fold is only for the check.

Two things have no registry name at all and are refused the same way: an empty part, and a part containing a backslash. Neither has an escape to map onto, for the reason a backslash is dangerous in the first place: any byte an escape used would be a byte a map key is entitled to contain.

Loading

This is Example in example_test.go, which go test compiles and runs.

type DB struct {
	Host string `ferry:"host"`
	Port int    `ferry:"port,default=5432"`
}

type Config struct {
	Name string `ferry:"name"`
	DB   DB     `ferry:"db"`
}

store := newMemory()
_ = store.Set(context.Background(), "", "name", winreg.Datum{Type: winreg.TypeString, Text: "checkout"})
_ = store.Set(context.Background(), "db", "host", winreg.Datum{Type: winreg.TypeString, Text: "db.internal"})

src := winreg.NewSource(winreg.LocalMachine, `SOFTWARE\Example`, winreg.Store(store))

cfg, err := ferry.Load[Config](context.Background(), src)
if err != nil {
	panic(err)
}

fmt.Printf("%s %s:%d\n", cfg.Name, cfg.DB.Host, cfg.DB.Port)
// Output: checkout db.internal:5432

A program on Windows passes no winreg.Store and reaches the machine's own registry. The example passes one so that it runs everywhere, which is the same seam this module's own tests use.

Saving

This is Example_dump in example_test.go.

type DB struct {
	Host string `ferry:"host"`
}

type Config struct {
	Name string   `ferry:"name"`
	DB   DB       `ferry:"db"`
	Tags []string `ferry:"tags"`
}

store := newMemory()
sink := winreg.NewSink(winreg.CurrentUser, `Software\Example`, winreg.Store(store))

cfg := Config{Name: "checkout", DB: DB{Host: "db.internal"}, Tags: []string{"eu", "prod"}}
if err := ferry.Dump(context.Background(), cfg, sink); err != nil {
	panic(err)
}

for _, subkey := range slices.Sorted(maps.Keys(store.vals)) {
	for _, name := range slices.Sorted(maps.Keys(store.vals[subkey])) {
		d := store.vals[subkey][name]
		fmt.Printf("%s\t%s\t%s\t%s\n", subkey, name, d.Type, d.Text)
	}
}
// Output:
// 	name	REG_SZ	checkout
// db	host	REG_SZ	db.internal
// tags	0	REG_SZ	eu
// tags	1	REG_SZ	prod

A save of a slice or a map is a replacement: what the previous save left under it and this one did not write is removed first. Everything else is a merge, and a field your struct does not map is left alone.

What a value is stored as

Reading is wide and writing is narrow.

read becomes
REG_SZ a string
REG_EXPAND_SZ a string, exactly as stored, never expanded
REG_DWORD, REG_QWORD a number
REG_BINARY bytes
REG_MULTI_SZ refused
write becomes
a []byte field REG_BINARY
text at an address already stored as REG_EXPAND_SZ REG_EXPAND_SZ, the new text
everything else REG_SZ

REG_EXPAND_SZ is read raw because expanding it is not reversible. %SystemRoot%-literal expands to C:\WINDOWS-literal, and a save afterwards would write that back over what the operator wrote.

REG_MULTI_SZ is refused because it spells a sequence inside one value, and ferry addresses each element of a sequence in its own right.

REG_SZ carries a number's own spelling intact: 007, 3.14159265358979 and 18446744073709551615 all come back exactly as they went in. That is why it is what a number is written as, and there is no option to choose another type.

The one type a save preserves is REG_EXPAND_SZ. A save reads the address first, and text written where the registry already holds an expandable string is stored as one, because retyping it would destroy the expansion for every other reader of that key - the same break this driver refuses to commit by expanding on read. It costs one read per string a save writes.

The seam

There is no dependency on any particular registry. Six methods, and the package works against whatever you have:

type Registry interface {
	Get(ctx context.Context, subkey, name string) (Datum, bool, error)
	List(ctx context.Context, subkey string) (Listing, bool, error)
	Set(ctx context.Context, subkey, name string, d Datum) error
	Create(ctx context.Context, subkey string) error
	DeleteValue(ctx context.Context, subkey, name string) error
	DeleteKey(ctx context.Context, subkey string) error
}

Absence is a result and not an error: Get and List report an object that is not there with found false and a nil error. Removal is idempotent, and DeleteKey removes everything under the key as well as the key itself. Set creates every subkey on the way down to the value it writes.

A Registry that can also say when it changed implements one more method, and that is what winreg.Watch needs:

type Notifier interface {
	Arm(ctx context.Context) (Change, error)
}

type Change interface {
	Wait(ctx context.Context) (bool, error)
	Close() error
}

Registering and waiting are two calls so that a registration can outlive a wait. The watcher arms the next one before it runs your callback, which is what stops a change landing during a reload from being lost.

The machine's own registry is behind //go:build windows and implements both. winreg.Store is where anything else is handed over, and it is why this module builds, and its tests run, on every platform.

The options

winreg.WithView(v) chooses which side of the WOW6432Node redirector this driver reads and writes: ViewNative, View64 or View32. On 64-bit Windows the registry keeps two copies of parts of the tree, and a 32-bit process is redirected into WOW6432Node without being told, so a 32-bit service and a 64-bit installer writing the same path write two different keys. Name the view rather than inheriting it, and give the same one to both halves.

winreg.Store(r) names the registry this driver reads and writes through. A nil argument is the machine's own registry, which is the default. This is what makes a test hermetic, and it is how a registry this package does not know about arrives.

winreg.Watch(ctx, onChange) calls onChange whenever anything under this driver's key changes, so that a process holding a loaded value can load a fresh one. It is refused at Bind when the registry behind the source reports no changes, and when the first registration cannot be placed. A key that does not exist yet is not a refusal: the registration goes on the nearest key above it, so the save that creates the key fires the watch and the watch moves down to it. Read its documentation before using it: the callback runs on the watching goroutine one call at a time, a panic in it takes the process down, and cancelling ctx is the only way to stop the watch.

There is no separator option. The hierarchy is the registry's own syntax, not a taste.

What this plane cannot do

The registry has types, and a save chooses between two of them. An operator who retyped a value to REG_DWORD by hand gets it back as REG_SZ on the next save: the data survives, the type annotation does not. REG_EXPAND_SZ is the exception and is preserved.

A save is ordered and it is not atomic. Every write is staged and nothing reaches the registry until the walk has succeeded, so a save that is refused leaves the registry byte for byte as it was. Once the commit starts, the removals a slice or a map implies run first, then the keys a present-and-empty container needs, then every value in the order the walk produced it - and a machine that fails half way through is left half way through. The registry does have transactions, and Microsoft deprecated them; this driver does not use them.

There is no null. A registry value cannot exist without a type, and every type this driver writes carries a payload, so a nil pointer to a value is refused rather than stored as something that would be indistinguishable from empty text. A container is different: a subkey that exists and holds nothing is a real object, so a non-nil struct pointer whose every field was omitted does survive a save and a load.

Two Go strings have no REG_SZ spelling. A registry string is UTF-16 and ends at its first NUL, so a string holding a NUL, and one holding bytes that are not valid UTF-8, are both refused. Store those in a []byte field, which is written as REG_BINARY and carries every byte.

Writing under HKEY_LOCAL_MACHINE needs administrator rights. A process without them is refused when the save starts, with an error reaching ferry.ErrReadOnly, rather than part way through.

The fold is Go's and the registry's is Windows'. The two agree on ASCII and can disagree outside it. Where they do, this driver folds less, so the pair that gets through is one the registry would merge.

A value name may be at most 16,383 characters. That is the registry's own limit, and there is no ferry limit under it.

A value name holding a backslash has no address here. The registry allows one and ferry cannot name it, so a key holding such a value cannot be loaded as a map or a slice: minting that member is refused with ErrIllegalName, and every later load of the same composite is refused the same way until the value is renamed or removed. A key only ferry has written never holds one.

Errors

sentinel what it reports
winreg.ErrIllegalName an address the registry has no name for: an empty part, or a part holding a backslash
winreg.ErrValueType a stored value whose registry type ferry cannot carry, which is REG_MULTI_SZ and anything exotic
winreg.ErrUnspellable a Go string REG_SZ cannot write down
winreg.ErrDeeperThanLeaf a subkey where a container's member takes a single value
winreg.ErrOption a hive or a view outside the sets this package declares
winreg.ErrWatch a watch that could not be opened
winreg.ErrNoRegistry no Windows registry on this machine, and no winreg.Store given

Each wraps one of ferry's own classes and stays reachable through ferry's wrapper, so errors.Is answers for it on what ferry.Load and ferry.Dump returned.

Documentation

Overview

Package winreg loads configuration from the Windows registry into a Go struct, and writes a struct back into it.

src := winreg.NewSource(winreg.LocalMachine, `SOFTWARE\Example`)
cfg, err := ferry.Load[Config](ctx, src)

sink := winreg.NewSink(winreg.CurrentUser, `Software\Example`)
err = ferry.Dump(ctx, cfg, sink)

Addresses are subkeys and values

The registry keeps two namespaces under every key: values, which hold data, and subkeys, which hold more of both. A field's address maps onto that exactly. A field tagged host is the value host under the key this driver was built over; a nested db.host is the value host under the subkey db; a slice tagged tags is the subkey tags holding the values 0, 1 and 2; a map of structs is the subkey it is tagged with, holding one subkey per key. A schema whose root is a single value is written at the key's own unnamed value, which regedit shows as (Default).

A value host and a subkey host under one key are two different objects and both are legal, so a struct with a field tagged a and another tagged a holding a nested struct loads and saves.

The registry does not care about case, so this driver does

Setting Host and then host leaves one value, named Host, holding the second write's data, and no error is raised anywhere. So this driver folds every part of an address to lower case before it compares them, and a schema naming two addresses that fold together is refused when the load starts, naming both. The case you wrote is what is stored, because the registry keeps whichever spelling wrote a name first.

The fold is Go's own lower-casing and the registry's is Windows' own table, and the two agree on ASCII and can disagree outside it. Where they do, this one folds less, so the pair that gets through is one the registry would merge.

What a value is stored as

Reading is wide and writing is narrow. A value stored as REG_SZ or REG_EXPAND_SZ arrives as text, one stored as REG_DWORD or REG_QWORD arrives as a number, and one stored as REG_BINARY arrives as bytes. REG_EXPAND_SZ is read exactly as it is stored, so %SystemRoot% reaches your field as those twelve characters and never as the directory. REG_MULTI_SZ is refused: it spells a sequence inside one value, and ferry addresses each element of a sequence in its own right.

A save writes a []byte field as REG_BINARY and everything else as REG_SZ, so a number is stored as its own text and 007, 3.14159265358979 and 18446744073709551615 all come back exactly as they went in. One exception: text written to an address the registry already holds as REG_EXPAND_SZ stays REG_EXPAND_SZ, so a save does not destroy an expansion other readers of that key depend on.

Sharp edges

A save replaces every other type a value was given. An operator who retyped a value to REG_DWORD by hand gets it back as REG_SZ on the next save: the data survives, the type annotation does not.

A save is ordered and it is not atomic. The removals a slice or a map implies run first, then the keys a present-and-empty container needs, then every value in the order the walk produced it. A machine that fails half way through leaves the registry half way through.

A registry string is UTF-16 and ends at its first NUL, so a Go string holding a NUL or holding bytes that are not valid UTF-8 is refused rather than stored mangled. Store those in a []byte field, which is written as REG_BINARY and carries every byte.

A 32-bit process on 64-bit Windows is redirected into WOW6432Node without being told, so a 32-bit service and a 64-bit installer writing the same path write two different keys. Name the view with WithView rather than inheriting it.

Writing under HKEY_LOCAL_MACHINE needs administrator rights, and a process without them is refused when the save starts rather than part way through it.

A value name may be at most 16,383 characters, which is the registry's own limit and not this driver's.

A value name holding a backslash is legal in the registry and has no address here, so a key that holds one cannot be loaded as a map or a slice: the member it would mint is refused. A key ferry alone writes never holds one.

Everywhere that is not Windows

The module builds and its tests run on every platform, and a source or a sink with no registry behind it refuses at Bind rather than pretending. Store is where a registry of your own is handed over, which is what a test supplies.

The design records behind these decisions are in docs/adr/.

Example

Example loads an annotated struct out of one registry key.

type DB struct {
	Host string `ferry:"host"`
	Port int    `ferry:"port,default=5432"`
}

type Config struct {
	Name string `ferry:"name"`
	DB   DB     `ferry:"db"`
}

store := newMemory()
_ = store.Set(context.Background(), "", "name", winreg.Datum{Type: winreg.TypeString, Text: "checkout"})
_ = store.Set(context.Background(), "db", "host", winreg.Datum{Type: winreg.TypeString, Text: "db.internal"})

src := winreg.NewSource(winreg.LocalMachine, `SOFTWARE\Example`, winreg.Store(store))

cfg, err := ferry.Load[Config](context.Background(), src)
if err != nil {
	panic(err)
}

fmt.Printf("%s %s:%d\n", cfg.Name, cfg.DB.Host, cfg.DB.Port)
Output:
checkout db.internal:5432
Example (Dump)

Example_dump writes a struct back into the registry, and shows what the two namespaces hold afterwards: a nested struct is a subkey, and a slice is a subkey holding one value per position.

type DB struct {
	Host string `ferry:"host"`
}

type Config struct {
	Name string   `ferry:"name"`
	DB   DB       `ferry:"db"`
	Tags []string `ferry:"tags"`
}

store := newMemory()
sink := winreg.NewSink(winreg.CurrentUser, `Software\Example`, winreg.Store(store))

cfg := Config{Name: "checkout", DB: DB{Host: "db.internal"}, Tags: []string{"eu", "prod"}}
if err := ferry.Dump(context.Background(), cfg, sink); err != nil {
	panic(err)
}

for _, subkey := range slices.Sorted(maps.Keys(store.vals)) {
	for _, name := range slices.Sorted(maps.Keys(store.vals[subkey])) {
		d := store.vals[subkey][name]
		fmt.Printf("%s\t%s\t%s\t%s\n", subkey, name, d.Type, d.Text)
	}
}
Output:
	name	REG_SZ	checkout
db	host	REG_SZ	db.internal
tags	0	REG_SZ	eu
tags	1	REG_SZ	prod

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrDeeperThanLeaf = errors.New("winreg: the registry holds a subkey where this address takes a value")

ErrDeeperThanLeaf reports a subkey the registry holds where the schema maps a single value.

A map[string]string over a key holding the subkey http, with no value of that name beside it, is the case: the members of a container are whatever the key holds, and one that is a subkey is a group of values rather than a value.

It wraps ferry.ErrPlane, and it stays reachable under ferry's wrapper, so errors.Is answers for it on what ferry.Load returned.

View Source
var ErrIllegalName = errors.New("winreg: this cannot be named in the registry")

ErrIllegalName reports an address the registry has no name for.

Two shapes, and no fold rescues either. An empty part has no name at all: two backslashes with nothing between them are one backslash, so the address would be written where another address already is. A part holding a backslash is worse, because it succeeds: the registry reads it as another step down its own hierarchy, so a map key "a\b" under /m would be written as the value b under the subkey m\a and read back as a different, empty member.

A tagged field is refused at Bind and a map key that mints one is refused as it is minted, in either case before the read or the write it belongs to.

It wraps ferry.ErrPlane, and it stays reachable under ferry's wrapper, so errors.Is answers for it on what ferry.Load and ferry.Dump returned.

View Source
var ErrNoRegistry = errors.New("winreg: there is no Windows registry here")

ErrNoRegistry reports a machine with no Windows registry on it.

It is what a source or a sink built without Store refuses with, at Bind and before any load, on every operating system but Windows. Supplying a Registry is what makes this package usable elsewhere, and it is how its own tests run.

It wraps ferry.ErrPlane, and it stays reachable under ferry's wrapper, so errors.Is answers for it on what ferry.Load and ferry.Dump returned.

View Source
var ErrOption = errors.New("winreg: unusable driver option")

ErrOption reports a driver option this source or sink cannot be built with: a hive outside the five this package declares, or a View outside the three.

NewSource and NewSink take options and return no error, so this lands at Bind, which is the first moment the driver is asked for anything. It wraps ferry.ErrPlane and stays reachable under ferry's wrapper, so errors.Is answers for it on what ferry.Load and ferry.Dump returned.

View Source
var ErrUnspellable = errors.New("winreg: a registry string cannot spell this text")

ErrUnspellable reports a value ferry carries that REG_SZ cannot write down.

A registry string is UTF-16 and NUL-terminated, so two Go strings have no faithful spelling here: one holding a NUL, which every Windows reader truncates at, and one that is not valid UTF-8, which the conversion to UTF-16 replaces with U+FFFD. Both are refused loudly rather than stored mangled. A []byte field is unaffected, because bytes are written as REG_BINARY and that type carries every byte including NUL.

It wraps ferry.ErrValue rather than ferry.ErrPlane, because nothing is wrong with the registry: the value has no representation here, and retrying it is pointless in the way an ErrValue promises.

It stays reachable under ferry's wrapper, so errors.Is answers for it on what ferry.Dump returned.

View Source
var ErrValueType = errors.New("winreg: this registry value has a type ferry cannot carry")

ErrValueType reports a registry value whose own type ferry has no address for.

REG_MULTI_SZ is the case that arises: it is a sequence spelled inside one value, so a field reading it would have to take the whole list as one string or this driver would have to invent addresses the registry does not have. Neither is honest, so it is refused at the address that holds it and the field is left alone.

It wraps ferry.ErrPlane, and it stays reachable under ferry's wrapper, so errors.Is answers for it on what ferry.Load returned.

View Source
var ErrWatch = errors.New("winreg: this watch could not be opened")

ErrWatch reports a watch this driver could not open.

A registry that reports no changes is the case: Watch over one is refused, because a watch that opens successfully and never fires is the failure mode the option exists to avoid.

It wraps ferry.ErrPlane, and it stays reachable under ferry's wrapper, so errors.Is answers for it on what ferry.Load returned.

Functions

This section is empty.

Types

type Change

type Change interface {
	// Wait blocks until the change this registration was armed for happens,
	// until ctx is done, or until the watch cannot be kept.
	//
	// It reports true where a change happened, including one that landed before
	// Wait was called. False with a nil error is the watch ending quietly, which
	// is what a cancelled context produces, and any error is the watch being
	// lost.
	Wait(ctx context.Context) (bool, error)

	// Close releases the registration. It is called once, after the wait, and
	// whatever it reports is discarded: there is nothing a watcher could do with
	// it.
	Close() error
}

Change is one armed registration: one wait, and the release that follows it.

It is what Notifier.Arm answers with, and a watcher waits on it once and closes it once.

type Common

type Common interface {
	Option
	SinkOption
}

Common is a setting both halves take: WithView and Store.

They are shared because a sink writing into one view or one registry and a source reading another is a plane that cannot round trip. Source and sink are two constructors, so nothing checks that the two agree, and the way to avoid it is to build both halves from one slice of these.

func Store

func Store(r Registry) Common

Store names the registry this driver reads and writes through.

src := winreg.NewSource(winreg.CurrentUser, `Software\Example`, winreg.Store(fake))

A nil argument is the machine's own registry, which is the default and which exists on Windows and nowhere else: elsewhere, a source or a sink built without this refuses at Bind with ErrNoRegistry.

It is what makes a test hermetic, and it is the seam a registry this package does not know about arrives through - a remote one, a snapshot of a hive, or a store that is registry-shaped without being a registry.

Give the same one to both halves, for the reason Common states.

func WithView

func WithView(v View) Common

WithView chooses which side of the registry redirector this driver reads and writes.

src := winreg.NewSource(winreg.LocalMachine, `SOFTWARE\Example`, winreg.WithView(winreg.View64))

On 64-bit Windows the registry keeps two copies of parts of the tree, and a 32-bit process is redirected into WOW6432Node without being told. So a 32-bit service and a 64-bit installer writing "the same" key write two different keys, and the way out is for both of them to name the view rather than to inherit it.

It defaults to ViewNative, which is whatever the running process would get on its own.

Give the same one to both halves. A sink writing the 64-bit view and a source reading the 32-bit one never meet, and nothing checks that the two agree.

type Datum

type Datum struct {
	// Type is what the registry records this value as.
	Type Type

	// Text is the payload of every type but [TypeBinary]: the stored text of a
	// string, and the base-10 spelling of a number.
	Text string

	// Binary is the payload of a [TypeBinary] value, and nil for every other
	// type.
	Binary []byte
}

Datum is one registry value: what the registry records it as, and what it holds.

One of the two payload fields carries the value and the other is empty, and which of them is decided by Datum.Type: TypeBinary uses Binary and every other type uses Text. A number's Text is its own base-10 spelling, which is what lets a value stored as REG_DWORD reach a Go field as a number without this driver having to decide how wide the field is.

type Hive

type Hive uint8

Hive is one of the registry's predefined root keys, and it is the first argument to NewSource and NewSink.

The zero Hive names none of them, so a hive nobody chose is refused at Bind rather than silently reading HKEY_CLASSES_ROOT.

const (
	// LocalMachine is HKEY_LOCAL_MACHINE, which is machine-wide and needs
	// administrator rights to write.
	LocalMachine Hive = iota + 1

	// CurrentUser is HKEY_CURRENT_USER, which is per user and writable by that
	// user.
	CurrentUser

	// ClassesRoot is HKEY_CLASSES_ROOT.
	ClassesRoot

	// Users is HKEY_USERS.
	Users

	// CurrentConfig is HKEY_CURRENT_CONFIG.
	CurrentConfig
)

The five hives a configuration is ever kept in.

func (Hive) String

func (h Hive) String() string

String is the hive's own Win32 name, which is what a report opens with.

type Listing

type Listing struct {
	// Values is the names of the values in this key, in whatever order the
	// registry gave them. An empty name is the key's own unnamed value.
	Values []string

	// Keys is the names of the immediate subkeys of this key, one segment each
	// and never a path.
	Keys []string
}

Listing is what one subkey holds directly: the names of its values, and the names of its immediate subkeys.

The two are separate because the registry keeps them in separate namespaces, so a value host and a subkey host under one key are two objects and both appear here.

type Notifier

type Notifier interface {
	// Arm registers for the next change under the driver's own key and answers
	// with the [Change] that waits for it.
	//
	// The registration is live when Arm returns, so a change between Arm and
	// [Change.Wait] is reported by that Wait rather than missed. ctx bounds the
	// registration itself and not the wait that follows it.
	Arm(ctx context.Context) (Change, error)
}

Notifier is implemented by a Registry that can report a change under the key the driver was built over, and it is what Watch needs.

It is optional. A Registry that is no Notifier is refused at Bind when a watch was asked for, because a watch that opens successfully and never fires is the failure the option exists to avoid.

Registering and waiting are two calls rather than one, and that is the whole point of the shape. Watch arms the next registration before it runs the callback, so a registration is live for the entire time the callback and the load inside it take. An implementation that registered inside the wait would have no registration during that window and would lose every change that landed in it.

type Option

type Option interface {
	// contains filtered or unexported methods
}

Option is a setting handed to NewSource.

Three of them: WithView and Store, which are also [SinkOption]s, and Watch, which belongs to the read half alone.

func Watch

func Watch(ctx context.Context, onChange func(context.Context)) Option

Watch calls onChange whenever anything under this driver's key changes, so that a process holding a loaded value can load a fresh one.

b, err := ferry.Bind[Config](winreg.NewSource(hive, path, winreg.Watch(ctx, reload)))

func reload(ctx context.Context) {
	cfg, err := b.Load(ctx) // a reload is a load
	...                     // publish it by replacement, never by mutation
}

It is opt-in and it is the only thing in this package that runs on a goroutine of its own. A source built without it touches the registry only when a load asks it to.

The watch begins when the source is built and ends when ctx is done, which is the only way to stop it: cancel the context you gave it, and the goroutine returns. The context reaches onChange as its argument, so a deadline, a cancellation and whatever the caller put in it are all in hand there.

The whole subtree is watched, so a change to any value or any subkey under this driver's key fires it, and a change elsewhere in the hive does not.

It refuses at Bind, before any load, when the registry behind this source reports no changes and when the first registration could not be placed. On Windows the machine's own registry always reports changes; a Store of your own has to say so as well.

A key that is not there yet is not a refusal. The registration goes on the nearest key above it that does exist, so a watch over the key a first save will create fires when that save creates it, and moves down to the key itself from then on. The cost is that until the key exists a change to something else under that ancestor wakes it too, which is a spurious call and costs one load.

Sharp edges, and they are the reason this is a callback and not a stream.

onChange runs on the watching goroutine and one call at a time. A slow callback delays the next look rather than running beside itself, and changes that land while it runs are one call afterwards rather than several: the next registration is placed before the callback starts, so the whole of a slow reload is covered by it.

A panic in onChange takes the process down, exactly as it would on a goroutine the caller started. Nothing here recovers it: there is no result to hand a failure back through, and a watch that swallowed the panic would leave a process that has silently stopped reloading.

Watching starts when the source is built, so it starts before ferry.Bind has handed back the binding the callback wants to load through, and a change can land while there is nothing yet to load through. A Signal from github.com/onhotpath/ferry/watch is what to pass here in that case: its Changed method records such a change rather than losing it, and the stream that opens afterwards begins with that reload.

A call says the key may have changed and nothing more. Load to find out what it holds now, which is correct whether the change was real or a rewrite of the same bytes.

A dump through Sink over the same key fires it, so a process that both watches and saves its own configuration hears its own writes. Nothing here suppresses that.

Losing the watch fires the callback once and stops. There is nowhere to report it, and the load that follows reports it through a surface the caller already handles. A cancelled context stops silently instead, so only losing the watch speaks.

type Registry

type Registry interface {
	// Get answers with the value stored at name under subkey, and with found
	// false where the registry holds no such value.
	Get(ctx context.Context, subkey, name string) (Datum, bool, error)

	// List answers with the value names and the immediate subkey names under
	// subkey, and with found false where the subkey is not there. A subkey that
	// exists and holds nothing is found with an empty [Listing], which is what
	// tells a container that is there and empty from one that was never written.
	List(ctx context.Context, subkey string) (Listing, bool, error)

	// Set writes one value, creating subkey and everything above it as needed,
	// and replacing whatever was at the name before including its type.
	Set(ctx context.Context, subkey, name string, d Datum) error

	// Create makes subkey and everything above it, and does nothing where it is
	// already there. The empty subkey is the driver's own key, so Create with it
	// is what a sink asks at the open to find out whether it may write at all.
	Create(ctx context.Context, subkey string) error

	// DeleteValue removes one value, and reports nothing where the value or the
	// subkey holding it is not there.
	DeleteValue(ctx context.Context, subkey, name string) error

	// DeleteKey removes one subkey and everything under it, and reports nothing
	// where it is not there.
	DeleteKey(ctx context.Context, subkey string) error
}

Registry is the Windows registry as this driver needs it: read one value, list what lies directly under one subkey, write one value, create one subkey, remove one value, remove one subkey and everything under it.

It is an interface rather than a dependency, so a test double, an in-memory store or a remote registry is a few lines and this package never learns which of them it is talking to. Store is where one is handed over, and a source or a sink built without one reaches the machine's own registry - which exists on Windows and nowhere else, so everywhere else the driver refuses at Bind.

Every subkey is relative to the hive and subkey path the source or the sink was constructed with, and the empty subkey is that key itself. The empty value name is the key's own unnamed value, which the registry editor shows as (Default).

Five things an implementer owns.

Absence is a result and not an error. Get reports a value the registry does not hold with found false and a nil error, and List does the same for a subkey that is not there, so a backend's own not-found stays distinguishable from a real failure. A zero-length value is a value the registry holds.

Removal is idempotent. DeleteValue and DeleteKey report nothing for an object that is not there, and DeleteKey removes everything beneath the subkey as well as the subkey itself, because the registry's own delete refuses a key that still has children and this driver has no use for that refusal.

Creation is implicit on the write path. Set creates every subkey on the way down to the value it writes, so a driver that stages a write at a\b\c does not have to create a and a\b first.

Cancellation is yours. The driver hands its caller's context to every call and adds no deadline of its own, so an implementation that ignores the context is the only thing standing between a cancelled load and a blocked one.

Safety for use from many goroutines at once is yours, and it is ordinary rather than exotic. A source or a sink is constructed once and a binding is held for the life of a process, so one of these is reached from wherever a load or a save happens - and this package's reader declares that it tolerates overlapping calls, so one load can reach it from several goroutines too.

type Sink

type Sink struct {
	// contains filtered or unexported fields
}

Sink is the write half of a registry plane.

sink := winreg.NewSink(winreg.CurrentUser, `Software\Example`)
err := ferry.Dump(ctx, cfg, sink)

It stages every write and performs them together at the end, so a save that fails leaves the registry untouched and one save reports every address it could not write rather than stopping at the first.

Staging is not a transaction. The writes go through the registry one value at a time, in the order the walk produced them, so a machine that fails part way through the commit is left part way through it. The registry does have transactions, and Microsoft deprecated them; this driver does not use them.

func NewSink

func NewSink(hive Hive, subkey string, opts ...SinkOption) *Sink

NewSink builds a sink over one subkey of one hive.

sink := winreg.NewSink(winreg.CurrentUser, `Software\Example`)
err := ferry.Dump(ctx, cfg, sink)

Give it the same Common settings the source has. Nothing checks that the two agree, and a sink writing the 64-bit view with a source reading the 32-bit one is a round trip that loses everything.

It touches nothing, and in particular it does not check that the key can be written. A sink over a hive this process may not write is legal to build, and the save refuses when it starts.

func (*Sink) Bind

func (s *Sink) Bind(addrs *ferry.AddressSet) (ferry.OpenWriterFunc, error)

Bind computes this schema's registry keys and checks them, exactly as Source.Bind does and for the same reasons.

It does no I/O, so a sink binds successfully against a hive it may not be allowed to write. That refusal lands when the save starts, which is before anything has been written rather than part way through.

type SinkOption

type SinkOption interface {
	// contains filtered or unexported methods
}

SinkOption is a setting handed to NewSink.

Two of them, WithView and Store, and both are [Option]s as well.

It is a separate type from Option so that each constructor takes the settings that mean something to it, and the other way round is a compile error rather than a setting that is quietly ignored.

type Source

type Source struct {
	// contains filtered or unexported fields
}

Source is the read half of a registry plane.

src := winreg.NewSource(winreg.LocalMachine, `SOFTWARE\Example`)
cfg, err := ferry.Load[Config](ctx, src)

It is a separate type from Sink, so a round trip names the key twice. The repetition buys the refusal being a compile error: code handed only a Source cannot save through it.

One Source may be used by many loads at once, from many goroutines, and so may a binding it hands back: the keys a binding holds are computed once, at Bind, and nothing writes to them afterwards.

func NewSource

func NewSource(hive Hive, subkey string, opts ...Option) *Source

NewSource builds a source over one subkey of one hive.

src := winreg.NewSource(winreg.CurrentUser, `Software\Example`)
cfg, err := ferry.Load[Config](ctx, src)

The subkey is a path under the hive and may be empty, which is the hive itself. Every address is read at or under it.

With no options it reads the machine's own registry in the view the running process would get. Change either with Store and WithView.

It touches nothing, and starts nothing, unless it is given Watch. That is the one setting that does something before a load: it opens a change notification here, on the caller's own goroutine, and watches from a goroutine of its own until the context it was given is done. A watch that cannot be opened is reported at Bind, because this call returns no error.

func (*Source) Bind

func (s *Source) Bind(addrs *ferry.AddressSet) (ferry.OpenFunc, error)

Bind computes this schema's registry keys and checks them, and it is where a schema this plane cannot hold is refused.

Two things are checked, before anything is read: that every address has a registry name at all, and that no two of one kind fold to the same name. The registry is case-insensitive, so /Host and /host are one value there and a schema naming both is refused here, naming both, rather than silently losing one of them.

It does no I/O, so it succeeds whatever the registry holds, and a source built with an option it cannot use is refused here rather than at the first read.

type Type

type Type uint8

Type is what the registry records a value as, and it is the type tag stored beside the data rather than anything ferry decides.

const (
	// TypeString is REG_SZ, and it is the only type this driver ever writes text
	// as.
	TypeString Type = iota

	// TypeExpandString is REG_EXPAND_SZ. It is read as the text that is actually
	// stored, never with its %VARIABLES% expanded.
	TypeExpandString

	// TypeDWord is REG_DWORD, a 32-bit unsigned integer.
	TypeDWord

	// TypeQWord is REG_QWORD, a 64-bit unsigned integer.
	TypeQWord

	// TypeBinary is REG_BINARY, and it is the only type this driver ever writes
	// bytes as.
	TypeBinary

	// TypeMultiString is REG_MULTI_SZ, a sequence spelled inside one value.
	TypeMultiString

	// TypeOther is every other registry type, which this driver reads as a value
	// it cannot carry rather than guessing at it.
	TypeOther
)

The registry value types this driver has an opinion about. Everything else the registry can hold arrives as TypeOther and is refused with ErrValueType.

func (Type) String

func (t Type) String() string

String is the Win32 name of the type, which is what a refusal prints.

type View

type View uint8

View chooses which side of the registry redirector a 32-bit and a 64-bit process see, and it is WithView's subject.

const (
	// ViewNative is the view the running process gets by default: a 32-bit
	// process on 64-bit Windows is redirected into WOW6432Node and a 64-bit one
	// is not. It is the default.
	ViewNative View = iota

	// View64 is the 64-bit view, whatever the process's own bitness.
	View64

	// View32 is the 32-bit view, under WOW6432Node.
	View32
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL