Files
snapd/asserts/internal/grouping.go
Miguel Pires d097436c1c many: fix formatting w/ gofmt 1.19
Go 1.19 includes some changes to gofmt which intend to make lists and
heading clearer when rendered (https://go.dev/doc/go1.19). This commit
is the result of running the new gofmt and manually fixing some of it.
This was necessary because the new gofmt assumed lines beginning w/ tabs
to start lists or examples. While this is often true in our codebase,
we occasionally also use tabs to indent the lines after a TODO or FIXME
prefix or in yaml (e.g., excerpts of a snap.yaml). This meant that a lot of the
reformatted comments were broken and had to be fixed manually.

Signed-off-by: Miguel Pires <miguel.pires@canonical.com>
2023-01-16 14:23:11 +01:00

287 lines
8.3 KiB
Go

// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2020 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package internal
import (
"bytes"
"encoding/base64"
"encoding/binary"
"fmt"
"sort"
)
// Groupings maintain labels to identify membership to one or more groups.
// Labels are implemented as subsets of integers from 0
// up to an excluded maximum, where the integers represent the groups.
// Assumptions:
// - most labels are for one group or very few
// - a few labels are sparse with more groups in them
// - very few comprise the universe of all groups
type Groupings struct {
n uint
maxGroup uint16
bitsetThreshold uint16
}
// NewGroupings creates a new Groupings supporting labels for membership
// to up n groups. n must be a positive multiple of 16 and <=65536.
func NewGroupings(n int) (*Groupings, error) {
if n <= 0 || n > 65536 {
return nil, fmt.Errorf("n=%d groups is outside of valid range (0, 65536]", n)
}
if n%16 != 0 {
return nil, fmt.Errorf("n=%d groups is not a multiple of 16", n)
}
return &Groupings{n: uint(n), bitsetThreshold: uint16(n / 16)}, nil
}
// N returns up to how many groups are supported.
// That is the value that was passed to NewGroupings.
func (gr *Groupings) N() int {
return int(gr.n)
}
// WithinRange checks whether group is within the admissible range for
// labeling otherwise it returns an error.
func (gr *Groupings) WithinRange(group uint16) error {
if uint(group) >= gr.n {
return fmt.Errorf("group exceeds admissible maximum: %d >= %d", group, gr.n)
}
return nil
}
type Grouping struct {
size uint16
elems []uint16
}
func (g Grouping) Copy() Grouping {
elems2 := make([]uint16, len(g.elems), cap(g.elems))
copy(elems2[:], g.elems[:])
g.elems = elems2
return g
}
// search locates group among the sorted Grouping elements, it returns:
// - true if found
// - false if not found
// - the index at which group should be inserted to keep the
// elements sorted if not found and the bit-set representation is not in use
func (gr *Groupings) search(g *Grouping, group uint16) (found bool, j uint16) {
if g.size > gr.bitsetThreshold {
return bitsetContains(g, group), 0
}
j = uint16(sort.Search(int(g.size), func(i int) bool { return g.elems[i] >= group }))
if j < g.size && g.elems[j] == group {
return true, 0
}
return false, j
}
func bitsetContains(g *Grouping, group uint16) bool {
return (g.elems[group/16] & (1 << (group % 16))) != 0
}
// AddTo adds the given group to the grouping.
func (gr *Groupings) AddTo(g *Grouping, group uint16) error {
if err := gr.WithinRange(group); err != nil {
return err
}
if group > gr.maxGroup {
gr.maxGroup = group
}
if g.size == 0 {
g.size = 1
g.elems = []uint16{group}
return nil
}
found, j := gr.search(g, group)
if found {
return nil
}
newsize := g.size + 1
if newsize > gr.bitsetThreshold {
// switching to a bit-set representation after the size point
// where the space cost is the same, the representation uses
// bitsetThreshold-many 16-bits words stored in elems.
// We don't always use the bit-set representation because
// * we expect small groupings and iteration to be common,
// iteration is more costly over the bit-set representation
// * serialization matches more or less what we do in memory,
// so again is more efficient for small groupings in the
// extensive representation.
if g.size == gr.bitsetThreshold {
prevelems := g.elems
g.elems = make([]uint16, gr.bitsetThreshold)
for _, e := range prevelems {
bitsetAdd(g, e)
}
}
g.size = newsize
bitsetAdd(g, group)
return nil
}
var newelems []uint16
if int(g.size) == cap(g.elems) {
newelems = make([]uint16, newsize, cap(g.elems)*2)
copy(newelems, g.elems[:j])
} else {
newelems = g.elems[:newsize]
}
if j < g.size {
copy(newelems[j+1:], g.elems[j:])
}
// inserting new group at j index keeping the elements sorted
newelems[j] = group
g.size = newsize
g.elems = newelems
return nil
}
func bitsetAdd(g *Grouping, group uint16) {
g.elems[group/16] |= 1 << (group % 16)
}
// Contains returns whether the given group is a member of the grouping.
func (gr *Groupings) Contains(g *Grouping, group uint16) bool {
found, _ := gr.search(g, group)
return found
}
// Serialize produces a string encoding the given integers.
func Serialize(elems []uint16) string {
b := bytes.NewBuffer(make([]byte, 0, len(elems)*2))
binary.Write(b, binary.LittleEndian, elems)
return base64.RawURLEncoding.EncodeToString(b.Bytes())
}
// Serialize produces a string representing the grouping label.
func (gr *Groupings) Serialize(g *Grouping) string {
// groupings are serialized as:
// * the actual element groups if there are up to
// bitsetThreshold elements: elems[0], elems[1], ...
// * otherwise the number of elements, followed by the bitset
// representation comprised of bitsetThreshold-many 16-bits words
// (stored using elems as well)
if g.size > gr.bitsetThreshold {
return gr.bitsetSerialize(g)
}
return Serialize(g.elems)
}
func (gr *Groupings) bitsetSerialize(g *Grouping) string {
b := bytes.NewBuffer(make([]byte, 0, (gr.bitsetThreshold+1)*2))
binary.Write(b, binary.LittleEndian, g.size)
binary.Write(b, binary.LittleEndian, g.elems)
return base64.RawURLEncoding.EncodeToString(b.Bytes())
}
const errSerializedLabelFmt = "invalid serialized grouping label: %v"
// Deserialize reconstructs a grouping out of the serialized label.
func (gr *Groupings) Deserialize(label string) (*Grouping, error) {
b, err := base64.RawURLEncoding.DecodeString(label)
if err != nil {
return nil, fmt.Errorf(errSerializedLabelFmt, err)
}
if len(b)%2 != 0 {
return nil, fmt.Errorf(errSerializedLabelFmt, "not divisible into 16-bits words")
}
m := len(b) / 2
var g Grouping
if m == int(gr.bitsetThreshold+1) {
// deserialize number of elements + bitset representation
// comprising bitsetThreshold-many 16-bits words
return gr.bitsetDeserialize(&g, b)
}
if m > int(gr.bitsetThreshold) {
return nil, fmt.Errorf(errSerializedLabelFmt, "too large")
}
g.size = uint16(m)
esz := uint16(1)
for esz < g.size {
esz *= 2
}
g.elems = make([]uint16, g.size, esz)
binary.Read(bytes.NewBuffer(b), binary.LittleEndian, g.elems)
for i, e := range g.elems {
if e > gr.maxGroup {
return nil, fmt.Errorf(errSerializedLabelFmt, "element larger than maximum group")
}
if i > 0 && g.elems[i-1] >= e {
return nil, fmt.Errorf(errSerializedLabelFmt, "not sorted")
}
}
return &g, nil
}
func (gr *Groupings) bitsetDeserialize(g *Grouping, b []byte) (*Grouping, error) {
buf := bytes.NewBuffer(b)
binary.Read(buf, binary.LittleEndian, &g.size)
if g.size > gr.maxGroup+1 {
return nil, fmt.Errorf(errSerializedLabelFmt, "bitset size cannot be possibly larger than maximum group plus 1")
}
if g.size <= gr.bitsetThreshold {
// should not have used a bitset repr for so few elements
return nil, fmt.Errorf(errSerializedLabelFmt, "bitset for too few elements")
}
g.elems = make([]uint16, gr.bitsetThreshold)
binary.Read(buf, binary.LittleEndian, g.elems)
return g, nil
}
// Iter iterates over the groups in the grouping and calls f with each of
// them. If f returns an error Iter immediately returns with it.
func (gr *Groupings) Iter(g *Grouping, f func(group uint16) error) error {
if g.size > gr.bitsetThreshold {
return gr.bitsetIter(g, f)
}
for _, e := range g.elems {
if err := f(e); err != nil {
return err
}
}
return nil
}
func (gr *Groupings) bitsetIter(g *Grouping, f func(group uint16) error) error {
c := g.size
for i := uint16(0); i <= gr.maxGroup/16; i++ {
w := g.elems[i]
if w == 0 {
continue
}
for j := uint16(0); w != 0; j++ {
if w&1 != 0 {
if err := f(i*16 + j); err != nil {
return err
}
c--
if c == 0 {
// found all elements
return nil
}
}
w >>= 1
}
}
return nil
}