Massive cleanup - coding style, tiny bugs, typos and other errors

This commit is contained in:
Sergei
2023-07-29 22:23:11 -07:00
committed by eleksir
parent 0f1177bd91
commit e8f8b3cb49
18 changed files with 767 additions and 392 deletions

View File

@@ -1,13 +1,12 @@
#!/usr/bin/env gmake -f
GOOPTS=CGO_ENABLED=0
CGO_ENABLED=0
BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath
all: clean build
build:
${GOOPTS} go build ${BUILDOPTS} -o "buny-jabber-bot" collection.go types.go globals.go lib.go event_parser.go \
buny.go main.go
go build ${BUILDOPTS} -o "buny-jabber-bot" collection.go types.go globals.go lib.go event_parser.go buny.go main.go
clean:
go clean

View File

@@ -2,13 +2,16 @@
Простой бот, работающий по протоколу xmpp. Его задача - банить абьюзеров чата.
Этот бот автоматизирует уборку человеческой грязи. Самый важный вопрос, который следует себе задать перед его
использованием - а не забаню ли я кого-то не того?
## Что он может?
Имеет возможность соединяться с сервером по протоколу xmpp как с шифрованием, так и без. Шифрование работает по
классическим механикам (шифрованный канал связи, без возможности коммуникации в незашифрованном виде), без поддержки
механизма start tls. Что-то в гошке или на стороне сервера при выборе StartTLS не работает.
Имеет возможность соединяться с сервером по протоколу xmpp как с шифрованием, так и без. (Пока что) Шифрование
работает по классическим механикам (шифрованный канал связи, без возможности коммуникации в незашифрованном виде),
без поддержки механизма start tls. Что-то в гошке или на стороне сервера при выборе StartTLS не работает.
Имеет возможность заносить пользователей в бан-лист согласно заданным в конфиге правилам.
Имеет возможность заносить пользователей в бан-лист, согласно заданным в чёрном списке правилам.
## Что он не может?
@@ -16,13 +19,9 @@
* Не умеет разгадывать капчу.
## Как заставить его работать?
Технически, бот собирается гошкой 1.20 под платформу linux. Работоспособность на других платформах не тестировалась.
Этот бот автоматизирует уборку человеческой грязи. Самый важный вопрос, кторый следует себе задать еред его
использоованием - а не забаню ли я кого-то не того?
Технически, бот собирается гошкой 1.20 поод платформу linux. Работоспособность на других платформах не тестировалась.
Чтобы получить готовые бинарники достаточно выполнить команду:
Чтобы получить готовые бинарники, достаточно выполнить команду:
```bash
make
@@ -30,8 +29,8 @@ make
в итоге получится бинарник **buny-jabber-bot**, которому нужен конфиг data/config.json для работы.
А также понадобятся **data/blacklist.json** и **data/whitelist.json** в которых списки правил, согласно которым *точно*
надо банить злодеев и списки пользователей, которых *точно* не надо банить.
А также понадобятся **data/blacklist.json** и **data/whitelist.json**, в которых находятся списки правил, злодеев и
списки проверенных пользователей.
### Замечание
@@ -73,12 +72,14 @@ presence-ов тех, кто находится в комнате на моме
### Можно ли почистить ban-list?
Да, бывают ситуации, когда по ошибке бот может забанить кого-то не того или, если не настроен белый список вообще ни в
каком виде, то будет банить всех входящих. И если вдруг произошло так, что в бан попали все админы комнаты, то разбанить
их может либо овнер (никто же не даст овнера боту), либо тот, кто зайдёт в комнату из-под аккаунта бота и разбанить всех
от его имени.
каком виде, то будет банить всех входящих.
В далёких планах есть идея релизовать очистку бан-листа при запуске бота с настройкой `"wipe_bans": true`` в
whitelist.json для канала; кроме того, есть планы такое реализовать и в качестве команды, когда у бота появятся команды.
И если вдруг произошло так, что в бан попали все админы комнаты, то...
Во-первых, давать овнера боту не надо ни при каких обстоятельствах.
Во-вторых, разбанить их может только овнер, для чего надо зайти в комнату из-под аккаунта бота и разбанить всех кого
надо.
## Дисклеймер
@@ -86,8 +87,8 @@ whitelist.json для канала; кроме того, есть планы т
## Благодарности
Во-первых `сообществу`, без него не было бы чятиков.
Во-первых, `сообществу`, без него не было бы чятиков.
Во-вторых `batman46(new)` из конференций на `jabber.ru`, за наблюдения и за некоторые идеи, воплощённыые в этом боте.
Во-вторых, `batman46(new)` из конференций на `jabber.ru`, за наблюдения и за некоторые идеи, воплощённые в этом боте.
В-третьих `U2` из конференции `radio@conference.jabber.ru` за мысли, идеи и за позитивный настрой.
В-третьих, `U2` из конференции `radio@conference.jabber.ru` за мысли, идеи и за позитивный настрой.

23
buny.go
View File

@@ -10,7 +10,7 @@ import (
log "github.com/sirupsen/logrus"
)
func buny(v xmpp.Presence) error {
func buny(v xmpp.Presence) error { //nolint:gocognit
var err error
// Если у presence-а есть JID и presence из одной из комнат, в которой мы есть и если его домен в чёрном
@@ -24,7 +24,7 @@ func buny(v xmpp.Presence) error {
room := strings.SplitN(v.From, "/", 2)[0]
if room == "" {
log.Infof("We got empty room in repsence event, which kinda strange: %s", spew.Sdump(v))
log.Infof("We got empty room field in presence event, which kinda strange: %s", spew.Sdump(v))
return err
}
@@ -58,14 +58,13 @@ func buny(v xmpp.Presence) error {
for _, cRoom := range roomsConnected {
if cRoom == room {
for _, bEntry := range blackList.Blacklist {
// Обработаем правила гобального чёрного списка
// Обработаем правила глобального чёрного списка
if bEntry.RoomName == "" {
for _, jid_regexp := range bEntry.JidRe {
re, err := regexp.Compile(jid_regexp)
for _, jidRegexp := range bEntry.JidRe {
re, err := regexp.Compile(jidRegexp)
if err != nil {
log.Errorf("Incorrect regexp in blacklist: %s, skipping", jid_regexp)
log.Errorf("Incorrect regexp in blacklist: %s, skipping", jidRegexp)
continue
}
@@ -91,7 +90,6 @@ func buny(v xmpp.Presence) error {
return err
}
}
continue
@@ -99,11 +97,11 @@ func buny(v xmpp.Presence) error {
// Обработаем правила конкретного канала, комнаты конференции
if bEntry.RoomName == room {
for _, jid_regexp := range bEntry.JidRe {
re, err := regexp.Compile(jid_regexp)
for _, jidRegexp := range bEntry.JidRe {
re, err := regexp.Compile(jidRegexp)
if err != nil {
log.Errorf("Incorrect regexp in blacklist: %s, skipping", jid_regexp)
log.Errorf("Incorrect regexp in blacklist: %s, skipping", jidRegexp)
continue
}
@@ -129,7 +127,6 @@ func buny(v xmpp.Presence) error {
return err
}
}
continue
@@ -141,3 +138,5 @@ func buny(v xmpp.Presence) error {
return err
}
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

View File

@@ -4,27 +4,27 @@ import (
"sync"
)
// Collection это sync.Map-ка, хранящая в себе произвольные итемы
// Collection это sync.Map-ка, хранящая в себе произвольные итемы.
type Collection struct {
items sync.Map
close chan struct{}
}
// Item представляет собой интерфейс для хранения произвольных данных
// Item представляет собой интерфейс для хранения произвольных данных.
type item struct {
data interface{}
}
// NewCollection создаёт инстанс коллекции
// NewCollection создаёт инстанс коллекции.
func NewCollection() *Collection {
c := &Collection{
c := &Collection{ //nolint:exhaustruct
close: make(chan struct{}),
}
return c
}
// Get достаёт данные по заданному ключу из коллекции
// (collection *Collection) Get(key interface{}) (interface{}, bool) достаёт данные по заданному ключу из коллекции.
func (collection *Collection) Get(key interface{}) (interface{}, bool) {
obj, exists := collection.items.Load(key)
@@ -37,14 +37,14 @@ func (collection *Collection) Get(key interface{}) (interface{}, bool) {
return item.data, true
}
// Set сохраняет данные с заданным ключом в коллекцию
// (collection *Collection) Set(key interface{}, value interface{}) сохраняет данные с заданным ключом в коллекцию.
func (collection *Collection) Set(key interface{}, value interface{}) {
collection.items.Store(key, item{
data: value,
})
}
// Range применяет функцию f ко всем ключам в коллекции
// (collection *Collection) Range(f func(key, value interface{}) bool) применяет функцию f ко всем ключам в коллекции.
func (collection *Collection) Range(f func(key, value interface{}) bool) {
fn := func(key, value interface{}) bool {
item := value.(item)
@@ -55,12 +55,12 @@ func (collection *Collection) Range(f func(key, value interface{}) bool) {
collection.items.Range(fn)
}
// Delete удаляет ключ и значение из коллекции данных
// (collection *Collection) Delete(key interface{}) удаляет ключ и значение из коллекции данных.
func (collection *Collection) Delete(key interface{}) {
collection.items.Delete(key)
}
// Close очищает и высвобождает ресурсы, занятые коллекцией
// (collection *Collection) Close() очищает и высвобождает ресурсы, занятые коллекцией.
func (collection *Collection) Close() {
collection.close <- struct{}{}
collection.items = sync.Map{}

View File

@@ -13,7 +13,7 @@ import (
"golang.org/x/exp/slices"
)
func parseEvent(e interface{}) {
func parseEvent(e interface{}) { //nolint:gocognit,gocyclo
lastServerActivity = time.Now().Unix()
switch v := e.(type) {
@@ -30,7 +30,7 @@ func parseEvent(e interface{}) {
// В то же время сообщения от людей приходят с пустым subject, но с заполненным text
if v.Text != "" {
// Чятики бывают групповые и не групповые, от этого зависит Remote, куда направлять сообщение
switch v.Type { //nolint:whitespace
switch v.Type { //nolint:wsl,whitespace
// Групповой чятик
case "groupchat":
@@ -87,16 +87,18 @@ func parseEvent(e interface{}) {
}
}
var iqStruct jabberSimpleIqGetQuery
var parseError bool
var (
iqStruct jabberSimpleIqGetQuery
parseError bool
)
if err := xml.Unmarshal(v.Query, &iqStruct); err == nil {
switch iqStruct.Xmlns { //nolint:whitespace
switch iqStruct.Xmlns { //nolint:wsl,whitespace
// Запрос номера версии приложения
case "jabber:iq:version":
log.Infof("Got IQ get request for version from %s", v.From)
answer := "<query xmlns=\"jabber:iq:version\">"
answer := "<query xmlns=\"jabber:iq:version\">" //nolint:wsl
answer += "<name>buny-jabber-bot</name>"
answer += "<version>1.с_чем-то</version>"
answer += "<os>линупс</os>"
@@ -109,7 +111,7 @@ func parseEvent(e interface{}) {
xmpp.IQTypeResult,
answer,
); err != nil {
log.Errorf("Unable to send verion info to jabber server: id=%s, err=%s", id, err)
log.Errorf("Unable to send version info to jabber server: id=%s, err=%s", id, err)
os.Exit(1)
}
@@ -186,9 +188,9 @@ func parseEvent(e interface{}) {
answer := "<query xmlns=\"http://jabber.org/protocol/disco#info\" "
answer += "node=\"http://jabber.org/protocol/commands\" />"
answer += "<error type=\"cancel\">"
answer += "<error type=\"cancel\">" //nolint:goconst
answer += "<service-unavailable xmlns=\"urn:ietf:params:xml:ns:xmpp-stanzas\" />"
answer += "</error>"
answer += "</error>" //nolint:goconst
if id, err := talk.RawInformation(
v.To,
@@ -287,7 +289,7 @@ func parseEvent(e interface{}) {
default:
log.Info("Got unknown IQ get pubsub something request, discarding")
log.Info(spew.Sdump(e))
parseError = true
parseError = true //nolint:wsl
}
} else {
parseError = true
@@ -311,6 +313,7 @@ func parseEvent(e interface{}) {
"",
); err != nil {
log.Errorf("Unable to send pong to jabber server: id=%s, err=%s", id, err)
os.Exit(1)
}
}
@@ -335,7 +338,7 @@ func parseEvent(e interface{}) {
// Похоже на pong от сервера (по стандарту в ответе нету query, но go-xmpp нам подсовывает это)
case v.From == config.Jabber.Server && v.To == talk.JID() && string(v.Query) == "<XMLElement></XMLElement>":
log.Debugf("Got S2C pong answer from %s to %s", v.From, v.To)
serverPingTimestampRx = time.Now().Unix()
serverPingTimestampRx = time.Now().Unix() //nolint:wsl
// Похоже на понг второй стадии xep-0410 MUC-Ping-а, который у нас не реализован
case v.To == talk.JID() && string(v.Query) == "<XMLElement></XMLElement>":
@@ -405,8 +408,10 @@ func parseEvent(e interface{}) {
case xmpp.IQTypeError:
// Если сервер не хочет пинговаться и отвечает ошибкой на пинг, то наверно он не умеет в пинги,
// хотя если мы его пингуем, значит он анонсировал такой capability. Вот, засранец!
var iqPingStruct jabberIqPing
var parseError = true
var (
iqPingStruct jabberIqPing
parseError = true
)
if err := xml.Unmarshal(v.Query, &iqPingStruct); err == nil {
if iqPingStruct.Xmlns == "urn:xmpp:ping" {
@@ -429,7 +434,7 @@ func parseEvent(e interface{}) {
// Это у нас пинг xep-0410 и мы не в комнате, предполагается, что надо бы заджойниться
if parseError {
var iqErrorCancelNotAcceptable jabberIqErrorCancelNotAccepatble
var iqErrorCancelNotAcceptable jabberIqErrorCancelNotAcceptable
if err := xml.Unmarshal(v.Query, &iqErrorCancelNotAcceptable); err == nil {
if v.To == talk.JID() {
@@ -503,8 +508,8 @@ func parseEvent(e interface{}) {
log.Errorf(spew.Sdump(v))
}
} else {
log.Infof("Presence notification, Type: %s, From: %s, To: %s Show: %s, Status: %s, Affilation: %s, Role: %s, JID: %s",
v.Type, v.From, v.To, v.Show, v.Status, v.Affilation, v.Role, v.JID)
log.Infof("Presence notification, Type: %s, From: %s, To: %s Show: %s, Status: %s, Affiliation: %s, Role: %s, JID: %s",
v.Type, v.From, v.To, v.Show, v.Status, v.Affiliation, v.Role, v.JID)
// Это наш собственный Presence
if v.Show == "" && v.Status == "" {
@@ -544,7 +549,7 @@ func parseEvent(e interface{}) {
for _, feature := range v.Features {
log.Debugf("MUC %s announced that it supports feature: %s", v.From, feature)
mucCaps[feature] = true
mucCaps[feature] = true //nolint:wsl
}
mucCapsList.Set(v.From, mucCaps)

View File

@@ -6,60 +6,60 @@ import (
"github.com/eleksir/go-xmpp"
)
// Config - это у нас глобальная штука :)
// Config - это у нас глобальная штука :) .
var config myConfig
// Списки тех, кого мы не баним никогда
// Списки тех, кого мы не баним никогда.
var whiteList myWhiteList
// Списки злодеев, которых мы банм
// Списки злодеев, которых мы баним.
var blackList myBlackList
// Ставится в true, если мы получили сигнал на выключение
// Ставится в true, если мы получили сигнал на выключение.
var shutdown = false
// Чтобы не организовывать драку за установку коннекта
// Чтобы не организовывать драку за установку коннекта.
var connecting = false
// Глобальное состояние соединения
// Глобальное состояние соединения.
var isConnected = false
// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов
// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов.
var sigChan = make(chan os.Signal, 1)
// Основной инстанс xmpp-клиента
// Основной инстанс xmpp-клиента.
var talk *xmpp.Client
// Опции подключения к xmpp-серверу
// Опции подключения к xmpp-серверу.
var options *xmpp.Options
// Список комнат, в которых находится бот
// Список комнат, в которых находится бот.
var roomsConnected []string
// Время последней активности, нужно для jabber:iq:last
// Время последней активности, нужно для jabber:iq:last.
var lastActivity int64
// Время последней активности, нужно для c2s пингов - посылаем пинги, только если давненько ничего не приходило с
// сервера
// сервера.
var lastServerActivity int64
// Время последней активности MUC-ов, нужно для пингов - посылаем пинги, только если давненько ничего не приходило из
// muc-ов
// muc-ов.
var lastMucActivity = NewCollection()
// Получен ли ответ на запрос disco#info к серверу
// Получен ли ответ на запрос disco#info к серверу.
var serverCapsQueried bool
// sync.Map-ка с капабилити сервера
// sync.Map-ка с капабилити сервера.
var serverCapsList = NewCollection()
// sync.Map-ка с комнатами и их capability
// sync.Map-ка с комнатами и их capability.
var mucCapsList = NewCollection()
// Время, когда был отправлен c2s ping
// Время, когда был отправлен c2s ping.
var serverPingTimestampTx int64
// Время, когда был принят s2c pong
// Время, когда был принят s2c pong.
var serverPingTimestampRx int64
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

4
go.mod
View File

@@ -4,10 +4,10 @@ go 1.20
require (
github.com/davecgh/go-spew v1.1.1
github.com/eleksir/go-xmpp v1.3.0
github.com/eleksir/go-xmpp v1.4.0
github.com/hjson/hjson-go v3.3.0+incompatible
github.com/sirupsen/logrus v1.9.3
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691
)
require (

8
go.sum
View File

@@ -1,8 +1,8 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/eleksir/go-xmpp v1.3.0 h1:A4vonXmHJ+3W4LW+BDM24GkAT0zRMhCAMZhAqTRarLE=
github.com/eleksir/go-xmpp v1.3.0/go.mod h1:KnvbXvUpttEVFl8l5eDO2Q7J97ZxtTMvOsDWULD2Rn8=
github.com/eleksir/go-xmpp v1.4.0 h1:CJ91zeoLoV0FqhhcvZnkBjeuaXWbWgRTJ/HrzKugM4s=
github.com/eleksir/go-xmpp v1.4.0/go.mod h1:KnvbXvUpttEVFl8l5eDO2Q7J97ZxtTMvOsDWULD2Rn8=
github.com/hjson/hjson-go v3.3.0+incompatible h1:Rqr+Ya+0aCJMjaE4s8E9YKvuJLuLVpEvz4ONum52vnI=
github.com/hjson/hjson-go v3.3.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -14,8 +14,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 h1:/yRP+0AN7mf5DkD3BAI6TOFnd51gEoDEb8o35jIFtgw=
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

235
lib.go
View File

@@ -15,7 +15,7 @@ import (
"golang.org/x/exp/slices"
)
// Хэндлер сигналов закрывает все бд, все сетевые соединения и сваливает из приложения
// sigHandler() Хэндлер сигналов закрывает все бд, все сетевые соединения и сваливает из приложения.
func sigHandler() {
log.Debug("Installing signal handler")
@@ -28,19 +28,19 @@ func sigHandler() {
case syscall.SIGQUIT:
log.Infoln("Got SIGQUIT, quitting")
// Заходим на новую итерацию, если у нас "неинтересный" сигнал
// Заходим на новую итерацию, если у нас "неинтересный" сигнал.
default:
continue
}
// Чтобы не срать в логи ошибками, проставим shutdown state приложения в true
// Чтобы не срать в логи ошибками, проставим shutdown state приложения в true.
shutdown = true
if isConnected && !shutdown {
log.Debug("Try to set our presence to Unavailable and status to Offline")
// Вот тут понадобится коллекция известных пользователей, чтобы им разослать presence, что бот свалил в offline
// Пока за неимением лучшего сообщим об этом самим себе
// Пока за неимением лучшего сообщим об этом самим себе.
for _, room := range roomsConnected {
if _, err := talk.SendPresence(
xmpp.Presence{ //nolint:exhaustruct
@@ -53,7 +53,7 @@ func sigHandler() {
}
}
// И закрываем соединение
// И закрываем соединение.
log.Debugf("Closing connection to jabber server")
if err := talk.Close(); err != nil {
@@ -65,13 +65,16 @@ func sigHandler() {
}
}
// Читает и валидирует конфиг, а также выставляет некоторые default-ы, если значений для параметров в конфиге нет
func readConfig() {
// readConfig читает и валидирует конфиг, а также выставляет некоторые default-ы, если значений для параметров в конфиге
// нет.
func readConfig() { //nolint:gocognit
configLoaded := false
executablePath, err := os.Executable()
if err != nil {
log.Errorf("Unable to get current executable path: %s", err)
os.Exit(1)
}
configJSONPath := fmt.Sprintf("%s/data/config.json", filepath.Dir(executablePath))
@@ -86,12 +89,12 @@ func readConfig() {
for _, location := range locations {
fileInfo, err := os.Stat(location)
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём.
if err != nil {
continue
}
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата.
if fileInfo.Size() > 65535 {
log.Warnf("Config file %s is too long for config, skipping", location)
@@ -100,7 +103,7 @@ func readConfig() {
buf, err := os.ReadFile(location)
// Не удалось прочитать, попробуем следующего кандидата
// Не удалось прочитать, попробуем следующего кандидата.
if err != nil {
log.Warnf("Skip reading config file %s: %s", location, err)
@@ -110,18 +113,21 @@ func readConfig() {
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
// сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется.
var sampleConfig myConfig
var tmp map[string]interface{}
var (
sampleConfig myConfig
tmp map[string]interface{}
)
err = hjson.Unmarshal(buf, &tmp)
// Не удалось распарсить - попробуем следующего кандидата
// Не удалось распарсить - попробуем следующего кандидата.
if err != nil {
log.Warnf("Skip parsing config file %s: %s", location, err)
continue
}
tmpjson, err := json.Marshal(tmp)
tmpJSON, err := json.Marshal(tmp)
// Не удалось преобразовать map-ку в json
if err != nil {
@@ -130,7 +136,7 @@ func readConfig() {
continue
}
if err := json.Unmarshal(tmpjson, &sampleConfig); err != nil {
if err := json.Unmarshal(tmpJSON, &sampleConfig); err != nil {
log.Warnf("Skip parsing config file %s: %s", location, err)
continue
@@ -138,20 +144,22 @@ func readConfig() {
// Значения для Jabber-клиента
if sampleConfig.Jabber.Server == "" {
sampleConfig.Jabber.Server = "localhost"
log.Errorf("Jabber server is not defined in config, using localhost")
log.Error("Jabber server is not defined in config, using localhost")
sampleConfig.Jabber.Server = "localhost" //nolint:wsl
}
if sampleConfig.Jabber.Port == 0 {
sampleConfig.Jabber.Port = 5552
sampleConfig.Jabber.Port = 5222
if sampleConfig.Jabber.Ssl {
if !sampleConfig.Jabber.StartTLS {
sampleConfig.Jabber.Port = 5553
sampleConfig.Jabber.Port = 5223
log.Info("Jabber port is not defined in config, using 5223")
} else {
log.Info("Jabber port is not defined in config, using 5222")
}
}
log.Infof("Jabber port is not defined in config, using 6667")
}
if !sampleConfig.Jabber.Ssl {
@@ -166,26 +174,38 @@ func readConfig() {
if sampleConfig.Jabber.ConnectionTimeout == 0 {
sampleConfig.Jabber.ConnectionTimeout = 10
log.Info("Jabber server connection_timeout not defined in config, using 10 seconds")
}
if sampleConfig.Jabber.ReconnectDelay == 0 {
sampleConfig.Jabber.ReconnectDelay = 3
log.Info("Jabber server reconnect_delay not defined in config, using 3 seconds")
}
if sampleConfig.Jabber.ServerPingDelay == 0 {
sampleConfig.Jabber.ServerPingDelay = 60
log.Info("Jabber server_ping_delay not defined in config, using 60 seconds")
}
if sampleConfig.Jabber.MucPingDelay == 0 {
sampleConfig.Jabber.MucPingDelay = 900
log.Info("Jabber muc_ping_delay not defined in config, using 900 seconds")
}
if sampleConfig.Jabber.MucRejoinDelay == 0 {
sampleConfig.Jabber.MucRejoinDelay = 3
log.Info("Jabber muc_rejoin_delay not defined in config, using 3 seconds")
}
if sampleConfig.Jabber.PingSplayDelay == 0 {
sampleConfig.Jabber.PingSplayDelay = 3
log.Info("Jabber ping_splay_delay not defined in config, using 3 seconds")
}
if sampleConfig.Jabber.Nick == "" {
@@ -195,10 +215,14 @@ func readConfig() {
if sampleConfig.Jabber.Resource == "" {
sampleConfig.Jabber.Resource = "buny bot"
log.Info("Jabber resource not defined in config, using buny bot")
}
if sampleConfig.Jabber.User == "" {
sampleConfig.Jabber.User = fmt.Sprintf("%s@%s", sampleConfig.Jabber.Nick, sampleConfig.Jabber.Server)
log.Infof("Jabber user not defined in config, guessing, it can be %s", sampleConfig.Jabber.User)
}
// Если sampleConfig.Jabber.Password не задан, то авторизации не будет
@@ -212,13 +236,15 @@ func readConfig() {
if sampleConfig.Loglevel == "" {
sampleConfig.Loglevel = "info"
log.Info("loglevel not defined in config, using info")
}
// sampleConfig.Log = "" if not set
config = sampleConfig
configLoaded = true
log.Infof("Using %s as config file", location)
log.Infof("Using %s as config file", location) //nolint:wsl
break
}
@@ -229,13 +255,14 @@ func readConfig() {
}
}
// Читает и валидирует белые списки пользователей
// readWhitelist() читает и валидирует белые списки пользователей.
func readWhitelist() {
whitelistLoaded := false
executablePath, err := os.Executable()
if err != nil {
log.Errorf("Unable to get current executable path: %s", err)
os.Exit(1)
}
whitelistJSONPath := fmt.Sprintf("%s/data/whitelist.json", filepath.Dir(executablePath))
@@ -274,8 +301,11 @@ func readWhitelist() {
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
// сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется.
var sampleWhitelist myWhiteList
var tmp map[string]interface{}
var (
sampleWhitelist myWhiteList
tmp map[string]interface{}
)
err = hjson.Unmarshal(buf, &tmp)
// Не удалось распарсить - попробуем следующего кандидата
@@ -285,7 +315,7 @@ func readWhitelist() {
continue
}
tmpjson, err := json.Marshal(tmp)
tmpJSON, err := json.Marshal(tmp)
// Не удалось преобразовать map-ку в json
if err != nil {
@@ -294,7 +324,7 @@ func readWhitelist() {
continue
}
if err := json.Unmarshal(tmpjson, &sampleWhitelist); err != nil {
if err := json.Unmarshal(tmpJSON, &sampleWhitelist); err != nil {
log.Warnf("Skip parsing whitelist file %s: %s", location, err)
continue
@@ -302,6 +332,7 @@ func readWhitelist() {
whiteList = sampleWhitelist
whitelistLoaded = true
log.Infof("Using %s as whiteList file", location)
break
@@ -309,17 +340,20 @@ func readWhitelist() {
if !whitelistLoaded {
log.Error("Whitelist was not loaded! Refusing to start.")
os.Exit(1)
}
}
// Читает и валидирует чёрные списки пользователей
// readBlacklist() читает и валидирует чёрные списки пользователей.
func readBlacklist() {
blacklistLoaded := false
executablePath, err := os.Executable()
if err != nil {
log.Errorf("Unable to get current executable path: %s", err)
os.Exit(1)
}
whitelistJSONPath := fmt.Sprintf("%s/data/blacklist.json", filepath.Dir(executablePath))
@@ -358,8 +392,11 @@ func readBlacklist() {
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
// сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется.
var sampleBlacklist myBlackList
var tmp map[string]interface{}
var (
sampleBlacklist myBlackList
tmp map[string]interface{}
)
err = hjson.Unmarshal(buf, &tmp)
// Не удалось распарсить - попробуем следующего кандидата
@@ -369,7 +406,7 @@ func readBlacklist() {
continue
}
tmpjson, err := json.Marshal(tmp)
tmpJSON, err := json.Marshal(tmp)
// Не удалось преобразовать map-ку в json
if err != nil {
@@ -378,7 +415,7 @@ func readBlacklist() {
continue
}
if err := json.Unmarshal(tmpjson, &sampleBlacklist); err != nil {
if err := json.Unmarshal(tmpJSON, &sampleBlacklist); err != nil {
log.Warnf("Skip parsing whitelist file %s: %s", location, err)
continue
@@ -386,6 +423,7 @@ func readBlacklist() {
blackList = sampleBlacklist
blacklistLoaded = true
log.Infof("Using %s as blacklist file", location)
break
@@ -393,10 +431,12 @@ func readBlacklist() {
if !blacklistLoaded {
log.Error("Blacklist was not loaded! Refusing to start.")
os.Exit(1)
}
}
// establishConnection() устанавливает соединение с jabber-сервером.
func establishConnection() {
var err error
@@ -404,39 +444,32 @@ func establishConnection() {
return
}
// проставляем глобальные переменные
// Проставляем глобальные переменные.
connecting = true
isConnected = false
roomsConnected = make([]string, 0)
for {
talk, err = options.NewClient()
talk, err = options.NewClient()
if err != nil {
log.Error(err)
if err != nil {
log.Error(err)
os.Exit(1)
} else {
break
}
os.Exit(1)
}
// По идее keepalive должен же проходить только, если мы уже на сервере, так?
for {
if _, err := talk.SendKeepAlive(); err != nil {
log.Errorf("Try to send initial KeepAlive, got error: %s", err)
time.Sleep(time.Duration(config.Jabber.ReconnectDelay) * time.Second)
if _, err := talk.SendKeepAlive(); err != nil {
log.Errorf("Try to send initial KeepAlive, got error: %s", err)
time.Sleep(time.Duration(config.Jabber.ReconnectDelay) * time.Second)
// Понятно, что мы уже не в процессе установления соединения, а произошла ошибка
connecting = false
os.Exit(1)
} else {
log.Info("Connected")
// Понятно, что мы уже не в процессе установления соединения, а произошла ошибка
connecting = false
break
}
os.Exit(1)
}
log.Info("Connected")
// Джойнимся к чятикам, но делаем это в фоне, чтобы не блочиться на ошибках, например, если бота забанили
for _, room := range config.Jabber.Channels {
go joinMuc(room)
@@ -467,22 +500,7 @@ func establishConnection() {
}
}
func reEstablishConnection() {
// Серверные capabilities нам *уже* неизвестны
serverCapsQueried = false
serverCapsList = NewCollection()
mucCapsList = NewCollection()
lastMucActivity = NewCollection()
// Пока соединение не установлено, мы про пинги ничего не знаем
serverPingTimestampRx = time.Now().Unix() // Считаем, что если коннект запустился, то первый пинг успешен
serverPingTimestampTx = 0
time.Sleep(time.Duration(config.Jabber.ReconnectDelay) * time.Second)
connecting = false
establishConnection()
}
// Джойнится к конференциям/каналам/комнатам в джаббере
// joinMuc(room string) джойнится к конференциям/каналам/комнатам в джаббере.
func joinMuc(room string) {
log.Debugf("Sending disco#info from %s to %s", talk.JID(), room)
@@ -492,11 +510,13 @@ func joinMuc(room string) {
os.Exit(1)
}
// Ждём, пока muc нам вернёт список фичей
// Ждём, пока muc нам вернёт список фичей.
for i := 0; i < (20 * int(config.Jabber.ConnectionTimeout)); i++ {
var myRoom interface{}
var supported bool
var exist bool
var (
myRoom interface{}
supported bool
exist bool
)
time.Sleep(50 * time.Millisecond)
@@ -508,11 +528,11 @@ func joinMuc(room string) {
if supported, exist = myRoom.(map[string]bool)["muc_unsecured"]; exist {
if supported {
break
} else {
log.Infof("Unable to join to password-protected room. Don't know how to enter passwords :)")
return
}
log.Infof("Unable to join to password-protected room. Don't know how to enter passwords :)")
return
}
}
@@ -520,20 +540,34 @@ func joinMuc(room string) {
log.Errorf("Unable to join to MUC: %s", room)
os.Exit(1)
} else {
log.Infof("Joining to MUC: %s", room)
}
// Ждём, когда прилетит presence из комнаты, тогда мы точно знаем, что мы вошли
log.Infof("Joining to MUC: %s", room)
// Ждём, когда прилетит presence из комнаты, тогда мы точно знаем, что мы вошли.
entered := false
for i := 0; i < (20 * int(config.Jabber.ConnectionTimeout)); i++ {
time.Sleep(50 * time.Millisecond)
if slices.Contains(roomsConnected, room) {
entered = true
break
}
}
// Вот теперь точно можно слать статус
if !entered {
log.Errorf(
"Unable to enter to MUC %s, join timeout after %d seconds (server does not return my presence for this room)",
room,
20*int(config.Jabber.ConnectionTimeout)+1,
)
return
}
// Вот теперь точно можно слать статус.
log.Infof("Joined to MUC: %s", room)
if _, err := talk.SendPresence(
@@ -547,8 +581,9 @@ func joinMuc(room string) {
}
}
// Проверяет живость соединения с сервером
func probeServerLiveness() {
// probeServerLiveness() проверяет живость соединения с сервером. Для многих серверов обязательная штука, без которой
// они выкидывают клиента через некоторое время неактивности.
func probeServerLiveness() { //nolint:gocognit
for {
if shutdown {
return
@@ -569,8 +604,10 @@ func probeServerLiveness() {
// }
if serverCapsQueried { // Сервер ответил на disco#info
var value interface{}
var exist bool
var (
value interface{}
exist bool
)
value, exist = serverCapsList.Get("urn:xmpp:ping")
@@ -602,18 +639,18 @@ func probeServerLiveness() {
if err := talk.PingC2S(talk.JID(), config.Jabber.Server); err != nil {
os.Exit(1)
} else {
serverPingTimestampTx = time.Now().Unix()
}
serverPingTimestampTx = time.Now().Unix()
}
} else { // Первая пуля пока не вылетела, отправляем
log.Debugf("Sending first c2s ping from %s to %s", talk.JID(), config.Jabber.Server)
if err := talk.PingC2S(talk.JID(), config.Jabber.Server); err != nil {
os.Exit(1)
} else {
serverPingTimestampTx = time.Now().Unix()
}
serverPingTimestampTx = time.Now().Unix()
}
// Сервер не анонсировал, что умеет в c2s пинги
@@ -622,8 +659,6 @@ func probeServerLiveness() {
if _, err := talk.SendKeepAlive(); err != nil {
os.Exit(1)
continue
}
}
} else { // Сервер не ответил на disco#info
@@ -631,28 +666,28 @@ func probeServerLiveness() {
if _, err := talk.SendKeepAlive(); err != nil {
os.Exit(1)
continue
}
}
}
}
// Пингует MUC-и, нужно для проверки, что клиент ещё находится в MUC-е
// probeMUCLiveness() Пингует MUC-и, нужно для проверки, что клиент ещё находится в MUC-е.
func probeMUCLiveness() {
for {
for _, room := range roomsConnected {
var exist bool
var lastActivityTs interface{}
var (
exist bool
lastActivityTS interface{}
)
// Если записи про комнату нету, то пинговать её бессмысленно.
if lastActivityTs, exist = lastMucActivity.Get(room); !exist {
if lastActivityTS, exist = lastMucActivity.Get(room); !exist {
continue
}
// Если время последней активности в чятике не превысило
// config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay, ничего не пингуем
if (time.Now().Unix() - lastActivityTs.(int64)) < (config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay) {
// config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay, ничего не пингуем.
if (time.Now().Unix() - lastActivityTS.(int64)) < (config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay) {
continue
}
@@ -673,10 +708,10 @@ func probeMUCLiveness() {
roomMap, exist = mucCapsList.Get(room)
// Пинги комнаты проводим, только если она записана, как прошедшая disco#info и поддерживающая
// Server Optimization
// Server Optimization.
if exist && roomMap.(map[string]bool)["http://jabber.org/protocol/muc#self-ping-optimization"] {
go func(room string) {
// Небольшая рандомная задержка перед пингом комнаты
// Небольшая рандомная задержка перед пингом комнаты.
sleepTime := time.Duration(rand.Int63n(1000*config.Jabber.PingSplayDelay)) * time.Millisecond //nolint:gosec
time.Sleep(sleepTime)

View File

@@ -94,8 +94,8 @@ func main() {
Debug: verboseClient,
Session: false,
Status: "xa",
StatusMessage: "echo bot is starting up",
DialTimeout: 10 * time.Second,
StatusMessage: fmt.Sprintf("%s bot is starting up", config.Jabber.Nick),
DialTimeout: time.Duration(config.Jabber.ConnectionTimeout) * time.Second,
}
go sigHandler()
@@ -145,7 +145,6 @@ func main() {
default:
// Это уже что-то странное.
// Вероятно, ошибка парсинга xml. Собственно, баг сервера, тут мы ничего поделать не можем
os.Exit(1)
}
}

View File

@@ -4,7 +4,7 @@ import (
"encoding/xml"
)
// Конфиг
// myConfig прототип структурки с конфигом.
type myConfig struct {
Jabber struct {
Server string `json:"server,omitempty"`
@@ -32,15 +32,16 @@ type myConfig struct {
NoEcho bool `json:"noecho,omitempty"`
}
// Белый список
// myWhiteList прототип структурки с белым списком jid-ов.
type myWhiteList struct {
Whitelist []struct {
RoomName string `json:"room_name,omitempty"`
Jid []string `json:"jid,omitempty"`
WipeBans bool `json:"wipe_bans,omitempty"`
} `json:"whitelist,omitampty"`
} `json:"whitelist,omitempty"`
}
// myBlackList прототип структурки с чёрным списком jid-ов.
type myBlackList struct {
Blacklist []struct {
RoomName string `json:"room_name,omitempty"`
@@ -48,6 +49,8 @@ type myBlackList struct {
} `json:"blacklist,omitempty"`
}
// jabberSimpleIqGetQuery прототип структурки для разбора запросов xmpp discovery query, например,
// https://xmpp.org/extensions/xep-0030.html#example-18 .
type jabberSimpleIqGetQuery struct {
XMLName xml.Name `xml:"query"`
Text string `xml:",chardata"`
@@ -55,6 +58,7 @@ type jabberSimpleIqGetQuery struct {
Node string `xml:"node,attr,omitempty"` // для xmlns="http://jabber.org/protocol/disco#items"
}
// jabberPubsubIQGetQuery прототип структурки для разбора запросов xmpp pubsub.
type jabberPubsubIQGetQuery struct {
XMLName xml.Name `xml:"pubsub"`
Text string `xml:",chardata"`
@@ -66,6 +70,8 @@ type jabberPubsubIQGetQuery struct {
} `xml:"items"`
}
// jabberTimeIqGetQuery прототип структурки для разбора IQ запросов на локальное время клиента,
// https://xmpp.org/extensions/xep-0202.html
type jabberTimeIqGetQuery struct {
// <time xmlns="urn:xmpp:time"/>
XMLName xml.Name `xml:"time"`
@@ -73,13 +79,16 @@ type jabberTimeIqGetQuery struct {
Xmlns string `xml:"xmlns,attr"`
}
// jabberIqPing прототип структурки для разбора IQ запросов на пинг клиента, https://xmpp.org/extensions/xep-0199.html
type jabberIqPing struct {
XMLName xml.Name `xml:"ping"`
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
}
type jabberIqErrorCancelNotAccepatble struct {
// jabberIqErrorCancelNotAcceptable прототип структурки для разбора IQ ответов, когда сервис (сервер, клиент, etc) не
// может или не хочет принимать наш iq-запрос https://xmpp.org/extensions/xep-0099.html
type jabberIqErrorCancelNotAcceptable struct {
XMLName xml.Name `xml:"error"`
Text string `xml:",chardata"`
Type string `xml:"type,attr"`

View File

@@ -664,7 +664,7 @@ func (c *Client) init(o *Options) error {
c.domain = domain
if o.Session {
//if server support session, open it
// if server support session, open it
fmt.Fprintf(StanzaWriter, "<iq to='%s' type='set' id='%x'><session xmlns='%s'/></iq>", xmlEscape(domain), cookie, nsSession)
}
@@ -700,7 +700,7 @@ func (c *Client) startTLSIfRequired(f *streamFeatures, o *Options, domain string
tc := o.TLSConfig
if tc == nil {
tc = DefaultConfig.Clone()
//TODO(scott): we should consider using the server's address or reverse lookup
// TODO(scott): we should consider using the server's address or reverse lookup
tc.ServerName = domain
}
t := tls.Client(c.conn, tc)
@@ -789,16 +789,16 @@ type Contact struct {
// Presence is an XMPP presence notification.
type Presence struct {
From string
To string
Type string
Show string
Status string
Priority string
ID string
Affilation string
Role string
JID string
From string
To string
Type string
Show string
Status string
Priority string
ID string
Affiliation string
Role string
JID string
}
type IQ struct {

44
vendor/golang.org/x/exp/slices/cmp.go generated vendored Normal file
View File

@@ -0,0 +1,44 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package slices
import "golang.org/x/exp/constraints"
// min is a version of the predeclared function from the Go 1.21 release.
func min[T constraints.Ordered](a, b T) T {
if a < b || isNaN(a) {
return a
}
return b
}
// max is a version of the predeclared function from the Go 1.21 release.
func max[T constraints.Ordered](a, b T) T {
if a > b || isNaN(a) {
return a
}
return b
}
// cmpLess is a copy of cmp.Less from the Go 1.21 release.
func cmpLess[T constraints.Ordered](x, y T) bool {
return (isNaN(x) && !isNaN(y)) || x < y
}
// cmpCompare is a copy of cmp.Compare from the Go 1.21 release.
func cmpCompare[T constraints.Ordered](x, y T) int {
xNaN := isNaN(x)
yNaN := isNaN(y)
if xNaN && yNaN {
return 0
}
if xNaN || x < y {
return -1
}
if yNaN || x > y {
return +1
}
return 0
}

View File

@@ -3,23 +3,20 @@
// license that can be found in the LICENSE file.
// Package slices defines various functions useful with slices of any type.
// Unless otherwise specified, these functions all apply to the elements
// of a slice at index 0 <= i < len(s).
//
// Note that the less function in IsSortedFunc, SortFunc, SortStableFunc requires a
// strict weak ordering (https://en.wikipedia.org/wiki/Weak_ordering#Strict_weak_orderings),
// or the sorting may fail to sort correctly. A common case is when sorting slices of
// floating-point numbers containing NaN values.
package slices
import "golang.org/x/exp/constraints"
import (
"unsafe"
"golang.org/x/exp/constraints"
)
// Equal reports whether two slices are equal: the same length and all
// elements equal. If the lengths are different, Equal returns false.
// Otherwise, the elements are compared in increasing index order, and the
// comparison stops at the first unequal pair.
// Floating point NaNs are not considered equal.
func Equal[E comparable](s1, s2 []E) bool {
func Equal[S ~[]E, E comparable](s1, s2 S) bool {
if len(s1) != len(s2) {
return false
}
@@ -31,12 +28,12 @@ func Equal[E comparable](s1, s2 []E) bool {
return true
}
// EqualFunc reports whether two slices are equal using a comparison
// EqualFunc reports whether two slices are equal using an equality
// function on each pair of elements. If the lengths are different,
// EqualFunc returns false. Otherwise, the elements are compared in
// increasing index order, and the comparison stops at the first index
// for which eq returns false.
func EqualFunc[E1, E2 any](s1 []E1, s2 []E2, eq func(E1, E2) bool) bool {
func EqualFunc[S1 ~[]E1, S2 ~[]E2, E1, E2 any](s1 S1, s2 S2, eq func(E1, E2) bool) bool {
if len(s1) != len(s2) {
return false
}
@@ -49,45 +46,37 @@ func EqualFunc[E1, E2 any](s1 []E1, s2 []E2, eq func(E1, E2) bool) bool {
return true
}
// Compare compares the elements of s1 and s2.
// The elements are compared sequentially, starting at index 0,
// Compare compares the elements of s1 and s2, using [cmp.Compare] on each pair
// of elements. The elements are compared sequentially, starting at index 0,
// until one element is not equal to the other.
// The result of comparing the first non-matching elements is returned.
// If both slices are equal until one of them ends, the shorter slice is
// considered less than the longer one.
// The result is 0 if s1 == s2, -1 if s1 < s2, and +1 if s1 > s2.
// Comparisons involving floating point NaNs are ignored.
func Compare[E constraints.Ordered](s1, s2 []E) int {
s2len := len(s2)
func Compare[S ~[]E, E constraints.Ordered](s1, s2 S) int {
for i, v1 := range s1 {
if i >= s2len {
if i >= len(s2) {
return +1
}
v2 := s2[i]
switch {
case v1 < v2:
return -1
case v1 > v2:
return +1
if c := cmpCompare(v1, v2); c != 0 {
return c
}
}
if len(s1) < s2len {
if len(s1) < len(s2) {
return -1
}
return 0
}
// CompareFunc is like Compare but uses a comparison function
// on each pair of elements. The elements are compared in increasing
// index order, and the comparisons stop after the first time cmp
// returns non-zero.
// CompareFunc is like [Compare] but uses a custom comparison function on each
// pair of elements.
// The result is the first non-zero result of cmp; if cmp always
// returns 0 the result is 0 if len(s1) == len(s2), -1 if len(s1) < len(s2),
// and +1 if len(s1) > len(s2).
func CompareFunc[E1, E2 any](s1 []E1, s2 []E2, cmp func(E1, E2) int) int {
s2len := len(s2)
func CompareFunc[S1 ~[]E1, S2 ~[]E2, E1, E2 any](s1 S1, s2 S2, cmp func(E1, E2) int) int {
for i, v1 := range s1 {
if i >= s2len {
if i >= len(s2) {
return +1
}
v2 := s2[i]
@@ -95,7 +84,7 @@ func CompareFunc[E1, E2 any](s1 []E1, s2 []E2, cmp func(E1, E2) int) int {
return c
}
}
if len(s1) < s2len {
if len(s1) < len(s2) {
return -1
}
return 0
@@ -103,7 +92,7 @@ func CompareFunc[E1, E2 any](s1 []E1, s2 []E2, cmp func(E1, E2) int) int {
// Index returns the index of the first occurrence of v in s,
// or -1 if not present.
func Index[E comparable](s []E, v E) int {
func Index[S ~[]E, E comparable](s S, v E) int {
for i := range s {
if v == s[i] {
return i
@@ -114,7 +103,7 @@ func Index[E comparable](s []E, v E) int {
// IndexFunc returns the first index i satisfying f(s[i]),
// or -1 if none do.
func IndexFunc[E any](s []E, f func(E) bool) int {
func IndexFunc[S ~[]E, E any](s S, f func(E) bool) int {
for i := range s {
if f(s[i]) {
return i
@@ -124,39 +113,104 @@ func IndexFunc[E any](s []E, f func(E) bool) int {
}
// Contains reports whether v is present in s.
func Contains[E comparable](s []E, v E) bool {
func Contains[S ~[]E, E comparable](s S, v E) bool {
return Index(s, v) >= 0
}
// ContainsFunc reports whether at least one
// element e of s satisfies f(e).
func ContainsFunc[E any](s []E, f func(E) bool) bool {
func ContainsFunc[S ~[]E, E any](s S, f func(E) bool) bool {
return IndexFunc(s, f) >= 0
}
// Insert inserts the values v... into s at index i,
// returning the modified slice.
// In the returned slice r, r[i] == v[0].
// The elements at s[i:] are shifted up to make room.
// In the returned slice r, r[i] == v[0],
// and r[i+len(v)] == value originally at r[i].
// Insert panics if i is out of range.
// This function is O(len(s) + len(v)).
func Insert[S ~[]E, E any](s S, i int, v ...E) S {
tot := len(s) + len(v)
if tot <= cap(s) {
s2 := s[:tot]
copy(s2[i+len(v):], s[i:])
m := len(v)
if m == 0 {
return s
}
n := len(s)
if i == n {
return append(s, v...)
}
if n+m > cap(s) {
// Use append rather than make so that we bump the size of
// the slice up to the next storage class.
// This is what Grow does but we don't call Grow because
// that might copy the values twice.
s2 := append(s[:i], make(S, n+m-i)...)
copy(s2[i:], v)
copy(s2[i+m:], s[i:])
return s2
}
s2 := make(S, tot)
copy(s2, s[:i])
copy(s2[i:], v)
copy(s2[i+len(v):], s[i:])
return s2
s = s[:n+m]
// before:
// s: aaaaaaaabbbbccccccccdddd
// ^ ^ ^ ^
// i i+m n n+m
// after:
// s: aaaaaaaavvvvbbbbcccccccc
// ^ ^ ^ ^
// i i+m n n+m
//
// a are the values that don't move in s.
// v are the values copied in from v.
// b and c are the values from s that are shifted up in index.
// d are the values that get overwritten, never to be seen again.
if !overlaps(v, s[i+m:]) {
// Easy case - v does not overlap either the c or d regions.
// (It might be in some of a or b, or elsewhere entirely.)
// The data we copy up doesn't write to v at all, so just do it.
copy(s[i+m:], s[i:])
// Now we have
// s: aaaaaaaabbbbbbbbcccccccc
// ^ ^ ^ ^
// i i+m n n+m
// Note the b values are duplicated.
copy(s[i:], v)
// Now we have
// s: aaaaaaaavvvvbbbbcccccccc
// ^ ^ ^ ^
// i i+m n n+m
// That's the result we want.
return s
}
// The hard case - v overlaps c or d. We can't just shift up
// the data because we'd move or clobber the values we're trying
// to insert.
// So instead, write v on top of d, then rotate.
copy(s[n:], v)
// Now we have
// s: aaaaaaaabbbbccccccccvvvv
// ^ ^ ^ ^
// i i+m n n+m
rotateRight(s[i:], m)
// Now we have
// s: aaaaaaaavvvvbbbbcccccccc
// ^ ^ ^ ^
// i i+m n n+m
// That's the result we want.
return s
}
// Delete removes the elements s[i:j] from s, returning the modified slice.
// Delete panics if s[i:j] is not a valid slice of s.
// Delete modifies the contents of the slice s; it does not create a new slice.
// Delete is O(len(s)-j), so if many items must be deleted, it is better to
// make a single call deleting them all together than to delete one at a time.
// Delete might not modify the elements s[len(s)-(j-i):len(s)]. If those
@@ -175,39 +229,106 @@ func Delete[S ~[]E, E any](s S, i, j int) S {
// zeroing those elements so that objects they reference can be garbage
// collected.
func DeleteFunc[S ~[]E, E any](s S, del func(E) bool) S {
i := IndexFunc(s, del)
if i == -1 {
return s
}
// Don't start copying elements until we find one to delete.
for i, v := range s {
if del(v) {
j := i
for i++; i < len(s); i++ {
v = s[i]
if !del(v) {
s[j] = v
j++
}
}
return s[:j]
for j := i + 1; j < len(s); j++ {
if v := s[j]; !del(v) {
s[i] = v
i++
}
}
return s
return s[:i]
}
// Replace replaces the elements s[i:j] by the given v, and returns the
// modified slice. Replace panics if s[i:j] is not a valid slice of s.
func Replace[S ~[]E, E any](s S, i, j int, v ...E) S {
_ = s[i:j] // verify that i:j is a valid subslice
if i == j {
return Insert(s, i, v...)
}
if j == len(s) {
return append(s[:i], v...)
}
tot := len(s[:i]) + len(v) + len(s[j:])
if tot <= cap(s) {
s2 := s[:tot]
copy(s2[i+len(v):], s[j:])
if tot > cap(s) {
// Too big to fit, allocate and copy over.
s2 := append(s[:i], make(S, tot-i)...) // See Insert
copy(s2[i:], v)
copy(s2[i+len(v):], s[j:])
return s2
}
s2 := make(S, tot)
copy(s2, s[:i])
copy(s2[i:], v)
copy(s2[i+len(v):], s[j:])
return s2
r := s[:tot]
if i+len(v) <= j {
// Easy, as v fits in the deleted portion.
copy(r[i:], v)
if i+len(v) != j {
copy(r[i+len(v):], s[j:])
}
return r
}
// We are expanding (v is bigger than j-i).
// The situation is something like this:
// (example has i=4,j=8,len(s)=16,len(v)=6)
// s: aaaaxxxxbbbbbbbbyy
// ^ ^ ^ ^
// i j len(s) tot
// a: prefix of s
// x: deleted range
// b: more of s
// y: area to expand into
if !overlaps(r[i+len(v):], v) {
// Easy, as v is not clobbered by the first copy.
copy(r[i+len(v):], s[j:])
copy(r[i:], v)
return r
}
// This is a situation where we don't have a single place to which
// we can copy v. Parts of it need to go to two different places.
// We want to copy the prefix of v into y and the suffix into x, then
// rotate |y| spots to the right.
//
// v[2:] v[:2]
// | |
// s: aaaavvvvbbbbbbbbvv
// ^ ^ ^ ^
// i j len(s) tot
//
// If either of those two destinations don't alias v, then we're good.
y := len(v) - (j - i) // length of y portion
if !overlaps(r[i:j], v) {
copy(r[i:j], v[y:])
copy(r[len(s):], v[:y])
rotateRight(r[i:], y)
return r
}
if !overlaps(r[len(s):], v) {
copy(r[len(s):], v[:y])
copy(r[i:j], v[y:])
rotateRight(r[i:], y)
return r
}
// Now we know that v overlaps both x and y.
// That means that the entirety of b is *inside* v.
// So we don't need to preserve b at all; instead we
// can copy v first, then copy the b part of v out of
// v to the right destination.
k := startIdx(v, s[j:])
copy(r[i:], v)
copy(r[i+len(v):], r[i+k:])
return r
}
// Clone returns a copy of the slice.
@@ -222,7 +343,8 @@ func Clone[S ~[]E, E any](s S) S {
// Compact replaces consecutive runs of equal elements with a single copy.
// This is like the uniq command found on Unix.
// Compact modifies the contents of the slice s; it does not create a new slice.
// Compact modifies the contents of the slice s and returns the modified slice,
// which may have a smaller length.
// When Compact discards m elements in total, it might not modify the elements
// s[len(s)-m:len(s)]. If those elements contain pointers you might consider
// zeroing those elements so that objects they reference can be garbage collected.
@@ -242,7 +364,8 @@ func Compact[S ~[]E, E comparable](s S) S {
return s[:i]
}
// CompactFunc is like Compact but uses a comparison function.
// CompactFunc is like [Compact] but uses an equality function to compare elements.
// For runs of elements that compare equal, CompactFunc keeps the first one.
func CompactFunc[S ~[]E, E any](s S, eq func(E, E) bool) S {
if len(s) < 2 {
return s
@@ -280,3 +403,97 @@ func Grow[S ~[]E, E any](s S, n int) S {
func Clip[S ~[]E, E any](s S) S {
return s[:len(s):len(s)]
}
// Rotation algorithm explanation:
//
// rotate left by 2
// start with
// 0123456789
// split up like this
// 01 234567 89
// swap first 2 and last 2
// 89 234567 01
// join first parts
// 89234567 01
// recursively rotate first left part by 2
// 23456789 01
// join at the end
// 2345678901
//
// rotate left by 8
// start with
// 0123456789
// split up like this
// 01 234567 89
// swap first 2 and last 2
// 89 234567 01
// join last parts
// 89 23456701
// recursively rotate second part left by 6
// 89 01234567
// join at the end
// 8901234567
// TODO: There are other rotate algorithms.
// This algorithm has the desirable property that it moves each element exactly twice.
// The triple-reverse algorithm is simpler and more cache friendly, but takes more writes.
// The follow-cycles algorithm can be 1-write but it is not very cache friendly.
// rotateLeft rotates b left by n spaces.
// s_final[i] = s_orig[i+r], wrapping around.
func rotateLeft[E any](s []E, r int) {
for r != 0 && r != len(s) {
if r*2 <= len(s) {
swap(s[:r], s[len(s)-r:])
s = s[:len(s)-r]
} else {
swap(s[:len(s)-r], s[r:])
s, r = s[len(s)-r:], r*2-len(s)
}
}
}
func rotateRight[E any](s []E, r int) {
rotateLeft(s, len(s)-r)
}
// swap swaps the contents of x and y. x and y must be equal length and disjoint.
func swap[E any](x, y []E) {
for i := 0; i < len(x); i++ {
x[i], y[i] = y[i], x[i]
}
}
// overlaps reports whether the memory ranges a[0:len(a)] and b[0:len(b)] overlap.
func overlaps[E any](a, b []E) bool {
if len(a) == 0 || len(b) == 0 {
return false
}
elemSize := unsafe.Sizeof(a[0])
if elemSize == 0 {
return false
}
// TODO: use a runtime/unsafe facility once one becomes available. See issue 12445.
// Also see crypto/internal/alias/alias.go:AnyOverlap
return uintptr(unsafe.Pointer(&a[0])) <= uintptr(unsafe.Pointer(&b[len(b)-1]))+(elemSize-1) &&
uintptr(unsafe.Pointer(&b[0])) <= uintptr(unsafe.Pointer(&a[len(a)-1]))+(elemSize-1)
}
// startIdx returns the index in haystack where the needle starts.
// prerequisite: the needle must be aliased entirely inside the haystack.
func startIdx[E any](haystack, needle []E) int {
p := &needle[0]
for i := range haystack {
if p == &haystack[i] {
return i
}
}
// TODO: what if the overlap is by a non-integral number of Es?
panic("needle not found")
}
// Reverse reverses the elements of the slice in place.
func Reverse[S ~[]E, E any](s S) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}

View File

@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:generate go run $GOROOT/src/sort/gen_sort_variants.go -exp
package slices
import (
@@ -11,57 +13,116 @@ import (
)
// Sort sorts a slice of any ordered type in ascending order.
// Sort may fail to sort correctly when sorting slices of floating-point
// numbers containing Not-a-number (NaN) values.
// Use slices.SortFunc(x, func(a, b float64) bool {return a < b || (math.IsNaN(a) && !math.IsNaN(b))})
// instead if the input may contain NaNs.
func Sort[E constraints.Ordered](x []E) {
// When sorting floating-point numbers, NaNs are ordered before other values.
func Sort[S ~[]E, E constraints.Ordered](x S) {
n := len(x)
pdqsortOrdered(x, 0, n, bits.Len(uint(n)))
}
// SortFunc sorts the slice x in ascending order as determined by the less function.
// This sort is not guaranteed to be stable.
// SortFunc sorts the slice x in ascending order as determined by the cmp
// function. This sort is not guaranteed to be stable.
// cmp(a, b) should return a negative number when a < b, a positive number when
// a > b and zero when a == b.
//
// SortFunc requires that less is a strict weak ordering.
// SortFunc requires that cmp is a strict weak ordering.
// See https://en.wikipedia.org/wiki/Weak_ordering#Strict_weak_orderings.
func SortFunc[E any](x []E, less func(a, b E) bool) {
func SortFunc[S ~[]E, E any](x S, cmp func(a, b E) int) {
n := len(x)
pdqsortLessFunc(x, 0, n, bits.Len(uint(n)), less)
pdqsortCmpFunc(x, 0, n, bits.Len(uint(n)), cmp)
}
// SortStableFunc sorts the slice x while keeping the original order of equal
// elements, using less to compare elements.
func SortStableFunc[E any](x []E, less func(a, b E) bool) {
stableLessFunc(x, len(x), less)
// elements, using cmp to compare elements in the same way as [SortFunc].
func SortStableFunc[S ~[]E, E any](x S, cmp func(a, b E) int) {
stableCmpFunc(x, len(x), cmp)
}
// IsSorted reports whether x is sorted in ascending order.
func IsSorted[E constraints.Ordered](x []E) bool {
func IsSorted[S ~[]E, E constraints.Ordered](x S) bool {
for i := len(x) - 1; i > 0; i-- {
if x[i] < x[i-1] {
if cmpLess(x[i], x[i-1]) {
return false
}
}
return true
}
// IsSortedFunc reports whether x is sorted in ascending order, with less as the
// comparison function.
func IsSortedFunc[E any](x []E, less func(a, b E) bool) bool {
// IsSortedFunc reports whether x is sorted in ascending order, with cmp as the
// comparison function as defined by [SortFunc].
func IsSortedFunc[S ~[]E, E any](x S, cmp func(a, b E) int) bool {
for i := len(x) - 1; i > 0; i-- {
if less(x[i], x[i-1]) {
if cmp(x[i], x[i-1]) < 0 {
return false
}
}
return true
}
// Min returns the minimal value in x. It panics if x is empty.
// For floating-point numbers, Min propagates NaNs (any NaN value in x
// forces the output to be NaN).
func Min[S ~[]E, E constraints.Ordered](x S) E {
if len(x) < 1 {
panic("slices.Min: empty list")
}
m := x[0]
for i := 1; i < len(x); i++ {
m = min(m, x[i])
}
return m
}
// MinFunc returns the minimal value in x, using cmp to compare elements.
// It panics if x is empty. If there is more than one minimal element
// according to the cmp function, MinFunc returns the first one.
func MinFunc[S ~[]E, E any](x S, cmp func(a, b E) int) E {
if len(x) < 1 {
panic("slices.MinFunc: empty list")
}
m := x[0]
for i := 1; i < len(x); i++ {
if cmp(x[i], m) < 0 {
m = x[i]
}
}
return m
}
// Max returns the maximal value in x. It panics if x is empty.
// For floating-point E, Max propagates NaNs (any NaN value in x
// forces the output to be NaN).
func Max[S ~[]E, E constraints.Ordered](x S) E {
if len(x) < 1 {
panic("slices.Max: empty list")
}
m := x[0]
for i := 1; i < len(x); i++ {
m = max(m, x[i])
}
return m
}
// MaxFunc returns the maximal value in x, using cmp to compare elements.
// It panics if x is empty. If there is more than one maximal element
// according to the cmp function, MaxFunc returns the first one.
func MaxFunc[S ~[]E, E any](x S, cmp func(a, b E) int) E {
if len(x) < 1 {
panic("slices.MaxFunc: empty list")
}
m := x[0]
for i := 1; i < len(x); i++ {
if cmp(x[i], m) > 0 {
m = x[i]
}
}
return m
}
// BinarySearch searches for target in a sorted slice and returns the position
// where target is found, or the position where target would appear in the
// sort order; it also returns a bool saying whether the target is really found
// in the slice. The slice must be sorted in increasing order.
func BinarySearch[E constraints.Ordered](x []E, target E) (int, bool) {
func BinarySearch[S ~[]E, E constraints.Ordered](x S, target E) (int, bool) {
// Inlining is faster than calling BinarySearchFunc with a lambda.
n := len(x)
// Define x[-1] < target and x[n] >= target.
@@ -70,24 +131,24 @@ func BinarySearch[E constraints.Ordered](x []E, target E) (int, bool) {
for i < j {
h := int(uint(i+j) >> 1) // avoid overflow when computing h
// i ≤ h < j
if x[h] < target {
if cmpLess(x[h], target) {
i = h + 1 // preserves x[i-1] < target
} else {
j = h // preserves x[j] >= target
}
}
// i == j, x[i-1] < target, and x[j] (= x[i]) >= target => answer is i.
return i, i < n && x[i] == target
return i, i < n && (x[i] == target || (isNaN(x[i]) && isNaN(target)))
}
// BinarySearchFunc works like BinarySearch, but uses a custom comparison
// BinarySearchFunc works like [BinarySearch], but uses a custom comparison
// function. The slice must be sorted in increasing order, where "increasing"
// is defined by cmp. cmp should return 0 if the slice element matches
// the target, a negative number if the slice element precedes the target,
// or a positive number if the slice element follows the target.
// cmp must implement the same ordering as the slice, such that if
// cmp(a, t) < 0 and cmp(b, t) >= 0, then a must precede b in the slice.
func BinarySearchFunc[E, T any](x []E, target T, cmp func(E, T) int) (int, bool) {
func BinarySearchFunc[S ~[]E, E, T any](x S, target T, cmp func(E, T) int) (int, bool) {
n := len(x)
// Define cmp(x[-1], target) < 0 and cmp(x[n], target) >= 0 .
// Invariant: cmp(x[i - 1], target) < 0, cmp(x[j], target) >= 0.
@@ -126,3 +187,9 @@ func (r *xorshift) Next() uint64 {
func nextPowerOfTwo(length int) uint {
return 1 << bits.Len(uint(length))
}
// isNaN reports whether x is a NaN without requiring the math package.
// This will always return false if T is not floating-point.
func isNaN[T constraints.Ordered](x T) bool {
return x != x
}

View File

@@ -6,28 +6,28 @@
package slices
// insertionSortLessFunc sorts data[a:b] using insertion sort.
func insertionSortLessFunc[E any](data []E, a, b int, less func(a, b E) bool) {
// insertionSortCmpFunc sorts data[a:b] using insertion sort.
func insertionSortCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) {
for i := a + 1; i < b; i++ {
for j := i; j > a && less(data[j], data[j-1]); j-- {
for j := i; j > a && (cmp(data[j], data[j-1]) < 0); j-- {
data[j], data[j-1] = data[j-1], data[j]
}
}
}
// siftDownLessFunc implements the heap property on data[lo:hi].
// siftDownCmpFunc implements the heap property on data[lo:hi].
// first is an offset into the array where the root of the heap lies.
func siftDownLessFunc[E any](data []E, lo, hi, first int, less func(a, b E) bool) {
func siftDownCmpFunc[E any](data []E, lo, hi, first int, cmp func(a, b E) int) {
root := lo
for {
child := 2*root + 1
if child >= hi {
break
}
if child+1 < hi && less(data[first+child], data[first+child+1]) {
if child+1 < hi && (cmp(data[first+child], data[first+child+1]) < 0) {
child++
}
if !less(data[first+root], data[first+child]) {
if !(cmp(data[first+root], data[first+child]) < 0) {
return
}
data[first+root], data[first+child] = data[first+child], data[first+root]
@@ -35,30 +35,30 @@ func siftDownLessFunc[E any](data []E, lo, hi, first int, less func(a, b E) bool
}
}
func heapSortLessFunc[E any](data []E, a, b int, less func(a, b E) bool) {
func heapSortCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) {
first := a
lo := 0
hi := b - a
// Build heap with greatest element at top.
for i := (hi - 1) / 2; i >= 0; i-- {
siftDownLessFunc(data, i, hi, first, less)
siftDownCmpFunc(data, i, hi, first, cmp)
}
// Pop elements, largest first, into end of data.
for i := hi - 1; i >= 0; i-- {
data[first], data[first+i] = data[first+i], data[first]
siftDownLessFunc(data, lo, i, first, less)
siftDownCmpFunc(data, lo, i, first, cmp)
}
}
// pdqsortLessFunc sorts data[a:b].
// pdqsortCmpFunc sorts data[a:b].
// The algorithm based on pattern-defeating quicksort(pdqsort), but without the optimizations from BlockQuicksort.
// pdqsort paper: https://arxiv.org/pdf/2106.05123.pdf
// C++ implementation: https://github.com/orlp/pdqsort
// Rust implementation: https://docs.rs/pdqsort/latest/pdqsort/
// limit is the number of allowed bad (very unbalanced) pivots before falling back to heapsort.
func pdqsortLessFunc[E any](data []E, a, b, limit int, less func(a, b E) bool) {
func pdqsortCmpFunc[E any](data []E, a, b, limit int, cmp func(a, b E) int) {
const maxInsertion = 12
var (
@@ -70,25 +70,25 @@ func pdqsortLessFunc[E any](data []E, a, b, limit int, less func(a, b E) bool) {
length := b - a
if length <= maxInsertion {
insertionSortLessFunc(data, a, b, less)
insertionSortCmpFunc(data, a, b, cmp)
return
}
// Fall back to heapsort if too many bad choices were made.
if limit == 0 {
heapSortLessFunc(data, a, b, less)
heapSortCmpFunc(data, a, b, cmp)
return
}
// If the last partitioning was imbalanced, we need to breaking patterns.
if !wasBalanced {
breakPatternsLessFunc(data, a, b, less)
breakPatternsCmpFunc(data, a, b, cmp)
limit--
}
pivot, hint := choosePivotLessFunc(data, a, b, less)
pivot, hint := choosePivotCmpFunc(data, a, b, cmp)
if hint == decreasingHint {
reverseRangeLessFunc(data, a, b, less)
reverseRangeCmpFunc(data, a, b, cmp)
// The chosen pivot was pivot-a elements after the start of the array.
// After reversing it is pivot-a elements before the end of the array.
// The idea came from Rust's implementation.
@@ -98,48 +98,48 @@ func pdqsortLessFunc[E any](data []E, a, b, limit int, less func(a, b E) bool) {
// The slice is likely already sorted.
if wasBalanced && wasPartitioned && hint == increasingHint {
if partialInsertionSortLessFunc(data, a, b, less) {
if partialInsertionSortCmpFunc(data, a, b, cmp) {
return
}
}
// Probably the slice contains many duplicate elements, partition the slice into
// elements equal to and elements greater than the pivot.
if a > 0 && !less(data[a-1], data[pivot]) {
mid := partitionEqualLessFunc(data, a, b, pivot, less)
if a > 0 && !(cmp(data[a-1], data[pivot]) < 0) {
mid := partitionEqualCmpFunc(data, a, b, pivot, cmp)
a = mid
continue
}
mid, alreadyPartitioned := partitionLessFunc(data, a, b, pivot, less)
mid, alreadyPartitioned := partitionCmpFunc(data, a, b, pivot, cmp)
wasPartitioned = alreadyPartitioned
leftLen, rightLen := mid-a, b-mid
balanceThreshold := length / 8
if leftLen < rightLen {
wasBalanced = leftLen >= balanceThreshold
pdqsortLessFunc(data, a, mid, limit, less)
pdqsortCmpFunc(data, a, mid, limit, cmp)
a = mid + 1
} else {
wasBalanced = rightLen >= balanceThreshold
pdqsortLessFunc(data, mid+1, b, limit, less)
pdqsortCmpFunc(data, mid+1, b, limit, cmp)
b = mid
}
}
}
// partitionLessFunc does one quicksort partition.
// partitionCmpFunc does one quicksort partition.
// Let p = data[pivot]
// Moves elements in data[a:b] around, so that data[i]<p and data[j]>=p for i<newpivot and j>newpivot.
// On return, data[newpivot] = p
func partitionLessFunc[E any](data []E, a, b, pivot int, less func(a, b E) bool) (newpivot int, alreadyPartitioned bool) {
func partitionCmpFunc[E any](data []E, a, b, pivot int, cmp func(a, b E) int) (newpivot int, alreadyPartitioned bool) {
data[a], data[pivot] = data[pivot], data[a]
i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned
for i <= j && less(data[i], data[a]) {
for i <= j && (cmp(data[i], data[a]) < 0) {
i++
}
for i <= j && !less(data[j], data[a]) {
for i <= j && !(cmp(data[j], data[a]) < 0) {
j--
}
if i > j {
@@ -151,10 +151,10 @@ func partitionLessFunc[E any](data []E, a, b, pivot int, less func(a, b E) bool)
j--
for {
for i <= j && less(data[i], data[a]) {
for i <= j && (cmp(data[i], data[a]) < 0) {
i++
}
for i <= j && !less(data[j], data[a]) {
for i <= j && !(cmp(data[j], data[a]) < 0) {
j--
}
if i > j {
@@ -168,17 +168,17 @@ func partitionLessFunc[E any](data []E, a, b, pivot int, less func(a, b E) bool)
return j, false
}
// partitionEqualLessFunc partitions data[a:b] into elements equal to data[pivot] followed by elements greater than data[pivot].
// partitionEqualCmpFunc partitions data[a:b] into elements equal to data[pivot] followed by elements greater than data[pivot].
// It assumed that data[a:b] does not contain elements smaller than the data[pivot].
func partitionEqualLessFunc[E any](data []E, a, b, pivot int, less func(a, b E) bool) (newpivot int) {
func partitionEqualCmpFunc[E any](data []E, a, b, pivot int, cmp func(a, b E) int) (newpivot int) {
data[a], data[pivot] = data[pivot], data[a]
i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned
for {
for i <= j && !less(data[a], data[i]) {
for i <= j && !(cmp(data[a], data[i]) < 0) {
i++
}
for i <= j && less(data[a], data[j]) {
for i <= j && (cmp(data[a], data[j]) < 0) {
j--
}
if i > j {
@@ -191,15 +191,15 @@ func partitionEqualLessFunc[E any](data []E, a, b, pivot int, less func(a, b E)
return i
}
// partialInsertionSortLessFunc partially sorts a slice, returns true if the slice is sorted at the end.
func partialInsertionSortLessFunc[E any](data []E, a, b int, less func(a, b E) bool) bool {
// partialInsertionSortCmpFunc partially sorts a slice, returns true if the slice is sorted at the end.
func partialInsertionSortCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) bool {
const (
maxSteps = 5 // maximum number of adjacent out-of-order pairs that will get shifted
shortestShifting = 50 // don't shift any elements on short arrays
)
i := a + 1
for j := 0; j < maxSteps; j++ {
for i < b && !less(data[i], data[i-1]) {
for i < b && !(cmp(data[i], data[i-1]) < 0) {
i++
}
@@ -216,7 +216,7 @@ func partialInsertionSortLessFunc[E any](data []E, a, b int, less func(a, b E) b
// Shift the smaller one to the left.
if i-a >= 2 {
for j := i - 1; j >= 1; j-- {
if !less(data[j], data[j-1]) {
if !(cmp(data[j], data[j-1]) < 0) {
break
}
data[j], data[j-1] = data[j-1], data[j]
@@ -225,7 +225,7 @@ func partialInsertionSortLessFunc[E any](data []E, a, b int, less func(a, b E) b
// Shift the greater one to the right.
if b-i >= 2 {
for j := i + 1; j < b; j++ {
if !less(data[j], data[j-1]) {
if !(cmp(data[j], data[j-1]) < 0) {
break
}
data[j], data[j-1] = data[j-1], data[j]
@@ -235,9 +235,9 @@ func partialInsertionSortLessFunc[E any](data []E, a, b int, less func(a, b E) b
return false
}
// breakPatternsLessFunc scatters some elements around in an attempt to break some patterns
// breakPatternsCmpFunc scatters some elements around in an attempt to break some patterns
// that might cause imbalanced partitions in quicksort.
func breakPatternsLessFunc[E any](data []E, a, b int, less func(a, b E) bool) {
func breakPatternsCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) {
length := b - a
if length >= 8 {
random := xorshift(length)
@@ -253,12 +253,12 @@ func breakPatternsLessFunc[E any](data []E, a, b int, less func(a, b E) bool) {
}
}
// choosePivotLessFunc chooses a pivot in data[a:b].
// choosePivotCmpFunc chooses a pivot in data[a:b].
//
// [0,8): chooses a static pivot.
// [8,shortestNinther): uses the simple median-of-three method.
// [shortestNinther,∞): uses the Tukey ninther method.
func choosePivotLessFunc[E any](data []E, a, b int, less func(a, b E) bool) (pivot int, hint sortedHint) {
func choosePivotCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) (pivot int, hint sortedHint) {
const (
shortestNinther = 50
maxSwaps = 4 * 3
@@ -276,12 +276,12 @@ func choosePivotLessFunc[E any](data []E, a, b int, less func(a, b E) bool) (piv
if l >= 8 {
if l >= shortestNinther {
// Tukey ninther method, the idea came from Rust's implementation.
i = medianAdjacentLessFunc(data, i, &swaps, less)
j = medianAdjacentLessFunc(data, j, &swaps, less)
k = medianAdjacentLessFunc(data, k, &swaps, less)
i = medianAdjacentCmpFunc(data, i, &swaps, cmp)
j = medianAdjacentCmpFunc(data, j, &swaps, cmp)
k = medianAdjacentCmpFunc(data, k, &swaps, cmp)
}
// Find the median among i, j, k and stores it into j.
j = medianLessFunc(data, i, j, k, &swaps, less)
j = medianCmpFunc(data, i, j, k, &swaps, cmp)
}
switch swaps {
@@ -294,29 +294,29 @@ func choosePivotLessFunc[E any](data []E, a, b int, less func(a, b E) bool) (piv
}
}
// order2LessFunc returns x,y where data[x] <= data[y], where x,y=a,b or x,y=b,a.
func order2LessFunc[E any](data []E, a, b int, swaps *int, less func(a, b E) bool) (int, int) {
if less(data[b], data[a]) {
// order2CmpFunc returns x,y where data[x] <= data[y], where x,y=a,b or x,y=b,a.
func order2CmpFunc[E any](data []E, a, b int, swaps *int, cmp func(a, b E) int) (int, int) {
if cmp(data[b], data[a]) < 0 {
*swaps++
return b, a
}
return a, b
}
// medianLessFunc returns x where data[x] is the median of data[a],data[b],data[c], where x is a, b, or c.
func medianLessFunc[E any](data []E, a, b, c int, swaps *int, less func(a, b E) bool) int {
a, b = order2LessFunc(data, a, b, swaps, less)
b, c = order2LessFunc(data, b, c, swaps, less)
a, b = order2LessFunc(data, a, b, swaps, less)
// medianCmpFunc returns x where data[x] is the median of data[a],data[b],data[c], where x is a, b, or c.
func medianCmpFunc[E any](data []E, a, b, c int, swaps *int, cmp func(a, b E) int) int {
a, b = order2CmpFunc(data, a, b, swaps, cmp)
b, c = order2CmpFunc(data, b, c, swaps, cmp)
a, b = order2CmpFunc(data, a, b, swaps, cmp)
return b
}
// medianAdjacentLessFunc finds the median of data[a - 1], data[a], data[a + 1] and stores the index into a.
func medianAdjacentLessFunc[E any](data []E, a int, swaps *int, less func(a, b E) bool) int {
return medianLessFunc(data, a-1, a, a+1, swaps, less)
// medianAdjacentCmpFunc finds the median of data[a - 1], data[a], data[a + 1] and stores the index into a.
func medianAdjacentCmpFunc[E any](data []E, a int, swaps *int, cmp func(a, b E) int) int {
return medianCmpFunc(data, a-1, a, a+1, swaps, cmp)
}
func reverseRangeLessFunc[E any](data []E, a, b int, less func(a, b E) bool) {
func reverseRangeCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) {
i := a
j := b - 1
for i < j {
@@ -326,37 +326,37 @@ func reverseRangeLessFunc[E any](data []E, a, b int, less func(a, b E) bool) {
}
}
func swapRangeLessFunc[E any](data []E, a, b, n int, less func(a, b E) bool) {
func swapRangeCmpFunc[E any](data []E, a, b, n int, cmp func(a, b E) int) {
for i := 0; i < n; i++ {
data[a+i], data[b+i] = data[b+i], data[a+i]
}
}
func stableLessFunc[E any](data []E, n int, less func(a, b E) bool) {
func stableCmpFunc[E any](data []E, n int, cmp func(a, b E) int) {
blockSize := 20 // must be > 0
a, b := 0, blockSize
for b <= n {
insertionSortLessFunc(data, a, b, less)
insertionSortCmpFunc(data, a, b, cmp)
a = b
b += blockSize
}
insertionSortLessFunc(data, a, n, less)
insertionSortCmpFunc(data, a, n, cmp)
for blockSize < n {
a, b = 0, 2*blockSize
for b <= n {
symMergeLessFunc(data, a, a+blockSize, b, less)
symMergeCmpFunc(data, a, a+blockSize, b, cmp)
a = b
b += 2 * blockSize
}
if m := a + blockSize; m < n {
symMergeLessFunc(data, a, m, n, less)
symMergeCmpFunc(data, a, m, n, cmp)
}
blockSize *= 2
}
}
// symMergeLessFunc merges the two sorted subsequences data[a:m] and data[m:b] using
// symMergeCmpFunc merges the two sorted subsequences data[a:m] and data[m:b] using
// the SymMerge algorithm from Pok-Son Kim and Arne Kutzner, "Stable Minimum
// Storage Merging by Symmetric Comparisons", in Susanne Albers and Tomasz
// Radzik, editors, Algorithms - ESA 2004, volume 3221 of Lecture Notes in
@@ -375,7 +375,7 @@ func stableLessFunc[E any](data []E, n int, less func(a, b E) bool) {
// symMerge assumes non-degenerate arguments: a < m && m < b.
// Having the caller check this condition eliminates many leaf recursion calls,
// which improves performance.
func symMergeLessFunc[E any](data []E, a, m, b int, less func(a, b E) bool) {
func symMergeCmpFunc[E any](data []E, a, m, b int, cmp func(a, b E) int) {
// Avoid unnecessary recursions of symMerge
// by direct insertion of data[a] into data[m:b]
// if data[a:m] only contains one element.
@@ -387,7 +387,7 @@ func symMergeLessFunc[E any](data []E, a, m, b int, less func(a, b E) bool) {
j := b
for i < j {
h := int(uint(i+j) >> 1)
if less(data[h], data[a]) {
if cmp(data[h], data[a]) < 0 {
i = h + 1
} else {
j = h
@@ -411,7 +411,7 @@ func symMergeLessFunc[E any](data []E, a, m, b int, less func(a, b E) bool) {
j := m
for i < j {
h := int(uint(i+j) >> 1)
if !less(data[m], data[h]) {
if !(cmp(data[m], data[h]) < 0) {
i = h + 1
} else {
j = h
@@ -438,7 +438,7 @@ func symMergeLessFunc[E any](data []E, a, m, b int, less func(a, b E) bool) {
for start < r {
c := int(uint(start+r) >> 1)
if !less(data[p-c], data[c]) {
if !(cmp(data[p-c], data[c]) < 0) {
start = c + 1
} else {
r = c
@@ -447,33 +447,33 @@ func symMergeLessFunc[E any](data []E, a, m, b int, less func(a, b E) bool) {
end := n - start
if start < m && m < end {
rotateLessFunc(data, start, m, end, less)
rotateCmpFunc(data, start, m, end, cmp)
}
if a < start && start < mid {
symMergeLessFunc(data, a, start, mid, less)
symMergeCmpFunc(data, a, start, mid, cmp)
}
if mid < end && end < b {
symMergeLessFunc(data, mid, end, b, less)
symMergeCmpFunc(data, mid, end, b, cmp)
}
}
// rotateLessFunc rotates two consecutive blocks u = data[a:m] and v = data[m:b] in data:
// rotateCmpFunc rotates two consecutive blocks u = data[a:m] and v = data[m:b] in data:
// Data of the form 'x u v y' is changed to 'x v u y'.
// rotate performs at most b-a many calls to data.Swap,
// and it assumes non-degenerate arguments: a < m && m < b.
func rotateLessFunc[E any](data []E, a, m, b int, less func(a, b E) bool) {
func rotateCmpFunc[E any](data []E, a, m, b int, cmp func(a, b E) int) {
i := m - a
j := b - m
for i != j {
if i > j {
swapRangeLessFunc(data, m-i, m, j, less)
swapRangeCmpFunc(data, m-i, m, j, cmp)
i -= j
} else {
swapRangeLessFunc(data, m-i, m+j-i, i, less)
swapRangeCmpFunc(data, m-i, m+j-i, i, cmp)
j -= i
}
}
// i == j
swapRangeLessFunc(data, m-i, m, i, less)
swapRangeCmpFunc(data, m-i, m, i, cmp)
}

View File

@@ -11,7 +11,7 @@ import "golang.org/x/exp/constraints"
// insertionSortOrdered sorts data[a:b] using insertion sort.
func insertionSortOrdered[E constraints.Ordered](data []E, a, b int) {
for i := a + 1; i < b; i++ {
for j := i; j > a && (data[j] < data[j-1]); j-- {
for j := i; j > a && cmpLess(data[j], data[j-1]); j-- {
data[j], data[j-1] = data[j-1], data[j]
}
}
@@ -26,10 +26,10 @@ func siftDownOrdered[E constraints.Ordered](data []E, lo, hi, first int) {
if child >= hi {
break
}
if child+1 < hi && (data[first+child] < data[first+child+1]) {
if child+1 < hi && cmpLess(data[first+child], data[first+child+1]) {
child++
}
if !(data[first+root] < data[first+child]) {
if !cmpLess(data[first+root], data[first+child]) {
return
}
data[first+root], data[first+child] = data[first+child], data[first+root]
@@ -107,7 +107,7 @@ func pdqsortOrdered[E constraints.Ordered](data []E, a, b, limit int) {
// Probably the slice contains many duplicate elements, partition the slice into
// elements equal to and elements greater than the pivot.
if a > 0 && !(data[a-1] < data[pivot]) {
if a > 0 && !cmpLess(data[a-1], data[pivot]) {
mid := partitionEqualOrdered(data, a, b, pivot)
a = mid
continue
@@ -138,10 +138,10 @@ func partitionOrdered[E constraints.Ordered](data []E, a, b, pivot int) (newpivo
data[a], data[pivot] = data[pivot], data[a]
i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned
for i <= j && (data[i] < data[a]) {
for i <= j && cmpLess(data[i], data[a]) {
i++
}
for i <= j && !(data[j] < data[a]) {
for i <= j && !cmpLess(data[j], data[a]) {
j--
}
if i > j {
@@ -153,10 +153,10 @@ func partitionOrdered[E constraints.Ordered](data []E, a, b, pivot int) (newpivo
j--
for {
for i <= j && (data[i] < data[a]) {
for i <= j && cmpLess(data[i], data[a]) {
i++
}
for i <= j && !(data[j] < data[a]) {
for i <= j && !cmpLess(data[j], data[a]) {
j--
}
if i > j {
@@ -177,10 +177,10 @@ func partitionEqualOrdered[E constraints.Ordered](data []E, a, b, pivot int) (ne
i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned
for {
for i <= j && !(data[a] < data[i]) {
for i <= j && !cmpLess(data[a], data[i]) {
i++
}
for i <= j && (data[a] < data[j]) {
for i <= j && cmpLess(data[a], data[j]) {
j--
}
if i > j {
@@ -201,7 +201,7 @@ func partialInsertionSortOrdered[E constraints.Ordered](data []E, a, b int) bool
)
i := a + 1
for j := 0; j < maxSteps; j++ {
for i < b && !(data[i] < data[i-1]) {
for i < b && !cmpLess(data[i], data[i-1]) {
i++
}
@@ -218,7 +218,7 @@ func partialInsertionSortOrdered[E constraints.Ordered](data []E, a, b int) bool
// Shift the smaller one to the left.
if i-a >= 2 {
for j := i - 1; j >= 1; j-- {
if !(data[j] < data[j-1]) {
if !cmpLess(data[j], data[j-1]) {
break
}
data[j], data[j-1] = data[j-1], data[j]
@@ -227,7 +227,7 @@ func partialInsertionSortOrdered[E constraints.Ordered](data []E, a, b int) bool
// Shift the greater one to the right.
if b-i >= 2 {
for j := i + 1; j < b; j++ {
if !(data[j] < data[j-1]) {
if !cmpLess(data[j], data[j-1]) {
break
}
data[j], data[j-1] = data[j-1], data[j]
@@ -298,7 +298,7 @@ func choosePivotOrdered[E constraints.Ordered](data []E, a, b int) (pivot int, h
// order2Ordered returns x,y where data[x] <= data[y], where x,y=a,b or x,y=b,a.
func order2Ordered[E constraints.Ordered](data []E, a, b int, swaps *int) (int, int) {
if data[b] < data[a] {
if cmpLess(data[b], data[a]) {
*swaps++
return b, a
}
@@ -389,7 +389,7 @@ func symMergeOrdered[E constraints.Ordered](data []E, a, m, b int) {
j := b
for i < j {
h := int(uint(i+j) >> 1)
if data[h] < data[a] {
if cmpLess(data[h], data[a]) {
i = h + 1
} else {
j = h
@@ -413,7 +413,7 @@ func symMergeOrdered[E constraints.Ordered](data []E, a, m, b int) {
j := m
for i < j {
h := int(uint(i+j) >> 1)
if !(data[m] < data[h]) {
if !cmpLess(data[m], data[h]) {
i = h + 1
} else {
j = h
@@ -440,7 +440,7 @@ func symMergeOrdered[E constraints.Ordered](data []E, a, m, b int) {
for start < r {
c := int(uint(start+r) >> 1)
if !(data[p-c] < data[c]) {
if !cmpLess(data[p-c], data[c]) {
start = c + 1
} else {
r = c

4
vendor/modules.txt vendored
View File

@@ -1,7 +1,7 @@
# github.com/davecgh/go-spew v1.1.1
## explicit
github.com/davecgh/go-spew/spew
# github.com/eleksir/go-xmpp v1.3.0
# github.com/eleksir/go-xmpp v1.4.0
## explicit; go 1.20
github.com/eleksir/go-xmpp
# github.com/hjson/hjson-go v3.3.0+incompatible
@@ -13,7 +13,7 @@ github.com/sirupsen/logrus
# golang.org/x/crypto v0.11.0
## explicit; go 1.17
golang.org/x/crypto/pbkdf2
# golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
# golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691
## explicit; go 1.20
golang.org/x/exp/constraints
golang.org/x/exp/slices