summaryrefslogtreecommitdiffhomepage
path: root/util
diff options
context:
space:
mode:
authorNick Khyl <nickk@tailscale.com>2024-12-05 13:16:48 -0600
committerNick Khyl <nickk@tailscale.com>2024-12-05 13:16:48 -0600
commit0267fe83b200f1702a2fa0a395442c02a053fadb (patch)
tree63654c55225eeb834de59a5a0bc8d19033c6145b /util
parent87546a5edf6b6503a87eeb2d666baba57398a066 (diff)
downloadtailscale-1.78.0.tar.xz
tailscale-1.78.0.zip
VERSION.txt: this is v1.78.0v1.78.0
Signed-off-by: Nick Khyl <nickk@tailscale.com>
Diffstat (limited to 'util')
-rw-r--r--util/cibuild/cibuild.go28
-rw-r--r--util/cstruct/cstruct.go356
-rw-r--r--util/cstruct/cstruct_example_test.go146
-rw-r--r--util/deephash/debug.go74
-rw-r--r--util/deephash/pointer.go228
-rw-r--r--util/deephash/pointer_norace.go26
-rw-r--r--util/deephash/pointer_race.go198
-rw-r--r--util/deephash/testtype/testtype.go30
-rw-r--r--util/dirwalk/dirwalk.go106
-rw-r--r--util/dirwalk/dirwalk_linux.go334
-rw-r--r--util/dirwalk/dirwalk_test.go182
-rw-r--r--util/goroutines/goroutines.go186
-rw-r--r--util/goroutines/goroutines_test.go58
-rw-r--r--util/groupmember/groupmember.go58
-rw-r--r--util/hashx/block512.go394
-rw-r--r--util/httphdr/httphdr.go394
-rw-r--r--util/httphdr/httphdr_test.go192
-rw-r--r--util/httpm/httpm.go72
-rw-r--r--util/httpm/httpm_test.go74
-rw-r--r--util/jsonutil/types.go32
-rw-r--r--util/jsonutil/unmarshal.go178
-rw-r--r--util/lineread/lineread.go74
-rw-r--r--util/linuxfw/linuxfwtest/linuxfwtest.go62
-rw-r--r--util/linuxfw/linuxfwtest/linuxfwtest_unsupported.go36
-rw-r--r--util/linuxfw/nftables_types.go190
-rw-r--r--util/mak/mak.go140
-rw-r--r--util/mak/mak_test.go176
-rw-r--r--util/multierr/multierr.go272
-rw-r--r--util/must/must.go50
-rw-r--r--util/osdiag/mksyscall.go26
-rw-r--r--util/osdiag/osdiag_windows_test.go256
-rw-r--r--util/osshare/filesharingstatus_noop.go24
-rw-r--r--util/pidowner/pidowner.go48
-rw-r--r--util/pidowner/pidowner_noimpl.go16
-rw-r--r--util/pidowner/pidowner_windows.go70
-rw-r--r--util/precompress/precompress.go258
-rw-r--r--util/quarantine/quarantine.go28
-rw-r--r--util/quarantine/quarantine_darwin.go112
-rw-r--r--util/quarantine/quarantine_default.go28
-rw-r--r--util/quarantine/quarantine_windows.go58
-rw-r--r--util/race/race_test.go198
-rw-r--r--util/racebuild/off.go16
-rw-r--r--util/racebuild/on.go16
-rw-r--r--util/racebuild/racebuild.go12
-rw-r--r--util/rands/rands.go50
-rw-r--r--util/rands/rands_test.go30
-rw-r--r--util/set/handle.go56
-rw-r--r--util/set/slice_test.go112
-rw-r--r--util/sysresources/memory.go20
-rw-r--r--util/sysresources/memory_bsd.go32
-rw-r--r--util/sysresources/memory_darwin.go32
-rw-r--r--util/sysresources/memory_linux.go38
-rw-r--r--util/sysresources/memory_unsupported.go16
-rw-r--r--util/sysresources/sysresources.go12
-rw-r--r--util/sysresources/sysresources_test.go50
-rw-r--r--util/systemd/doc.go26
-rw-r--r--util/systemd/systemd_linux.go154
-rw-r--r--util/systemd/systemd_nonlinux.go18
-rw-r--r--util/testenv/testenv.go42
-rw-r--r--util/truncate/truncate_test.go72
-rw-r--r--util/uniq/slice.go124
-rw-r--r--util/winutil/authenticode/mksyscall.go36
-rw-r--r--util/winutil/policy/policy_windows.go310
-rw-r--r--util/winutil/policy/policy_windows_test.go76
64 files changed, 3409 insertions, 3409 deletions
diff --git a/util/cibuild/cibuild.go b/util/cibuild/cibuild.go
index c1e337f9a..c3dee6154 100644
--- a/util/cibuild/cibuild.go
+++ b/util/cibuild/cibuild.go
@@ -1,14 +1,14 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package cibuild reports runtime CI information.
-package cibuild
-
-import "os"
-
-// On reports whether the current binary is executing on a CI system.
-func On() bool {
- // CI env variable is set by GitHub.
- // https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables
- return os.Getenv("GITHUB_ACTIONS") != "" || os.Getenv("CI") == "true"
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package cibuild reports runtime CI information.
+package cibuild
+
+import "os"
+
+// On reports whether the current binary is executing on a CI system.
+func On() bool {
+ // CI env variable is set by GitHub.
+ // https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables
+ return os.Getenv("GITHUB_ACTIONS") != "" || os.Getenv("CI") == "true"
+}
diff --git a/util/cstruct/cstruct.go b/util/cstruct/cstruct.go
index 464dc5dc3..e32c90830 100644
--- a/util/cstruct/cstruct.go
+++ b/util/cstruct/cstruct.go
@@ -1,178 +1,178 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package cstruct provides a helper for decoding binary data that is in the
-// form of a padded C structure.
-package cstruct
-
-import (
- "errors"
- "io"
-
- "github.com/josharian/native"
-)
-
-// Size of a pointer-typed value, in bits
-const pointerSize = 32 << (^uintptr(0) >> 63)
-
-// We assume that non-64-bit platforms are 32-bit; we don't expect Go to run on
-// a 16- or 8-bit architecture any time soon.
-const is64Bit = pointerSize == 64
-
-// Decoder reads and decodes padded fields from a slice of bytes. All fields
-// are decoded with native endianness.
-//
-// Methods of a Decoder do not return errors, but rather store any error within
-// the Decoder. The first error can be obtained via the Err method; after the
-// first error, methods will return the zero value for their type.
-type Decoder struct {
- b []byte
- off int
- err error
- dbuf [8]byte // for decoding
-}
-
-// NewDecoder creates a Decoder from a byte slice.
-func NewDecoder(b []byte) *Decoder {
- return &Decoder{b: b}
-}
-
-var errUnsupportedSize = errors.New("unsupported size")
-
-func padBytes(offset, size int) int {
- if offset == 0 || size == 1 {
- return 0
- }
- remainder := offset % size
- return size - remainder
-}
-
-func (d *Decoder) getField(b []byte) error {
- size := len(b)
-
- // We only support fields that are multiples of 2 (or 1-sized)
- if size != 1 && size&1 == 1 {
- return errUnsupportedSize
- }
-
- // Fields are aligned to their size
- padBytes := padBytes(d.off, size)
- if d.off+size+padBytes > len(d.b) {
- return io.EOF
- }
- d.off += padBytes
-
- copy(b, d.b[d.off:d.off+size])
- d.off += size
- return nil
-}
-
-// Err returns the first error that was encountered by this Decoder.
-func (d *Decoder) Err() error {
- return d.err
-}
-
-// Offset returns the current read offset for data in the buffer.
-func (d *Decoder) Offset() int {
- return d.off
-}
-
-// Byte returns a single byte from the buffer.
-func (d *Decoder) Byte() byte {
- if d.err != nil {
- return 0
- }
-
- if err := d.getField(d.dbuf[0:1]); err != nil {
- d.err = err
- return 0
- }
- return d.dbuf[0]
-}
-
-// Byte returns a number of bytes from the buffer based on the size of the
-// input slice. No padding is applied.
-//
-// If an error is encountered or this Decoder has previously encountered an
-// error, no changes are made to the provided buffer.
-func (d *Decoder) Bytes(b []byte) {
- if d.err != nil {
- return
- }
-
- // No padding for byte slices
- size := len(b)
- if d.off+size >= len(d.b) {
- d.err = io.EOF
- return
- }
- copy(b, d.b[d.off:d.off+size])
- d.off += size
-}
-
-// Uint16 returns a uint16 decoded from the buffer.
-func (d *Decoder) Uint16() uint16 {
- if d.err != nil {
- return 0
- }
-
- if err := d.getField(d.dbuf[0:2]); err != nil {
- d.err = err
- return 0
- }
- return native.Endian.Uint16(d.dbuf[0:2])
-}
-
-// Uint32 returns a uint32 decoded from the buffer.
-func (d *Decoder) Uint32() uint32 {
- if d.err != nil {
- return 0
- }
-
- if err := d.getField(d.dbuf[0:4]); err != nil {
- d.err = err
- return 0
- }
- return native.Endian.Uint32(d.dbuf[0:4])
-}
-
-// Uint64 returns a uint64 decoded from the buffer.
-func (d *Decoder) Uint64() uint64 {
- if d.err != nil {
- return 0
- }
-
- if err := d.getField(d.dbuf[0:8]); err != nil {
- d.err = err
- return 0
- }
- return native.Endian.Uint64(d.dbuf[0:8])
-}
-
-// Uintptr returns a uintptr decoded from the buffer.
-func (d *Decoder) Uintptr() uintptr {
- if d.err != nil {
- return 0
- }
-
- if is64Bit {
- return uintptr(d.Uint64())
- } else {
- return uintptr(d.Uint32())
- }
-}
-
-// Int16 returns a int16 decoded from the buffer.
-func (d *Decoder) Int16() int16 {
- return int16(d.Uint16())
-}
-
-// Int32 returns a int32 decoded from the buffer.
-func (d *Decoder) Int32() int32 {
- return int32(d.Uint32())
-}
-
-// Int64 returns a int64 decoded from the buffer.
-func (d *Decoder) Int64() int64 {
- return int64(d.Uint64())
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package cstruct provides a helper for decoding binary data that is in the
+// form of a padded C structure.
+package cstruct
+
+import (
+ "errors"
+ "io"
+
+ "github.com/josharian/native"
+)
+
+// Size of a pointer-typed value, in bits
+const pointerSize = 32 << (^uintptr(0) >> 63)
+
+// We assume that non-64-bit platforms are 32-bit; we don't expect Go to run on
+// a 16- or 8-bit architecture any time soon.
+const is64Bit = pointerSize == 64
+
+// Decoder reads and decodes padded fields from a slice of bytes. All fields
+// are decoded with native endianness.
+//
+// Methods of a Decoder do not return errors, but rather store any error within
+// the Decoder. The first error can be obtained via the Err method; after the
+// first error, methods will return the zero value for their type.
+type Decoder struct {
+ b []byte
+ off int
+ err error
+ dbuf [8]byte // for decoding
+}
+
+// NewDecoder creates a Decoder from a byte slice.
+func NewDecoder(b []byte) *Decoder {
+ return &Decoder{b: b}
+}
+
+var errUnsupportedSize = errors.New("unsupported size")
+
+func padBytes(offset, size int) int {
+ if offset == 0 || size == 1 {
+ return 0
+ }
+ remainder := offset % size
+ return size - remainder
+}
+
+func (d *Decoder) getField(b []byte) error {
+ size := len(b)
+
+ // We only support fields that are multiples of 2 (or 1-sized)
+ if size != 1 && size&1 == 1 {
+ return errUnsupportedSize
+ }
+
+ // Fields are aligned to their size
+ padBytes := padBytes(d.off, size)
+ if d.off+size+padBytes > len(d.b) {
+ return io.EOF
+ }
+ d.off += padBytes
+
+ copy(b, d.b[d.off:d.off+size])
+ d.off += size
+ return nil
+}
+
+// Err returns the first error that was encountered by this Decoder.
+func (d *Decoder) Err() error {
+ return d.err
+}
+
+// Offset returns the current read offset for data in the buffer.
+func (d *Decoder) Offset() int {
+ return d.off
+}
+
+// Byte returns a single byte from the buffer.
+func (d *Decoder) Byte() byte {
+ if d.err != nil {
+ return 0
+ }
+
+ if err := d.getField(d.dbuf[0:1]); err != nil {
+ d.err = err
+ return 0
+ }
+ return d.dbuf[0]
+}
+
+// Byte returns a number of bytes from the buffer based on the size of the
+// input slice. No padding is applied.
+//
+// If an error is encountered or this Decoder has previously encountered an
+// error, no changes are made to the provided buffer.
+func (d *Decoder) Bytes(b []byte) {
+ if d.err != nil {
+ return
+ }
+
+ // No padding for byte slices
+ size := len(b)
+ if d.off+size >= len(d.b) {
+ d.err = io.EOF
+ return
+ }
+ copy(b, d.b[d.off:d.off+size])
+ d.off += size
+}
+
+// Uint16 returns a uint16 decoded from the buffer.
+func (d *Decoder) Uint16() uint16 {
+ if d.err != nil {
+ return 0
+ }
+
+ if err := d.getField(d.dbuf[0:2]); err != nil {
+ d.err = err
+ return 0
+ }
+ return native.Endian.Uint16(d.dbuf[0:2])
+}
+
+// Uint32 returns a uint32 decoded from the buffer.
+func (d *Decoder) Uint32() uint32 {
+ if d.err != nil {
+ return 0
+ }
+
+ if err := d.getField(d.dbuf[0:4]); err != nil {
+ d.err = err
+ return 0
+ }
+ return native.Endian.Uint32(d.dbuf[0:4])
+}
+
+// Uint64 returns a uint64 decoded from the buffer.
+func (d *Decoder) Uint64() uint64 {
+ if d.err != nil {
+ return 0
+ }
+
+ if err := d.getField(d.dbuf[0:8]); err != nil {
+ d.err = err
+ return 0
+ }
+ return native.Endian.Uint64(d.dbuf[0:8])
+}
+
+// Uintptr returns a uintptr decoded from the buffer.
+func (d *Decoder) Uintptr() uintptr {
+ if d.err != nil {
+ return 0
+ }
+
+ if is64Bit {
+ return uintptr(d.Uint64())
+ } else {
+ return uintptr(d.Uint32())
+ }
+}
+
+// Int16 returns a int16 decoded from the buffer.
+func (d *Decoder) Int16() int16 {
+ return int16(d.Uint16())
+}
+
+// Int32 returns a int32 decoded from the buffer.
+func (d *Decoder) Int32() int32 {
+ return int32(d.Uint32())
+}
+
+// Int64 returns a int64 decoded from the buffer.
+func (d *Decoder) Int64() int64 {
+ return int64(d.Uint64())
+}
diff --git a/util/cstruct/cstruct_example_test.go b/util/cstruct/cstruct_example_test.go
index 17032267b..a36cbf9f0 100644
--- a/util/cstruct/cstruct_example_test.go
+++ b/util/cstruct/cstruct_example_test.go
@@ -1,73 +1,73 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Only built on 64-bit platforms to avoid complexity
-
-//go:build amd64 || arm64 || mips64le || ppc64le || riscv64
-
-package cstruct
-
-import "fmt"
-
-// This test provides a semi-realistic example of how you can
-// use this package to decode a C structure.
-func ExampleDecoder() {
- // Our example C structure:
- // struct mystruct {
- // char *p;
- // char c;
- // /* implicit: char _pad[3]; */
- // int x;
- // };
- //
- // The Go structure definition:
- type myStruct struct {
- Ptr uintptr
- Ch byte
- Intval uint32
- }
-
- // Our "in-memory" version of the above structure
- buf := []byte{
- 1, 2, 3, 4, 0, 0, 0, 0, // ptr
- 5, // ch
- 99, 99, 99, // padding
- 78, 6, 0, 0, // x
- }
- d := NewDecoder(buf)
-
- // Decode the structure; if one of these function returns an error,
- // then subsequent decoder functions will return the zero value.
- var x myStruct
- x.Ptr = d.Uintptr()
- x.Ch = d.Byte()
- x.Intval = d.Uint32()
-
- // Note that per the Go language spec:
- // [...] when evaluating the operands of an expression, assignment,
- // or return statement, all function calls, method calls, and
- // (channel) communication operations are evaluated in lexical
- // left-to-right order
- //
- // Since each field is assigned via a function call, one could use the
- // following snippet to decode the struct.
- // x := myStruct{
- // Ptr: d.Uintptr(),
- // Ch: d.Byte(),
- // Intval: d.Uint32(),
- // }
- //
- // However, this means that reordering the fields in the initialization
- // statement–normally a semantically identical operation–would change
- // the way the structure is parsed. Thus we do it as above with
- // explicit ordering.
-
- // After finishing with the decoder, check errors
- if err := d.Err(); err != nil {
- panic(err)
- }
-
- // Print the decoder offset and structure
- fmt.Printf("off=%d struct=%#v\n", d.Offset(), x)
- // Output: off=16 struct=cstruct.myStruct{Ptr:0x4030201, Ch:0x5, Intval:0x64e}
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Only built on 64-bit platforms to avoid complexity
+
+//go:build amd64 || arm64 || mips64le || ppc64le || riscv64
+
+package cstruct
+
+import "fmt"
+
+// This test provides a semi-realistic example of how you can
+// use this package to decode a C structure.
+func ExampleDecoder() {
+ // Our example C structure:
+ // struct mystruct {
+ // char *p;
+ // char c;
+ // /* implicit: char _pad[3]; */
+ // int x;
+ // };
+ //
+ // The Go structure definition:
+ type myStruct struct {
+ Ptr uintptr
+ Ch byte
+ Intval uint32
+ }
+
+ // Our "in-memory" version of the above structure
+ buf := []byte{
+ 1, 2, 3, 4, 0, 0, 0, 0, // ptr
+ 5, // ch
+ 99, 99, 99, // padding
+ 78, 6, 0, 0, // x
+ }
+ d := NewDecoder(buf)
+
+ // Decode the structure; if one of these function returns an error,
+ // then subsequent decoder functions will return the zero value.
+ var x myStruct
+ x.Ptr = d.Uintptr()
+ x.Ch = d.Byte()
+ x.Intval = d.Uint32()
+
+ // Note that per the Go language spec:
+ // [...] when evaluating the operands of an expression, assignment,
+ // or return statement, all function calls, method calls, and
+ // (channel) communication operations are evaluated in lexical
+ // left-to-right order
+ //
+ // Since each field is assigned via a function call, one could use the
+ // following snippet to decode the struct.
+ // x := myStruct{
+ // Ptr: d.Uintptr(),
+ // Ch: d.Byte(),
+ // Intval: d.Uint32(),
+ // }
+ //
+ // However, this means that reordering the fields in the initialization
+ // statement–normally a semantically identical operation–would change
+ // the way the structure is parsed. Thus we do it as above with
+ // explicit ordering.
+
+ // After finishing with the decoder, check errors
+ if err := d.Err(); err != nil {
+ panic(err)
+ }
+
+ // Print the decoder offset and structure
+ fmt.Printf("off=%d struct=%#v\n", d.Offset(), x)
+ // Output: off=16 struct=cstruct.myStruct{Ptr:0x4030201, Ch:0x5, Intval:0x64e}
+}
diff --git a/util/deephash/debug.go b/util/deephash/debug.go
index 50b3d5605..ff417e583 100644
--- a/util/deephash/debug.go
+++ b/util/deephash/debug.go
@@ -1,37 +1,37 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build deephash_debug
-
-package deephash
-
-import "fmt"
-
-func (h *hasher) HashBytes(b []byte) {
- fmt.Printf("B(%q)+", b)
- h.Block512.HashBytes(b)
-}
-func (h *hasher) HashString(s string) {
- fmt.Printf("S(%q)+", s)
- h.Block512.HashString(s)
-}
-func (h *hasher) HashUint8(n uint8) {
- fmt.Printf("U8(%d)+", n)
- h.Block512.HashUint8(n)
-}
-func (h *hasher) HashUint16(n uint16) {
- fmt.Printf("U16(%d)+", n)
- h.Block512.HashUint16(n)
-}
-func (h *hasher) HashUint32(n uint32) {
- fmt.Printf("U32(%d)+", n)
- h.Block512.HashUint32(n)
-}
-func (h *hasher) HashUint64(n uint64) {
- fmt.Printf("U64(%d)+", n)
- h.Block512.HashUint64(n)
-}
-func (h *hasher) Sum(b []byte) []byte {
- fmt.Println("FIN")
- return h.Block512.Sum(b)
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build deephash_debug
+
+package deephash
+
+import "fmt"
+
+func (h *hasher) HashBytes(b []byte) {
+ fmt.Printf("B(%q)+", b)
+ h.Block512.HashBytes(b)
+}
+func (h *hasher) HashString(s string) {
+ fmt.Printf("S(%q)+", s)
+ h.Block512.HashString(s)
+}
+func (h *hasher) HashUint8(n uint8) {
+ fmt.Printf("U8(%d)+", n)
+ h.Block512.HashUint8(n)
+}
+func (h *hasher) HashUint16(n uint16) {
+ fmt.Printf("U16(%d)+", n)
+ h.Block512.HashUint16(n)
+}
+func (h *hasher) HashUint32(n uint32) {
+ fmt.Printf("U32(%d)+", n)
+ h.Block512.HashUint32(n)
+}
+func (h *hasher) HashUint64(n uint64) {
+ fmt.Printf("U64(%d)+", n)
+ h.Block512.HashUint64(n)
+}
+func (h *hasher) Sum(b []byte) []byte {
+ fmt.Println("FIN")
+ return h.Block512.Sum(b)
+}
diff --git a/util/deephash/pointer.go b/util/deephash/pointer.go
index aafae47a2..71b11d7ff 100644
--- a/util/deephash/pointer.go
+++ b/util/deephash/pointer.go
@@ -1,114 +1,114 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package deephash
-
-import (
- "net/netip"
- "reflect"
- "time"
- "unsafe"
-)
-
-// unsafePointer is an untyped pointer.
-// It is the caller's responsibility to call operations on the correct type.
-//
-// This pointer only ever points to a small set of kinds or types:
-// time.Time, netip.Addr, string, array, slice, struct, map, pointer, interface,
-// or a pointer to memory that is directly hashable.
-//
-// Arrays are represented as pointers to the first element.
-// Structs are represented as pointers to the first field.
-// Slices are represented as pointers to a slice header.
-// Pointers are represented as pointers to a pointer.
-//
-// We do not support direct operations on maps and interfaces, and instead
-// rely on pointer.asValue to convert the pointer back to a reflect.Value.
-// Conversion of an unsafe.Pointer to reflect.Value guarantees that the
-// read-only flag in the reflect.Value is unpopulated, avoiding panics that may
-// otherwise have occurred since the value was obtained from an unexported field.
-type unsafePointer struct{ p unsafe.Pointer }
-
-func unsafePointerOf(v reflect.Value) unsafePointer {
- return unsafePointer{v.UnsafePointer()}
-}
-func (p unsafePointer) isNil() bool {
- return p.p == nil
-}
-
-// pointerElem dereferences a pointer.
-// p must point to a pointer.
-func (p unsafePointer) pointerElem() unsafePointer {
- return unsafePointer{*(*unsafe.Pointer)(p.p)}
-}
-
-// sliceLen returns the slice length.
-// p must point to a slice.
-func (p unsafePointer) sliceLen() int {
- return (*reflect.SliceHeader)(p.p).Len
-}
-
-// sliceArray returns a pointer to the underlying slice array.
-// p must point to a slice.
-func (p unsafePointer) sliceArray() unsafePointer {
- return unsafePointer{unsafe.Pointer((*reflect.SliceHeader)(p.p).Data)}
-}
-
-// arrayIndex returns a pointer to an element in the array.
-// p must point to an array.
-func (p unsafePointer) arrayIndex(index int, size uintptr) unsafePointer {
- return unsafePointer{unsafe.Add(p.p, uintptr(index)*size)}
-}
-
-// structField returns a pointer to a field in a struct.
-// p must pointer to a struct.
-func (p unsafePointer) structField(index int, offset, size uintptr) unsafePointer {
- return unsafePointer{unsafe.Add(p.p, offset)}
-}
-
-// asString casts p as a *string.
-func (p unsafePointer) asString() *string {
- return (*string)(p.p)
-}
-
-// asTime casts p as a *time.Time.
-func (p unsafePointer) asTime() *time.Time {
- return (*time.Time)(p.p)
-}
-
-// asAddr casts p as a *netip.Addr.
-func (p unsafePointer) asAddr() *netip.Addr {
- return (*netip.Addr)(p.p)
-}
-
-// asValue casts p as a reflect.Value containing a pointer to value of t.
-func (p unsafePointer) asValue(typ reflect.Type) reflect.Value {
- return reflect.NewAt(typ, p.p)
-}
-
-// asMemory returns the memory pointer at by p for a specified size.
-func (p unsafePointer) asMemory(size uintptr) []byte {
- return unsafe.Slice((*byte)(p.p), size)
-}
-
-// visitStack is a stack of pointers visited.
-// Pointers are pushed onto the stack when visited, and popped when leaving.
-// The integer value is the depth at which the pointer was visited.
-// The length of this stack should be zero after every hashing operation.
-type visitStack map[unsafe.Pointer]int
-
-func (v visitStack) seen(p unsafe.Pointer) (int, bool) {
- idx, ok := v[p]
- return idx, ok
-}
-
-func (v *visitStack) push(p unsafe.Pointer) {
- if *v == nil {
- *v = make(map[unsafe.Pointer]int)
- }
- (*v)[p] = len(*v)
-}
-
-func (v visitStack) pop(p unsafe.Pointer) {
- delete(v, p)
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package deephash
+
+import (
+ "net/netip"
+ "reflect"
+ "time"
+ "unsafe"
+)
+
+// unsafePointer is an untyped pointer.
+// It is the caller's responsibility to call operations on the correct type.
+//
+// This pointer only ever points to a small set of kinds or types:
+// time.Time, netip.Addr, string, array, slice, struct, map, pointer, interface,
+// or a pointer to memory that is directly hashable.
+//
+// Arrays are represented as pointers to the first element.
+// Structs are represented as pointers to the first field.
+// Slices are represented as pointers to a slice header.
+// Pointers are represented as pointers to a pointer.
+//
+// We do not support direct operations on maps and interfaces, and instead
+// rely on pointer.asValue to convert the pointer back to a reflect.Value.
+// Conversion of an unsafe.Pointer to reflect.Value guarantees that the
+// read-only flag in the reflect.Value is unpopulated, avoiding panics that may
+// otherwise have occurred since the value was obtained from an unexported field.
+type unsafePointer struct{ p unsafe.Pointer }
+
+func unsafePointerOf(v reflect.Value) unsafePointer {
+ return unsafePointer{v.UnsafePointer()}
+}
+func (p unsafePointer) isNil() bool {
+ return p.p == nil
+}
+
+// pointerElem dereferences a pointer.
+// p must point to a pointer.
+func (p unsafePointer) pointerElem() unsafePointer {
+ return unsafePointer{*(*unsafe.Pointer)(p.p)}
+}
+
+// sliceLen returns the slice length.
+// p must point to a slice.
+func (p unsafePointer) sliceLen() int {
+ return (*reflect.SliceHeader)(p.p).Len
+}
+
+// sliceArray returns a pointer to the underlying slice array.
+// p must point to a slice.
+func (p unsafePointer) sliceArray() unsafePointer {
+ return unsafePointer{unsafe.Pointer((*reflect.SliceHeader)(p.p).Data)}
+}
+
+// arrayIndex returns a pointer to an element in the array.
+// p must point to an array.
+func (p unsafePointer) arrayIndex(index int, size uintptr) unsafePointer {
+ return unsafePointer{unsafe.Add(p.p, uintptr(index)*size)}
+}
+
+// structField returns a pointer to a field in a struct.
+// p must pointer to a struct.
+func (p unsafePointer) structField(index int, offset, size uintptr) unsafePointer {
+ return unsafePointer{unsafe.Add(p.p, offset)}
+}
+
+// asString casts p as a *string.
+func (p unsafePointer) asString() *string {
+ return (*string)(p.p)
+}
+
+// asTime casts p as a *time.Time.
+func (p unsafePointer) asTime() *time.Time {
+ return (*time.Time)(p.p)
+}
+
+// asAddr casts p as a *netip.Addr.
+func (p unsafePointer) asAddr() *netip.Addr {
+ return (*netip.Addr)(p.p)
+}
+
+// asValue casts p as a reflect.Value containing a pointer to value of t.
+func (p unsafePointer) asValue(typ reflect.Type) reflect.Value {
+ return reflect.NewAt(typ, p.p)
+}
+
+// asMemory returns the memory pointer at by p for a specified size.
+func (p unsafePointer) asMemory(size uintptr) []byte {
+ return unsafe.Slice((*byte)(p.p), size)
+}
+
+// visitStack is a stack of pointers visited.
+// Pointers are pushed onto the stack when visited, and popped when leaving.
+// The integer value is the depth at which the pointer was visited.
+// The length of this stack should be zero after every hashing operation.
+type visitStack map[unsafe.Pointer]int
+
+func (v visitStack) seen(p unsafe.Pointer) (int, bool) {
+ idx, ok := v[p]
+ return idx, ok
+}
+
+func (v *visitStack) push(p unsafe.Pointer) {
+ if *v == nil {
+ *v = make(map[unsafe.Pointer]int)
+ }
+ (*v)[p] = len(*v)
+}
+
+func (v visitStack) pop(p unsafe.Pointer) {
+ delete(v, p)
+}
diff --git a/util/deephash/pointer_norace.go b/util/deephash/pointer_norace.go
index f98a70f6a..499372000 100644
--- a/util/deephash/pointer_norace.go
+++ b/util/deephash/pointer_norace.go
@@ -1,13 +1,13 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build !race
-
-package deephash
-
-import "reflect"
-
-type pointer = unsafePointer
-
-// pointerOf returns a pointer from v, which must be a reflect.Pointer.
-func pointerOf(v reflect.Value) pointer { return unsafePointerOf(v) }
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !race
+
+package deephash
+
+import "reflect"
+
+type pointer = unsafePointer
+
+// pointerOf returns a pointer from v, which must be a reflect.Pointer.
+func pointerOf(v reflect.Value) pointer { return unsafePointerOf(v) }
diff --git a/util/deephash/pointer_race.go b/util/deephash/pointer_race.go
index c638c7d39..93a358b6d 100644
--- a/util/deephash/pointer_race.go
+++ b/util/deephash/pointer_race.go
@@ -1,99 +1,99 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build race
-
-package deephash
-
-import (
- "fmt"
- "net/netip"
- "reflect"
- "time"
-)
-
-// pointer is a typed pointer that performs safety checks for every operation.
-type pointer struct {
- unsafePointer
- t reflect.Type // type of pointed-at value; may be nil
- n uintptr // size of valid memory after p
-}
-
-// pointerOf returns a pointer from v, which must be a reflect.Pointer.
-func pointerOf(v reflect.Value) pointer {
- assert(v.Kind() == reflect.Pointer, "got %v, want pointer", v.Kind())
- te := v.Type().Elem()
- return pointer{unsafePointerOf(v), te, te.Size()}
-}
-
-func (p pointer) pointerElem() pointer {
- assert(p.t.Kind() == reflect.Pointer, "got %v, want pointer", p.t.Kind())
- te := p.t.Elem()
- return pointer{p.unsafePointer.pointerElem(), te, te.Size()}
-}
-
-func (p pointer) sliceLen() int {
- assert(p.t.Kind() == reflect.Slice, "got %v, want slice", p.t.Kind())
- return p.unsafePointer.sliceLen()
-}
-
-func (p pointer) sliceArray() pointer {
- assert(p.t.Kind() == reflect.Slice, "got %v, want slice", p.t.Kind())
- n := p.sliceLen()
- assert(n >= 0, "got negative slice length %d", n)
- ta := reflect.ArrayOf(n, p.t.Elem())
- return pointer{p.unsafePointer.sliceArray(), ta, ta.Size()}
-}
-
-func (p pointer) arrayIndex(index int, size uintptr) pointer {
- assert(p.t.Kind() == reflect.Array, "got %v, want array", p.t.Kind())
- assert(0 <= index && index < p.t.Len(), "got array of size %d, want to access element %d", p.t.Len(), index)
- assert(p.t.Elem().Size() == size, "got element size of %d, want %d", p.t.Elem().Size(), size)
- te := p.t.Elem()
- return pointer{p.unsafePointer.arrayIndex(index, size), te, te.Size()}
-}
-
-func (p pointer) structField(index int, offset, size uintptr) pointer {
- assert(p.t.Kind() == reflect.Struct, "got %v, want struct", p.t.Kind())
- assert(p.n >= offset, "got size of %d, want excessive start offset of %d", p.n, offset)
- assert(p.n >= offset+size, "got size of %d, want excessive end offset of %d", p.n, offset+size)
- if index < 0 {
- return pointer{p.unsafePointer.structField(index, offset, size), nil, size}
- }
- sf := p.t.Field(index)
- t := sf.Type
- assert(sf.Offset == offset, "got offset of %d, want offset %d", sf.Offset, offset)
- assert(t.Size() == size, "got size of %d, want size %d", t.Size(), size)
- return pointer{p.unsafePointer.structField(index, offset, size), t, t.Size()}
-}
-
-func (p pointer) asString() *string {
- assert(p.t.Kind() == reflect.String, "got %v, want string", p.t)
- return p.unsafePointer.asString()
-}
-
-func (p pointer) asTime() *time.Time {
- assert(p.t == timeTimeType, "got %v, want %v", p.t, timeTimeType)
- return p.unsafePointer.asTime()
-}
-
-func (p pointer) asAddr() *netip.Addr {
- assert(p.t == netipAddrType, "got %v, want %v", p.t, netipAddrType)
- return p.unsafePointer.asAddr()
-}
-
-func (p pointer) asValue(typ reflect.Type) reflect.Value {
- assert(p.t == typ, "got %v, want %v", p.t, typ)
- return p.unsafePointer.asValue(typ)
-}
-
-func (p pointer) asMemory(size uintptr) []byte {
- assert(p.n >= size, "got size of %d, want excessive size of %d", p.n, size)
- return p.unsafePointer.asMemory(size)
-}
-
-func assert(b bool, f string, a ...any) {
- if !b {
- panic(fmt.Sprintf(f, a...))
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build race
+
+package deephash
+
+import (
+ "fmt"
+ "net/netip"
+ "reflect"
+ "time"
+)
+
+// pointer is a typed pointer that performs safety checks for every operation.
+type pointer struct {
+ unsafePointer
+ t reflect.Type // type of pointed-at value; may be nil
+ n uintptr // size of valid memory after p
+}
+
+// pointerOf returns a pointer from v, which must be a reflect.Pointer.
+func pointerOf(v reflect.Value) pointer {
+ assert(v.Kind() == reflect.Pointer, "got %v, want pointer", v.Kind())
+ te := v.Type().Elem()
+ return pointer{unsafePointerOf(v), te, te.Size()}
+}
+
+func (p pointer) pointerElem() pointer {
+ assert(p.t.Kind() == reflect.Pointer, "got %v, want pointer", p.t.Kind())
+ te := p.t.Elem()
+ return pointer{p.unsafePointer.pointerElem(), te, te.Size()}
+}
+
+func (p pointer) sliceLen() int {
+ assert(p.t.Kind() == reflect.Slice, "got %v, want slice", p.t.Kind())
+ return p.unsafePointer.sliceLen()
+}
+
+func (p pointer) sliceArray() pointer {
+ assert(p.t.Kind() == reflect.Slice, "got %v, want slice", p.t.Kind())
+ n := p.sliceLen()
+ assert(n >= 0, "got negative slice length %d", n)
+ ta := reflect.ArrayOf(n, p.t.Elem())
+ return pointer{p.unsafePointer.sliceArray(), ta, ta.Size()}
+}
+
+func (p pointer) arrayIndex(index int, size uintptr) pointer {
+ assert(p.t.Kind() == reflect.Array, "got %v, want array", p.t.Kind())
+ assert(0 <= index && index < p.t.Len(), "got array of size %d, want to access element %d", p.t.Len(), index)
+ assert(p.t.Elem().Size() == size, "got element size of %d, want %d", p.t.Elem().Size(), size)
+ te := p.t.Elem()
+ return pointer{p.unsafePointer.arrayIndex(index, size), te, te.Size()}
+}
+
+func (p pointer) structField(index int, offset, size uintptr) pointer {
+ assert(p.t.Kind() == reflect.Struct, "got %v, want struct", p.t.Kind())
+ assert(p.n >= offset, "got size of %d, want excessive start offset of %d", p.n, offset)
+ assert(p.n >= offset+size, "got size of %d, want excessive end offset of %d", p.n, offset+size)
+ if index < 0 {
+ return pointer{p.unsafePointer.structField(index, offset, size), nil, size}
+ }
+ sf := p.t.Field(index)
+ t := sf.Type
+ assert(sf.Offset == offset, "got offset of %d, want offset %d", sf.Offset, offset)
+ assert(t.Size() == size, "got size of %d, want size %d", t.Size(), size)
+ return pointer{p.unsafePointer.structField(index, offset, size), t, t.Size()}
+}
+
+func (p pointer) asString() *string {
+ assert(p.t.Kind() == reflect.String, "got %v, want string", p.t)
+ return p.unsafePointer.asString()
+}
+
+func (p pointer) asTime() *time.Time {
+ assert(p.t == timeTimeType, "got %v, want %v", p.t, timeTimeType)
+ return p.unsafePointer.asTime()
+}
+
+func (p pointer) asAddr() *netip.Addr {
+ assert(p.t == netipAddrType, "got %v, want %v", p.t, netipAddrType)
+ return p.unsafePointer.asAddr()
+}
+
+func (p pointer) asValue(typ reflect.Type) reflect.Value {
+ assert(p.t == typ, "got %v, want %v", p.t, typ)
+ return p.unsafePointer.asValue(typ)
+}
+
+func (p pointer) asMemory(size uintptr) []byte {
+ assert(p.n >= size, "got size of %d, want excessive size of %d", p.n, size)
+ return p.unsafePointer.asMemory(size)
+}
+
+func assert(b bool, f string, a ...any) {
+ if !b {
+ panic(fmt.Sprintf(f, a...))
+ }
+}
diff --git a/util/deephash/testtype/testtype.go b/util/deephash/testtype/testtype.go
index 3c90053d6..2df38da87 100644
--- a/util/deephash/testtype/testtype.go
+++ b/util/deephash/testtype/testtype.go
@@ -1,15 +1,15 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package testtype contains types for testing deephash.
-package testtype
-
-import "time"
-
-type UnexportedAddressableTime struct {
- t time.Time
-}
-
-func NewUnexportedAddressableTime(t time.Time) *UnexportedAddressableTime {
- return &UnexportedAddressableTime{t: t}
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package testtype contains types for testing deephash.
+package testtype
+
+import "time"
+
+type UnexportedAddressableTime struct {
+ t time.Time
+}
+
+func NewUnexportedAddressableTime(t time.Time) *UnexportedAddressableTime {
+ return &UnexportedAddressableTime{t: t}
+}
diff --git a/util/dirwalk/dirwalk.go b/util/dirwalk/dirwalk.go
index 811766892..a05ee3553 100644
--- a/util/dirwalk/dirwalk.go
+++ b/util/dirwalk/dirwalk.go
@@ -1,53 +1,53 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package dirwalk contains code to walk a directory.
-package dirwalk
-
-import (
- "io"
- "io/fs"
- "os"
-
- "go4.org/mem"
-)
-
-var osWalkShallow func(name mem.RO, fn WalkFunc) error
-
-// WalkFunc is the callback type used with WalkShallow.
-//
-// The name and de are only valid for the duration of func's call
-// and should not be retained.
-type WalkFunc func(name mem.RO, de fs.DirEntry) error
-
-// WalkShallow reads the entries in the named directory and calls fn for each.
-// It does not recurse into subdirectories.
-//
-// If fn returns an error, iteration stops and WalkShallow returns that value.
-//
-// On Linux, WalkShallow does not allocate, so long as certain methods on the
-// WalkFunc's DirEntry are not called which necessarily allocate.
-func WalkShallow(dirName mem.RO, fn WalkFunc) error {
- if f := osWalkShallow; f != nil {
- return f(dirName, fn)
- }
- of, err := os.Open(dirName.StringCopy())
- if err != nil {
- return err
- }
- defer of.Close()
- for {
- fis, err := of.ReadDir(100)
- for _, de := range fis {
- if err := fn(mem.S(de.Name()), de); err != nil {
- return err
- }
- }
- if err != nil {
- if err == io.EOF {
- return nil
- }
- return err
- }
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package dirwalk contains code to walk a directory.
+package dirwalk
+
+import (
+ "io"
+ "io/fs"
+ "os"
+
+ "go4.org/mem"
+)
+
+var osWalkShallow func(name mem.RO, fn WalkFunc) error
+
+// WalkFunc is the callback type used with WalkShallow.
+//
+// The name and de are only valid for the duration of func's call
+// and should not be retained.
+type WalkFunc func(name mem.RO, de fs.DirEntry) error
+
+// WalkShallow reads the entries in the named directory and calls fn for each.
+// It does not recurse into subdirectories.
+//
+// If fn returns an error, iteration stops and WalkShallow returns that value.
+//
+// On Linux, WalkShallow does not allocate, so long as certain methods on the
+// WalkFunc's DirEntry are not called which necessarily allocate.
+func WalkShallow(dirName mem.RO, fn WalkFunc) error {
+ if f := osWalkShallow; f != nil {
+ return f(dirName, fn)
+ }
+ of, err := os.Open(dirName.StringCopy())
+ if err != nil {
+ return err
+ }
+ defer of.Close()
+ for {
+ fis, err := of.ReadDir(100)
+ for _, de := range fis {
+ if err := fn(mem.S(de.Name()), de); err != nil {
+ return err
+ }
+ }
+ if err != nil {
+ if err == io.EOF {
+ return nil
+ }
+ return err
+ }
+ }
+}
diff --git a/util/dirwalk/dirwalk_linux.go b/util/dirwalk/dirwalk_linux.go
index 256467ebd..714783145 100644
--- a/util/dirwalk/dirwalk_linux.go
+++ b/util/dirwalk/dirwalk_linux.go
@@ -1,167 +1,167 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package dirwalk
-
-import (
- "fmt"
- "io/fs"
- "os"
- "path/filepath"
- "sync"
- "syscall"
- "unsafe"
-
- "go4.org/mem"
- "golang.org/x/sys/unix"
-)
-
-func init() {
- osWalkShallow = linuxWalkShallow
-}
-
-var dirEntPool = &sync.Pool{New: func() any { return new(linuxDirEnt) }}
-
-func linuxWalkShallow(dirName mem.RO, fn WalkFunc) error {
- const blockSize = 8 << 10
- buf := make([]byte, blockSize) // stack-allocated; doesn't escape
-
- nameb := mem.Append(buf[:0], dirName)
- nameb = append(nameb, 0)
-
- fd, err := sysOpen(nameb)
- if err != nil {
- return err
- }
- defer syscall.Close(fd)
-
- bufp := 0 // starting read position in buf
- nbuf := 0 // end valid data in buf
-
- de := dirEntPool.Get().(*linuxDirEnt)
- defer de.cleanAndPutInPool()
- de.root = dirName
-
- for {
- if bufp >= nbuf {
- bufp = 0
- nbuf, err = readDirent(fd, buf)
- if err != nil {
- return err
- }
- if nbuf <= 0 {
- return nil
- }
- }
- consumed, name := parseDirEnt(&de.d, buf[bufp:nbuf])
- bufp += consumed
- if len(name) == 0 || string(name) == "." || string(name) == ".." {
- continue
- }
- de.name = mem.B(name)
- if err := fn(de.name, de); err != nil {
- return err
- }
- }
-}
-
-type linuxDirEnt struct {
- root mem.RO
- d syscall.Dirent
- name mem.RO
-}
-
-func (de *linuxDirEnt) cleanAndPutInPool() {
- de.root = mem.RO{}
- de.name = mem.RO{}
- dirEntPool.Put(de)
-}
-
-func (de *linuxDirEnt) Name() string { return de.name.StringCopy() }
-func (de *linuxDirEnt) Info() (fs.FileInfo, error) {
- return os.Lstat(filepath.Join(de.root.StringCopy(), de.name.StringCopy()))
-}
-func (de *linuxDirEnt) IsDir() bool {
- return de.d.Type == syscall.DT_DIR
-}
-func (de *linuxDirEnt) Type() fs.FileMode {
- switch de.d.Type {
- case syscall.DT_BLK:
- return fs.ModeDevice // shrug
- case syscall.DT_CHR:
- return fs.ModeCharDevice
- case syscall.DT_DIR:
- return fs.ModeDir
- case syscall.DT_FIFO:
- return fs.ModeNamedPipe
- case syscall.DT_LNK:
- return fs.ModeSymlink
- case syscall.DT_REG:
- return 0
- case syscall.DT_SOCK:
- return fs.ModeSocket
- default:
- return fs.ModeIrregular // shrug
- }
-}
-
-func direntNamlen(dirent *syscall.Dirent) int {
- const fixedHdr = uint16(unsafe.Offsetof(syscall.Dirent{}.Name))
- limit := dirent.Reclen - fixedHdr
- const dirNameLen = 256 // sizeof syscall.Dirent.Name
- if limit > dirNameLen {
- limit = dirNameLen
- }
- for i := uint16(0); i < limit; i++ {
- if dirent.Name[i] == 0 {
- return int(i)
- }
- }
- panic("failed to find terminating 0 byte in dirent")
-}
-
-func parseDirEnt(dirent *syscall.Dirent, buf []byte) (consumed int, name []byte) {
- // golang.org/issue/37269
- copy(unsafe.Slice((*byte)(unsafe.Pointer(dirent)), unsafe.Sizeof(syscall.Dirent{})), buf)
- if v := unsafe.Offsetof(dirent.Reclen) + unsafe.Sizeof(dirent.Reclen); uintptr(len(buf)) < v {
- panic(fmt.Sprintf("buf size of %d smaller than dirent header size %d", len(buf), v))
- }
- if len(buf) < int(dirent.Reclen) {
- panic(fmt.Sprintf("buf size %d < record length %d", len(buf), dirent.Reclen))
- }
- consumed = int(dirent.Reclen)
- if dirent.Ino == 0 { // File absent in directory.
- return
- }
- name = unsafe.Slice((*byte)(unsafe.Pointer(&dirent.Name[0])), direntNamlen(dirent))
- return
-}
-
-func sysOpen(name []byte) (fd int, err error) {
- if len(name) == 0 || name[len(name)-1] != 0 {
- return 0, syscall.EINVAL
- }
- var dirfd int = unix.AT_FDCWD
- for {
- r0, _, e1 := syscall.Syscall(unix.SYS_OPENAT, uintptr(dirfd),
- uintptr(unsafe.Pointer(&name[0])), 0)
- if e1 == 0 {
- return int(r0), nil
- }
- if e1 == syscall.EINTR {
- // Since https://golang.org/doc/go1.14#runtime we
- // need to loop on EINTR on more places.
- continue
- }
- return 0, syscall.Errno(e1)
- }
-}
-
-func readDirent(fd int, buf []byte) (n int, err error) {
- for {
- nbuf, err := syscall.ReadDirent(fd, buf)
- if err != syscall.EINTR {
- return nbuf, err
- }
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package dirwalk
+
+import (
+ "fmt"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "sync"
+ "syscall"
+ "unsafe"
+
+ "go4.org/mem"
+ "golang.org/x/sys/unix"
+)
+
+func init() {
+ osWalkShallow = linuxWalkShallow
+}
+
+var dirEntPool = &sync.Pool{New: func() any { return new(linuxDirEnt) }}
+
+func linuxWalkShallow(dirName mem.RO, fn WalkFunc) error {
+ const blockSize = 8 << 10
+ buf := make([]byte, blockSize) // stack-allocated; doesn't escape
+
+ nameb := mem.Append(buf[:0], dirName)
+ nameb = append(nameb, 0)
+
+ fd, err := sysOpen(nameb)
+ if err != nil {
+ return err
+ }
+ defer syscall.Close(fd)
+
+ bufp := 0 // starting read position in buf
+ nbuf := 0 // end valid data in buf
+
+ de := dirEntPool.Get().(*linuxDirEnt)
+ defer de.cleanAndPutInPool()
+ de.root = dirName
+
+ for {
+ if bufp >= nbuf {
+ bufp = 0
+ nbuf, err = readDirent(fd, buf)
+ if err != nil {
+ return err
+ }
+ if nbuf <= 0 {
+ return nil
+ }
+ }
+ consumed, name := parseDirEnt(&de.d, buf[bufp:nbuf])
+ bufp += consumed
+ if len(name) == 0 || string(name) == "." || string(name) == ".." {
+ continue
+ }
+ de.name = mem.B(name)
+ if err := fn(de.name, de); err != nil {
+ return err
+ }
+ }
+}
+
+type linuxDirEnt struct {
+ root mem.RO
+ d syscall.Dirent
+ name mem.RO
+}
+
+func (de *linuxDirEnt) cleanAndPutInPool() {
+ de.root = mem.RO{}
+ de.name = mem.RO{}
+ dirEntPool.Put(de)
+}
+
+func (de *linuxDirEnt) Name() string { return de.name.StringCopy() }
+func (de *linuxDirEnt) Info() (fs.FileInfo, error) {
+ return os.Lstat(filepath.Join(de.root.StringCopy(), de.name.StringCopy()))
+}
+func (de *linuxDirEnt) IsDir() bool {
+ return de.d.Type == syscall.DT_DIR
+}
+func (de *linuxDirEnt) Type() fs.FileMode {
+ switch de.d.Type {
+ case syscall.DT_BLK:
+ return fs.ModeDevice // shrug
+ case syscall.DT_CHR:
+ return fs.ModeCharDevice
+ case syscall.DT_DIR:
+ return fs.ModeDir
+ case syscall.DT_FIFO:
+ return fs.ModeNamedPipe
+ case syscall.DT_LNK:
+ return fs.ModeSymlink
+ case syscall.DT_REG:
+ return 0
+ case syscall.DT_SOCK:
+ return fs.ModeSocket
+ default:
+ return fs.ModeIrregular // shrug
+ }
+}
+
+func direntNamlen(dirent *syscall.Dirent) int {
+ const fixedHdr = uint16(unsafe.Offsetof(syscall.Dirent{}.Name))
+ limit := dirent.Reclen - fixedHdr
+ const dirNameLen = 256 // sizeof syscall.Dirent.Name
+ if limit > dirNameLen {
+ limit = dirNameLen
+ }
+ for i := uint16(0); i < limit; i++ {
+ if dirent.Name[i] == 0 {
+ return int(i)
+ }
+ }
+ panic("failed to find terminating 0 byte in dirent")
+}
+
+func parseDirEnt(dirent *syscall.Dirent, buf []byte) (consumed int, name []byte) {
+ // golang.org/issue/37269
+ copy(unsafe.Slice((*byte)(unsafe.Pointer(dirent)), unsafe.Sizeof(syscall.Dirent{})), buf)
+ if v := unsafe.Offsetof(dirent.Reclen) + unsafe.Sizeof(dirent.Reclen); uintptr(len(buf)) < v {
+ panic(fmt.Sprintf("buf size of %d smaller than dirent header size %d", len(buf), v))
+ }
+ if len(buf) < int(dirent.Reclen) {
+ panic(fmt.Sprintf("buf size %d < record length %d", len(buf), dirent.Reclen))
+ }
+ consumed = int(dirent.Reclen)
+ if dirent.Ino == 0 { // File absent in directory.
+ return
+ }
+ name = unsafe.Slice((*byte)(unsafe.Pointer(&dirent.Name[0])), direntNamlen(dirent))
+ return
+}
+
+func sysOpen(name []byte) (fd int, err error) {
+ if len(name) == 0 || name[len(name)-1] != 0 {
+ return 0, syscall.EINVAL
+ }
+ var dirfd int = unix.AT_FDCWD
+ for {
+ r0, _, e1 := syscall.Syscall(unix.SYS_OPENAT, uintptr(dirfd),
+ uintptr(unsafe.Pointer(&name[0])), 0)
+ if e1 == 0 {
+ return int(r0), nil
+ }
+ if e1 == syscall.EINTR {
+ // Since https://golang.org/doc/go1.14#runtime we
+ // need to loop on EINTR on more places.
+ continue
+ }
+ return 0, syscall.Errno(e1)
+ }
+}
+
+func readDirent(fd int, buf []byte) (n int, err error) {
+ for {
+ nbuf, err := syscall.ReadDirent(fd, buf)
+ if err != syscall.EINTR {
+ return nbuf, err
+ }
+ }
+}
diff --git a/util/dirwalk/dirwalk_test.go b/util/dirwalk/dirwalk_test.go
index 15ebc13dd..e2e41f634 100644
--- a/util/dirwalk/dirwalk_test.go
+++ b/util/dirwalk/dirwalk_test.go
@@ -1,91 +1,91 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package dirwalk
-
-import (
- "fmt"
- "os"
- "path/filepath"
- "reflect"
- "runtime"
- "sort"
- "testing"
-
- "go4.org/mem"
- "tailscale.com/tstest"
-)
-
-func TestWalkShallowOSSpecific(t *testing.T) {
- if osWalkShallow == nil {
- t.Skip("no OS-specific implementation")
- }
- testWalkShallow(t, false)
-}
-
-func TestWalkShallowPortable(t *testing.T) {
- testWalkShallow(t, true)
-}
-
-func testWalkShallow(t *testing.T, portable bool) {
- if portable {
- tstest.Replace(t, &osWalkShallow, nil)
- }
- d := t.TempDir()
-
- t.Run("basics", func(t *testing.T) {
- if err := os.WriteFile(filepath.Join(d, "foo"), []byte("1"), 0600); err != nil {
- t.Fatal(err)
- }
- if err := os.WriteFile(filepath.Join(d, "bar"), []byte("22"), 0400); err != nil {
- t.Fatal(err)
- }
- if err := os.Mkdir(filepath.Join(d, "baz"), 0777); err != nil {
- t.Fatal(err)
- }
-
- var got []string
- if err := WalkShallow(mem.S(d), func(name mem.RO, de os.DirEntry) error {
- var size int64
- if fi, err := de.Info(); err != nil {
- t.Errorf("Info stat error on %q: %v", de.Name(), err)
- } else if !fi.IsDir() {
- size = fi.Size()
- }
- got = append(got, fmt.Sprintf("%q %q dir=%v type=%d size=%v", name.StringCopy(), de.Name(), de.IsDir(), de.Type(), size))
- return nil
- }); err != nil {
- t.Fatal(err)
- }
- sort.Strings(got)
- want := []string{
- `"bar" "bar" dir=false type=0 size=2`,
- `"baz" "baz" dir=true type=2147483648 size=0`,
- `"foo" "foo" dir=false type=0 size=1`,
- }
- if !reflect.DeepEqual(got, want) {
- t.Errorf("mismatch:\n got %#q\nwant %#q", got, want)
- }
- })
-
- t.Run("err_not_exist", func(t *testing.T) {
- err := WalkShallow(mem.S(filepath.Join(d, "not_exist")), func(name mem.RO, de os.DirEntry) error {
- return nil
- })
- if !os.IsNotExist(err) {
- t.Errorf("unexpected error: %v", err)
- }
- })
-
- t.Run("allocs", func(t *testing.T) {
- allocs := int(testing.AllocsPerRun(1000, func() {
- if err := WalkShallow(mem.S(d), func(name mem.RO, de os.DirEntry) error { return nil }); err != nil {
- t.Fatal(err)
- }
- }))
- t.Logf("allocs = %v", allocs)
- if !portable && runtime.GOOS == "linux" && allocs != 0 {
- t.Errorf("unexpected allocs: got %v, want 0", allocs)
- }
- })
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package dirwalk
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "reflect"
+ "runtime"
+ "sort"
+ "testing"
+
+ "go4.org/mem"
+ "tailscale.com/tstest"
+)
+
+func TestWalkShallowOSSpecific(t *testing.T) {
+ if osWalkShallow == nil {
+ t.Skip("no OS-specific implementation")
+ }
+ testWalkShallow(t, false)
+}
+
+func TestWalkShallowPortable(t *testing.T) {
+ testWalkShallow(t, true)
+}
+
+func testWalkShallow(t *testing.T, portable bool) {
+ if portable {
+ tstest.Replace(t, &osWalkShallow, nil)
+ }
+ d := t.TempDir()
+
+ t.Run("basics", func(t *testing.T) {
+ if err := os.WriteFile(filepath.Join(d, "foo"), []byte("1"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(d, "bar"), []byte("22"), 0400); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Mkdir(filepath.Join(d, "baz"), 0777); err != nil {
+ t.Fatal(err)
+ }
+
+ var got []string
+ if err := WalkShallow(mem.S(d), func(name mem.RO, de os.DirEntry) error {
+ var size int64
+ if fi, err := de.Info(); err != nil {
+ t.Errorf("Info stat error on %q: %v", de.Name(), err)
+ } else if !fi.IsDir() {
+ size = fi.Size()
+ }
+ got = append(got, fmt.Sprintf("%q %q dir=%v type=%d size=%v", name.StringCopy(), de.Name(), de.IsDir(), de.Type(), size))
+ return nil
+ }); err != nil {
+ t.Fatal(err)
+ }
+ sort.Strings(got)
+ want := []string{
+ `"bar" "bar" dir=false type=0 size=2`,
+ `"baz" "baz" dir=true type=2147483648 size=0`,
+ `"foo" "foo" dir=false type=0 size=1`,
+ }
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("mismatch:\n got %#q\nwant %#q", got, want)
+ }
+ })
+
+ t.Run("err_not_exist", func(t *testing.T) {
+ err := WalkShallow(mem.S(filepath.Join(d, "not_exist")), func(name mem.RO, de os.DirEntry) error {
+ return nil
+ })
+ if !os.IsNotExist(err) {
+ t.Errorf("unexpected error: %v", err)
+ }
+ })
+
+ t.Run("allocs", func(t *testing.T) {
+ allocs := int(testing.AllocsPerRun(1000, func() {
+ if err := WalkShallow(mem.S(d), func(name mem.RO, de os.DirEntry) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ }))
+ t.Logf("allocs = %v", allocs)
+ if !portable && runtime.GOOS == "linux" && allocs != 0 {
+ t.Errorf("unexpected allocs: got %v, want 0", allocs)
+ }
+ })
+}
diff --git a/util/goroutines/goroutines.go b/util/goroutines/goroutines.go
index 9758b0758..24c61b37c 100644
--- a/util/goroutines/goroutines.go
+++ b/util/goroutines/goroutines.go
@@ -1,93 +1,93 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// The goroutines package contains utilities for getting active goroutines.
-package goroutines
-
-import (
- "bytes"
- "fmt"
- "runtime"
- "strconv"
-)
-
-// ScrubbedGoroutineDump returns either the current goroutine's stack or all
-// goroutines' stacks, but with the actual values of arguments scrubbed out,
-// lest it contain some private key material.
-func ScrubbedGoroutineDump(all bool) []byte {
- var buf []byte
- // Grab stacks multiple times into increasingly larger buffer sizes
- // to minimize the risk that we blow past our iOS memory limit.
- for size := 1 << 10; size <= 1<<20; size += 1 << 10 {
- buf = make([]byte, size)
- buf = buf[:runtime.Stack(buf, all)]
- if len(buf) < size {
- // It fit.
- break
- }
- }
- return scrubHex(buf)
-}
-
-func scrubHex(buf []byte) []byte {
- saw := map[string][]byte{} // "0x123" => "v1%3" (unique value 1 and its value mod 8)
-
- foreachHexAddress(buf, func(in []byte) {
- if string(in) == "0x0" {
- return
- }
- if v, ok := saw[string(in)]; ok {
- for i := range in {
- in[i] = '_'
- }
- copy(in, v)
- return
- }
- inStr := string(in)
- u64, err := strconv.ParseUint(string(in[2:]), 16, 64)
- for i := range in {
- in[i] = '_'
- }
- if err != nil {
- in[0] = '?'
- return
- }
- v := []byte(fmt.Sprintf("v%d%%%d", len(saw)+1, u64%8))
- saw[inStr] = v
- copy(in, v)
- })
- return buf
-}
-
-var ohx = []byte("0x")
-
-// foreachHexAddress calls f with each subslice of b that matches
-// regexp `0x[0-9a-f]*`.
-func foreachHexAddress(b []byte, f func([]byte)) {
- for len(b) > 0 {
- i := bytes.Index(b, ohx)
- if i == -1 {
- return
- }
- b = b[i:]
- hx := hexPrefix(b)
- f(hx)
- b = b[len(hx):]
- }
-}
-
-func hexPrefix(b []byte) []byte {
- for i, c := range b {
- if i < 2 {
- continue
- }
- if !isHexByte(c) {
- return b[:i]
- }
- }
- return b
-}
-
-func isHexByte(b byte) bool {
- return '0' <= b && b <= '9' || 'a' <= b && b <= 'f' || 'A' <= b && b <= 'F'
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// The goroutines package contains utilities for getting active goroutines.
+package goroutines
+
+import (
+ "bytes"
+ "fmt"
+ "runtime"
+ "strconv"
+)
+
+// ScrubbedGoroutineDump returns either the current goroutine's stack or all
+// goroutines' stacks, but with the actual values of arguments scrubbed out,
+// lest it contain some private key material.
+func ScrubbedGoroutineDump(all bool) []byte {
+ var buf []byte
+ // Grab stacks multiple times into increasingly larger buffer sizes
+ // to minimize the risk that we blow past our iOS memory limit.
+ for size := 1 << 10; size <= 1<<20; size += 1 << 10 {
+ buf = make([]byte, size)
+ buf = buf[:runtime.Stack(buf, all)]
+ if len(buf) < size {
+ // It fit.
+ break
+ }
+ }
+ return scrubHex(buf)
+}
+
+func scrubHex(buf []byte) []byte {
+ saw := map[string][]byte{} // "0x123" => "v1%3" (unique value 1 and its value mod 8)
+
+ foreachHexAddress(buf, func(in []byte) {
+ if string(in) == "0x0" {
+ return
+ }
+ if v, ok := saw[string(in)]; ok {
+ for i := range in {
+ in[i] = '_'
+ }
+ copy(in, v)
+ return
+ }
+ inStr := string(in)
+ u64, err := strconv.ParseUint(string(in[2:]), 16, 64)
+ for i := range in {
+ in[i] = '_'
+ }
+ if err != nil {
+ in[0] = '?'
+ return
+ }
+ v := []byte(fmt.Sprintf("v%d%%%d", len(saw)+1, u64%8))
+ saw[inStr] = v
+ copy(in, v)
+ })
+ return buf
+}
+
+var ohx = []byte("0x")
+
+// foreachHexAddress calls f with each subslice of b that matches
+// regexp `0x[0-9a-f]*`.
+func foreachHexAddress(b []byte, f func([]byte)) {
+ for len(b) > 0 {
+ i := bytes.Index(b, ohx)
+ if i == -1 {
+ return
+ }
+ b = b[i:]
+ hx := hexPrefix(b)
+ f(hx)
+ b = b[len(hx):]
+ }
+}
+
+func hexPrefix(b []byte) []byte {
+ for i, c := range b {
+ if i < 2 {
+ continue
+ }
+ if !isHexByte(c) {
+ return b[:i]
+ }
+ }
+ return b
+}
+
+func isHexByte(b byte) bool {
+ return '0' <= b && b <= '9' || 'a' <= b && b <= 'f' || 'A' <= b && b <= 'F'
+}
diff --git a/util/goroutines/goroutines_test.go b/util/goroutines/goroutines_test.go
index ae17c399c..df6560fe5 100644
--- a/util/goroutines/goroutines_test.go
+++ b/util/goroutines/goroutines_test.go
@@ -1,29 +1,29 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package goroutines
-
-import "testing"
-
-func TestScrubbedGoroutineDump(t *testing.T) {
- t.Logf("Got:\n%s\n", ScrubbedGoroutineDump(true))
-}
-
-func TestScrubHex(t *testing.T) {
- tests := []struct {
- in, want string
- }{
- {"foo", "foo"},
- {"", ""},
- {"0x", "?_"},
- {"0x001 and same 0x001", "v1%1_ and same v1%1_"},
- {"0x008 and same 0x008", "v1%0_ and same v1%0_"},
- {"0x001 and diff 0x002", "v1%1_ and diff v2%2_"},
- }
- for _, tt := range tests {
- got := scrubHex([]byte(tt.in))
- if string(got) != tt.want {
- t.Errorf("for input:\n%s\n\ngot:\n%s\n\nwant:\n%s\n", tt.in, got, tt.want)
- }
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package goroutines
+
+import "testing"
+
+func TestScrubbedGoroutineDump(t *testing.T) {
+ t.Logf("Got:\n%s\n", ScrubbedGoroutineDump(true))
+}
+
+func TestScrubHex(t *testing.T) {
+ tests := []struct {
+ in, want string
+ }{
+ {"foo", "foo"},
+ {"", ""},
+ {"0x", "?_"},
+ {"0x001 and same 0x001", "v1%1_ and same v1%1_"},
+ {"0x008 and same 0x008", "v1%0_ and same v1%0_"},
+ {"0x001 and diff 0x002", "v1%1_ and diff v2%2_"},
+ }
+ for _, tt := range tests {
+ got := scrubHex([]byte(tt.in))
+ if string(got) != tt.want {
+ t.Errorf("for input:\n%s\n\ngot:\n%s\n\nwant:\n%s\n", tt.in, got, tt.want)
+ }
+ }
+}
diff --git a/util/groupmember/groupmember.go b/util/groupmember/groupmember.go
index d60416816..38431a7ff 100644
--- a/util/groupmember/groupmember.go
+++ b/util/groupmember/groupmember.go
@@ -1,29 +1,29 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package groupmember verifies group membership of the provided user on the
-// local system.
-package groupmember
-
-import (
- "os/user"
- "slices"
-)
-
-// IsMemberOfGroup reports whether the provided user is a member of
-// the provided system group.
-func IsMemberOfGroup(group, userName string) (bool, error) {
- u, err := user.Lookup(userName)
- if err != nil {
- return false, err
- }
- g, err := user.LookupGroup(group)
- if err != nil {
- return false, err
- }
- ugids, err := u.GroupIds()
- if err != nil {
- return false, err
- }
- return slices.Contains(ugids, g.Gid), nil
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package groupmember verifies group membership of the provided user on the
+// local system.
+package groupmember
+
+import (
+ "os/user"
+ "slices"
+)
+
+// IsMemberOfGroup reports whether the provided user is a member of
+// the provided system group.
+func IsMemberOfGroup(group, userName string) (bool, error) {
+ u, err := user.Lookup(userName)
+ if err != nil {
+ return false, err
+ }
+ g, err := user.LookupGroup(group)
+ if err != nil {
+ return false, err
+ }
+ ugids, err := u.GroupIds()
+ if err != nil {
+ return false, err
+ }
+ return slices.Contains(ugids, g.Gid), nil
+}
diff --git a/util/hashx/block512.go b/util/hashx/block512.go
index e637c0c03..dd69ccd35 100644
--- a/util/hashx/block512.go
+++ b/util/hashx/block512.go
@@ -1,197 +1,197 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package hashx provides a concrete implementation of [hash.Hash]
-// that operates on a particular block size.
-package hashx
-
-import (
- "encoding/binary"
- "fmt"
- "hash"
- "unsafe"
-)
-
-var _ hash.Hash = (*Block512)(nil)
-
-// Block512 wraps a [hash.Hash] for functions that operate on 512-bit block sizes.
-// It has efficient methods for hashing fixed-width integers.
-//
-// A hashing algorithm that operates on 512-bit block sizes should be used.
-// The hash still operates correctly even with misaligned block sizes,
-// but operates less efficiently.
-//
-// Example algorithms with 512-bit block sizes include:
-// - MD4 (https://golang.org/x/crypto/md4)
-// - MD5 (https://golang.org/pkg/crypto/md5)
-// - BLAKE2s (https://golang.org/x/crypto/blake2s)
-// - BLAKE3
-// - RIPEMD (https://golang.org/x/crypto/ripemd160)
-// - SHA-0
-// - SHA-1 (https://golang.org/pkg/crypto/sha1)
-// - SHA-2 (https://golang.org/pkg/crypto/sha256)
-// - Whirlpool
-//
-// See https://en.wikipedia.org/wiki/Comparison_of_cryptographic_hash_functions#Parameters
-// for a list of hash functions and their block sizes.
-//
-// Block512 assumes that [hash.Hash.Write] never fails and
-// never allows the provided buffer to escape.
-type Block512 struct {
- hash.Hash
-
- x [512 / 8]byte
- nx int
-}
-
-// New512 constructs a new Block512 that wraps h.
-//
-// It reports an error if the block sizes do not match.
-// Misaligned block sizes perform poorly, but execute correctly.
-// The error may be ignored if performance is not a concern.
-func New512(h hash.Hash) (*Block512, error) {
- b := &Block512{Hash: h}
- if len(b.x)%h.BlockSize() != 0 {
- return b, fmt.Errorf("hashx.Block512: inefficient use of hash.Hash with %d-bit block size", 8*h.BlockSize())
- }
- return b, nil
-}
-
-// Write hashes the contents of b.
-func (h *Block512) Write(b []byte) (int, error) {
- h.HashBytes(b)
- return len(b), nil
-}
-
-// Sum appends the current hash to b and returns the resulting slice.
-//
-// It flushes any partially completed blocks to the underlying [hash.Hash],
-// which may cause future operations to be misaligned and less efficient
-// until [Block512.Reset] is called.
-func (h *Block512) Sum(b []byte) []byte {
- if h.nx > 0 {
- h.Hash.Write(h.x[:h.nx])
- h.nx = 0
- }
-
- // Unfortunately hash.Hash.Sum always causes the input to escape since
- // escape analysis cannot prove anything past an interface method call.
- // Assuming h already escapes, we call Sum with h.x first,
- // and then copy the result to b.
- sum := h.Hash.Sum(h.x[:0])
- return append(b, sum...)
-}
-
-// Reset resets Block512 to its initial state.
-// It recursively resets the underlying [hash.Hash].
-func (h *Block512) Reset() {
- h.Hash.Reset()
- h.nx = 0
-}
-
-// HashUint8 hashes n as a 1-byte integer.
-func (h *Block512) HashUint8(n uint8) {
- // NOTE: This method is carefully written to be inlineable.
- if h.nx <= len(h.x)-1 {
- h.x[h.nx] = n
- h.nx += 1
- } else {
- h.hashUint8Slow(n) // mark "noinline" to keep this within inline budget
- }
-}
-
-//go:noinline
-func (h *Block512) hashUint8Slow(n uint8) { h.hashUint(uint64(n), 1) }
-
-// HashUint16 hashes n as a 2-byte little-endian integer.
-func (h *Block512) HashUint16(n uint16) {
- // NOTE: This method is carefully written to be inlineable.
- if h.nx <= len(h.x)-2 {
- binary.LittleEndian.PutUint16(h.x[h.nx:], n)
- h.nx += 2
- } else {
- h.hashUint16Slow(n) // mark "noinline" to keep this within inline budget
- }
-}
-
-//go:noinline
-func (h *Block512) hashUint16Slow(n uint16) { h.hashUint(uint64(n), 2) }
-
-// HashUint32 hashes n as a 4-byte little-endian integer.
-func (h *Block512) HashUint32(n uint32) {
- // NOTE: This method is carefully written to be inlineable.
- if h.nx <= len(h.x)-4 {
- binary.LittleEndian.PutUint32(h.x[h.nx:], n)
- h.nx += 4
- } else {
- h.hashUint32Slow(n) // mark "noinline" to keep this within inline budget
- }
-}
-
-//go:noinline
-func (h *Block512) hashUint32Slow(n uint32) { h.hashUint(uint64(n), 4) }
-
-// HashUint64 hashes n as a 8-byte little-endian integer.
-func (h *Block512) HashUint64(n uint64) {
- // NOTE: This method is carefully written to be inlineable.
- if h.nx <= len(h.x)-8 {
- binary.LittleEndian.PutUint64(h.x[h.nx:], n)
- h.nx += 8
- } else {
- h.hashUint64Slow(n) // mark "noinline" to keep this within inline budget
- }
-}
-
-//go:noinline
-func (h *Block512) hashUint64Slow(n uint64) { h.hashUint(uint64(n), 8) }
-
-func (h *Block512) hashUint(n uint64, i int) {
- for ; i > 0; i-- {
- if h.nx == len(h.x) {
- h.Hash.Write(h.x[:])
- h.nx = 0
- }
- h.x[h.nx] = byte(n)
- h.nx += 1
- n >>= 8
- }
-}
-
-// HashBytes hashes the contents of b.
-// It does not explicitly hash the length separately.
-func (h *Block512) HashBytes(b []byte) {
- // Nearly identical to sha256.digest.Write.
- if h.nx > 0 {
- n := copy(h.x[h.nx:], b)
- h.nx += n
- if h.nx == len(h.x) {
- h.Hash.Write(h.x[:])
- h.nx = 0
- }
- b = b[n:]
- }
- if len(b) >= len(h.x) {
- n := len(b) &^ (len(h.x) - 1) // n is a multiple of len(h.x)
- h.Hash.Write(b[:n])
- b = b[n:]
- }
- if len(b) > 0 {
- h.nx = copy(h.x[:], b)
- }
-}
-
-// HashString hashes the contents of s.
-// It does not explicitly hash the length separately.
-func (h *Block512) HashString(s string) {
- // TODO: Avoid unsafe when standard hashers implement io.StringWriter.
- // See https://go.dev/issue/38776.
- type stringHeader struct {
- p unsafe.Pointer
- n int
- }
- p := (*stringHeader)(unsafe.Pointer(&s))
- b := unsafe.Slice((*byte)(p.p), p.n)
- h.HashBytes(b)
-}
-
-// TODO: Add Hash.MarshalBinary and Hash.UnmarshalBinary?
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package hashx provides a concrete implementation of [hash.Hash]
+// that operates on a particular block size.
+package hashx
+
+import (
+ "encoding/binary"
+ "fmt"
+ "hash"
+ "unsafe"
+)
+
+var _ hash.Hash = (*Block512)(nil)
+
+// Block512 wraps a [hash.Hash] for functions that operate on 512-bit block sizes.
+// It has efficient methods for hashing fixed-width integers.
+//
+// A hashing algorithm that operates on 512-bit block sizes should be used.
+// The hash still operates correctly even with misaligned block sizes,
+// but operates less efficiently.
+//
+// Example algorithms with 512-bit block sizes include:
+// - MD4 (https://golang.org/x/crypto/md4)
+// - MD5 (https://golang.org/pkg/crypto/md5)
+// - BLAKE2s (https://golang.org/x/crypto/blake2s)
+// - BLAKE3
+// - RIPEMD (https://golang.org/x/crypto/ripemd160)
+// - SHA-0
+// - SHA-1 (https://golang.org/pkg/crypto/sha1)
+// - SHA-2 (https://golang.org/pkg/crypto/sha256)
+// - Whirlpool
+//
+// See https://en.wikipedia.org/wiki/Comparison_of_cryptographic_hash_functions#Parameters
+// for a list of hash functions and their block sizes.
+//
+// Block512 assumes that [hash.Hash.Write] never fails and
+// never allows the provided buffer to escape.
+type Block512 struct {
+ hash.Hash
+
+ x [512 / 8]byte
+ nx int
+}
+
+// New512 constructs a new Block512 that wraps h.
+//
+// It reports an error if the block sizes do not match.
+// Misaligned block sizes perform poorly, but execute correctly.
+// The error may be ignored if performance is not a concern.
+func New512(h hash.Hash) (*Block512, error) {
+ b := &Block512{Hash: h}
+ if len(b.x)%h.BlockSize() != 0 {
+ return b, fmt.Errorf("hashx.Block512: inefficient use of hash.Hash with %d-bit block size", 8*h.BlockSize())
+ }
+ return b, nil
+}
+
+// Write hashes the contents of b.
+func (h *Block512) Write(b []byte) (int, error) {
+ h.HashBytes(b)
+ return len(b), nil
+}
+
+// Sum appends the current hash to b and returns the resulting slice.
+//
+// It flushes any partially completed blocks to the underlying [hash.Hash],
+// which may cause future operations to be misaligned and less efficient
+// until [Block512.Reset] is called.
+func (h *Block512) Sum(b []byte) []byte {
+ if h.nx > 0 {
+ h.Hash.Write(h.x[:h.nx])
+ h.nx = 0
+ }
+
+ // Unfortunately hash.Hash.Sum always causes the input to escape since
+ // escape analysis cannot prove anything past an interface method call.
+ // Assuming h already escapes, we call Sum with h.x first,
+ // and then copy the result to b.
+ sum := h.Hash.Sum(h.x[:0])
+ return append(b, sum...)
+}
+
+// Reset resets Block512 to its initial state.
+// It recursively resets the underlying [hash.Hash].
+func (h *Block512) Reset() {
+ h.Hash.Reset()
+ h.nx = 0
+}
+
+// HashUint8 hashes n as a 1-byte integer.
+func (h *Block512) HashUint8(n uint8) {
+ // NOTE: This method is carefully written to be inlineable.
+ if h.nx <= len(h.x)-1 {
+ h.x[h.nx] = n
+ h.nx += 1
+ } else {
+ h.hashUint8Slow(n) // mark "noinline" to keep this within inline budget
+ }
+}
+
+//go:noinline
+func (h *Block512) hashUint8Slow(n uint8) { h.hashUint(uint64(n), 1) }
+
+// HashUint16 hashes n as a 2-byte little-endian integer.
+func (h *Block512) HashUint16(n uint16) {
+ // NOTE: This method is carefully written to be inlineable.
+ if h.nx <= len(h.x)-2 {
+ binary.LittleEndian.PutUint16(h.x[h.nx:], n)
+ h.nx += 2
+ } else {
+ h.hashUint16Slow(n) // mark "noinline" to keep this within inline budget
+ }
+}
+
+//go:noinline
+func (h *Block512) hashUint16Slow(n uint16) { h.hashUint(uint64(n), 2) }
+
+// HashUint32 hashes n as a 4-byte little-endian integer.
+func (h *Block512) HashUint32(n uint32) {
+ // NOTE: This method is carefully written to be inlineable.
+ if h.nx <= len(h.x)-4 {
+ binary.LittleEndian.PutUint32(h.x[h.nx:], n)
+ h.nx += 4
+ } else {
+ h.hashUint32Slow(n) // mark "noinline" to keep this within inline budget
+ }
+}
+
+//go:noinline
+func (h *Block512) hashUint32Slow(n uint32) { h.hashUint(uint64(n), 4) }
+
+// HashUint64 hashes n as a 8-byte little-endian integer.
+func (h *Block512) HashUint64(n uint64) {
+ // NOTE: This method is carefully written to be inlineable.
+ if h.nx <= len(h.x)-8 {
+ binary.LittleEndian.PutUint64(h.x[h.nx:], n)
+ h.nx += 8
+ } else {
+ h.hashUint64Slow(n) // mark "noinline" to keep this within inline budget
+ }
+}
+
+//go:noinline
+func (h *Block512) hashUint64Slow(n uint64) { h.hashUint(uint64(n), 8) }
+
+func (h *Block512) hashUint(n uint64, i int) {
+ for ; i > 0; i-- {
+ if h.nx == len(h.x) {
+ h.Hash.Write(h.x[:])
+ h.nx = 0
+ }
+ h.x[h.nx] = byte(n)
+ h.nx += 1
+ n >>= 8
+ }
+}
+
+// HashBytes hashes the contents of b.
+// It does not explicitly hash the length separately.
+func (h *Block512) HashBytes(b []byte) {
+ // Nearly identical to sha256.digest.Write.
+ if h.nx > 0 {
+ n := copy(h.x[h.nx:], b)
+ h.nx += n
+ if h.nx == len(h.x) {
+ h.Hash.Write(h.x[:])
+ h.nx = 0
+ }
+ b = b[n:]
+ }
+ if len(b) >= len(h.x) {
+ n := len(b) &^ (len(h.x) - 1) // n is a multiple of len(h.x)
+ h.Hash.Write(b[:n])
+ b = b[n:]
+ }
+ if len(b) > 0 {
+ h.nx = copy(h.x[:], b)
+ }
+}
+
+// HashString hashes the contents of s.
+// It does not explicitly hash the length separately.
+func (h *Block512) HashString(s string) {
+ // TODO: Avoid unsafe when standard hashers implement io.StringWriter.
+ // See https://go.dev/issue/38776.
+ type stringHeader struct {
+ p unsafe.Pointer
+ n int
+ }
+ p := (*stringHeader)(unsafe.Pointer(&s))
+ b := unsafe.Slice((*byte)(p.p), p.n)
+ h.HashBytes(b)
+}
+
+// TODO: Add Hash.MarshalBinary and Hash.UnmarshalBinary?
diff --git a/util/httphdr/httphdr.go b/util/httphdr/httphdr.go
index 852e28b8f..b78b165c6 100644
--- a/util/httphdr/httphdr.go
+++ b/util/httphdr/httphdr.go
@@ -1,197 +1,197 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package httphdr implements functionality for parsing and formatting
-// standard HTTP headers.
-package httphdr
-
-import (
- "bytes"
- "strconv"
- "strings"
-)
-
-// Range is a range of bytes within some content.
-type Range struct {
- // Start is the starting offset.
- // It is zero if Length is negative; it must not be negative.
- Start int64
- // Length is the length of the content.
- // It is zero if the length extends to the end of the content.
- // It is negative if the length is relative to the end (e.g., last 5 bytes).
- Length int64
-}
-
-// ows is optional whitespace.
-const ows = " \t" // per RFC 7230, section 3.2.3
-
-// ParseRange parses a "Range" header per RFC 7233, section 3.
-// It only handles "Range" headers where the units is "bytes".
-// The "Range" header is usually only specified in GET requests.
-func ParseRange(hdr string) (ranges []Range, ok bool) {
- // Grammar per RFC 7233, appendix D:
- // Range = byte-ranges-specifier | other-ranges-specifier
- // byte-ranges-specifier = bytes-unit "=" byte-range-set
- // bytes-unit = "bytes"
- // byte-range-set =
- // *("," OWS)
- // (byte-range-spec | suffix-byte-range-spec)
- // *(OWS "," [OWS ( byte-range-spec | suffix-byte-range-spec )])
- // byte-range-spec = first-byte-pos "-" [last-byte-pos]
- // suffix-byte-range-spec = "-" suffix-length
- // We do not support other-ranges-specifier.
- // All other identifiers are 1*DIGIT.
- hdr = strings.Trim(hdr, ows) // per RFC 7230, section 3.2
- units, elems, hasUnits := strings.Cut(hdr, "=")
- elems = strings.TrimLeft(elems, ","+ows)
- for _, elem := range strings.Split(elems, ",") {
- elem = strings.Trim(elem, ows) // per RFC 7230, section 7
- switch {
- case strings.HasPrefix(elem, "-"): // i.e., "-" suffix-length
- n, ok := parseNumber(strings.TrimPrefix(elem, "-"))
- if !ok {
- return ranges, false
- }
- ranges = append(ranges, Range{0, -n})
- case strings.HasSuffix(elem, "-"): // i.e., first-byte-pos "-"
- n, ok := parseNumber(strings.TrimSuffix(elem, "-"))
- if !ok {
- return ranges, false
- }
- ranges = append(ranges, Range{n, 0})
- default: // i.e., first-byte-pos "-" last-byte-pos
- prefix, suffix, hasDash := strings.Cut(elem, "-")
- n, ok2 := parseNumber(prefix)
- m, ok3 := parseNumber(suffix)
- if !hasDash || !ok2 || !ok3 || m < n {
- return ranges, false
- }
- ranges = append(ranges, Range{n, m - n + 1})
- }
- }
- return ranges, units == "bytes" && hasUnits && len(ranges) > 0 // must see at least one element per RFC 7233, section 2.1
-}
-
-// FormatRange formats a "Range" header per RFC 7233, section 3.
-// It only handles "Range" headers where the units is "bytes".
-// The "Range" header is usually only specified in GET requests.
-func FormatRange(ranges []Range) (hdr string, ok bool) {
- b := []byte("bytes=")
- for _, r := range ranges {
- switch {
- case r.Length > 0: // i.e., first-byte-pos "-" last-byte-pos
- if r.Start < 0 {
- return string(b), false
- }
- b = strconv.AppendUint(b, uint64(r.Start), 10)
- b = append(b, '-')
- b = strconv.AppendUint(b, uint64(r.Start+r.Length-1), 10)
- b = append(b, ',')
- case r.Length == 0: // i.e., first-byte-pos "-"
- if r.Start < 0 {
- return string(b), false
- }
- b = strconv.AppendUint(b, uint64(r.Start), 10)
- b = append(b, '-')
- b = append(b, ',')
- case r.Length < 0: // i.e., "-" suffix-length
- if r.Start != 0 {
- return string(b), false
- }
- b = append(b, '-')
- b = strconv.AppendUint(b, uint64(-r.Length), 10)
- b = append(b, ',')
- default:
- return string(b), false
- }
- }
- return string(bytes.TrimRight(b, ",")), len(ranges) > 0
-}
-
-// ParseContentRange parses a "Content-Range" header per RFC 7233, section 4.2.
-// It only handles "Content-Range" headers where the units is "bytes".
-// The "Content-Range" header is usually only specified in HTTP responses.
-//
-// If only the completeLength is specified, then start and length are both zero.
-//
-// Otherwise, the parses the start and length and the optional completeLength,
-// which is -1 if unspecified. The start is non-negative and the length is positive.
-func ParseContentRange(hdr string) (start, length, completeLength int64, ok bool) {
- // Grammar per RFC 7233, appendix D:
- // Content-Range = byte-content-range | other-content-range
- // byte-content-range = bytes-unit SP (byte-range-resp | unsatisfied-range)
- // bytes-unit = "bytes"
- // byte-range-resp = byte-range "/" (complete-length | "*")
- // unsatisfied-range = "*/" complete-length
- // byte-range = first-byte-pos "-" last-byte-pos
- // We do not support other-content-range.
- // All other identifiers are 1*DIGIT.
- hdr = strings.Trim(hdr, ows) // per RFC 7230, section 3.2
- suffix, hasUnits := strings.CutPrefix(hdr, "bytes ")
- suffix, unsatisfied := strings.CutPrefix(suffix, "*/")
- if unsatisfied { // i.e., unsatisfied-range
- n, ok := parseNumber(suffix)
- if !ok {
- return start, length, completeLength, false
- }
- completeLength = n
- } else { // i.e., byte-range "/" (complete-length | "*")
- prefix, suffix, hasDash := strings.Cut(suffix, "-")
- middle, suffix, hasSlash := strings.Cut(suffix, "/")
- n, ok0 := parseNumber(prefix)
- m, ok1 := parseNumber(middle)
- o, ok2 := parseNumber(suffix)
- if suffix == "*" {
- o, ok2 = -1, true
- }
- if !hasDash || !hasSlash || !ok0 || !ok1 || !ok2 || m < n || (o >= 0 && o <= m) {
- return start, length, completeLength, false
- }
- start = n
- length = m - n + 1
- completeLength = o
- }
- return start, length, completeLength, hasUnits
-}
-
-// FormatContentRange parses a "Content-Range" header per RFC 7233, section 4.2.
-// It only handles "Content-Range" headers where the units is "bytes".
-// The "Content-Range" header is usually only specified in HTTP responses.
-//
-// If start and length are non-positive, then it encodes just the completeLength,
-// which must be a non-negative value.
-//
-// Otherwise, it encodes the start and length as a byte-range,
-// and optionally emits the complete length if it is non-negative.
-// The length must be positive (as RFC 7233 uses inclusive end offsets).
-func FormatContentRange(start, length, completeLength int64) (hdr string, ok bool) {
- b := []byte("bytes ")
- switch {
- case start <= 0 && length <= 0 && completeLength >= 0: // i.e., unsatisfied-range
- b = append(b, "*/"...)
- b = strconv.AppendUint(b, uint64(completeLength), 10)
- ok = true
- case start >= 0 && length > 0: // i.e., byte-range "/" (complete-length | "*")
- b = strconv.AppendUint(b, uint64(start), 10)
- b = append(b, '-')
- b = strconv.AppendUint(b, uint64(start+length-1), 10)
- b = append(b, '/')
- if completeLength >= 0 {
- b = strconv.AppendUint(b, uint64(completeLength), 10)
- ok = completeLength >= start+length && start+length > 0
- } else {
- b = append(b, '*')
- ok = true
- }
- }
- return string(b), ok
-}
-
-// parseNumber parses s as an unsigned decimal integer.
-// It parses according to the 1*DIGIT grammar, which allows leading zeros.
-func parseNumber(s string) (int64, bool) {
- suffix := strings.TrimLeft(s, "0123456789")
- prefix := s[:len(s)-len(suffix)]
- n, err := strconv.ParseInt(prefix, 10, 64)
- return n, suffix == "" && err == nil
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package httphdr implements functionality for parsing and formatting
+// standard HTTP headers.
+package httphdr
+
+import (
+ "bytes"
+ "strconv"
+ "strings"
+)
+
+// Range is a range of bytes within some content.
+type Range struct {
+ // Start is the starting offset.
+ // It is zero if Length is negative; it must not be negative.
+ Start int64
+ // Length is the length of the content.
+ // It is zero if the length extends to the end of the content.
+ // It is negative if the length is relative to the end (e.g., last 5 bytes).
+ Length int64
+}
+
+// ows is optional whitespace.
+const ows = " \t" // per RFC 7230, section 3.2.3
+
+// ParseRange parses a "Range" header per RFC 7233, section 3.
+// It only handles "Range" headers where the units is "bytes".
+// The "Range" header is usually only specified in GET requests.
+func ParseRange(hdr string) (ranges []Range, ok bool) {
+ // Grammar per RFC 7233, appendix D:
+ // Range = byte-ranges-specifier | other-ranges-specifier
+ // byte-ranges-specifier = bytes-unit "=" byte-range-set
+ // bytes-unit = "bytes"
+ // byte-range-set =
+ // *("," OWS)
+ // (byte-range-spec | suffix-byte-range-spec)
+ // *(OWS "," [OWS ( byte-range-spec | suffix-byte-range-spec )])
+ // byte-range-spec = first-byte-pos "-" [last-byte-pos]
+ // suffix-byte-range-spec = "-" suffix-length
+ // We do not support other-ranges-specifier.
+ // All other identifiers are 1*DIGIT.
+ hdr = strings.Trim(hdr, ows) // per RFC 7230, section 3.2
+ units, elems, hasUnits := strings.Cut(hdr, "=")
+ elems = strings.TrimLeft(elems, ","+ows)
+ for _, elem := range strings.Split(elems, ",") {
+ elem = strings.Trim(elem, ows) // per RFC 7230, section 7
+ switch {
+ case strings.HasPrefix(elem, "-"): // i.e., "-" suffix-length
+ n, ok := parseNumber(strings.TrimPrefix(elem, "-"))
+ if !ok {
+ return ranges, false
+ }
+ ranges = append(ranges, Range{0, -n})
+ case strings.HasSuffix(elem, "-"): // i.e., first-byte-pos "-"
+ n, ok := parseNumber(strings.TrimSuffix(elem, "-"))
+ if !ok {
+ return ranges, false
+ }
+ ranges = append(ranges, Range{n, 0})
+ default: // i.e., first-byte-pos "-" last-byte-pos
+ prefix, suffix, hasDash := strings.Cut(elem, "-")
+ n, ok2 := parseNumber(prefix)
+ m, ok3 := parseNumber(suffix)
+ if !hasDash || !ok2 || !ok3 || m < n {
+ return ranges, false
+ }
+ ranges = append(ranges, Range{n, m - n + 1})
+ }
+ }
+ return ranges, units == "bytes" && hasUnits && len(ranges) > 0 // must see at least one element per RFC 7233, section 2.1
+}
+
+// FormatRange formats a "Range" header per RFC 7233, section 3.
+// It only handles "Range" headers where the units is "bytes".
+// The "Range" header is usually only specified in GET requests.
+func FormatRange(ranges []Range) (hdr string, ok bool) {
+ b := []byte("bytes=")
+ for _, r := range ranges {
+ switch {
+ case r.Length > 0: // i.e., first-byte-pos "-" last-byte-pos
+ if r.Start < 0 {
+ return string(b), false
+ }
+ b = strconv.AppendUint(b, uint64(r.Start), 10)
+ b = append(b, '-')
+ b = strconv.AppendUint(b, uint64(r.Start+r.Length-1), 10)
+ b = append(b, ',')
+ case r.Length == 0: // i.e., first-byte-pos "-"
+ if r.Start < 0 {
+ return string(b), false
+ }
+ b = strconv.AppendUint(b, uint64(r.Start), 10)
+ b = append(b, '-')
+ b = append(b, ',')
+ case r.Length < 0: // i.e., "-" suffix-length
+ if r.Start != 0 {
+ return string(b), false
+ }
+ b = append(b, '-')
+ b = strconv.AppendUint(b, uint64(-r.Length), 10)
+ b = append(b, ',')
+ default:
+ return string(b), false
+ }
+ }
+ return string(bytes.TrimRight(b, ",")), len(ranges) > 0
+}
+
+// ParseContentRange parses a "Content-Range" header per RFC 7233, section 4.2.
+// It only handles "Content-Range" headers where the units is "bytes".
+// The "Content-Range" header is usually only specified in HTTP responses.
+//
+// If only the completeLength is specified, then start and length are both zero.
+//
+// Otherwise, the parses the start and length and the optional completeLength,
+// which is -1 if unspecified. The start is non-negative and the length is positive.
+func ParseContentRange(hdr string) (start, length, completeLength int64, ok bool) {
+ // Grammar per RFC 7233, appendix D:
+ // Content-Range = byte-content-range | other-content-range
+ // byte-content-range = bytes-unit SP (byte-range-resp | unsatisfied-range)
+ // bytes-unit = "bytes"
+ // byte-range-resp = byte-range "/" (complete-length | "*")
+ // unsatisfied-range = "*/" complete-length
+ // byte-range = first-byte-pos "-" last-byte-pos
+ // We do not support other-content-range.
+ // All other identifiers are 1*DIGIT.
+ hdr = strings.Trim(hdr, ows) // per RFC 7230, section 3.2
+ suffix, hasUnits := strings.CutPrefix(hdr, "bytes ")
+ suffix, unsatisfied := strings.CutPrefix(suffix, "*/")
+ if unsatisfied { // i.e., unsatisfied-range
+ n, ok := parseNumber(suffix)
+ if !ok {
+ return start, length, completeLength, false
+ }
+ completeLength = n
+ } else { // i.e., byte-range "/" (complete-length | "*")
+ prefix, suffix, hasDash := strings.Cut(suffix, "-")
+ middle, suffix, hasSlash := strings.Cut(suffix, "/")
+ n, ok0 := parseNumber(prefix)
+ m, ok1 := parseNumber(middle)
+ o, ok2 := parseNumber(suffix)
+ if suffix == "*" {
+ o, ok2 = -1, true
+ }
+ if !hasDash || !hasSlash || !ok0 || !ok1 || !ok2 || m < n || (o >= 0 && o <= m) {
+ return start, length, completeLength, false
+ }
+ start = n
+ length = m - n + 1
+ completeLength = o
+ }
+ return start, length, completeLength, hasUnits
+}
+
+// FormatContentRange parses a "Content-Range" header per RFC 7233, section 4.2.
+// It only handles "Content-Range" headers where the units is "bytes".
+// The "Content-Range" header is usually only specified in HTTP responses.
+//
+// If start and length are non-positive, then it encodes just the completeLength,
+// which must be a non-negative value.
+//
+// Otherwise, it encodes the start and length as a byte-range,
+// and optionally emits the complete length if it is non-negative.
+// The length must be positive (as RFC 7233 uses inclusive end offsets).
+func FormatContentRange(start, length, completeLength int64) (hdr string, ok bool) {
+ b := []byte("bytes ")
+ switch {
+ case start <= 0 && length <= 0 && completeLength >= 0: // i.e., unsatisfied-range
+ b = append(b, "*/"...)
+ b = strconv.AppendUint(b, uint64(completeLength), 10)
+ ok = true
+ case start >= 0 && length > 0: // i.e., byte-range "/" (complete-length | "*")
+ b = strconv.AppendUint(b, uint64(start), 10)
+ b = append(b, '-')
+ b = strconv.AppendUint(b, uint64(start+length-1), 10)
+ b = append(b, '/')
+ if completeLength >= 0 {
+ b = strconv.AppendUint(b, uint64(completeLength), 10)
+ ok = completeLength >= start+length && start+length > 0
+ } else {
+ b = append(b, '*')
+ ok = true
+ }
+ }
+ return string(b), ok
+}
+
+// parseNumber parses s as an unsigned decimal integer.
+// It parses according to the 1*DIGIT grammar, which allows leading zeros.
+func parseNumber(s string) (int64, bool) {
+ suffix := strings.TrimLeft(s, "0123456789")
+ prefix := s[:len(s)-len(suffix)]
+ n, err := strconv.ParseInt(prefix, 10, 64)
+ return n, suffix == "" && err == nil
+}
diff --git a/util/httphdr/httphdr_test.go b/util/httphdr/httphdr_test.go
index 81feeaca0..77ec0c324 100644
--- a/util/httphdr/httphdr_test.go
+++ b/util/httphdr/httphdr_test.go
@@ -1,96 +1,96 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package httphdr
-
-import (
- "testing"
-
- "github.com/google/go-cmp/cmp"
-)
-
-func valOk[T any](v T, ok bool) (out struct {
- V T
- Ok bool
-}) {
- out.V = v
- out.Ok = ok
- return out
-}
-
-func TestRange(t *testing.T) {
- tests := []struct {
- in string
- want []Range
- wantOk bool
- roundtrip bool
- }{
- {"", nil, false, false},
- {"1-3", nil, false, false},
- {"units=1-3", []Range{{1, 3}}, false, false},
- {"bytes=1-3", []Range{{1, 3}}, true, true},
- {"bytes=#-3", nil, false, false},
- {"bytes=#-", nil, false, false},
- {"bytes=13", nil, false, false},
- {"bytes=1-#", nil, false, false},
- {"bytes=-#", nil, false, false},
- {"bytes= , , , ,\t , \t 1-3", []Range{{1, 3}}, true, false},
- {"bytes=1-1", []Range{{1, 1}}, true, true},
- {"bytes=01-01", []Range{{1, 1}}, true, false},
- {"bytes=1-0", nil, false, false},
- {"bytes=0-5,2-3", []Range{{0, 6}, {2, 2}}, true, true},
- {"bytes=2-3,0-5", []Range{{2, 2}, {0, 6}}, true, true},
- {"bytes=0-5,2-,-5", []Range{{0, 6}, {2, 0}, {0, -5}}, true, true},
- }
-
- for _, tt := range tests {
- got, gotOk := ParseRange(tt.in)
- if d := cmp.Diff(valOk(got, gotOk), valOk(tt.want, tt.wantOk)); d != "" {
- t.Errorf("ParseRange(%q) mismatch (-got +want):\n%s", tt.in, d)
- }
- if tt.roundtrip {
- got, gotOk := FormatRange(tt.want)
- if d := cmp.Diff(valOk(got, gotOk), valOk(tt.in, tt.wantOk)); d != "" {
- t.Errorf("FormatRange(%v) mismatch (-got +want):\n%s", tt.want, d)
- }
- }
- }
-}
-
-type contentRange struct{ Start, Length, CompleteLength int64 }
-
-func TestContentRange(t *testing.T) {
- tests := []struct {
- in string
- want contentRange
- wantOk bool
- roundtrip bool
- }{
- {"", contentRange{}, false, false},
- {"bytes 5-6/*", contentRange{5, 2, -1}, true, true},
- {"units 5-6/*", contentRange{}, false, false},
- {"bytes 5-6/*", contentRange{}, false, false},
- {"bytes 5-5/*", contentRange{5, 1, -1}, true, true},
- {"bytes 5-4/*", contentRange{}, false, false},
- {"bytes 5-5/6", contentRange{5, 1, 6}, true, true},
- {"bytes 05-005/0006", contentRange{5, 1, 6}, true, false},
- {"bytes 5-5/5", contentRange{}, false, false},
- {"bytes #-5/6", contentRange{}, false, false},
- {"bytes 5-#/6", contentRange{}, false, false},
- {"bytes 5-5/#", contentRange{}, false, false},
- }
-
- for _, tt := range tests {
- start, length, completeLength, gotOk := ParseContentRange(tt.in)
- got := contentRange{start, length, completeLength}
- if d := cmp.Diff(valOk(got, gotOk), valOk(tt.want, tt.wantOk)); d != "" {
- t.Errorf("ParseContentRange mismatch (-got +want):\n%s", d)
- }
- if tt.roundtrip {
- got, gotOk := FormatContentRange(tt.want.Start, tt.want.Length, tt.want.CompleteLength)
- if d := cmp.Diff(valOk(got, gotOk), valOk(tt.in, tt.wantOk)); d != "" {
- t.Errorf("FormatContentRange mismatch (-got +want):\n%s", d)
- }
- }
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package httphdr
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+func valOk[T any](v T, ok bool) (out struct {
+ V T
+ Ok bool
+}) {
+ out.V = v
+ out.Ok = ok
+ return out
+}
+
+func TestRange(t *testing.T) {
+ tests := []struct {
+ in string
+ want []Range
+ wantOk bool
+ roundtrip bool
+ }{
+ {"", nil, false, false},
+ {"1-3", nil, false, false},
+ {"units=1-3", []Range{{1, 3}}, false, false},
+ {"bytes=1-3", []Range{{1, 3}}, true, true},
+ {"bytes=#-3", nil, false, false},
+ {"bytes=#-", nil, false, false},
+ {"bytes=13", nil, false, false},
+ {"bytes=1-#", nil, false, false},
+ {"bytes=-#", nil, false, false},
+ {"bytes= , , , ,\t , \t 1-3", []Range{{1, 3}}, true, false},
+ {"bytes=1-1", []Range{{1, 1}}, true, true},
+ {"bytes=01-01", []Range{{1, 1}}, true, false},
+ {"bytes=1-0", nil, false, false},
+ {"bytes=0-5,2-3", []Range{{0, 6}, {2, 2}}, true, true},
+ {"bytes=2-3,0-5", []Range{{2, 2}, {0, 6}}, true, true},
+ {"bytes=0-5,2-,-5", []Range{{0, 6}, {2, 0}, {0, -5}}, true, true},
+ }
+
+ for _, tt := range tests {
+ got, gotOk := ParseRange(tt.in)
+ if d := cmp.Diff(valOk(got, gotOk), valOk(tt.want, tt.wantOk)); d != "" {
+ t.Errorf("ParseRange(%q) mismatch (-got +want):\n%s", tt.in, d)
+ }
+ if tt.roundtrip {
+ got, gotOk := FormatRange(tt.want)
+ if d := cmp.Diff(valOk(got, gotOk), valOk(tt.in, tt.wantOk)); d != "" {
+ t.Errorf("FormatRange(%v) mismatch (-got +want):\n%s", tt.want, d)
+ }
+ }
+ }
+}
+
+type contentRange struct{ Start, Length, CompleteLength int64 }
+
+func TestContentRange(t *testing.T) {
+ tests := []struct {
+ in string
+ want contentRange
+ wantOk bool
+ roundtrip bool
+ }{
+ {"", contentRange{}, false, false},
+ {"bytes 5-6/*", contentRange{5, 2, -1}, true, true},
+ {"units 5-6/*", contentRange{}, false, false},
+ {"bytes 5-6/*", contentRange{}, false, false},
+ {"bytes 5-5/*", contentRange{5, 1, -1}, true, true},
+ {"bytes 5-4/*", contentRange{}, false, false},
+ {"bytes 5-5/6", contentRange{5, 1, 6}, true, true},
+ {"bytes 05-005/0006", contentRange{5, 1, 6}, true, false},
+ {"bytes 5-5/5", contentRange{}, false, false},
+ {"bytes #-5/6", contentRange{}, false, false},
+ {"bytes 5-#/6", contentRange{}, false, false},
+ {"bytes 5-5/#", contentRange{}, false, false},
+ }
+
+ for _, tt := range tests {
+ start, length, completeLength, gotOk := ParseContentRange(tt.in)
+ got := contentRange{start, length, completeLength}
+ if d := cmp.Diff(valOk(got, gotOk), valOk(tt.want, tt.wantOk)); d != "" {
+ t.Errorf("ParseContentRange mismatch (-got +want):\n%s", d)
+ }
+ if tt.roundtrip {
+ got, gotOk := FormatContentRange(tt.want.Start, tt.want.Length, tt.want.CompleteLength)
+ if d := cmp.Diff(valOk(got, gotOk), valOk(tt.in, tt.wantOk)); d != "" {
+ t.Errorf("FormatContentRange mismatch (-got +want):\n%s", d)
+ }
+ }
+ }
+}
diff --git a/util/httpm/httpm.go b/util/httpm/httpm.go
index a9a691b8a..05292f0fa 100644
--- a/util/httpm/httpm.go
+++ b/util/httpm/httpm.go
@@ -1,36 +1,36 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package httpm has shorter names for HTTP method constants.
-//
-// Some background: originally Go didn't have http.MethodGet, http.MethodPost
-// and life was good and people just wrote readable "GET" and "POST". But then
-// in a moment of weakness Brad and others maintaining net/http caved and let
-// the http.MethodFoo constants be added and code's been less readable since.
-// Now the substance of the method name is hidden away at the end after
-// "http.Method" and they all blend together and it's hard to read code using
-// them.
-//
-// This package is a compromise. It provides constants, but shorter and closer
-// to how it used to look. It does violate Go style
-// (https://github.com/golang/go/wiki/CodeReviewComments#mixed-caps) that says
-// constants shouldn't be SCREAM_CASE. But this isn't INT_MAX; it's GET and
-// POST, which are already defined as all caps.
-//
-// It would be tempting to make these constants be typed but then they wouldn't
-// be assignable to things in net/http that just want string. Oh well.
-package httpm
-
-const (
- GET = "GET"
- HEAD = "HEAD"
- POST = "POST"
- PUT = "PUT"
- PATCH = "PATCH"
- DELETE = "DELETE"
- CONNECT = "CONNECT"
- OPTIONS = "OPTIONS"
- TRACE = "TRACE"
- SPACEJUMP = "SPACEJUMP" // https://www.w3.org/Protocols/HTTP/Methods/SpaceJump.html
- BREW = "BREW" // https://datatracker.ietf.org/doc/html/rfc2324#section-2.1.1
-)
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package httpm has shorter names for HTTP method constants.
+//
+// Some background: originally Go didn't have http.MethodGet, http.MethodPost
+// and life was good and people just wrote readable "GET" and "POST". But then
+// in a moment of weakness Brad and others maintaining net/http caved and let
+// the http.MethodFoo constants be added and code's been less readable since.
+// Now the substance of the method name is hidden away at the end after
+// "http.Method" and they all blend together and it's hard to read code using
+// them.
+//
+// This package is a compromise. It provides constants, but shorter and closer
+// to how it used to look. It does violate Go style
+// (https://github.com/golang/go/wiki/CodeReviewComments#mixed-caps) that says
+// constants shouldn't be SCREAM_CASE. But this isn't INT_MAX; it's GET and
+// POST, which are already defined as all caps.
+//
+// It would be tempting to make these constants be typed but then they wouldn't
+// be assignable to things in net/http that just want string. Oh well.
+package httpm
+
+const (
+ GET = "GET"
+ HEAD = "HEAD"
+ POST = "POST"
+ PUT = "PUT"
+ PATCH = "PATCH"
+ DELETE = "DELETE"
+ CONNECT = "CONNECT"
+ OPTIONS = "OPTIONS"
+ TRACE = "TRACE"
+ SPACEJUMP = "SPACEJUMP" // https://www.w3.org/Protocols/HTTP/Methods/SpaceJump.html
+ BREW = "BREW" // https://datatracker.ietf.org/doc/html/rfc2324#section-2.1.1
+)
diff --git a/util/httpm/httpm_test.go b/util/httpm/httpm_test.go
index 0c71edc2f..cbe327d95 100644
--- a/util/httpm/httpm_test.go
+++ b/util/httpm/httpm_test.go
@@ -1,37 +1,37 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package httpm
-
-import (
- "os"
- "os/exec"
- "path/filepath"
- "strings"
- "testing"
-)
-
-func TestUsedConsistently(t *testing.T) {
- dir, err := os.Getwd()
- if err != nil {
- t.Fatal(err)
- }
- rootDir := filepath.Join(dir, "../..")
-
- // If we don't have a .git directory, we're not in a git checkout (e.g.
- // a downstream package); skip this test.
- if _, err := os.Stat(filepath.Join(rootDir, ".git")); err != nil {
- t.Skipf("skipping test since .git doesn't exist: %v", err)
- }
-
- cmd := exec.Command("git", "grep", "-l", "-F", "http.Method")
- cmd.Dir = rootDir
- matches, _ := cmd.Output()
- for _, fn := range strings.Split(strings.TrimSpace(string(matches)), "\n") {
- switch fn {
- case "util/httpm/httpm.go", "util/httpm/httpm_test.go":
- continue
- }
- t.Errorf("http.MethodFoo constant used in %s; use httpm.FOO instead", fn)
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package httpm
+
+import (
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+func TestUsedConsistently(t *testing.T) {
+ dir, err := os.Getwd()
+ if err != nil {
+ t.Fatal(err)
+ }
+ rootDir := filepath.Join(dir, "../..")
+
+ // If we don't have a .git directory, we're not in a git checkout (e.g.
+ // a downstream package); skip this test.
+ if _, err := os.Stat(filepath.Join(rootDir, ".git")); err != nil {
+ t.Skipf("skipping test since .git doesn't exist: %v", err)
+ }
+
+ cmd := exec.Command("git", "grep", "-l", "-F", "http.Method")
+ cmd.Dir = rootDir
+ matches, _ := cmd.Output()
+ for _, fn := range strings.Split(strings.TrimSpace(string(matches)), "\n") {
+ switch fn {
+ case "util/httpm/httpm.go", "util/httpm/httpm_test.go":
+ continue
+ }
+ t.Errorf("http.MethodFoo constant used in %s; use httpm.FOO instead", fn)
+ }
+}
diff --git a/util/jsonutil/types.go b/util/jsonutil/types.go
index 057473249..2ee53f44a 100644
--- a/util/jsonutil/types.go
+++ b/util/jsonutil/types.go
@@ -1,16 +1,16 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package jsonutil
-
-// Bytes is a byte slice in a json-encoded struct.
-// encoding/json assumes that []byte fields are hex-encoded.
-// Bytes are not hex-encoded; they are treated the same as strings.
-// This can avoid unnecessary allocations due to a round trip through strings.
-type Bytes []byte
-
-func (b *Bytes) UnmarshalText(text []byte) error {
- // Copy the contexts of text.
- *b = append(*b, text...)
- return nil
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package jsonutil
+
+// Bytes is a byte slice in a json-encoded struct.
+// encoding/json assumes that []byte fields are hex-encoded.
+// Bytes are not hex-encoded; they are treated the same as strings.
+// This can avoid unnecessary allocations due to a round trip through strings.
+type Bytes []byte
+
+func (b *Bytes) UnmarshalText(text []byte) error {
+ // Copy the contexts of text.
+ *b = append(*b, text...)
+ return nil
+}
diff --git a/util/jsonutil/unmarshal.go b/util/jsonutil/unmarshal.go
index b1eb4ea87..13aea0c87 100644
--- a/util/jsonutil/unmarshal.go
+++ b/util/jsonutil/unmarshal.go
@@ -1,89 +1,89 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package jsonutil provides utilities to improve JSON performance.
-// It includes an Unmarshal wrapper that amortizes allocated garbage over subsequent runs
-// and a Bytes type to reduce allocations when unmarshalling a non-hex-encoded string into a []byte.
-package jsonutil
-
-import (
- "bytes"
- "encoding/json"
- "sync"
-)
-
-// decoder is a re-usable json decoder.
-type decoder struct {
- dec *json.Decoder
- r *bytes.Reader
-}
-
-var readerPool = sync.Pool{
- New: func() any {
- return bytes.NewReader(nil)
- },
-}
-
-var decoderPool = sync.Pool{
- New: func() any {
- var d decoder
- d.r = readerPool.Get().(*bytes.Reader)
- d.dec = json.NewDecoder(d.r)
- return &d
- },
-}
-
-// Unmarshal is similar to encoding/json.Unmarshal.
-// There are three major differences:
-//
-// On error, encoding/json.Unmarshal zeros v.
-// This Unmarshal may leave partial data in v.
-// Always check the error before using v!
-// (Future improvements may remove this bug.)
-//
-// The errors they return don't always match perfectly.
-// If you do error matching more precise than err != nil,
-// don't use this Unmarshal.
-//
-// This Unmarshal allocates considerably less memory.
-func Unmarshal(b []byte, v any) error {
- d := decoderPool.Get().(*decoder)
- d.r.Reset(b)
- off := d.dec.InputOffset()
- err := d.dec.Decode(v)
- d.r.Reset(nil) // don't keep a reference to b
- // In case of error, report the offset in this byte slice,
- // instead of in the totality of all bytes this decoder has processed.
- // It is not possible to make all errors match json.Unmarshal exactly,
- // but we can at least try.
- switch jsonerr := err.(type) {
- case *json.SyntaxError:
- jsonerr.Offset -= off
- case *json.UnmarshalTypeError:
- jsonerr.Offset -= off
- case nil:
- // json.Unmarshal fails if there's any extra junk in the input.
- // json.Decoder does not; see https://github.com/golang/go/issues/36225.
- // We need to check for anything left over in the buffer.
- if d.dec.More() {
- // TODO: Provide a better error message.
- // Unfortunately, we can't set the msg field.
- // The offset doesn't perfectly match json:
- // Ours is at the end of the valid data,
- // and theirs is at the beginning of the extra data after whitespace.
- // Close enough, though.
- err = &json.SyntaxError{Offset: d.dec.InputOffset() - off}
-
- // TODO: zero v. This is hard; see encoding/json.indirect.
- }
- }
- if err == nil {
- decoderPool.Put(d)
- } else {
- // There might be junk left in the decoder's buffer.
- // There's no way to flush it, no Reset method.
- // Abandoned the decoder but reuse the reader.
- readerPool.Put(d.r)
- }
- return err
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package jsonutil provides utilities to improve JSON performance.
+// It includes an Unmarshal wrapper that amortizes allocated garbage over subsequent runs
+// and a Bytes type to reduce allocations when unmarshalling a non-hex-encoded string into a []byte.
+package jsonutil
+
+import (
+ "bytes"
+ "encoding/json"
+ "sync"
+)
+
+// decoder is a re-usable json decoder.
+type decoder struct {
+ dec *json.Decoder
+ r *bytes.Reader
+}
+
+var readerPool = sync.Pool{
+ New: func() any {
+ return bytes.NewReader(nil)
+ },
+}
+
+var decoderPool = sync.Pool{
+ New: func() any {
+ var d decoder
+ d.r = readerPool.Get().(*bytes.Reader)
+ d.dec = json.NewDecoder(d.r)
+ return &d
+ },
+}
+
+// Unmarshal is similar to encoding/json.Unmarshal.
+// There are three major differences:
+//
+// On error, encoding/json.Unmarshal zeros v.
+// This Unmarshal may leave partial data in v.
+// Always check the error before using v!
+// (Future improvements may remove this bug.)
+//
+// The errors they return don't always match perfectly.
+// If you do error matching more precise than err != nil,
+// don't use this Unmarshal.
+//
+// This Unmarshal allocates considerably less memory.
+func Unmarshal(b []byte, v any) error {
+ d := decoderPool.Get().(*decoder)
+ d.r.Reset(b)
+ off := d.dec.InputOffset()
+ err := d.dec.Decode(v)
+ d.r.Reset(nil) // don't keep a reference to b
+ // In case of error, report the offset in this byte slice,
+ // instead of in the totality of all bytes this decoder has processed.
+ // It is not possible to make all errors match json.Unmarshal exactly,
+ // but we can at least try.
+ switch jsonerr := err.(type) {
+ case *json.SyntaxError:
+ jsonerr.Offset -= off
+ case *json.UnmarshalTypeError:
+ jsonerr.Offset -= off
+ case nil:
+ // json.Unmarshal fails if there's any extra junk in the input.
+ // json.Decoder does not; see https://github.com/golang/go/issues/36225.
+ // We need to check for anything left over in the buffer.
+ if d.dec.More() {
+ // TODO: Provide a better error message.
+ // Unfortunately, we can't set the msg field.
+ // The offset doesn't perfectly match json:
+ // Ours is at the end of the valid data,
+ // and theirs is at the beginning of the extra data after whitespace.
+ // Close enough, though.
+ err = &json.SyntaxError{Offset: d.dec.InputOffset() - off}
+
+ // TODO: zero v. This is hard; see encoding/json.indirect.
+ }
+ }
+ if err == nil {
+ decoderPool.Put(d)
+ } else {
+ // There might be junk left in the decoder's buffer.
+ // There's no way to flush it, no Reset method.
+ // Abandoned the decoder but reuse the reader.
+ readerPool.Put(d.r)
+ }
+ return err
+}
diff --git a/util/lineread/lineread.go b/util/lineread/lineread.go
index 6b01d2b69..2a7486e0a 100644
--- a/util/lineread/lineread.go
+++ b/util/lineread/lineread.go
@@ -1,37 +1,37 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package lineread reads lines from files. It's not fancy, but it got repetitive.
-package lineread
-
-import (
- "bufio"
- "io"
- "os"
-)
-
-// File opens name and calls fn for each line. It returns an error if the Open failed
-// or once fn returns an error.
-func File(name string, fn func(line []byte) error) error {
- f, err := os.Open(name)
- if err != nil {
- return err
- }
- defer f.Close()
- return Reader(f, fn)
-}
-
-// Reader calls fn for each line.
-// If fn returns an error, Reader stops reading and returns that error.
-// Reader may also return errors encountered reading and parsing from r.
-// To stop reading early, use a sentinel "stop" error value and ignore
-// it when returned from Reader.
-func Reader(r io.Reader, fn func(line []byte) error) error {
- bs := bufio.NewScanner(r)
- for bs.Scan() {
- if err := fn(bs.Bytes()); err != nil {
- return err
- }
- }
- return bs.Err()
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package lineread reads lines from files. It's not fancy, but it got repetitive.
+package lineread
+
+import (
+ "bufio"
+ "io"
+ "os"
+)
+
+// File opens name and calls fn for each line. It returns an error if the Open failed
+// or once fn returns an error.
+func File(name string, fn func(line []byte) error) error {
+ f, err := os.Open(name)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ return Reader(f, fn)
+}
+
+// Reader calls fn for each line.
+// If fn returns an error, Reader stops reading and returns that error.
+// Reader may also return errors encountered reading and parsing from r.
+// To stop reading early, use a sentinel "stop" error value and ignore
+// it when returned from Reader.
+func Reader(r io.Reader, fn func(line []byte) error) error {
+ bs := bufio.NewScanner(r)
+ for bs.Scan() {
+ if err := fn(bs.Bytes()); err != nil {
+ return err
+ }
+ }
+ return bs.Err()
+}
diff --git a/util/linuxfw/linuxfwtest/linuxfwtest.go b/util/linuxfw/linuxfwtest/linuxfwtest.go
index ee2cbd1b2..04f179199 100644
--- a/util/linuxfw/linuxfwtest/linuxfwtest.go
+++ b/util/linuxfw/linuxfwtest/linuxfwtest.go
@@ -1,31 +1,31 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build cgo && linux
-
-// Package linuxfwtest contains tests for the linuxfw package. Go does not
-// support cgo in tests, and we don't want the main package to have a cgo
-// dependency, so we put all the tests here and call them from the main package
-// in tests intead.
-package linuxfwtest
-
-import (
- "testing"
- "unsafe"
-)
-
-/*
-#include <sys/socket.h> // socket()
-*/
-import "C"
-
-type SizeInfo struct {
- SizeofSocklen uintptr
-}
-
-func TestSizes(t *testing.T, si *SizeInfo) {
- want := unsafe.Sizeof(C.socklen_t(0))
- if want != si.SizeofSocklen {
- t.Errorf("sockLen has wrong size; want=%d got=%d", want, si.SizeofSocklen)
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build cgo && linux
+
+// Package linuxfwtest contains tests for the linuxfw package. Go does not
+// support cgo in tests, and we don't want the main package to have a cgo
+// dependency, so we put all the tests here and call them from the main package
+// in tests intead.
+package linuxfwtest
+
+import (
+ "testing"
+ "unsafe"
+)
+
+/*
+#include <sys/socket.h> // socket()
+*/
+import "C"
+
+type SizeInfo struct {
+ SizeofSocklen uintptr
+}
+
+func TestSizes(t *testing.T, si *SizeInfo) {
+ want := unsafe.Sizeof(C.socklen_t(0))
+ if want != si.SizeofSocklen {
+ t.Errorf("sockLen has wrong size; want=%d got=%d", want, si.SizeofSocklen)
+ }
+}
diff --git a/util/linuxfw/linuxfwtest/linuxfwtest_unsupported.go b/util/linuxfw/linuxfwtest/linuxfwtest_unsupported.go
index 6e9569900..d5e297da7 100644
--- a/util/linuxfw/linuxfwtest/linuxfwtest_unsupported.go
+++ b/util/linuxfw/linuxfwtest/linuxfwtest_unsupported.go
@@ -1,18 +1,18 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build !cgo || !linux
-
-package linuxfwtest
-
-import (
- "testing"
-)
-
-type SizeInfo struct {
- SizeofSocklen uintptr
-}
-
-func TestSizes(t *testing.T, si *SizeInfo) {
- t.Skip("not supported without cgo")
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !cgo || !linux
+
+package linuxfwtest
+
+import (
+ "testing"
+)
+
+type SizeInfo struct {
+ SizeofSocklen uintptr
+}
+
+func TestSizes(t *testing.T, si *SizeInfo) {
+ t.Skip("not supported without cgo")
+}
diff --git a/util/linuxfw/nftables_types.go b/util/linuxfw/nftables_types.go
index b6e24d2a6..a8c5a0730 100644
--- a/util/linuxfw/nftables_types.go
+++ b/util/linuxfw/nftables_types.go
@@ -1,95 +1,95 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// TODO(#8502): add support for more architectures
-//go:build linux && (arm64 || amd64)
-
-package linuxfw
-
-import (
- "github.com/google/nftables/expr"
- "github.com/google/nftables/xt"
-)
-
-var metaKeyNames = map[expr.MetaKey]string{
- expr.MetaKeyLEN: "LEN",
- expr.MetaKeyPROTOCOL: "PROTOCOL",
- expr.MetaKeyPRIORITY: "PRIORITY",
- expr.MetaKeyMARK: "MARK",
- expr.MetaKeyIIF: "IIF",
- expr.MetaKeyOIF: "OIF",
- expr.MetaKeyIIFNAME: "IIFNAME",
- expr.MetaKeyOIFNAME: "OIFNAME",
- expr.MetaKeyIIFTYPE: "IIFTYPE",
- expr.MetaKeyOIFTYPE: "OIFTYPE",
- expr.MetaKeySKUID: "SKUID",
- expr.MetaKeySKGID: "SKGID",
- expr.MetaKeyNFTRACE: "NFTRACE",
- expr.MetaKeyRTCLASSID: "RTCLASSID",
- expr.MetaKeySECMARK: "SECMARK",
- expr.MetaKeyNFPROTO: "NFPROTO",
- expr.MetaKeyL4PROTO: "L4PROTO",
- expr.MetaKeyBRIIIFNAME: "BRIIIFNAME",
- expr.MetaKeyBRIOIFNAME: "BRIOIFNAME",
- expr.MetaKeyPKTTYPE: "PKTTYPE",
- expr.MetaKeyCPU: "CPU",
- expr.MetaKeyIIFGROUP: "IIFGROUP",
- expr.MetaKeyOIFGROUP: "OIFGROUP",
- expr.MetaKeyCGROUP: "CGROUP",
- expr.MetaKeyPRANDOM: "PRANDOM",
-}
-
-var cmpOpNames = map[expr.CmpOp]string{
- expr.CmpOpEq: "EQ",
- expr.CmpOpNeq: "NEQ",
- expr.CmpOpLt: "LT",
- expr.CmpOpLte: "LTE",
- expr.CmpOpGt: "GT",
- expr.CmpOpGte: "GTE",
-}
-
-var verdictNames = map[expr.VerdictKind]string{
- expr.VerdictReturn: "RETURN",
- expr.VerdictGoto: "GOTO",
- expr.VerdictJump: "JUMP",
- expr.VerdictBreak: "BREAK",
- expr.VerdictContinue: "CONTINUE",
- expr.VerdictDrop: "DROP",
- expr.VerdictAccept: "ACCEPT",
- expr.VerdictStolen: "STOLEN",
- expr.VerdictQueue: "QUEUE",
- expr.VerdictRepeat: "REPEAT",
- expr.VerdictStop: "STOP",
-}
-
-var payloadOperationTypeNames = map[expr.PayloadOperationType]string{
- expr.PayloadLoad: "LOAD",
- expr.PayloadWrite: "WRITE",
-}
-
-var payloadBaseNames = map[expr.PayloadBase]string{
- expr.PayloadBaseLLHeader: "ll-header",
- expr.PayloadBaseNetworkHeader: "network-header",
- expr.PayloadBaseTransportHeader: "transport-header",
-}
-
-var packetTypeNames = map[int]string{
- 0 /* PACKET_HOST */ : "unicast",
- 1 /* PACKET_BROADCAST */ : "broadcast",
- 2 /* PACKET_MULTICAST */ : "multicast",
-}
-
-var addrTypeFlagNames = map[xt.AddrTypeFlags]string{
- xt.AddrTypeUnspec: "unspec",
- xt.AddrTypeUnicast: "unicast",
- xt.AddrTypeLocal: "local",
- xt.AddrTypeBroadcast: "broadcast",
- xt.AddrTypeAnycast: "anycast",
- xt.AddrTypeMulticast: "multicast",
- xt.AddrTypeBlackhole: "blackhole",
- xt.AddrTypeUnreachable: "unreachable",
- xt.AddrTypeProhibit: "prohibit",
- xt.AddrTypeThrow: "throw",
- xt.AddrTypeNat: "nat",
- xt.AddrTypeXresolve: "xresolve",
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// TODO(#8502): add support for more architectures
+//go:build linux && (arm64 || amd64)
+
+package linuxfw
+
+import (
+ "github.com/google/nftables/expr"
+ "github.com/google/nftables/xt"
+)
+
+var metaKeyNames = map[expr.MetaKey]string{
+ expr.MetaKeyLEN: "LEN",
+ expr.MetaKeyPROTOCOL: "PROTOCOL",
+ expr.MetaKeyPRIORITY: "PRIORITY",
+ expr.MetaKeyMARK: "MARK",
+ expr.MetaKeyIIF: "IIF",
+ expr.MetaKeyOIF: "OIF",
+ expr.MetaKeyIIFNAME: "IIFNAME",
+ expr.MetaKeyOIFNAME: "OIFNAME",
+ expr.MetaKeyIIFTYPE: "IIFTYPE",
+ expr.MetaKeyOIFTYPE: "OIFTYPE",
+ expr.MetaKeySKUID: "SKUID",
+ expr.MetaKeySKGID: "SKGID",
+ expr.MetaKeyNFTRACE: "NFTRACE",
+ expr.MetaKeyRTCLASSID: "RTCLASSID",
+ expr.MetaKeySECMARK: "SECMARK",
+ expr.MetaKeyNFPROTO: "NFPROTO",
+ expr.MetaKeyL4PROTO: "L4PROTO",
+ expr.MetaKeyBRIIIFNAME: "BRIIIFNAME",
+ expr.MetaKeyBRIOIFNAME: "BRIOIFNAME",
+ expr.MetaKeyPKTTYPE: "PKTTYPE",
+ expr.MetaKeyCPU: "CPU",
+ expr.MetaKeyIIFGROUP: "IIFGROUP",
+ expr.MetaKeyOIFGROUP: "OIFGROUP",
+ expr.MetaKeyCGROUP: "CGROUP",
+ expr.MetaKeyPRANDOM: "PRANDOM",
+}
+
+var cmpOpNames = map[expr.CmpOp]string{
+ expr.CmpOpEq: "EQ",
+ expr.CmpOpNeq: "NEQ",
+ expr.CmpOpLt: "LT",
+ expr.CmpOpLte: "LTE",
+ expr.CmpOpGt: "GT",
+ expr.CmpOpGte: "GTE",
+}
+
+var verdictNames = map[expr.VerdictKind]string{
+ expr.VerdictReturn: "RETURN",
+ expr.VerdictGoto: "GOTO",
+ expr.VerdictJump: "JUMP",
+ expr.VerdictBreak: "BREAK",
+ expr.VerdictContinue: "CONTINUE",
+ expr.VerdictDrop: "DROP",
+ expr.VerdictAccept: "ACCEPT",
+ expr.VerdictStolen: "STOLEN",
+ expr.VerdictQueue: "QUEUE",
+ expr.VerdictRepeat: "REPEAT",
+ expr.VerdictStop: "STOP",
+}
+
+var payloadOperationTypeNames = map[expr.PayloadOperationType]string{
+ expr.PayloadLoad: "LOAD",
+ expr.PayloadWrite: "WRITE",
+}
+
+var payloadBaseNames = map[expr.PayloadBase]string{
+ expr.PayloadBaseLLHeader: "ll-header",
+ expr.PayloadBaseNetworkHeader: "network-header",
+ expr.PayloadBaseTransportHeader: "transport-header",
+}
+
+var packetTypeNames = map[int]string{
+ 0 /* PACKET_HOST */ : "unicast",
+ 1 /* PACKET_BROADCAST */ : "broadcast",
+ 2 /* PACKET_MULTICAST */ : "multicast",
+}
+
+var addrTypeFlagNames = map[xt.AddrTypeFlags]string{
+ xt.AddrTypeUnspec: "unspec",
+ xt.AddrTypeUnicast: "unicast",
+ xt.AddrTypeLocal: "local",
+ xt.AddrTypeBroadcast: "broadcast",
+ xt.AddrTypeAnycast: "anycast",
+ xt.AddrTypeMulticast: "multicast",
+ xt.AddrTypeBlackhole: "blackhole",
+ xt.AddrTypeUnreachable: "unreachable",
+ xt.AddrTypeProhibit: "prohibit",
+ xt.AddrTypeThrow: "throw",
+ xt.AddrTypeNat: "nat",
+ xt.AddrTypeXresolve: "xresolve",
+}
diff --git a/util/mak/mak.go b/util/mak/mak.go
index b421fb0ed..b0d64daa4 100644
--- a/util/mak/mak.go
+++ b/util/mak/mak.go
@@ -1,70 +1,70 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package mak helps make maps. It contains generic helpers to make/assign
-// things, notably to maps, but also slices.
-package mak
-
-import (
- "fmt"
- "reflect"
-)
-
-// Set populates an entry in a map, making the map if necessary.
-//
-// That is, it assigns (*m)[k] = v, making *m if it was nil.
-func Set[K comparable, V any, T ~map[K]V](m *T, k K, v V) {
- if *m == nil {
- *m = make(map[K]V)
- }
- (*m)[k] = v
-}
-
-// NonNil takes a pointer to a Go data structure
-// (currently only a slice or a map) and makes sure it's non-nil for
-// JSON serialization. (In particular, JavaScript clients usually want
-// the field to be defined after they decode the JSON.)
-//
-// Deprecated: use NonNilSliceForJSON or NonNilMapForJSON instead.
-func NonNil(ptr any) {
- if ptr == nil {
- panic("nil interface")
- }
- rv := reflect.ValueOf(ptr)
- if rv.Kind() != reflect.Ptr {
- panic(fmt.Sprintf("kind %v, not Ptr", rv.Kind()))
- }
- if rv.Pointer() == 0 {
- panic("nil pointer")
- }
- rv = rv.Elem()
- if rv.Pointer() != 0 {
- return
- }
- switch rv.Type().Kind() {
- case reflect.Slice:
- rv.Set(reflect.MakeSlice(rv.Type(), 0, 0))
- case reflect.Map:
- rv.Set(reflect.MakeMap(rv.Type()))
- }
-}
-
-// NonNilSliceForJSON makes sure that *slicePtr is non-nil so it will
-// won't be omitted from JSON serialization and possibly confuse JavaScript
-// clients expecting it to be present.
-func NonNilSliceForJSON[T any, S ~[]T](slicePtr *S) {
- if *slicePtr != nil {
- return
- }
- *slicePtr = make([]T, 0)
-}
-
-// NonNilMapForJSON makes sure that *slicePtr is non-nil so it will
-// won't be omitted from JSON serialization and possibly confuse JavaScript
-// clients expecting it to be present.
-func NonNilMapForJSON[K comparable, V any, M ~map[K]V](mapPtr *M) {
- if *mapPtr != nil {
- return
- }
- *mapPtr = make(M)
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package mak helps make maps. It contains generic helpers to make/assign
+// things, notably to maps, but also slices.
+package mak
+
+import (
+ "fmt"
+ "reflect"
+)
+
+// Set populates an entry in a map, making the map if necessary.
+//
+// That is, it assigns (*m)[k] = v, making *m if it was nil.
+func Set[K comparable, V any, T ~map[K]V](m *T, k K, v V) {
+ if *m == nil {
+ *m = make(map[K]V)
+ }
+ (*m)[k] = v
+}
+
+// NonNil takes a pointer to a Go data structure
+// (currently only a slice or a map) and makes sure it's non-nil for
+// JSON serialization. (In particular, JavaScript clients usually want
+// the field to be defined after they decode the JSON.)
+//
+// Deprecated: use NonNilSliceForJSON or NonNilMapForJSON instead.
+func NonNil(ptr any) {
+ if ptr == nil {
+ panic("nil interface")
+ }
+ rv := reflect.ValueOf(ptr)
+ if rv.Kind() != reflect.Ptr {
+ panic(fmt.Sprintf("kind %v, not Ptr", rv.Kind()))
+ }
+ if rv.Pointer() == 0 {
+ panic("nil pointer")
+ }
+ rv = rv.Elem()
+ if rv.Pointer() != 0 {
+ return
+ }
+ switch rv.Type().Kind() {
+ case reflect.Slice:
+ rv.Set(reflect.MakeSlice(rv.Type(), 0, 0))
+ case reflect.Map:
+ rv.Set(reflect.MakeMap(rv.Type()))
+ }
+}
+
+// NonNilSliceForJSON makes sure that *slicePtr is non-nil so it will
+// won't be omitted from JSON serialization and possibly confuse JavaScript
+// clients expecting it to be present.
+func NonNilSliceForJSON[T any, S ~[]T](slicePtr *S) {
+ if *slicePtr != nil {
+ return
+ }
+ *slicePtr = make([]T, 0)
+}
+
+// NonNilMapForJSON makes sure that *slicePtr is non-nil so it will
+// won't be omitted from JSON serialization and possibly confuse JavaScript
+// clients expecting it to be present.
+func NonNilMapForJSON[K comparable, V any, M ~map[K]V](mapPtr *M) {
+ if *mapPtr != nil {
+ return
+ }
+ *mapPtr = make(M)
+}
diff --git a/util/mak/mak_test.go b/util/mak/mak_test.go
index 4de499a9d..dc1d7e93d 100644
--- a/util/mak/mak_test.go
+++ b/util/mak/mak_test.go
@@ -1,88 +1,88 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package mak contains code to help make things.
-package mak
-
-import (
- "reflect"
- "testing"
-)
-
-type M map[string]int
-
-func TestSet(t *testing.T) {
- t.Run("unnamed", func(t *testing.T) {
- var m map[string]int
- Set(&m, "foo", 42)
- Set(&m, "bar", 1)
- Set(&m, "bar", 2)
- want := map[string]int{
- "foo": 42,
- "bar": 2,
- }
- if got := m; !reflect.DeepEqual(got, want) {
- t.Errorf("got %v; want %v", got, want)
- }
- })
- t.Run("named", func(t *testing.T) {
- var m M
- Set(&m, "foo", 1)
- Set(&m, "bar", 1)
- Set(&m, "bar", 2)
- want := M{
- "foo": 1,
- "bar": 2,
- }
- if got := m; !reflect.DeepEqual(got, want) {
- t.Errorf("got %v; want %v", got, want)
- }
- })
-}
-
-func TestNonNil(t *testing.T) {
- var s []string
- NonNil(&s)
- if len(s) != 0 {
- t.Errorf("slice len = %d; want 0", len(s))
- }
- if s == nil {
- t.Error("slice still nil")
- }
-
- s = append(s, "foo")
- NonNil(&s)
- if len(s) != 1 {
- t.Errorf("len = %d; want 1", len(s))
- }
- if s[0] != "foo" {
- t.Errorf("value = %q; want foo", s)
- }
-
- var m map[string]string
- NonNil(&m)
- if len(m) != 0 {
- t.Errorf("map len = %d; want 0", len(s))
- }
- if m == nil {
- t.Error("map still nil")
- }
-}
-
-func TestNonNilMapForJSON(t *testing.T) {
- type M map[string]int
- var m M
- NonNilMapForJSON(&m)
- if m == nil {
- t.Fatal("still nil")
- }
-}
-
-func TestNonNilSliceForJSON(t *testing.T) {
- type S []int
- var s S
- NonNilSliceForJSON(&s)
- if s == nil {
- t.Fatal("still nil")
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package mak contains code to help make things.
+package mak
+
+import (
+ "reflect"
+ "testing"
+)
+
+type M map[string]int
+
+func TestSet(t *testing.T) {
+ t.Run("unnamed", func(t *testing.T) {
+ var m map[string]int
+ Set(&m, "foo", 42)
+ Set(&m, "bar", 1)
+ Set(&m, "bar", 2)
+ want := map[string]int{
+ "foo": 42,
+ "bar": 2,
+ }
+ if got := m; !reflect.DeepEqual(got, want) {
+ t.Errorf("got %v; want %v", got, want)
+ }
+ })
+ t.Run("named", func(t *testing.T) {
+ var m M
+ Set(&m, "foo", 1)
+ Set(&m, "bar", 1)
+ Set(&m, "bar", 2)
+ want := M{
+ "foo": 1,
+ "bar": 2,
+ }
+ if got := m; !reflect.DeepEqual(got, want) {
+ t.Errorf("got %v; want %v", got, want)
+ }
+ })
+}
+
+func TestNonNil(t *testing.T) {
+ var s []string
+ NonNil(&s)
+ if len(s) != 0 {
+ t.Errorf("slice len = %d; want 0", len(s))
+ }
+ if s == nil {
+ t.Error("slice still nil")
+ }
+
+ s = append(s, "foo")
+ NonNil(&s)
+ if len(s) != 1 {
+ t.Errorf("len = %d; want 1", len(s))
+ }
+ if s[0] != "foo" {
+ t.Errorf("value = %q; want foo", s)
+ }
+
+ var m map[string]string
+ NonNil(&m)
+ if len(m) != 0 {
+ t.Errorf("map len = %d; want 0", len(s))
+ }
+ if m == nil {
+ t.Error("map still nil")
+ }
+}
+
+func TestNonNilMapForJSON(t *testing.T) {
+ type M map[string]int
+ var m M
+ NonNilMapForJSON(&m)
+ if m == nil {
+ t.Fatal("still nil")
+ }
+}
+
+func TestNonNilSliceForJSON(t *testing.T) {
+ type S []int
+ var s S
+ NonNilSliceForJSON(&s)
+ if s == nil {
+ t.Fatal("still nil")
+ }
+}
diff --git a/util/multierr/multierr.go b/util/multierr/multierr.go
index 93ca068f5..5ec36f644 100644
--- a/util/multierr/multierr.go
+++ b/util/multierr/multierr.go
@@ -1,136 +1,136 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package multierr provides a simple multiple-error type.
-// It was inspired by github.com/go-multierror/multierror.
-package multierr
-
-import (
- "errors"
- "slices"
- "strings"
-)
-
-// An Error represents multiple errors.
-type Error struct {
- errs []error
-}
-
-// Error implements the error interface.
-func (e Error) Error() string {
- s := new(strings.Builder)
- s.WriteString("multiple errors:")
- for _, err := range e.errs {
- s.WriteString("\n\t")
- s.WriteString(err.Error())
- }
- return s.String()
-}
-
-// Errors returns a slice containing all errors in e.
-func (e Error) Errors() []error {
- return slices.Clone(e.errs)
-}
-
-// Unwrap returns the underlying errors as-is.
-func (e Error) Unwrap() []error {
- // Do not clone since Unwrap requires callers to not mutate the slice.
- // See the documentation in the Go "errors" package.
- return e.errs
-}
-
-// New returns an error composed from errs.
-// Some errors in errs get special treatment:
-// - nil errors are discarded
-// - errors of type Error are expanded into the top level
-//
-// If the resulting slice has length 0, New returns nil.
-// If the resulting slice has length 1, New returns that error.
-// If the resulting slice has length > 1, New returns that slice as an Error.
-func New(errs ...error) error {
- // First count the number of errors to avoid allocating.
- var n int
- var errFirst error
- for _, e := range errs {
- switch e := e.(type) {
- case nil:
- continue
- case Error:
- n += len(e.errs)
- if errFirst == nil && len(e.errs) > 0 {
- errFirst = e.errs[0]
- }
- default:
- n++
- if errFirst == nil {
- errFirst = e
- }
- }
- }
- if n <= 1 {
- return errFirst // nil if n == 0
- }
-
- // More than one error, allocate slice and construct the multi-error.
- dst := make([]error, 0, n)
- for _, e := range errs {
- switch e := e.(type) {
- case nil:
- continue
- case Error:
- dst = append(dst, e.errs...)
- default:
- dst = append(dst, e)
- }
- }
- return Error{errs: dst}
-}
-
-// Is reports whether any error in e matches target.
-func (e Error) Is(target error) bool {
- for _, err := range e.errs {
- if errors.Is(err, target) {
- return true
- }
- }
- return false
-}
-
-// As finds the first error in e that matches target, and if any is found,
-// sets target to that error value and returns true. Otherwise, it returns false.
-func (e Error) As(target any) bool {
- for _, err := range e.errs {
- if ok := errors.As(err, target); ok {
- return true
- }
- }
- return false
-}
-
-// Range performs a pre-order, depth-first iteration of the error tree
-// by successively unwrapping all error values.
-// For each iteration it calls fn with the current error value and
-// stops iteration if it ever reports false.
-func Range(err error, fn func(error) bool) bool {
- if err == nil {
- return true
- }
- if !fn(err) {
- return false
- }
- switch err := err.(type) {
- case interface{ Unwrap() error }:
- if err := err.Unwrap(); err != nil {
- if !Range(err, fn) {
- return false
- }
- }
- case interface{ Unwrap() []error }:
- for _, err := range err.Unwrap() {
- if !Range(err, fn) {
- return false
- }
- }
- }
- return true
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package multierr provides a simple multiple-error type.
+// It was inspired by github.com/go-multierror/multierror.
+package multierr
+
+import (
+ "errors"
+ "slices"
+ "strings"
+)
+
+// An Error represents multiple errors.
+type Error struct {
+ errs []error
+}
+
+// Error implements the error interface.
+func (e Error) Error() string {
+ s := new(strings.Builder)
+ s.WriteString("multiple errors:")
+ for _, err := range e.errs {
+ s.WriteString("\n\t")
+ s.WriteString(err.Error())
+ }
+ return s.String()
+}
+
+// Errors returns a slice containing all errors in e.
+func (e Error) Errors() []error {
+ return slices.Clone(e.errs)
+}
+
+// Unwrap returns the underlying errors as-is.
+func (e Error) Unwrap() []error {
+ // Do not clone since Unwrap requires callers to not mutate the slice.
+ // See the documentation in the Go "errors" package.
+ return e.errs
+}
+
+// New returns an error composed from errs.
+// Some errors in errs get special treatment:
+// - nil errors are discarded
+// - errors of type Error are expanded into the top level
+//
+// If the resulting slice has length 0, New returns nil.
+// If the resulting slice has length 1, New returns that error.
+// If the resulting slice has length > 1, New returns that slice as an Error.
+func New(errs ...error) error {
+ // First count the number of errors to avoid allocating.
+ var n int
+ var errFirst error
+ for _, e := range errs {
+ switch e := e.(type) {
+ case nil:
+ continue
+ case Error:
+ n += len(e.errs)
+ if errFirst == nil && len(e.errs) > 0 {
+ errFirst = e.errs[0]
+ }
+ default:
+ n++
+ if errFirst == nil {
+ errFirst = e
+ }
+ }
+ }
+ if n <= 1 {
+ return errFirst // nil if n == 0
+ }
+
+ // More than one error, allocate slice and construct the multi-error.
+ dst := make([]error, 0, n)
+ for _, e := range errs {
+ switch e := e.(type) {
+ case nil:
+ continue
+ case Error:
+ dst = append(dst, e.errs...)
+ default:
+ dst = append(dst, e)
+ }
+ }
+ return Error{errs: dst}
+}
+
+// Is reports whether any error in e matches target.
+func (e Error) Is(target error) bool {
+ for _, err := range e.errs {
+ if errors.Is(err, target) {
+ return true
+ }
+ }
+ return false
+}
+
+// As finds the first error in e that matches target, and if any is found,
+// sets target to that error value and returns true. Otherwise, it returns false.
+func (e Error) As(target any) bool {
+ for _, err := range e.errs {
+ if ok := errors.As(err, target); ok {
+ return true
+ }
+ }
+ return false
+}
+
+// Range performs a pre-order, depth-first iteration of the error tree
+// by successively unwrapping all error values.
+// For each iteration it calls fn with the current error value and
+// stops iteration if it ever reports false.
+func Range(err error, fn func(error) bool) bool {
+ if err == nil {
+ return true
+ }
+ if !fn(err) {
+ return false
+ }
+ switch err := err.(type) {
+ case interface{ Unwrap() error }:
+ if err := err.Unwrap(); err != nil {
+ if !Range(err, fn) {
+ return false
+ }
+ }
+ case interface{ Unwrap() []error }:
+ for _, err := range err.Unwrap() {
+ if !Range(err, fn) {
+ return false
+ }
+ }
+ }
+ return true
+}
diff --git a/util/must/must.go b/util/must/must.go
index 21965daa9..056986fca 100644
--- a/util/must/must.go
+++ b/util/must/must.go
@@ -1,25 +1,25 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package must assists in calling functions that must succeed.
-//
-// Example usage:
-//
-// var target = must.Get(url.Parse(...))
-// must.Do(close())
-package must
-
-// Do panics if err is non-nil.
-func Do(err error) {
- if err != nil {
- panic(err)
- }
-}
-
-// Get returns v as is. It panics if err is non-nil.
-func Get[T any](v T, err error) T {
- if err != nil {
- panic(err)
- }
- return v
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package must assists in calling functions that must succeed.
+//
+// Example usage:
+//
+// var target = must.Get(url.Parse(...))
+// must.Do(close())
+package must
+
+// Do panics if err is non-nil.
+func Do(err error) {
+ if err != nil {
+ panic(err)
+ }
+}
+
+// Get returns v as is. It panics if err is non-nil.
+func Get[T any](v T, err error) T {
+ if err != nil {
+ panic(err)
+ }
+ return v
+}
diff --git a/util/osdiag/mksyscall.go b/util/osdiag/mksyscall.go
index bcbe113b0..f20be7f92 100644
--- a/util/osdiag/mksyscall.go
+++ b/util/osdiag/mksyscall.go
@@ -1,13 +1,13 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package osdiag
-
-//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go mksyscall.go
-//go:generate go run golang.org/x/tools/cmd/goimports -w zsyscall_windows.go
-
-//sys globalMemoryStatusEx(memStatus *_MEMORYSTATUSEX) (err error) [int32(failretval)==0] = kernel32.GlobalMemoryStatusEx
-//sys regEnumValue(key registry.Key, index uint32, valueName *uint16, valueNameLen *uint32, reserved *uint32, valueType *uint32, pData *byte, cbData *uint32) (ret error) [failretval!=0] = advapi32.RegEnumValueW
-//sys wscEnumProtocols(iProtocols *int32, protocolBuffer *wsaProtocolInfo, bufLen *uint32, errno *int32) (ret int32) = ws2_32.WSCEnumProtocols
-//sys wscGetProviderInfo(providerId *windows.GUID, infoType _WSC_PROVIDER_INFO_TYPE, info unsafe.Pointer, infoSize *uintptr, flags uint32, errno *int32) (ret int32) = ws2_32.WSCGetProviderInfo
-//sys wscGetProviderPath(providerId *windows.GUID, providerDllPath *uint16, providerDllPathLen *int32, errno *int32) (ret int32) = ws2_32.WSCGetProviderPath
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package osdiag
+
+//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go mksyscall.go
+//go:generate go run golang.org/x/tools/cmd/goimports -w zsyscall_windows.go
+
+//sys globalMemoryStatusEx(memStatus *_MEMORYSTATUSEX) (err error) [int32(failretval)==0] = kernel32.GlobalMemoryStatusEx
+//sys regEnumValue(key registry.Key, index uint32, valueName *uint16, valueNameLen *uint32, reserved *uint32, valueType *uint32, pData *byte, cbData *uint32) (ret error) [failretval!=0] = advapi32.RegEnumValueW
+//sys wscEnumProtocols(iProtocols *int32, protocolBuffer *wsaProtocolInfo, bufLen *uint32, errno *int32) (ret int32) = ws2_32.WSCEnumProtocols
+//sys wscGetProviderInfo(providerId *windows.GUID, infoType _WSC_PROVIDER_INFO_TYPE, info unsafe.Pointer, infoSize *uintptr, flags uint32, errno *int32) (ret int32) = ws2_32.WSCGetProviderInfo
+//sys wscGetProviderPath(providerId *windows.GUID, providerDllPath *uint16, providerDllPathLen *int32, errno *int32) (ret int32) = ws2_32.WSCGetProviderPath
diff --git a/util/osdiag/osdiag_windows_test.go b/util/osdiag/osdiag_windows_test.go
index b29b602cc..776852a34 100644
--- a/util/osdiag/osdiag_windows_test.go
+++ b/util/osdiag/osdiag_windows_test.go
@@ -1,128 +1,128 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package osdiag
-
-import (
- "errors"
- "fmt"
- "maps"
- "strings"
- "testing"
-
- "golang.org/x/sys/windows/registry"
-)
-
-func makeLongBinaryValue() []byte {
- buf := make([]byte, maxBinaryValueLen*2)
- for i, _ := range buf {
- buf[i] = byte(i % 0xFF)
- }
- return buf
-}
-
-var testData = map[string]any{
- "": "I am the default",
- "StringEmpty": "",
- "StringShort": "Hello",
- "StringLong": strings.Repeat("7", initialValueBufLen+1),
- "MultiStringEmpty": []string{},
- "MultiStringSingle": []string{"Foo"},
- "MultiStringSingleEmpty": []string{""},
- "MultiString": []string{"Foo", "Bar", "Baz"},
- "MultiStringWithEmptyBeginning": []string{"", "Foo", "Bar"},
- "MultiStringWithEmptyMiddle": []string{"Foo", "", "Bar"},
- "MultiStringWithEmptyEnd": []string{"Foo", "Bar", ""},
- "DWord": uint32(0x12345678),
- "QWord": uint64(0x123456789abcdef0),
- "BinaryEmpty": []byte{},
- "BinaryShort": []byte{0x01, 0x02, 0x03, 0x04},
- "BinaryLong": makeLongBinaryValue(),
-}
-
-const (
- keyNameTest = `SOFTWARE\Tailscale Test`
- subKeyNameTest = "SubKey"
-)
-
-func setValues(t *testing.T, k registry.Key) {
- for vk, v := range testData {
- var err error
- switch tv := v.(type) {
- case string:
- err = k.SetStringValue(vk, tv)
- case []string:
- err = k.SetStringsValue(vk, tv)
- case uint32:
- err = k.SetDWordValue(vk, tv)
- case uint64:
- err = k.SetQWordValue(vk, tv)
- case []byte:
- err = k.SetBinaryValue(vk, tv)
- default:
- t.Fatalf("Unknown type")
- }
-
- if err != nil {
- t.Fatalf("Error setting %q: %v", vk, err)
- }
- }
-}
-
-func TestRegistrySupportInfo(t *testing.T) {
- // Make sure the key doesn't exist yet
- k, err := registry.OpenKey(registry.CURRENT_USER, keyNameTest, registry.READ)
- switch {
- case err == nil:
- k.Close()
- t.Fatalf("Test key already exists")
- case !errors.Is(err, registry.ErrNotExist):
- t.Fatal(err)
- }
-
- func() {
- k, _, err := registry.CreateKey(registry.CURRENT_USER, keyNameTest, registry.WRITE)
- if err != nil {
- t.Fatalf("Error creating test key: %v", err)
- }
- defer k.Close()
-
- setValues(t, k)
-
- sk, _, err := registry.CreateKey(k, subKeyNameTest, registry.WRITE)
- if err != nil {
- t.Fatalf("Error creating test subkey: %v", err)
- }
- defer sk.Close()
-
- setValues(t, sk)
- }()
-
- t.Cleanup(func() {
- registry.DeleteKey(registry.CURRENT_USER, keyNameTest+"\\"+subKeyNameTest)
- registry.DeleteKey(registry.CURRENT_USER, keyNameTest)
- })
-
- wantValuesData := maps.Clone(testData)
- wantValuesData["BinaryLong"] = (wantValuesData["BinaryLong"].([]byte))[:maxBinaryValueLen]
-
- wantKeyData := make(map[string]any)
- maps.Copy(wantKeyData, wantValuesData)
- wantSubKeyData := make(map[string]any)
- maps.Copy(wantSubKeyData, wantValuesData)
- wantKeyData[subKeyNameTest] = wantSubKeyData
-
- wantData := map[string]any{
- "HKCU\\" + keyNameTest: wantKeyData,
- }
-
- gotData, err := getRegistrySupportInfo(registry.CURRENT_USER, []string{keyNameTest})
- if err != nil {
- t.Errorf("getRegistrySupportInfo error: %v", err)
- }
-
- want, got := fmt.Sprintf("%#v", wantData), fmt.Sprintf("%#v", gotData)
- if want != got {
- t.Errorf("Compare error: want\n%s,\ngot %s", want, got)
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package osdiag
+
+import (
+ "errors"
+ "fmt"
+ "maps"
+ "strings"
+ "testing"
+
+ "golang.org/x/sys/windows/registry"
+)
+
+func makeLongBinaryValue() []byte {
+ buf := make([]byte, maxBinaryValueLen*2)
+ for i, _ := range buf {
+ buf[i] = byte(i % 0xFF)
+ }
+ return buf
+}
+
+var testData = map[string]any{
+ "": "I am the default",
+ "StringEmpty": "",
+ "StringShort": "Hello",
+ "StringLong": strings.Repeat("7", initialValueBufLen+1),
+ "MultiStringEmpty": []string{},
+ "MultiStringSingle": []string{"Foo"},
+ "MultiStringSingleEmpty": []string{""},
+ "MultiString": []string{"Foo", "Bar", "Baz"},
+ "MultiStringWithEmptyBeginning": []string{"", "Foo", "Bar"},
+ "MultiStringWithEmptyMiddle": []string{"Foo", "", "Bar"},
+ "MultiStringWithEmptyEnd": []string{"Foo", "Bar", ""},
+ "DWord": uint32(0x12345678),
+ "QWord": uint64(0x123456789abcdef0),
+ "BinaryEmpty": []byte{},
+ "BinaryShort": []byte{0x01, 0x02, 0x03, 0x04},
+ "BinaryLong": makeLongBinaryValue(),
+}
+
+const (
+ keyNameTest = `SOFTWARE\Tailscale Test`
+ subKeyNameTest = "SubKey"
+)
+
+func setValues(t *testing.T, k registry.Key) {
+ for vk, v := range testData {
+ var err error
+ switch tv := v.(type) {
+ case string:
+ err = k.SetStringValue(vk, tv)
+ case []string:
+ err = k.SetStringsValue(vk, tv)
+ case uint32:
+ err = k.SetDWordValue(vk, tv)
+ case uint64:
+ err = k.SetQWordValue(vk, tv)
+ case []byte:
+ err = k.SetBinaryValue(vk, tv)
+ default:
+ t.Fatalf("Unknown type")
+ }
+
+ if err != nil {
+ t.Fatalf("Error setting %q: %v", vk, err)
+ }
+ }
+}
+
+func TestRegistrySupportInfo(t *testing.T) {
+ // Make sure the key doesn't exist yet
+ k, err := registry.OpenKey(registry.CURRENT_USER, keyNameTest, registry.READ)
+ switch {
+ case err == nil:
+ k.Close()
+ t.Fatalf("Test key already exists")
+ case !errors.Is(err, registry.ErrNotExist):
+ t.Fatal(err)
+ }
+
+ func() {
+ k, _, err := registry.CreateKey(registry.CURRENT_USER, keyNameTest, registry.WRITE)
+ if err != nil {
+ t.Fatalf("Error creating test key: %v", err)
+ }
+ defer k.Close()
+
+ setValues(t, k)
+
+ sk, _, err := registry.CreateKey(k, subKeyNameTest, registry.WRITE)
+ if err != nil {
+ t.Fatalf("Error creating test subkey: %v", err)
+ }
+ defer sk.Close()
+
+ setValues(t, sk)
+ }()
+
+ t.Cleanup(func() {
+ registry.DeleteKey(registry.CURRENT_USER, keyNameTest+"\\"+subKeyNameTest)
+ registry.DeleteKey(registry.CURRENT_USER, keyNameTest)
+ })
+
+ wantValuesData := maps.Clone(testData)
+ wantValuesData["BinaryLong"] = (wantValuesData["BinaryLong"].([]byte))[:maxBinaryValueLen]
+
+ wantKeyData := make(map[string]any)
+ maps.Copy(wantKeyData, wantValuesData)
+ wantSubKeyData := make(map[string]any)
+ maps.Copy(wantSubKeyData, wantValuesData)
+ wantKeyData[subKeyNameTest] = wantSubKeyData
+
+ wantData := map[string]any{
+ "HKCU\\" + keyNameTest: wantKeyData,
+ }
+
+ gotData, err := getRegistrySupportInfo(registry.CURRENT_USER, []string{keyNameTest})
+ if err != nil {
+ t.Errorf("getRegistrySupportInfo error: %v", err)
+ }
+
+ want, got := fmt.Sprintf("%#v", wantData), fmt.Sprintf("%#v", gotData)
+ if want != got {
+ t.Errorf("Compare error: want\n%s,\ngot %s", want, got)
+ }
+}
diff --git a/util/osshare/filesharingstatus_noop.go b/util/osshare/filesharingstatus_noop.go
index 7f2b13190..6be4131a9 100644
--- a/util/osshare/filesharingstatus_noop.go
+++ b/util/osshare/filesharingstatus_noop.go
@@ -1,12 +1,12 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build !windows
-
-package osshare
-
-import (
- "tailscale.com/types/logger"
-)
-
-func SetFileSharingEnabled(enabled bool, logf logger.Logf) {}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !windows
+
+package osshare
+
+import (
+ "tailscale.com/types/logger"
+)
+
+func SetFileSharingEnabled(enabled bool, logf logger.Logf) {}
diff --git a/util/pidowner/pidowner.go b/util/pidowner/pidowner.go
index 56bb640b7..62ea85d78 100644
--- a/util/pidowner/pidowner.go
+++ b/util/pidowner/pidowner.go
@@ -1,24 +1,24 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package pidowner handles lookups from process ID to its owning user.
-package pidowner
-
-import (
- "errors"
- "runtime"
-)
-
-var ErrNotImplemented = errors.New("not implemented for GOOS=" + runtime.GOOS)
-
-var ErrProcessNotFound = errors.New("process not found")
-
-// OwnerOfPID returns the user ID that owns the given process ID.
-//
-// The returned user ID is suitable to passing to os/user.LookupId.
-//
-// The returned error will be ErrNotImplemented for operating systems where
-// this isn't supported.
-func OwnerOfPID(pid int) (userID string, err error) {
- return ownerOfPID(pid)
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package pidowner handles lookups from process ID to its owning user.
+package pidowner
+
+import (
+ "errors"
+ "runtime"
+)
+
+var ErrNotImplemented = errors.New("not implemented for GOOS=" + runtime.GOOS)
+
+var ErrProcessNotFound = errors.New("process not found")
+
+// OwnerOfPID returns the user ID that owns the given process ID.
+//
+// The returned user ID is suitable to passing to os/user.LookupId.
+//
+// The returned error will be ErrNotImplemented for operating systems where
+// this isn't supported.
+func OwnerOfPID(pid int) (userID string, err error) {
+ return ownerOfPID(pid)
+}
diff --git a/util/pidowner/pidowner_noimpl.go b/util/pidowner/pidowner_noimpl.go
index 50add492f..a631e3f24 100644
--- a/util/pidowner/pidowner_noimpl.go
+++ b/util/pidowner/pidowner_noimpl.go
@@ -1,8 +1,8 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build !windows && !linux
-
-package pidowner
-
-func ownerOfPID(pid int) (userID string, err error) { return "", ErrNotImplemented }
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !windows && !linux
+
+package pidowner
+
+func ownerOfPID(pid int) (userID string, err error) { return "", ErrNotImplemented }
diff --git a/util/pidowner/pidowner_windows.go b/util/pidowner/pidowner_windows.go
index dbf13ac81..c7b2512a4 100644
--- a/util/pidowner/pidowner_windows.go
+++ b/util/pidowner/pidowner_windows.go
@@ -1,35 +1,35 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package pidowner
-
-import (
- "fmt"
- "syscall"
-
- "golang.org/x/sys/windows"
-)
-
-func ownerOfPID(pid int) (userID string, err error) {
- procHnd, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION, false, uint32(pid))
- if err == syscall.Errno(0x57) { // invalid parameter, for PIDs that don't exist
- return "", ErrProcessNotFound
- }
- if err != nil {
- return "", fmt.Errorf("OpenProcess: %T %#v", err, err)
- }
- defer windows.CloseHandle(procHnd)
-
- var tok windows.Token
- if err := windows.OpenProcessToken(procHnd, windows.TOKEN_QUERY, &tok); err != nil {
- return "", fmt.Errorf("OpenProcessToken: %w", err)
- }
-
- tokUser, err := tok.GetTokenUser()
- if err != nil {
- return "", fmt.Errorf("GetTokenUser: %w", err)
- }
-
- sid := tokUser.User.Sid
- return sid.String(), nil
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package pidowner
+
+import (
+ "fmt"
+ "syscall"
+
+ "golang.org/x/sys/windows"
+)
+
+func ownerOfPID(pid int) (userID string, err error) {
+ procHnd, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION, false, uint32(pid))
+ if err == syscall.Errno(0x57) { // invalid parameter, for PIDs that don't exist
+ return "", ErrProcessNotFound
+ }
+ if err != nil {
+ return "", fmt.Errorf("OpenProcess: %T %#v", err, err)
+ }
+ defer windows.CloseHandle(procHnd)
+
+ var tok windows.Token
+ if err := windows.OpenProcessToken(procHnd, windows.TOKEN_QUERY, &tok); err != nil {
+ return "", fmt.Errorf("OpenProcessToken: %w", err)
+ }
+
+ tokUser, err := tok.GetTokenUser()
+ if err != nil {
+ return "", fmt.Errorf("GetTokenUser: %w", err)
+ }
+
+ sid := tokUser.User.Sid
+ return sid.String(), nil
+}
diff --git a/util/precompress/precompress.go b/util/precompress/precompress.go
index 6d1a26efd..e9bebb333 100644
--- a/util/precompress/precompress.go
+++ b/util/precompress/precompress.go
@@ -1,129 +1,129 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package precompress provides build- and serving-time support for
-// precompressed static resources, to avoid the cost of repeatedly compressing
-// unchanging resources.
-package precompress
-
-import (
- "bytes"
- "compress/gzip"
- "io"
- "io/fs"
- "net/http"
- "os"
- "path"
- "path/filepath"
-
- "github.com/andybalholm/brotli"
- "golang.org/x/sync/errgroup"
- "tailscale.com/tsweb"
-)
-
-// PrecompressDir compresses static assets in dirPath using Gzip and Brotli, so
-// that they can be later served with OpenPrecompressedFile.
-func PrecompressDir(dirPath string, options Options) error {
- var eg errgroup.Group
- err := fs.WalkDir(os.DirFS(dirPath), ".", func(p string, d fs.DirEntry, err error) error {
- if err != nil {
- return err
- }
- if d.IsDir() {
- return nil
- }
- if !compressibleExtensions[filepath.Ext(p)] {
- return nil
- }
- p = path.Join(dirPath, p)
- if options.ProgressFn != nil {
- options.ProgressFn(p)
- }
-
- eg.Go(func() error {
- return Precompress(p, options)
- })
- return nil
- })
- if err != nil {
- return err
- }
- return eg.Wait()
-}
-
-type Options struct {
- // FastCompression controls whether compression should be optimized for
- // speed rather than size.
- FastCompression bool
- // ProgressFn, if non-nil, is invoked when a file in the directory is about
- // to be compressed.
- ProgressFn func(path string)
-}
-
-// OpenPrecompressedFile opens a file from fs, preferring compressed versions
-// generated by PrecompressDir if possible.
-func OpenPrecompressedFile(w http.ResponseWriter, r *http.Request, path string, fs fs.FS) (fs.File, error) {
- if tsweb.AcceptsEncoding(r, "br") {
- if f, err := fs.Open(path + ".br"); err == nil {
- w.Header().Set("Content-Encoding", "br")
- return f, nil
- }
- }
- if tsweb.AcceptsEncoding(r, "gzip") {
- if f, err := fs.Open(path + ".gz"); err == nil {
- w.Header().Set("Content-Encoding", "gzip")
- return f, nil
- }
- }
-
- return fs.Open(path)
-}
-
-var compressibleExtensions = map[string]bool{
- ".js": true,
- ".css": true,
-}
-
-func Precompress(path string, options Options) error {
- contents, err := os.ReadFile(path)
- if err != nil {
- return err
- }
- fi, err := os.Lstat(path)
- if err != nil {
- return err
- }
-
- gzipLevel := gzip.BestCompression
- if options.FastCompression {
- gzipLevel = gzip.BestSpeed
- }
- err = writeCompressed(contents, func(w io.Writer) (io.WriteCloser, error) {
- return gzip.NewWriterLevel(w, gzipLevel)
- }, path+".gz", fi.Mode())
- if err != nil {
- return err
- }
- brotliLevel := brotli.BestCompression
- if options.FastCompression {
- brotliLevel = brotli.BestSpeed
- }
- return writeCompressed(contents, func(w io.Writer) (io.WriteCloser, error) {
- return brotli.NewWriterLevel(w, brotliLevel), nil
- }, path+".br", fi.Mode())
-}
-
-func writeCompressed(contents []byte, compressedWriterCreator func(io.Writer) (io.WriteCloser, error), outputPath string, outputMode fs.FileMode) error {
- var buf bytes.Buffer
- compressedWriter, err := compressedWriterCreator(&buf)
- if err != nil {
- return err
- }
- if _, err := compressedWriter.Write(contents); err != nil {
- return err
- }
- if err := compressedWriter.Close(); err != nil {
- return err
- }
- return os.WriteFile(outputPath, buf.Bytes(), outputMode)
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package precompress provides build- and serving-time support for
+// precompressed static resources, to avoid the cost of repeatedly compressing
+// unchanging resources.
+package precompress
+
+import (
+ "bytes"
+ "compress/gzip"
+ "io"
+ "io/fs"
+ "net/http"
+ "os"
+ "path"
+ "path/filepath"
+
+ "github.com/andybalholm/brotli"
+ "golang.org/x/sync/errgroup"
+ "tailscale.com/tsweb"
+)
+
+// PrecompressDir compresses static assets in dirPath using Gzip and Brotli, so
+// that they can be later served with OpenPrecompressedFile.
+func PrecompressDir(dirPath string, options Options) error {
+ var eg errgroup.Group
+ err := fs.WalkDir(os.DirFS(dirPath), ".", func(p string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if d.IsDir() {
+ return nil
+ }
+ if !compressibleExtensions[filepath.Ext(p)] {
+ return nil
+ }
+ p = path.Join(dirPath, p)
+ if options.ProgressFn != nil {
+ options.ProgressFn(p)
+ }
+
+ eg.Go(func() error {
+ return Precompress(p, options)
+ })
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+ return eg.Wait()
+}
+
+type Options struct {
+ // FastCompression controls whether compression should be optimized for
+ // speed rather than size.
+ FastCompression bool
+ // ProgressFn, if non-nil, is invoked when a file in the directory is about
+ // to be compressed.
+ ProgressFn func(path string)
+}
+
+// OpenPrecompressedFile opens a file from fs, preferring compressed versions
+// generated by PrecompressDir if possible.
+func OpenPrecompressedFile(w http.ResponseWriter, r *http.Request, path string, fs fs.FS) (fs.File, error) {
+ if tsweb.AcceptsEncoding(r, "br") {
+ if f, err := fs.Open(path + ".br"); err == nil {
+ w.Header().Set("Content-Encoding", "br")
+ return f, nil
+ }
+ }
+ if tsweb.AcceptsEncoding(r, "gzip") {
+ if f, err := fs.Open(path + ".gz"); err == nil {
+ w.Header().Set("Content-Encoding", "gzip")
+ return f, nil
+ }
+ }
+
+ return fs.Open(path)
+}
+
+var compressibleExtensions = map[string]bool{
+ ".js": true,
+ ".css": true,
+}
+
+func Precompress(path string, options Options) error {
+ contents, err := os.ReadFile(path)
+ if err != nil {
+ return err
+ }
+ fi, err := os.Lstat(path)
+ if err != nil {
+ return err
+ }
+
+ gzipLevel := gzip.BestCompression
+ if options.FastCompression {
+ gzipLevel = gzip.BestSpeed
+ }
+ err = writeCompressed(contents, func(w io.Writer) (io.WriteCloser, error) {
+ return gzip.NewWriterLevel(w, gzipLevel)
+ }, path+".gz", fi.Mode())
+ if err != nil {
+ return err
+ }
+ brotliLevel := brotli.BestCompression
+ if options.FastCompression {
+ brotliLevel = brotli.BestSpeed
+ }
+ return writeCompressed(contents, func(w io.Writer) (io.WriteCloser, error) {
+ return brotli.NewWriterLevel(w, brotliLevel), nil
+ }, path+".br", fi.Mode())
+}
+
+func writeCompressed(contents []byte, compressedWriterCreator func(io.Writer) (io.WriteCloser, error), outputPath string, outputMode fs.FileMode) error {
+ var buf bytes.Buffer
+ compressedWriter, err := compressedWriterCreator(&buf)
+ if err != nil {
+ return err
+ }
+ if _, err := compressedWriter.Write(contents); err != nil {
+ return err
+ }
+ if err := compressedWriter.Close(); err != nil {
+ return err
+ }
+ return os.WriteFile(outputPath, buf.Bytes(), outputMode)
+}
diff --git a/util/quarantine/quarantine.go b/util/quarantine/quarantine.go
index 7ad65a81d..488465ba0 100644
--- a/util/quarantine/quarantine.go
+++ b/util/quarantine/quarantine.go
@@ -1,14 +1,14 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package quarantine sets platform specific "quarantine" attributes on files
-// that are received from other hosts.
-package quarantine
-
-import "os"
-
-// SetOnFile sets the platform-specific quarantine attribute (if any) on the
-// provided file.
-func SetOnFile(f *os.File) error {
- return setQuarantineAttr(f)
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package quarantine sets platform specific "quarantine" attributes on files
+// that are received from other hosts.
+package quarantine
+
+import "os"
+
+// SetOnFile sets the platform-specific quarantine attribute (if any) on the
+// provided file.
+func SetOnFile(f *os.File) error {
+ return setQuarantineAttr(f)
+}
diff --git a/util/quarantine/quarantine_darwin.go b/util/quarantine/quarantine_darwin.go
index 35405d9cc..b7757f334 100644
--- a/util/quarantine/quarantine_darwin.go
+++ b/util/quarantine/quarantine_darwin.go
@@ -1,56 +1,56 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package quarantine
-
-import (
- "fmt"
- "os"
- "strings"
- "time"
-
- "github.com/google/uuid"
- "golang.org/x/sys/unix"
-)
-
-func setQuarantineAttr(f *os.File) error {
- sc, err := f.SyscallConn()
- if err != nil {
- return err
- }
-
- now := time.Now()
-
- // We uppercase the UUID to match what other applications on macOS do
- id := strings.ToUpper(uuid.New().String())
-
- // kLSQuarantineTypeOtherDownload; this matches what AirDrop sets when
- // receiving a file.
- quarantineType := "0001"
-
- // This format is under-documented, but the following links contain a
- // reasonably comprehensive overview:
- // https://eclecticlight.co/2020/10/29/quarantine-and-the-quarantine-flag/
- // https://nixhacker.com/security-protection-in-macos-1/
- // https://ilostmynotes.blogspot.com/2012/06/gatekeeper-xprotect-and-quarantine.html
- attrData := fmt.Sprintf("%s;%x;%s;%s",
- quarantineType, // quarantine value
- now.Unix(), // time in hex
- "Tailscale", // application
- id, // UUID
- )
-
- var innerErr error
- err = sc.Control(func(fd uintptr) {
- innerErr = unix.Fsetxattr(
- int(fd),
- "com.apple.quarantine", // attr
- []byte(attrData),
- 0,
- )
- })
- if err != nil {
- return err
- }
- return innerErr
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package quarantine
+
+import (
+ "fmt"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/google/uuid"
+ "golang.org/x/sys/unix"
+)
+
+func setQuarantineAttr(f *os.File) error {
+ sc, err := f.SyscallConn()
+ if err != nil {
+ return err
+ }
+
+ now := time.Now()
+
+ // We uppercase the UUID to match what other applications on macOS do
+ id := strings.ToUpper(uuid.New().String())
+
+ // kLSQuarantineTypeOtherDownload; this matches what AirDrop sets when
+ // receiving a file.
+ quarantineType := "0001"
+
+ // This format is under-documented, but the following links contain a
+ // reasonably comprehensive overview:
+ // https://eclecticlight.co/2020/10/29/quarantine-and-the-quarantine-flag/
+ // https://nixhacker.com/security-protection-in-macos-1/
+ // https://ilostmynotes.blogspot.com/2012/06/gatekeeper-xprotect-and-quarantine.html
+ attrData := fmt.Sprintf("%s;%x;%s;%s",
+ quarantineType, // quarantine value
+ now.Unix(), // time in hex
+ "Tailscale", // application
+ id, // UUID
+ )
+
+ var innerErr error
+ err = sc.Control(func(fd uintptr) {
+ innerErr = unix.Fsetxattr(
+ int(fd),
+ "com.apple.quarantine", // attr
+ []byte(attrData),
+ 0,
+ )
+ })
+ if err != nil {
+ return err
+ }
+ return innerErr
+}
diff --git a/util/quarantine/quarantine_default.go b/util/quarantine/quarantine_default.go
index 65954a4d2..65a14ed26 100644
--- a/util/quarantine/quarantine_default.go
+++ b/util/quarantine/quarantine_default.go
@@ -1,14 +1,14 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build !darwin && !windows
-
-package quarantine
-
-import (
- "os"
-)
-
-func setQuarantineAttr(f *os.File) error {
- return nil
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !darwin && !windows
+
+package quarantine
+
+import (
+ "os"
+)
+
+func setQuarantineAttr(f *os.File) error {
+ return nil
+}
diff --git a/util/quarantine/quarantine_windows.go b/util/quarantine/quarantine_windows.go
index 6fdf4e699..3052c2c6d 100644
--- a/util/quarantine/quarantine_windows.go
+++ b/util/quarantine/quarantine_windows.go
@@ -1,29 +1,29 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package quarantine
-
-import (
- "os"
- "strings"
-)
-
-func setQuarantineAttr(f *os.File) error {
- // Documentation on this can be found here:
- // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/6e3f7352-d11c-4d76-8c39-2516a9df36e8
- //
- // Additional information can be found at:
- // https://www.digital-detective.net/forensic-analysis-of-zone-identifier-stream/
- // https://bugzilla.mozilla.org/show_bug.cgi?id=1433179
- content := strings.Join([]string{
- "[ZoneTransfer]",
-
- // "URLZONE_INTERNET"
- // https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/ms537175(v=vs.85)
- "ZoneId=3",
-
- // TODO(andrew): should/could we add ReferrerUrl or HostUrl?
- }, "\r\n")
-
- return os.WriteFile(f.Name()+":Zone.Identifier", []byte(content), 0)
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package quarantine
+
+import (
+ "os"
+ "strings"
+)
+
+func setQuarantineAttr(f *os.File) error {
+ // Documentation on this can be found here:
+ // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/6e3f7352-d11c-4d76-8c39-2516a9df36e8
+ //
+ // Additional information can be found at:
+ // https://www.digital-detective.net/forensic-analysis-of-zone-identifier-stream/
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1433179
+ content := strings.Join([]string{
+ "[ZoneTransfer]",
+
+ // "URLZONE_INTERNET"
+ // https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/ms537175(v=vs.85)
+ "ZoneId=3",
+
+ // TODO(andrew): should/could we add ReferrerUrl or HostUrl?
+ }, "\r\n")
+
+ return os.WriteFile(f.Name()+":Zone.Identifier", []byte(content), 0)
+}
diff --git a/util/race/race_test.go b/util/race/race_test.go
index d38382712..17ea76459 100644
--- a/util/race/race_test.go
+++ b/util/race/race_test.go
@@ -1,99 +1,99 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package race
-
-import (
- "context"
- "errors"
- "testing"
- "time"
-
- "tailscale.com/tstest"
-)
-
-func TestRaceSuccess1(t *testing.T) {
- tstest.ResourceCheck(t)
-
- const want = "success"
- rh := New[string](
- 10*time.Second,
- func(context.Context) (string, error) {
- return want, nil
- }, func(context.Context) (string, error) {
- t.Fatal("should not be called")
- return "", nil
- })
- res, err := rh.Start(context.Background())
- if err != nil {
- t.Fatal(err)
- }
- if res != want {
- t.Errorf("got res=%q, want %q", res, want)
- }
-}
-
-func TestRaceRetry(t *testing.T) {
- tstest.ResourceCheck(t)
-
- const want = "fallback"
- rh := New[string](
- 10*time.Second,
- func(context.Context) (string, error) {
- return "", errors.New("some error")
- }, func(context.Context) (string, error) {
- return want, nil
- })
- res, err := rh.Start(context.Background())
- if err != nil {
- t.Fatal(err)
- }
- if res != want {
- t.Errorf("got res=%q, want %q", res, want)
- }
-}
-
-func TestRaceTimeout(t *testing.T) {
- tstest.ResourceCheck(t)
-
- const want = "fallback"
- rh := New[string](
- 100*time.Millisecond,
- func(ctx context.Context) (string, error) {
- // Block forever
- <-ctx.Done()
- return "", ctx.Err()
- }, func(context.Context) (string, error) {
- return want, nil
- })
- res, err := rh.Start(context.Background())
- if err != nil {
- t.Fatal(err)
- }
- if res != want {
- t.Errorf("got res=%q, want %q", res, want)
- }
-}
-
-func TestRaceError(t *testing.T) {
- tstest.ResourceCheck(t)
-
- err1 := errors.New("error 1")
- err2 := errors.New("error 2")
-
- rh := New[string](
- 100*time.Millisecond,
- func(ctx context.Context) (string, error) {
- return "", err1
- }, func(context.Context) (string, error) {
- return "", err2
- })
-
- _, err := rh.Start(context.Background())
- if !errors.Is(err, err1) {
- t.Errorf("wanted err to contain err1; got %v", err)
- }
- if !errors.Is(err, err2) {
- t.Errorf("wanted err to contain err2; got %v", err)
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package race
+
+import (
+ "context"
+ "errors"
+ "testing"
+ "time"
+
+ "tailscale.com/tstest"
+)
+
+func TestRaceSuccess1(t *testing.T) {
+ tstest.ResourceCheck(t)
+
+ const want = "success"
+ rh := New[string](
+ 10*time.Second,
+ func(context.Context) (string, error) {
+ return want, nil
+ }, func(context.Context) (string, error) {
+ t.Fatal("should not be called")
+ return "", nil
+ })
+ res, err := rh.Start(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res != want {
+ t.Errorf("got res=%q, want %q", res, want)
+ }
+}
+
+func TestRaceRetry(t *testing.T) {
+ tstest.ResourceCheck(t)
+
+ const want = "fallback"
+ rh := New[string](
+ 10*time.Second,
+ func(context.Context) (string, error) {
+ return "", errors.New("some error")
+ }, func(context.Context) (string, error) {
+ return want, nil
+ })
+ res, err := rh.Start(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res != want {
+ t.Errorf("got res=%q, want %q", res, want)
+ }
+}
+
+func TestRaceTimeout(t *testing.T) {
+ tstest.ResourceCheck(t)
+
+ const want = "fallback"
+ rh := New[string](
+ 100*time.Millisecond,
+ func(ctx context.Context) (string, error) {
+ // Block forever
+ <-ctx.Done()
+ return "", ctx.Err()
+ }, func(context.Context) (string, error) {
+ return want, nil
+ })
+ res, err := rh.Start(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res != want {
+ t.Errorf("got res=%q, want %q", res, want)
+ }
+}
+
+func TestRaceError(t *testing.T) {
+ tstest.ResourceCheck(t)
+
+ err1 := errors.New("error 1")
+ err2 := errors.New("error 2")
+
+ rh := New[string](
+ 100*time.Millisecond,
+ func(ctx context.Context) (string, error) {
+ return "", err1
+ }, func(context.Context) (string, error) {
+ return "", err2
+ })
+
+ _, err := rh.Start(context.Background())
+ if !errors.Is(err, err1) {
+ t.Errorf("wanted err to contain err1; got %v", err)
+ }
+ if !errors.Is(err, err2) {
+ t.Errorf("wanted err to contain err2; got %v", err)
+ }
+}
diff --git a/util/racebuild/off.go b/util/racebuild/off.go
index 8f4fe998f..a0dba0f32 100644
--- a/util/racebuild/off.go
+++ b/util/racebuild/off.go
@@ -1,8 +1,8 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build !race
-
-package racebuild
-
-const On = false
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !race
+
+package racebuild
+
+const On = false
diff --git a/util/racebuild/on.go b/util/racebuild/on.go
index 69ae2bcae..c60bca2e6 100644
--- a/util/racebuild/on.go
+++ b/util/racebuild/on.go
@@ -1,8 +1,8 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build race
-
-package racebuild
-
-const On = true
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build race
+
+package racebuild
+
+const On = true
diff --git a/util/racebuild/racebuild.go b/util/racebuild/racebuild.go
index d061276cb..c1a43eb96 100644
--- a/util/racebuild/racebuild.go
+++ b/util/racebuild/racebuild.go
@@ -1,6 +1,6 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package racebuild exports a constant about whether the current binary
-// was built with the race detector.
-package racebuild
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package racebuild exports a constant about whether the current binary
+// was built with the race detector.
+package racebuild
diff --git a/util/rands/rands.go b/util/rands/rands.go
index d83e1e558..dcd75c5f3 100644
--- a/util/rands/rands.go
+++ b/util/rands/rands.go
@@ -1,25 +1,25 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package rands contains utility functions for randomness.
-package rands
-
-import (
- crand "crypto/rand"
- "encoding/hex"
-)
-
-// HexString returns a string of n cryptographically random lowercase
-// hex characters.
-//
-// That is, HexString(3) returns something like "0fc", containing 12
-// bits of randomness.
-func HexString(n int) string {
- nb := n / 2
- if n%2 == 1 {
- nb++
- }
- b := make([]byte, nb)
- crand.Read(b)
- return hex.EncodeToString(b)[:n]
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package rands contains utility functions for randomness.
+package rands
+
+import (
+ crand "crypto/rand"
+ "encoding/hex"
+)
+
+// HexString returns a string of n cryptographically random lowercase
+// hex characters.
+//
+// That is, HexString(3) returns something like "0fc", containing 12
+// bits of randomness.
+func HexString(n int) string {
+ nb := n / 2
+ if n%2 == 1 {
+ nb++
+ }
+ b := make([]byte, nb)
+ crand.Read(b)
+ return hex.EncodeToString(b)[:n]
+}
diff --git a/util/rands/rands_test.go b/util/rands/rands_test.go
index 5813f2bb4..ec339f94b 100644
--- a/util/rands/rands_test.go
+++ b/util/rands/rands_test.go
@@ -1,15 +1,15 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package rands
-
-import "testing"
-
-func TestHexString(t *testing.T) {
- for i := 0; i <= 8; i++ {
- s := HexString(i)
- if len(s) != i {
- t.Errorf("HexString(%v) = %q; want len %v, not %v", i, s, i, len(s))
- }
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package rands
+
+import "testing"
+
+func TestHexString(t *testing.T) {
+ for i := 0; i <= 8; i++ {
+ s := HexString(i)
+ if len(s) != i {
+ t.Errorf("HexString(%v) = %q; want len %v, not %v", i, s, i, len(s))
+ }
+ }
+}
diff --git a/util/set/handle.go b/util/set/handle.go
index 471ceeba2..61b4eb93d 100644
--- a/util/set/handle.go
+++ b/util/set/handle.go
@@ -1,28 +1,28 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package set
-
-// HandleSet is a set of T.
-//
-// It is not safe for concurrent use.
-type HandleSet[T any] map[Handle]T
-
-// Handle is an opaque comparable value that's used as the map key in a
-// HandleSet. The only way to get one is to call HandleSet.Add.
-type Handle struct {
- v *byte
-}
-
-// Add adds the element (map value) e to the set.
-//
-// It returns the handle (map key) with which e can be removed, using a map
-// delete.
-func (s *HandleSet[T]) Add(e T) Handle {
- h := Handle{new(byte)}
- if *s == nil {
- *s = make(HandleSet[T])
- }
- (*s)[h] = e
- return h
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package set
+
+// HandleSet is a set of T.
+//
+// It is not safe for concurrent use.
+type HandleSet[T any] map[Handle]T
+
+// Handle is an opaque comparable value that's used as the map key in a
+// HandleSet. The only way to get one is to call HandleSet.Add.
+type Handle struct {
+ v *byte
+}
+
+// Add adds the element (map value) e to the set.
+//
+// It returns the handle (map key) with which e can be removed, using a map
+// delete.
+func (s *HandleSet[T]) Add(e T) Handle {
+ h := Handle{new(byte)}
+ if *s == nil {
+ *s = make(HandleSet[T])
+ }
+ (*s)[h] = e
+ return h
+}
diff --git a/util/set/slice_test.go b/util/set/slice_test.go
index 9134c2962..ca57e52e8 100644
--- a/util/set/slice_test.go
+++ b/util/set/slice_test.go
@@ -1,56 +1,56 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package set
-
-import (
- "testing"
-
- qt "github.com/frankban/quicktest"
-)
-
-func TestSliceSet(t *testing.T) {
- c := qt.New(t)
-
- var ss Slice[int]
- c.Check(len(ss.slice), qt.Equals, 0)
- ss.Add(1)
- c.Check(len(ss.slice), qt.Equals, 1)
- c.Check(len(ss.set), qt.Equals, 0)
- c.Check(ss.Contains(1), qt.Equals, true)
- c.Check(ss.Contains(2), qt.Equals, false)
-
- ss.Add(1)
- c.Check(len(ss.slice), qt.Equals, 1)
- c.Check(len(ss.set), qt.Equals, 0)
-
- ss.Add(2)
- ss.Add(3)
- ss.Add(4)
- ss.Add(5)
- ss.Add(6)
- ss.Add(7)
- ss.Add(8)
- c.Check(len(ss.slice), qt.Equals, 8)
- c.Check(len(ss.set), qt.Equals, 0)
-
- ss.Add(9)
- c.Check(len(ss.slice), qt.Equals, 9)
- c.Check(len(ss.set), qt.Equals, 9)
-
- ss.Remove(4)
- c.Check(len(ss.slice), qt.Equals, 8)
- c.Check(len(ss.set), qt.Equals, 8)
- c.Assert(ss.Contains(4), qt.IsFalse)
-
- // Ensure that the order of insertion is maintained
- c.Assert(ss.Slice().AsSlice(), qt.DeepEquals, []int{1, 2, 3, 5, 6, 7, 8, 9})
- ss.Add(4)
- c.Check(len(ss.slice), qt.Equals, 9)
- c.Check(len(ss.set), qt.Equals, 9)
- c.Assert(ss.Contains(4), qt.IsTrue)
- c.Assert(ss.Slice().AsSlice(), qt.DeepEquals, []int{1, 2, 3, 5, 6, 7, 8, 9, 4})
-
- ss.Add(1, 234, 556)
- c.Assert(ss.Slice().AsSlice(), qt.DeepEquals, []int{1, 2, 3, 5, 6, 7, 8, 9, 4, 234, 556})
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package set
+
+import (
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+)
+
+func TestSliceSet(t *testing.T) {
+ c := qt.New(t)
+
+ var ss Slice[int]
+ c.Check(len(ss.slice), qt.Equals, 0)
+ ss.Add(1)
+ c.Check(len(ss.slice), qt.Equals, 1)
+ c.Check(len(ss.set), qt.Equals, 0)
+ c.Check(ss.Contains(1), qt.Equals, true)
+ c.Check(ss.Contains(2), qt.Equals, false)
+
+ ss.Add(1)
+ c.Check(len(ss.slice), qt.Equals, 1)
+ c.Check(len(ss.set), qt.Equals, 0)
+
+ ss.Add(2)
+ ss.Add(3)
+ ss.Add(4)
+ ss.Add(5)
+ ss.Add(6)
+ ss.Add(7)
+ ss.Add(8)
+ c.Check(len(ss.slice), qt.Equals, 8)
+ c.Check(len(ss.set), qt.Equals, 0)
+
+ ss.Add(9)
+ c.Check(len(ss.slice), qt.Equals, 9)
+ c.Check(len(ss.set), qt.Equals, 9)
+
+ ss.Remove(4)
+ c.Check(len(ss.slice), qt.Equals, 8)
+ c.Check(len(ss.set), qt.Equals, 8)
+ c.Assert(ss.Contains(4), qt.IsFalse)
+
+ // Ensure that the order of insertion is maintained
+ c.Assert(ss.Slice().AsSlice(), qt.DeepEquals, []int{1, 2, 3, 5, 6, 7, 8, 9})
+ ss.Add(4)
+ c.Check(len(ss.slice), qt.Equals, 9)
+ c.Check(len(ss.set), qt.Equals, 9)
+ c.Assert(ss.Contains(4), qt.IsTrue)
+ c.Assert(ss.Slice().AsSlice(), qt.DeepEquals, []int{1, 2, 3, 5, 6, 7, 8, 9, 4})
+
+ ss.Add(1, 234, 556)
+ c.Assert(ss.Slice().AsSlice(), qt.DeepEquals, []int{1, 2, 3, 5, 6, 7, 8, 9, 4, 234, 556})
+}
diff --git a/util/sysresources/memory.go b/util/sysresources/memory.go
index 7363155cd..8bf784e13 100644
--- a/util/sysresources/memory.go
+++ b/util/sysresources/memory.go
@@ -1,10 +1,10 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package sysresources
-
-// TotalMemory returns the total accessible system memory, in bytes. If the
-// value cannot be determined, then 0 will be returned.
-func TotalMemory() uint64 {
- return totalMemoryImpl()
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package sysresources
+
+// TotalMemory returns the total accessible system memory, in bytes. If the
+// value cannot be determined, then 0 will be returned.
+func TotalMemory() uint64 {
+ return totalMemoryImpl()
+}
diff --git a/util/sysresources/memory_bsd.go b/util/sysresources/memory_bsd.go
index 26850dce6..39d3a18a9 100644
--- a/util/sysresources/memory_bsd.go
+++ b/util/sysresources/memory_bsd.go
@@ -1,16 +1,16 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build freebsd || openbsd || dragonfly || netbsd
-
-package sysresources
-
-import "golang.org/x/sys/unix"
-
-func totalMemoryImpl() uint64 {
- val, err := unix.SysctlUint64("hw.physmem")
- if err != nil {
- return 0
- }
- return val
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build freebsd || openbsd || dragonfly || netbsd
+
+package sysresources
+
+import "golang.org/x/sys/unix"
+
+func totalMemoryImpl() uint64 {
+ val, err := unix.SysctlUint64("hw.physmem")
+ if err != nil {
+ return 0
+ }
+ return val
+}
diff --git a/util/sysresources/memory_darwin.go b/util/sysresources/memory_darwin.go
index e07bac0cd..2f74b6cec 100644
--- a/util/sysresources/memory_darwin.go
+++ b/util/sysresources/memory_darwin.go
@@ -1,16 +1,16 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build darwin
-
-package sysresources
-
-import "golang.org/x/sys/unix"
-
-func totalMemoryImpl() uint64 {
- val, err := unix.SysctlUint64("hw.memsize")
- if err != nil {
- return 0
- }
- return val
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build darwin
+
+package sysresources
+
+import "golang.org/x/sys/unix"
+
+func totalMemoryImpl() uint64 {
+ val, err := unix.SysctlUint64("hw.memsize")
+ if err != nil {
+ return 0
+ }
+ return val
+}
diff --git a/util/sysresources/memory_linux.go b/util/sysresources/memory_linux.go
index 0239b0e80..f3c51469f 100644
--- a/util/sysresources/memory_linux.go
+++ b/util/sysresources/memory_linux.go
@@ -1,19 +1,19 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build linux
-
-package sysresources
-
-import "golang.org/x/sys/unix"
-
-func totalMemoryImpl() uint64 {
- var info unix.Sysinfo_t
-
- if err := unix.Sysinfo(&info); err != nil {
- return 0
- }
-
- // uint64 casts are required since these might be uint32s
- return uint64(info.Totalram) * uint64(info.Unit)
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build linux
+
+package sysresources
+
+import "golang.org/x/sys/unix"
+
+func totalMemoryImpl() uint64 {
+ var info unix.Sysinfo_t
+
+ if err := unix.Sysinfo(&info); err != nil {
+ return 0
+ }
+
+ // uint64 casts are required since these might be uint32s
+ return uint64(info.Totalram) * uint64(info.Unit)
+}
diff --git a/util/sysresources/memory_unsupported.go b/util/sysresources/memory_unsupported.go
index 0fde256e0..f80ef4e6e 100644
--- a/util/sysresources/memory_unsupported.go
+++ b/util/sysresources/memory_unsupported.go
@@ -1,8 +1,8 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build !(linux || darwin || freebsd || openbsd || dragonfly || netbsd)
-
-package sysresources
-
-func totalMemoryImpl() uint64 { return 0 }
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !(linux || darwin || freebsd || openbsd || dragonfly || netbsd)
+
+package sysresources
+
+func totalMemoryImpl() uint64 { return 0 }
diff --git a/util/sysresources/sysresources.go b/util/sysresources/sysresources.go
index 32d972ab1..1cce164a7 100644
--- a/util/sysresources/sysresources.go
+++ b/util/sysresources/sysresources.go
@@ -1,6 +1,6 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package sysresources provides OS-independent methods of determining the
-// resources available to the current system.
-package sysresources
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package sysresources provides OS-independent methods of determining the
+// resources available to the current system.
+package sysresources
diff --git a/util/sysresources/sysresources_test.go b/util/sysresources/sysresources_test.go
index 331ad913b..af9662042 100644
--- a/util/sysresources/sysresources_test.go
+++ b/util/sysresources/sysresources_test.go
@@ -1,25 +1,25 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package sysresources
-
-import (
- "runtime"
- "testing"
-)
-
-func TestTotalMemory(t *testing.T) {
- switch runtime.GOOS {
- case "linux":
- case "freebsd", "openbsd", "dragonfly", "netbsd":
- case "darwin":
- default:
- t.Skipf("not supported on runtime.GOOS=%q yet", runtime.GOOS)
- }
-
- mem := TotalMemory()
- if mem == 0 {
- t.Fatal("wanted TotalMemory > 0")
- }
- t.Logf("total memory: %v bytes", mem)
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package sysresources
+
+import (
+ "runtime"
+ "testing"
+)
+
+func TestTotalMemory(t *testing.T) {
+ switch runtime.GOOS {
+ case "linux":
+ case "freebsd", "openbsd", "dragonfly", "netbsd":
+ case "darwin":
+ default:
+ t.Skipf("not supported on runtime.GOOS=%q yet", runtime.GOOS)
+ }
+
+ mem := TotalMemory()
+ if mem == 0 {
+ t.Fatal("wanted TotalMemory > 0")
+ }
+ t.Logf("total memory: %v bytes", mem)
+}
diff --git a/util/systemd/doc.go b/util/systemd/doc.go
index 0c28e1823..296f74e9d 100644
--- a/util/systemd/doc.go
+++ b/util/systemd/doc.go
@@ -1,13 +1,13 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-/*
-Package systemd contains a minimal wrapper around systemd-notify to enable
-applications to signal readiness and status to systemd.
-
-This package will only have effect on Linux systems running Tailscale in a
-systemd unit with the Type=notify flag set. On other operating systems (or
-when running in a Linux distro without being run from inside systemd) this
-package will become a no-op.
-*/
-package systemd
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+/*
+Package systemd contains a minimal wrapper around systemd-notify to enable
+applications to signal readiness and status to systemd.
+
+This package will only have effect on Linux systems running Tailscale in a
+systemd unit with the Type=notify flag set. On other operating systems (or
+when running in a Linux distro without being run from inside systemd) this
+package will become a no-op.
+*/
+package systemd
diff --git a/util/systemd/systemd_linux.go b/util/systemd/systemd_linux.go
index 909cfcb20..34d6daff3 100644
--- a/util/systemd/systemd_linux.go
+++ b/util/systemd/systemd_linux.go
@@ -1,77 +1,77 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build linux
-
-package systemd
-
-import (
- "errors"
- "log"
- "os"
- "sync"
-
- "github.com/mdlayher/sdnotify"
-)
-
-var getNotifyOnce struct {
- sync.Once
- v *sdnotify.Notifier
-}
-
-type logOnce struct {
- sync.Once
-}
-
-func (l *logOnce) logf(format string, args ...any) {
- l.Once.Do(func() {
- log.Printf(format, args...)
- })
-}
-
-var (
- readyOnce = &logOnce{}
- statusOnce = &logOnce{}
-)
-
-func notifier() *sdnotify.Notifier {
- getNotifyOnce.Do(func() {
- var err error
- getNotifyOnce.v, err = sdnotify.New()
- // Not exist means probably not running under systemd, so don't log.
- if err != nil && !errors.Is(err, os.ErrNotExist) {
- log.Printf("systemd: systemd-notifier error: %v", err)
- }
- })
- return getNotifyOnce.v
-}
-
-// Ready signals readiness to systemd. This will unblock service dependents from starting.
-func Ready() {
- err := notifier().Notify(sdnotify.Ready)
- if err != nil {
- readyOnce.logf("systemd: error notifying: %v", err)
- }
-}
-
-// Status sends a single line status update to systemd so that information shows up
-// in systemctl output. For example:
-//
-// $ systemctl status tailscale
-// ● tailscale.service - Tailscale client daemon
-// Loaded: loaded (/nix/store/qc312qcy907wz80fqrgbbm8a9djafmlg-unit-tailscale.service/tailscale.service; enabled; vendor preset: enabled)
-// Active: active (running) since Tue 2020-11-24 17:54:07 EST; 13h ago
-// Main PID: 26741 (.tailscaled-wra)
-// Status: "Connected; user@host.domain.tld; 100.101.102.103"
-// IP: 0B in, 0B out
-// Tasks: 22 (limit: 4915)
-// Memory: 30.9M
-// CPU: 2min 38.469s
-// CGroup: /system.slice/tailscale.service
-// └─26741 /nix/store/sv6cj4mw2jajm9xkbwj07k29dj30lh0n-tailscale-date.20200727/bin/tailscaled --port 41641
-func Status(format string, args ...any) {
- err := notifier().Notify(sdnotify.Statusf(format, args...))
- if err != nil {
- statusOnce.logf("systemd: error notifying: %v", err)
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build linux
+
+package systemd
+
+import (
+ "errors"
+ "log"
+ "os"
+ "sync"
+
+ "github.com/mdlayher/sdnotify"
+)
+
+var getNotifyOnce struct {
+ sync.Once
+ v *sdnotify.Notifier
+}
+
+type logOnce struct {
+ sync.Once
+}
+
+func (l *logOnce) logf(format string, args ...any) {
+ l.Once.Do(func() {
+ log.Printf(format, args...)
+ })
+}
+
+var (
+ readyOnce = &logOnce{}
+ statusOnce = &logOnce{}
+)
+
+func notifier() *sdnotify.Notifier {
+ getNotifyOnce.Do(func() {
+ var err error
+ getNotifyOnce.v, err = sdnotify.New()
+ // Not exist means probably not running under systemd, so don't log.
+ if err != nil && !errors.Is(err, os.ErrNotExist) {
+ log.Printf("systemd: systemd-notifier error: %v", err)
+ }
+ })
+ return getNotifyOnce.v
+}
+
+// Ready signals readiness to systemd. This will unblock service dependents from starting.
+func Ready() {
+ err := notifier().Notify(sdnotify.Ready)
+ if err != nil {
+ readyOnce.logf("systemd: error notifying: %v", err)
+ }
+}
+
+// Status sends a single line status update to systemd so that information shows up
+// in systemctl output. For example:
+//
+// $ systemctl status tailscale
+// ● tailscale.service - Tailscale client daemon
+// Loaded: loaded (/nix/store/qc312qcy907wz80fqrgbbm8a9djafmlg-unit-tailscale.service/tailscale.service; enabled; vendor preset: enabled)
+// Active: active (running) since Tue 2020-11-24 17:54:07 EST; 13h ago
+// Main PID: 26741 (.tailscaled-wra)
+// Status: "Connected; user@host.domain.tld; 100.101.102.103"
+// IP: 0B in, 0B out
+// Tasks: 22 (limit: 4915)
+// Memory: 30.9M
+// CPU: 2min 38.469s
+// CGroup: /system.slice/tailscale.service
+// └─26741 /nix/store/sv6cj4mw2jajm9xkbwj07k29dj30lh0n-tailscale-date.20200727/bin/tailscaled --port 41641
+func Status(format string, args ...any) {
+ err := notifier().Notify(sdnotify.Statusf(format, args...))
+ if err != nil {
+ statusOnce.logf("systemd: error notifying: %v", err)
+ }
+}
diff --git a/util/systemd/systemd_nonlinux.go b/util/systemd/systemd_nonlinux.go
index 36214020c..d8b20665f 100644
--- a/util/systemd/systemd_nonlinux.go
+++ b/util/systemd/systemd_nonlinux.go
@@ -1,9 +1,9 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build !linux
-
-package systemd
-
-func Ready() {}
-func Status(string, ...any) {}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !linux
+
+package systemd
+
+func Ready() {}
+func Status(string, ...any) {}
diff --git a/util/testenv/testenv.go b/util/testenv/testenv.go
index 12ada9003..02c688803 100644
--- a/util/testenv/testenv.go
+++ b/util/testenv/testenv.go
@@ -1,21 +1,21 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package testenv provides utility functions for tests. It does not depend on
-// the `testing` package to allow usage in non-test code.
-package testenv
-
-import (
- "flag"
-
- "tailscale.com/types/lazy"
-)
-
-var lazyInTest lazy.SyncValue[bool]
-
-// InTest reports whether the current binary is a test binary.
-func InTest() bool {
- return lazyInTest.Get(func() bool {
- return flag.Lookup("test.v") != nil
- })
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package testenv provides utility functions for tests. It does not depend on
+// the `testing` package to allow usage in non-test code.
+package testenv
+
+import (
+ "flag"
+
+ "tailscale.com/types/lazy"
+)
+
+var lazyInTest lazy.SyncValue[bool]
+
+// InTest reports whether the current binary is a test binary.
+func InTest() bool {
+ return lazyInTest.Get(func() bool {
+ return flag.Lookup("test.v") != nil
+ })
+}
diff --git a/util/truncate/truncate_test.go b/util/truncate/truncate_test.go
index c0d9e6e14..6ead55a6a 100644
--- a/util/truncate/truncate_test.go
+++ b/util/truncate/truncate_test.go
@@ -1,36 +1,36 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package truncate_test
-
-import (
- "testing"
-
- "tailscale.com/util/truncate"
-)
-
-func TestString(t *testing.T) {
- tests := []struct {
- input string
- size int
- want string
- }{
- {"", 1000, ""}, // n > length
- {"abc", 4, "abc"}, // n > length
- {"abc", 3, "abc"}, // n == length
- {"abcdefg", 4, "abcd"}, // n < length, safe
- {"abcdefg", 0, ""}, // n < length, safe
- {"abc\U0001fc2d", 3, "abc"}, // n < length, at boundary
- {"abc\U0001fc2d", 4, "abc"}, // n < length, mid-rune
- {"abc\U0001fc2d", 5, "abc"}, // n < length, mid-rune
- {"abc\U0001fc2d", 6, "abc"}, // n < length, mid-rune
- {"abc\U0001fc2defg", 7, "abc"}, // n < length, cut multibyte
- }
-
- for _, tc := range tests {
- got := truncate.String(tc.input, tc.size)
- if got != tc.want {
- t.Errorf("truncate(%q, %d): got %q, want %q", tc.input, tc.size, got, tc.want)
- }
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package truncate_test
+
+import (
+ "testing"
+
+ "tailscale.com/util/truncate"
+)
+
+func TestString(t *testing.T) {
+ tests := []struct {
+ input string
+ size int
+ want string
+ }{
+ {"", 1000, ""}, // n > length
+ {"abc", 4, "abc"}, // n > length
+ {"abc", 3, "abc"}, // n == length
+ {"abcdefg", 4, "abcd"}, // n < length, safe
+ {"abcdefg", 0, ""}, // n < length, safe
+ {"abc\U0001fc2d", 3, "abc"}, // n < length, at boundary
+ {"abc\U0001fc2d", 4, "abc"}, // n < length, mid-rune
+ {"abc\U0001fc2d", 5, "abc"}, // n < length, mid-rune
+ {"abc\U0001fc2d", 6, "abc"}, // n < length, mid-rune
+ {"abc\U0001fc2defg", 7, "abc"}, // n < length, cut multibyte
+ }
+
+ for _, tc := range tests {
+ got := truncate.String(tc.input, tc.size)
+ if got != tc.want {
+ t.Errorf("truncate(%q, %d): got %q, want %q", tc.input, tc.size, got, tc.want)
+ }
+ }
+}
diff --git a/util/uniq/slice.go b/util/uniq/slice.go
index 4ab933a9d..fb46cc491 100644
--- a/util/uniq/slice.go
+++ b/util/uniq/slice.go
@@ -1,62 +1,62 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package uniq provides removal of adjacent duplicate elements in slices.
-// It is similar to the unix command uniq.
-package uniq
-
-// ModifySlice removes adjacent duplicate elements from the given slice. It
-// adjusts the length of the slice appropriately and zeros the tail.
-//
-// ModifySlice does O(len(*slice)) operations.
-func ModifySlice[E comparable](slice *[]E) {
- // Remove duplicates
- dst := 0
- for i := 1; i < len(*slice); i++ {
- if (*slice)[i] == (*slice)[dst] {
- continue
- }
- dst++
- (*slice)[dst] = (*slice)[i]
- }
-
- // Zero out the elements we removed at the end of the slice
- end := dst + 1
- var zero E
- for i := end; i < len(*slice); i++ {
- (*slice)[i] = zero
- }
-
- // Truncate the slice
- if end < len(*slice) {
- *slice = (*slice)[:end]
- }
-}
-
-// ModifySliceFunc is the same as ModifySlice except that it allows using a
-// custom comparison function.
-//
-// eq should report whether the two provided elements are equal.
-func ModifySliceFunc[E any](slice *[]E, eq func(i, j E) bool) {
- // Remove duplicates
- dst := 0
- for i := 1; i < len(*slice); i++ {
- if eq((*slice)[dst], (*slice)[i]) {
- continue
- }
- dst++
- (*slice)[dst] = (*slice)[i]
- }
-
- // Zero out the elements we removed at the end of the slice
- end := dst + 1
- var zero E
- for i := end; i < len(*slice); i++ {
- (*slice)[i] = zero
- }
-
- // Truncate the slice
- if end < len(*slice) {
- *slice = (*slice)[:end]
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package uniq provides removal of adjacent duplicate elements in slices.
+// It is similar to the unix command uniq.
+package uniq
+
+// ModifySlice removes adjacent duplicate elements from the given slice. It
+// adjusts the length of the slice appropriately and zeros the tail.
+//
+// ModifySlice does O(len(*slice)) operations.
+func ModifySlice[E comparable](slice *[]E) {
+ // Remove duplicates
+ dst := 0
+ for i := 1; i < len(*slice); i++ {
+ if (*slice)[i] == (*slice)[dst] {
+ continue
+ }
+ dst++
+ (*slice)[dst] = (*slice)[i]
+ }
+
+ // Zero out the elements we removed at the end of the slice
+ end := dst + 1
+ var zero E
+ for i := end; i < len(*slice); i++ {
+ (*slice)[i] = zero
+ }
+
+ // Truncate the slice
+ if end < len(*slice) {
+ *slice = (*slice)[:end]
+ }
+}
+
+// ModifySliceFunc is the same as ModifySlice except that it allows using a
+// custom comparison function.
+//
+// eq should report whether the two provided elements are equal.
+func ModifySliceFunc[E any](slice *[]E, eq func(i, j E) bool) {
+ // Remove duplicates
+ dst := 0
+ for i := 1; i < len(*slice); i++ {
+ if eq((*slice)[dst], (*slice)[i]) {
+ continue
+ }
+ dst++
+ (*slice)[dst] = (*slice)[i]
+ }
+
+ // Zero out the elements we removed at the end of the slice
+ end := dst + 1
+ var zero E
+ for i := end; i < len(*slice); i++ {
+ (*slice)[i] = zero
+ }
+
+ // Truncate the slice
+ if end < len(*slice) {
+ *slice = (*slice)[:end]
+ }
+}
diff --git a/util/winutil/authenticode/mksyscall.go b/util/winutil/authenticode/mksyscall.go
index 8b7cabe6e..7c6b33973 100644
--- a/util/winutil/authenticode/mksyscall.go
+++ b/util/winutil/authenticode/mksyscall.go
@@ -1,18 +1,18 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package authenticode
-
-//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go mksyscall.go
-//go:generate go run golang.org/x/tools/cmd/goimports -w zsyscall_windows.go
-
-//sys cryptCATAdminAcquireContext2(hCatAdmin *_HCATADMIN, pgSubsystem *windows.GUID, hashAlgorithm *uint16, strongHashPolicy *windows.CertStrongSignPara, flags uint32) (err error) [int32(failretval)==0] = wintrust.CryptCATAdminAcquireContext2
-//sys cryptCATAdminCalcHashFromFileHandle2(hCatAdmin _HCATADMIN, file windows.Handle, pcbHash *uint32, pbHash *byte, flags uint32) (err error) [int32(failretval)==0] = wintrust.CryptCATAdminCalcHashFromFileHandle2
-//sys cryptCATAdminCatalogInfoFromContext(hCatInfo _HCATINFO, catInfo *_CATALOG_INFO, flags uint32) (err error) [int32(failretval)==0] = wintrust.CryptCATCatalogInfoFromContext
-//sys cryptCATAdminEnumCatalogFromHash(hCatAdmin _HCATADMIN, pbHash *byte, cbHash uint32, flags uint32, prevCatInfo *_HCATINFO) (ret _HCATINFO, err error) [ret==0] = wintrust.CryptCATAdminEnumCatalogFromHash
-//sys cryptCATAdminReleaseCatalogContext(hCatAdmin _HCATADMIN, hCatInfo _HCATINFO, flags uint32) (err error) [int32(failretval)==0] = wintrust.CryptCATAdminReleaseCatalogContext
-//sys cryptCATAdminReleaseContext(hCatAdmin _HCATADMIN, flags uint32) (err error) [int32(failretval)==0] = wintrust.CryptCATAdminReleaseContext
-//sys cryptMsgClose(cryptMsg windows.Handle) (err error) [int32(failretval)==0] = crypt32.CryptMsgClose
-//sys cryptMsgGetParam(cryptMsg windows.Handle, paramType uint32, index uint32, data unsafe.Pointer, dataLen *uint32) (err error) [int32(failretval)==0] = crypt32.CryptMsgGetParam
-//sys cryptVerifyMessageSignature(pVerifyPara *_CRYPT_VERIFY_MESSAGE_PARA, signerIndex uint32, pbSignedBlob *byte, cbSignedBlob uint32, pbDecoded *byte, pdbDecoded *uint32, ppSignerCert **windows.CertContext) (err error) [int32(failretval)==0] = crypt32.CryptVerifyMessageSignature
-//sys msiGetFileSignatureInformation(signedObjectPath *uint16, flags uint32, certCtx **windows.CertContext, pbHashData *byte, cbHashData *uint32) (ret wingoes.HRESULT) = msi.MsiGetFileSignatureInformationW
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package authenticode
+
+//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go mksyscall.go
+//go:generate go run golang.org/x/tools/cmd/goimports -w zsyscall_windows.go
+
+//sys cryptCATAdminAcquireContext2(hCatAdmin *_HCATADMIN, pgSubsystem *windows.GUID, hashAlgorithm *uint16, strongHashPolicy *windows.CertStrongSignPara, flags uint32) (err error) [int32(failretval)==0] = wintrust.CryptCATAdminAcquireContext2
+//sys cryptCATAdminCalcHashFromFileHandle2(hCatAdmin _HCATADMIN, file windows.Handle, pcbHash *uint32, pbHash *byte, flags uint32) (err error) [int32(failretval)==0] = wintrust.CryptCATAdminCalcHashFromFileHandle2
+//sys cryptCATAdminCatalogInfoFromContext(hCatInfo _HCATINFO, catInfo *_CATALOG_INFO, flags uint32) (err error) [int32(failretval)==0] = wintrust.CryptCATCatalogInfoFromContext
+//sys cryptCATAdminEnumCatalogFromHash(hCatAdmin _HCATADMIN, pbHash *byte, cbHash uint32, flags uint32, prevCatInfo *_HCATINFO) (ret _HCATINFO, err error) [ret==0] = wintrust.CryptCATAdminEnumCatalogFromHash
+//sys cryptCATAdminReleaseCatalogContext(hCatAdmin _HCATADMIN, hCatInfo _HCATINFO, flags uint32) (err error) [int32(failretval)==0] = wintrust.CryptCATAdminReleaseCatalogContext
+//sys cryptCATAdminReleaseContext(hCatAdmin _HCATADMIN, flags uint32) (err error) [int32(failretval)==0] = wintrust.CryptCATAdminReleaseContext
+//sys cryptMsgClose(cryptMsg windows.Handle) (err error) [int32(failretval)==0] = crypt32.CryptMsgClose
+//sys cryptMsgGetParam(cryptMsg windows.Handle, paramType uint32, index uint32, data unsafe.Pointer, dataLen *uint32) (err error) [int32(failretval)==0] = crypt32.CryptMsgGetParam
+//sys cryptVerifyMessageSignature(pVerifyPara *_CRYPT_VERIFY_MESSAGE_PARA, signerIndex uint32, pbSignedBlob *byte, cbSignedBlob uint32, pbDecoded *byte, pdbDecoded *uint32, ppSignerCert **windows.CertContext) (err error) [int32(failretval)==0] = crypt32.CryptVerifyMessageSignature
+//sys msiGetFileSignatureInformation(signedObjectPath *uint16, flags uint32, certCtx **windows.CertContext, pbHashData *byte, cbHashData *uint32) (ret wingoes.HRESULT) = msi.MsiGetFileSignatureInformationW
diff --git a/util/winutil/policy/policy_windows.go b/util/winutil/policy/policy_windows.go
index 89142951f..4674696fa 100644
--- a/util/winutil/policy/policy_windows.go
+++ b/util/winutil/policy/policy_windows.go
@@ -1,155 +1,155 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package policy contains higher-level abstractions for accessing Windows enterprise policies.
-package policy
-
-import (
- "time"
-
- "tailscale.com/util/winutil"
-)
-
-// PreferenceOptionPolicy is a policy that governs whether a boolean variable
-// is forcibly assigned an administrator-defined value, or allowed to receive
-// a user-defined value.
-type PreferenceOptionPolicy int
-
-const (
- showChoiceByPolicy PreferenceOptionPolicy = iota
- neverByPolicy
- alwaysByPolicy
-)
-
-// Show returns if the UI option that controls the choice administered by this
-// policy should be shown. Currently this is true if and only if the policy is
-// showChoiceByPolicy.
-func (p PreferenceOptionPolicy) Show() bool {
- return p == showChoiceByPolicy
-}
-
-// ShouldEnable checks if the choice administered by this policy should be
-// enabled. If the administrator has chosen a setting, the administrator's
-// setting is returned, otherwise userChoice is returned.
-func (p PreferenceOptionPolicy) ShouldEnable(userChoice bool) bool {
- switch p {
- case neverByPolicy:
- return false
- case alwaysByPolicy:
- return true
- default:
- return userChoice
- }
-}
-
-// GetPreferenceOptionPolicy loads a policy from the registry that can be
-// managed by an enterprise policy management system and allows administrative
-// overrides of users' choices in a way that we do not want tailcontrol to have
-// the authority to set. It describes user-decides/always/never options, where
-// "always" and "never" remove the user's ability to make a selection. If not
-// present or set to a different value, "user-decides" is the default.
-func GetPreferenceOptionPolicy(name string) PreferenceOptionPolicy {
- opt, err := winutil.GetPolicyString(name)
- if opt == "" || err != nil {
- return showChoiceByPolicy
- }
- switch opt {
- case "always":
- return alwaysByPolicy
- case "never":
- return neverByPolicy
- default:
- return showChoiceByPolicy
- }
-}
-
-// VisibilityPolicy is a policy that controls whether or not a particular
-// component of a user interface is to be shown.
-type VisibilityPolicy byte
-
-const (
- visibleByPolicy VisibilityPolicy = 'v'
- hiddenByPolicy VisibilityPolicy = 'h'
-)
-
-// Show reports whether the UI option administered by this policy should be shown.
-// Currently this is true if and only if the policy is visibleByPolicy.
-func (p VisibilityPolicy) Show() bool {
- return p == visibleByPolicy
-}
-
-// GetVisibilityPolicy loads a policy from the registry that can be managed
-// by an enterprise policy management system and describes show/hide decisions
-// for UI elements. The registry value should be a string set to "show" (return
-// true) or "hide" (return true). If not present or set to a different value,
-// "show" (return false) is the default.
-func GetVisibilityPolicy(name string) VisibilityPolicy {
- opt, err := winutil.GetPolicyString(name)
- if opt == "" || err != nil {
- return visibleByPolicy
- }
- switch opt {
- case "hide":
- return hiddenByPolicy
- default:
- return visibleByPolicy
- }
-}
-
-// GetDurationPolicy loads a policy from the registry that can be managed
-// by an enterprise policy management system and describes a duration for some
-// action. The registry value should be a string that time.ParseDuration
-// understands. If the registry value is "" or can not be processed,
-// defaultValue is returned instead.
-func GetDurationPolicy(name string, defaultValue time.Duration) time.Duration {
- opt, err := winutil.GetPolicyString(name)
- if opt == "" || err != nil {
- return defaultValue
- }
- v, err := time.ParseDuration(opt)
- if err != nil || v < 0 {
- return defaultValue
- }
- return v
-}
-
-// SelectControlURL returns the ControlURL to use based on a value in
-// the registry (LoginURL) and the one on disk (in the GUI's
-// prefs.conf). If both are empty, it returns a default value. (It
-// always return a non-empty value)
-//
-// See https://github.com/tailscale/tailscale/issues/2798 for some background.
-func SelectControlURL(reg, disk string) string {
- const def = "https://controlplane.tailscale.com"
-
- // Prior to Dec 2020's commit 739b02e6, the installer
- // wrote a LoginURL value of https://login.tailscale.com to the registry.
- const oldRegDef = "https://login.tailscale.com"
-
- // If they have an explicit value in the registry, use it,
- // unless it's an old default value from an old installer.
- // Then we have to see which is better.
- if reg != "" {
- if reg != oldRegDef {
- // Something explicit in the registry that we didn't
- // set ourselves by the installer.
- return reg
- }
- if disk == "" {
- // Something in the registry is better than nothing on disk.
- return reg
- }
- if disk != def && disk != oldRegDef {
- // The value in the registry is the old
- // default (login.tailscale.com) but the value
- // on disk is neither our old nor new default
- // value, so it must be some custom thing that
- // the user cares about. Prefer the disk value.
- return disk
- }
- }
- if disk != "" {
- return disk
- }
- return def
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package policy contains higher-level abstractions for accessing Windows enterprise policies.
+package policy
+
+import (
+ "time"
+
+ "tailscale.com/util/winutil"
+)
+
+// PreferenceOptionPolicy is a policy that governs whether a boolean variable
+// is forcibly assigned an administrator-defined value, or allowed to receive
+// a user-defined value.
+type PreferenceOptionPolicy int
+
+const (
+ showChoiceByPolicy PreferenceOptionPolicy = iota
+ neverByPolicy
+ alwaysByPolicy
+)
+
+// Show returns if the UI option that controls the choice administered by this
+// policy should be shown. Currently this is true if and only if the policy is
+// showChoiceByPolicy.
+func (p PreferenceOptionPolicy) Show() bool {
+ return p == showChoiceByPolicy
+}
+
+// ShouldEnable checks if the choice administered by this policy should be
+// enabled. If the administrator has chosen a setting, the administrator's
+// setting is returned, otherwise userChoice is returned.
+func (p PreferenceOptionPolicy) ShouldEnable(userChoice bool) bool {
+ switch p {
+ case neverByPolicy:
+ return false
+ case alwaysByPolicy:
+ return true
+ default:
+ return userChoice
+ }
+}
+
+// GetPreferenceOptionPolicy loads a policy from the registry that can be
+// managed by an enterprise policy management system and allows administrative
+// overrides of users' choices in a way that we do not want tailcontrol to have
+// the authority to set. It describes user-decides/always/never options, where
+// "always" and "never" remove the user's ability to make a selection. If not
+// present or set to a different value, "user-decides" is the default.
+func GetPreferenceOptionPolicy(name string) PreferenceOptionPolicy {
+ opt, err := winutil.GetPolicyString(name)
+ if opt == "" || err != nil {
+ return showChoiceByPolicy
+ }
+ switch opt {
+ case "always":
+ return alwaysByPolicy
+ case "never":
+ return neverByPolicy
+ default:
+ return showChoiceByPolicy
+ }
+}
+
+// VisibilityPolicy is a policy that controls whether or not a particular
+// component of a user interface is to be shown.
+type VisibilityPolicy byte
+
+const (
+ visibleByPolicy VisibilityPolicy = 'v'
+ hiddenByPolicy VisibilityPolicy = 'h'
+)
+
+// Show reports whether the UI option administered by this policy should be shown.
+// Currently this is true if and only if the policy is visibleByPolicy.
+func (p VisibilityPolicy) Show() bool {
+ return p == visibleByPolicy
+}
+
+// GetVisibilityPolicy loads a policy from the registry that can be managed
+// by an enterprise policy management system and describes show/hide decisions
+// for UI elements. The registry value should be a string set to "show" (return
+// true) or "hide" (return true). If not present or set to a different value,
+// "show" (return false) is the default.
+func GetVisibilityPolicy(name string) VisibilityPolicy {
+ opt, err := winutil.GetPolicyString(name)
+ if opt == "" || err != nil {
+ return visibleByPolicy
+ }
+ switch opt {
+ case "hide":
+ return hiddenByPolicy
+ default:
+ return visibleByPolicy
+ }
+}
+
+// GetDurationPolicy loads a policy from the registry that can be managed
+// by an enterprise policy management system and describes a duration for some
+// action. The registry value should be a string that time.ParseDuration
+// understands. If the registry value is "" or can not be processed,
+// defaultValue is returned instead.
+func GetDurationPolicy(name string, defaultValue time.Duration) time.Duration {
+ opt, err := winutil.GetPolicyString(name)
+ if opt == "" || err != nil {
+ return defaultValue
+ }
+ v, err := time.ParseDuration(opt)
+ if err != nil || v < 0 {
+ return defaultValue
+ }
+ return v
+}
+
+// SelectControlURL returns the ControlURL to use based on a value in
+// the registry (LoginURL) and the one on disk (in the GUI's
+// prefs.conf). If both are empty, it returns a default value. (It
+// always return a non-empty value)
+//
+// See https://github.com/tailscale/tailscale/issues/2798 for some background.
+func SelectControlURL(reg, disk string) string {
+ const def = "https://controlplane.tailscale.com"
+
+ // Prior to Dec 2020's commit 739b02e6, the installer
+ // wrote a LoginURL value of https://login.tailscale.com to the registry.
+ const oldRegDef = "https://login.tailscale.com"
+
+ // If they have an explicit value in the registry, use it,
+ // unless it's an old default value from an old installer.
+ // Then we have to see which is better.
+ if reg != "" {
+ if reg != oldRegDef {
+ // Something explicit in the registry that we didn't
+ // set ourselves by the installer.
+ return reg
+ }
+ if disk == "" {
+ // Something in the registry is better than nothing on disk.
+ return reg
+ }
+ if disk != def && disk != oldRegDef {
+ // The value in the registry is the old
+ // default (login.tailscale.com) but the value
+ // on disk is neither our old nor new default
+ // value, so it must be some custom thing that
+ // the user cares about. Prefer the disk value.
+ return disk
+ }
+ }
+ if disk != "" {
+ return disk
+ }
+ return def
+}
diff --git a/util/winutil/policy/policy_windows_test.go b/util/winutil/policy/policy_windows_test.go
index cf2390c56..ebfd185de 100644
--- a/util/winutil/policy/policy_windows_test.go
+++ b/util/winutil/policy/policy_windows_test.go
@@ -1,38 +1,38 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package policy
-
-import "testing"
-
-func TestSelectControlURL(t *testing.T) {
- tests := []struct {
- reg, disk, want string
- }{
- // Modern default case.
- {"", "", "https://controlplane.tailscale.com"},
-
- // For a user who installed prior to Dec 2020, with
- // stuff in their registry.
- {"https://login.tailscale.com", "", "https://login.tailscale.com"},
-
- // Ignore pre-Dec'20 LoginURL from installer if prefs
- // prefs overridden manually to an on-prem control
- // server.
- {"https://login.tailscale.com", "http://on-prem", "http://on-prem"},
-
- // Something unknown explicitly set in the registry always wins.
- {"http://explicit-reg", "", "http://explicit-reg"},
- {"http://explicit-reg", "http://on-prem", "http://explicit-reg"},
- {"http://explicit-reg", "https://login.tailscale.com", "http://explicit-reg"},
- {"http://explicit-reg", "https://controlplane.tailscale.com", "http://explicit-reg"},
-
- // If nothing in the registry, disk wins.
- {"", "http://on-prem", "http://on-prem"},
- }
- for _, tt := range tests {
- if got := SelectControlURL(tt.reg, tt.disk); got != tt.want {
- t.Errorf("(reg %q, disk %q) = %q; want %q", tt.reg, tt.disk, got, tt.want)
- }
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package policy
+
+import "testing"
+
+func TestSelectControlURL(t *testing.T) {
+ tests := []struct {
+ reg, disk, want string
+ }{
+ // Modern default case.
+ {"", "", "https://controlplane.tailscale.com"},
+
+ // For a user who installed prior to Dec 2020, with
+ // stuff in their registry.
+ {"https://login.tailscale.com", "", "https://login.tailscale.com"},
+
+ // Ignore pre-Dec'20 LoginURL from installer if prefs
+ // prefs overridden manually to an on-prem control
+ // server.
+ {"https://login.tailscale.com", "http://on-prem", "http://on-prem"},
+
+ // Something unknown explicitly set in the registry always wins.
+ {"http://explicit-reg", "", "http://explicit-reg"},
+ {"http://explicit-reg", "http://on-prem", "http://explicit-reg"},
+ {"http://explicit-reg", "https://login.tailscale.com", "http://explicit-reg"},
+ {"http://explicit-reg", "https://controlplane.tailscale.com", "http://explicit-reg"},
+
+ // If nothing in the registry, disk wins.
+ {"", "http://on-prem", "http://on-prem"},
+ }
+ for _, tt := range tests {
+ if got := SelectControlURL(tt.reg, tt.disk); got != tt.want {
+ t.Errorf("(reg %q, disk %q) = %q; want %q", tt.reg, tt.disk, got, tt.want)
+ }
+ }
+}