// Copyright (c) Tailscale Inc & contributors // SPDX-License-Identifier: BSD-3-Clause // Package prefs contains types and functions to work with arbitrary // preference hierarchies. // // Specifically, the package provides [Item], [List], [Map], [StructList] and [StructMap] // types which represent individual preferences in a user-defined prefs struct. // A valid prefs struct must contain one or more exported fields of the preference types, // either directly or within nested structs, but not pointers to these types. // Additionally to preferences, a prefs struct may contain any number of // non-preference fields that will be marshalled and unmarshalled but are // otherwise ignored by the prefs package. // // The preference types are compatible with the [tailscale.com/cmd/viewer] and // [tailscale.com/cmd/cloner] utilities. It is recommended to generate a read-only view // of the user-defined prefs structure and use it in place of prefs whenever the prefs // should not be modified. package prefs import ( "errors" jsonv2 "github.com/go-json-experiment/json" "github.com/go-json-experiment/json/jsontext" "tailscale.com/types/opt" ) var ( // ErrManaged is the error returned when attempting to modify a managed preference. ErrManaged = errors.New("cannot modify a managed preference") // ErrReadOnly is the error returned when attempting to modify a read-only preference. ErrReadOnly = errors.New("cannot modify a read-only preference") ) // metadata holds type-agnostic preference metadata. type metadata struct { // Managed indicates whether the preference is managed via MDM, Group Policy, or other means. Managed bool `json:",omitzero"` // ReadOnly indicates whether the preference is read-only due to any other reasons, // such as user's access rights. ReadOnly bool `json:",omitzero"` } // serializable is a JSON-serializable preference data. type serializable[T any] struct { // Value is an optional preference value that is set when the preference is // configured by the user or managed by an admin. Value opt.Value[T] `json:",omitzero"` // Default is the default preference value to be used // when the preference has not been configured. Default T `json:",omitzero"` // Metadata is any additional type-agnostic preference metadata to be serialized. // // The `inline` tag option was renamed to `embed` in Go 1.27's // encoding/json/v2, but the pinned go-json-experiment module // (used on older Go versions) only knows `inline`. Each // implementation ignores the option it doesn't know, so specify // both until we require Go 1.27 and drop `inline`. // //lint:ignore SA5008 staticcheck doesn't know Go 1.27's `embed` option yet Metadata metadata `json:",inline,embed"` } // preference is an embeddable type that provides a common implementation for // concrete preference types, such as [Item], [List], [Map], [StructList] and [StructMap]. type preference[T any] struct { s serializable[T] } // preferenceOf returns a preference with the specified value and/or [Options]. func preferenceOf[T any](v opt.Value[T], opts ...Options) preference[T] { var m metadata for _, o := range opts { o(&m) } return preference[T]{serializable[T]{Value: v, Metadata: m}} } // IsSet reports whether p has a value set. func (p preference[T]) IsSet() bool { return p.s.Value.IsSet() } // Value returns the value of p if the preference has a value set. // Otherwise, it returns its default value. func (p preference[T]) Value() T { val, _ := p.ValueOk() return val } // ValueOk returns the value of p and true if the preference has a value set. // Otherwise, it returns its default value and false. func (p preference[T]) ValueOk() (val T, ok bool) { if val, ok = p.s.Value.GetOk(); ok { return val, true } return p.DefaultValue(), false } // SetValue configures the preference with the specified value. // It fails and returns [ErrManaged] if p is a managed preference, // and [ErrReadOnly] if p is a read-only preference. func (p *preference[T]) SetValue(val T) error { switch { case p.s.Metadata.Managed: return ErrManaged case p.s.Metadata.ReadOnly: return ErrReadOnly default: p.s.Value.Set(val) return nil } } // ClearValue resets the preference to an unconfigured state. // It fails and returns [ErrManaged] if p is a managed preference, // and [ErrReadOnly] if p is a read-only preference. func (p *preference[T]) ClearValue() error { switch { case p.s.Metadata.Managed: return ErrManaged case p.s.Metadata.ReadOnly: return ErrReadOnly default: p.s.Value.Clear() return nil } } // DefaultValue returns the default value of p. func (p preference[T]) DefaultValue() T { return p.s.Default } // SetDefaultValue sets the default value of p. func (p *preference[T]) SetDefaultValue(def T) { p.s.Default = def } // IsManaged reports whether p is managed via MDM, Group Policy, or similar means. func (p preference[T]) IsManaged() bool { return p.s.Metadata.Managed } // SetManagedValue configures the preference with the specified value // and marks the preference as managed. func (p *preference[T]) SetManagedValue(val T) { p.s.Value.Set(val) p.s.Metadata.Managed = true } // ClearManaged clears the managed flag of the preference without altering its value. func (p *preference[T]) ClearManaged() { p.s.Metadata.Managed = false } // IsReadOnly reports whether p is read-only and cannot be changed by user. func (p preference[T]) IsReadOnly() bool { return p.s.Metadata.ReadOnly || p.s.Metadata.Managed } // SetReadOnly sets the read-only status of p, preventing changes by a user if set to true. func (p *preference[T]) SetReadOnly(readonly bool) { p.s.Metadata.ReadOnly = readonly } var ( _ jsonv2.MarshalerTo = (*preference[struct{}])(nil) _ jsonv2.UnmarshalerFrom = (*preference[struct{}])(nil) ) // MarshalJSONTo implements [jsonv2.MarshalerTo]. func (p preference[T]) MarshalJSONTo(out *jsontext.Encoder) error { // Pin v2 semantics so the wire format (notably the handling of // the `inline` and `omitzero` tag options in [serializable]) // stays the same even when this method is reached via // encoding/json v1, which as of Go 1.27 dispatches here with v1 // options that would otherwise leak into this call. return jsonv2.MarshalEncode(out, &p.s, jsonv2.DefaultOptionsV2()) } // UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom]. func (p *preference[T]) UnmarshalJSONFrom(in *jsontext.Decoder) error { // Pin v2 semantics; see MarshalJSONTo. return jsonv2.UnmarshalDecode(in, &p.s, jsonv2.DefaultOptionsV2()) } // MarshalJSON implements [json.Marshaler]. func (p preference[T]) MarshalJSON() ([]byte, error) { return jsonv2.Marshal(p) // uses MarshalJSONTo } // UnmarshalJSON implements [json.Unmarshaler]. func (p *preference[T]) UnmarshalJSON(b []byte) error { return jsonv2.Unmarshal(b, p) // uses UnmarshalJSONFrom }