From 32d6e1cb045860f3194c097547eaf6482b83c0d9 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Tue, 28 Jan 2025 11:46:15 +0200 Subject: [PATCH 01/33] [kext] Fix dev build documentation --- windows_kext/README.md | 26 ++++++++++---------------- windows_kext/link-dev.ps1 | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 16 deletions(-) create mode 100644 windows_kext/link-dev.ps1 diff --git a/windows_kext/README.md b/windows_kext/README.md index ce80d0b1..0e4daa80 100644 --- a/windows_kext/README.md +++ b/windows_kext/README.md @@ -1,7 +1,7 @@ # Portmaster Windows kext Implementation of Safing's Portmaster Windows kernel extension in Rust. -### Documentation +### Documentation - [Driver](driver/README.md) -> entry point. - [WDK](wdk/README.md) -> Windows Driver Kit interface. @@ -9,8 +9,7 @@ Implementation of Safing's Portmaster Windows kernel extension in Rust. - [Release](release/README.md) -> Guide how to do a release build. - [Windows Filtering Platform - MS](https://learn.microsoft.com/en-us/windows-hardware/drivers/network/roadmap-for-developing-wfp-callout-drivers) -> The driver is build on top of WFP. - -### Building +### Building (For testing and development) The Windows Portmaster Kernel Extension is currently only developed and tested for the amd64 (64-bit) architecture. @@ -53,23 +52,18 @@ __Build driver:__ ```sh cd driver - cargo build + cargo build --release ``` > Build also works on linux __Link and sign:__ -On a windows machine copy `driver.lib` form the project target directory (`driver/target/x86_64-pc-windows-msvc/debug/driver.lib`) in the same folder as `link.bat`. -Run `link.bat`. +On a windows machine copy `driver.lib` from the project target directory (`driver/target/x86_64-pc-windows-msvc/release/driver.lib`) in the same folder as `link-dev.ps1`. +Run `link-dev.ps1`. -`driver.sys` should appear in the folder. Load and use the driver. +`driver.sys` should appear in the folder. -### Test -- Install go - - https://go.dev/dl/ - -```sh - cd kext_tester - go run . +Sign the driver with the test certificate: ``` - -> make sure the hardcoded path in main.go is pointing to the correct `.sys` file + SignTool sign /v /s TestCertStoreName /n TestCertName driver.sys +``` +Load and use the driver. diff --git a/windows_kext/link-dev.ps1 b/windows_kext/link-dev.ps1 new file mode 100644 index 00000000..39ba4e3d --- /dev/null +++ b/windows_kext/link-dev.ps1 @@ -0,0 +1,21 @@ +# Example script for creating debug builds. Libraries may change depending on the version of the WDK that is installed. + +$SDK_Version = "10.0.26100.0" + +link.exe /OUT:driver.sys ` +/MANIFEST:NO /PROFILE /Driver ` +"C:\Program Files (x86)\Windows Kits\10\lib\$SDK_Version\km\x64\wdmsec.lib" ` +"C:\Program Files (x86)\Windows Kits\10\lib\$SDK_Version\km\x64\ndis.lib" ` +"C:\Program Files (x86)\Windows Kits\10\lib\$SDK_Version\km\x64\fwpkclnt.lib" ` +"C:\Program Files (x86)\Windows Kits\10\lib\$SDK_Version\km\x64\BufferOverflowK.lib" ` +"C:\Program Files (x86)\Windows Kits\10\lib\$SDK_Version\km\x64\ntoskrnl.lib" ` +"C:\Program Files (x86)\Windows Kits\10\lib\$SDK_Version\km\x64\hal.lib" ` +"C:\Program Files (x86)\Windows Kits\10\lib\$SDK_Version\km\x64\wmilib.lib" ` +"C:\Program Files (x86)\Windows Kits\10\lib\wdf\kmdf\x64\1.15\WdfLdr.lib" ` +"C:\Program Files (x86)\Windows Kits\10\lib\wdf\kmdf\x64\1.15\WdfDriverEntry.lib" ` + "driver.lib" ` +/RELEASE /VERSION:"10.0" /DEBUG /MACHINE:X64 /ENTRY:"FxDriverEntry" /OPT:REF /INCREMENTAL:NO /SUBSYSTEM:NATIVE",6.01" /OPT:ICF /ERRORREPORT:PROMPT /MERGE:"_TEXT=.text;_PAGE=PAGE" /NOLOGO /NODEFAULTLIB /SECTION:"INIT,d" + +if(!$?) { + Exit $LASTEXITCODE +} From 857df4086f0ce448ed9e581b96b6e8abc768ebb6 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Tue, 28 Jan 2025 14:26:44 +0200 Subject: [PATCH 02/33] [kext] Fix dev build documentation (releasing procedure) --- windows_kext/README.md | 4 +++ windows_kext/release/README.md | 34 ++++++++++++-------- windows_kext/release/src/main.rs | 10 ++++-- windows_kext/release/templates/build_cab.ps1 | 2 +- 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/windows_kext/README.md b/windows_kext/README.md index 0e4daa80..4d2628b9 100644 --- a/windows_kext/README.md +++ b/windows_kext/README.md @@ -9,6 +9,10 @@ Implementation of Safing's Portmaster Windows kernel extension in Rust. - [Release](release/README.md) -> Guide how to do a release build. - [Windows Filtering Platform - MS](https://learn.microsoft.com/en-us/windows-hardware/drivers/network/roadmap-for-developing-wfp-callout-drivers) -> The driver is build on top of WFP. +### Building (For release) + +Please refer to [release/README.md](release/README.md) for details about the release procedure. + ### Building (For testing and development) The Windows Portmaster Kernel Extension is currently only developed and tested for the amd64 (64-bit) architecture. diff --git a/windows_kext/release/README.md b/windows_kext/release/README.md index 319bbd2c..d9e7692a 100644 --- a/windows_kext/release/README.md +++ b/windows_kext/release/README.md @@ -1,25 +1,31 @@ # Kext release tool -### Generate the zip file +## Generate the zip file + +- Make sure the deriver version in `kextinterface/version.txt` is up to date -- Make sure `kextinterface/version.txt` is up to date - Execute: `cargo run` - * This will generate release `kext_release_vX-X-X.zip` file. Which contains all the necessary files to make the release. + _This will generate release `portmaster-kext-release-bundle-vX-X-X-X.zip` file. Which contains all the necessary files to make the release._ -### Generate the cab file +## Generate the cab file -- Copy the zip and extract it on a windows machine. - * Visual Studio 2022 and WDK need to be installed. -- From VS Command Prompt / PowerShell run: -``` -cd kext_release_v.../ -./build_cab.bat -``` -> Script is written for VS `$SDK_Version = "10.0.22621.0"`. If different version is used update the script. + **Precondition:** Visual Studio 2022 and WDK need to be installed. -- Sing the cab file +- copy the zip and extract it on a windows machine. -### Let Microsoft Sign +- update `.\build_cab.ps1`: set correct SDK version you use. + _e.g.: $SDK_Version = "10.0.26100.0" (see in `C:\Program Files (x86)\Windows Kits\10\Lib`)_ + +- Use "Developer PowerShell for VS": + + ```powershell + cd portmaster-kext-release-bundle-v... + .\build_cab.ps1 + ``` + +- Sing the the output cab file: `portmaster-kext-release-bundle-v...\PortmasterKext_v....cab` + +## Let Microsoft Sign - Go to https://partner.microsoft.com/en-us/dashboard/hardware/driver/New - Enter "PortmasterKext vX.X.X #1" as the product name diff --git a/windows_kext/release/src/main.rs b/windows_kext/release/src/main.rs index b956322f..81bf1e81 100644 --- a/windows_kext/release/src/main.rs +++ b/windows_kext/release/src/main.rs @@ -10,13 +10,17 @@ static LIB_PATH: &'static str = "./build/x86_64-pc-windows-msvc/release/driver.l fn main() { build_driver(); - println!( - "Building kext v{}-{}-{} #{}", + + let filename = format!( + "portmaster-kext-release-bundle-v{}-{}-{}-{}.zip", VERSION[0], VERSION[1], VERSION[2], VERSION[3] ); + println!("Building KEXT: {}", filename); + // Create Zip that will hold all the release files and scripts. - let file = File::create("portmaster-kext-release-bundle.zip").unwrap(); + let file = File::create(&filename).unwrap(); + let mut zip = zip::ZipWriter::new(file); // Write files to zip diff --git a/windows_kext/release/templates/build_cab.ps1 b/windows_kext/release/templates/build_cab.ps1 index aefce048..802a4103 100644 --- a/windows_kext/release/templates/build_cab.ps1 +++ b/windows_kext/release/templates/build_cab.ps1 @@ -1,7 +1,7 @@ # Remove previous cab build Remove-Item -Path "PortmasterKext_v2-0-0.cab" -ErrorAction SilentlyContinue -$SDK_Version = "10.0.22621.0" +$SDK_Version = "10.0.26100.0" # Build metadata file rc -I "C:\Program Files (x86)\Windows Kits\10\Include\$SDK_Version\um" ` From d8108bff0ecbddb53cb8a9b5f0a8a8a20fdf9e38 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Fri, 31 Jan 2025 17:50:00 +0200 Subject: [PATCH 03/33] [fix] UI: Error when clicking on 'Apps' in the application configuration https://github.com/safing/portmaster/issues/1721 --- desktop/angular/src/app/pages/app-view/app-view.html | 2 +- desktop/angular/src/main.ts | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/desktop/angular/src/app/pages/app-view/app-view.html b/desktop/angular/src/app/pages/app-view/app-view.html index 0bba8b7b..72a7ad81 100644 --- a/desktop/angular/src/app/pages/app-view/app-view.html +++ b/desktop/angular/src/app/pages/app-view/app-view.html @@ -3,7 +3,7 @@
- Apps +
Apps
diff --git a/desktop/angular/src/main.ts b/desktop/angular/src/main.ts index 1d83337d..f6fc9b2c 100644 --- a/desktop/angular/src/main.ts +++ b/desktop/angular/src/main.ts @@ -27,9 +27,15 @@ if (typeof (CSS as any)['registerProperty'] === 'function') { } function handleExternalResources(e: Event) { + // TODO: + // This code executes "openExternal()" when any "" element in the app is clicked. + // This could potentially be a security issue. + // We should consider restricting this to only external links that belong to a certain domain (e.g., https://safing.io). + // get click target let target: HTMLElement | null = e.target as HTMLElement; - // traverse until we reach an a tag + + // traverse until we reach element "" while (!!target && target.tagName !== "A") { target = target.parentElement; } From 40b443282f3b4f6d3332de9f569eb35e06c8f37a Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Tue, 11 Feb 2025 13:55:29 +0200 Subject: [PATCH 04/33] [service] Fix IPv6 payload layer set --- service/firewall/interception/windowskext2/handler.go | 1 + 1 file changed, 1 insertion(+) diff --git a/service/firewall/interception/windowskext2/handler.go b/service/firewall/interception/windowskext2/handler.go index d144fa63..3dd5aa9d 100644 --- a/service/firewall/interception/windowskext2/handler.go +++ b/service/firewall/interception/windowskext2/handler.go @@ -107,6 +107,7 @@ func Handler(ctx context.Context, packets chan packet.Packet, bandwidthUpdate ch newPacket := &Packet{ verdictRequest: conn.ID, payload: conn.Payload, + payloadLayer: conn.PayloadLayer, verdictSet: abool.NewBool(false), } info := newPacket.Info() From fe8a560f9e39284bf464de5738c1852ad1821912 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Mon, 24 Feb 2025 17:50:41 +0200 Subject: [PATCH 05/33] [fix] Improve sticky domain handling in getStickiedHub function --- spn/crew/sticky.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spn/crew/sticky.go b/spn/crew/sticky.go index c6686c3a..baea2979 100644 --- a/spn/crew/sticky.go +++ b/spn/crew/sticky.go @@ -71,7 +71,8 @@ func getStickiedHub(conn *network.Connection) (sticksTo *stickyHub) { // If the IP did not stick and we have a domain, check if that sticks. if sticksTo == nil && conn.Entity.Domain != "" { - sticksTo, ok := stickyDomains[makeStickyDomainKey(conn)] + var ok bool + sticksTo, ok = stickyDomains[makeStickyDomainKey(conn)] if ok && !sticksTo.isExpired() { sticksTo.LastSeen = time.Now() } @@ -146,6 +147,8 @@ func (t *Tunnel) avoidDestinationHub() { Avoid: true, } log.Warningf("spn/crew: avoiding %s for %s", t.dstPin.Hub, ipKey) + + // TODO: Question: Should we avoid the domain as well? (stickyDomains) } func cleanStickyHubs(ctx *mgr.WorkerCtx) error { From c742c7dfd1c667af5d22cd4b1eefd63ece635fdd Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 24 Feb 2025 17:24:02 +0100 Subject: [PATCH 06/33] Add SQLite database storage backend --- base/database/query/query.go | 5 + base/database/record/base.go | 17 +- base/database/record/meta.go | 10 + base/database/record/record.go | 1 + base/database/record/wrapper.go | 35 +- base/database/storage/sqlite/bobgen.yaml | 6 + .../storage/sqlite/migrations/0_settings.sql | 7 + .../storage/sqlite/migrations/1_initial.sql | 19 + .../storage/sqlite/migrations_config.yml | 5 + .../storage/sqlite/models/bob_main.bob.go | 131 +++++ .../sqlite/models/bob_main_test.bob.go | 9 + .../storage/sqlite/models/records.bob.go | 553 ++++++++++++++++++ base/database/storage/sqlite/schema.go | 59 ++ base/database/storage/sqlite/sqlite.go | 376 ++++++++++++ base/database/storage/sqlite/sqlite_test.go | 199 +++++++ .../database/storage/sqlite/testdata/.gitkeep | 0 go.mod | 95 ++- go.sum | 269 +++++++-- 18 files changed, 1722 insertions(+), 74 deletions(-) create mode 100644 base/database/storage/sqlite/bobgen.yaml create mode 100644 base/database/storage/sqlite/migrations/0_settings.sql create mode 100644 base/database/storage/sqlite/migrations/1_initial.sql create mode 100644 base/database/storage/sqlite/migrations_config.yml create mode 100644 base/database/storage/sqlite/models/bob_main.bob.go create mode 100644 base/database/storage/sqlite/models/bob_main_test.bob.go create mode 100644 base/database/storage/sqlite/models/records.bob.go create mode 100644 base/database/storage/sqlite/schema.go create mode 100644 base/database/storage/sqlite/sqlite.go create mode 100644 base/database/storage/sqlite/sqlite_test.go create mode 100644 base/database/storage/sqlite/testdata/.gitkeep diff --git a/base/database/query/query.go b/base/database/query/query.go index d1be1408..5d4bea42 100644 --- a/base/database/query/query.go +++ b/base/database/query/query.go @@ -99,6 +99,11 @@ func (q *Query) MatchesKey(dbKey string) bool { return strings.HasPrefix(dbKey, q.dbKeyPrefix) } +// HasWhereCondition returns whether the query has a "where" condition set. +func (q *Query) HasWhereCondition() bool { + return q.where != nil +} + // MatchesRecord checks whether the query matches the supplied database record (value only). func (q *Query) MatchesRecord(r record.Record) bool { if q.where == nil { diff --git a/base/database/record/base.go b/base/database/record/base.go index f26c165a..19d30773 100644 --- a/base/database/record/base.go +++ b/base/database/record/base.go @@ -102,7 +102,7 @@ func (b *Base) SetMeta(meta *Meta) { b.meta = meta } -// Marshal marshals the object, without the database key or metadata. It returns nil if the record is deleted. +// Marshal marshals the format and data. func (b *Base) Marshal(self Record, format uint8) ([]byte, error) { if b.Meta() == nil { return nil, errors.New("missing meta") @@ -119,7 +119,20 @@ func (b *Base) Marshal(self Record, format uint8) ([]byte, error) { return dumped, nil } -// MarshalRecord packs the object, including metadata, into a byte array for saving in a database. +// MarshalDataOnly marshals the data only. +func (b *Base) MarshalDataOnly(self Record, format uint8) ([]byte, error) { + if b.Meta() == nil { + return nil, errors.New("missing meta") + } + + if b.Meta().Deleted > 0 { + return nil, nil + } + + return dsd.DumpWithoutIdentifier(self, format, "") +} + +// MarshalRecord marshals the data, format and metadata. func (b *Base) MarshalRecord(self Record) ([]byte, error) { if b.Meta() == nil { return nil, errors.New("missing meta") diff --git a/base/database/record/meta.go b/base/database/record/meta.go index 54a0e614..8810535d 100644 --- a/base/database/record/meta.go +++ b/base/database/record/meta.go @@ -49,11 +49,21 @@ func (m *Meta) MakeCrownJewel() { m.cronjewel = true } +// IsCrownJewel returns whether the database record is marked as a crownjewel. +func (m *Meta) IsCrownJewel() bool { + return m.cronjewel +} + // MakeSecret sets the database record as secret, meaning that it may only be used internally, and not by interfacing processes, such as the UI. func (m *Meta) MakeSecret() { m.secret = true } +// IsSecret returns whether the database record is marked as a secret. +func (m *Meta) IsSecret() bool { + return m.secret +} + // Update updates the internal meta states and should be called before writing the record to the database. func (m *Meta) Update() { now := time.Now().Unix() diff --git a/base/database/record/record.go b/base/database/record/record.go index f18dc898..505b4942 100644 --- a/base/database/record/record.go +++ b/base/database/record/record.go @@ -20,6 +20,7 @@ type Record interface { // Serialization. Marshal(self Record, format uint8) ([]byte, error) + MarshalDataOnly(self Record, format uint8) ([]byte, error) MarshalRecord(self Record) ([]byte, error) GetAccessor(self Record) accessor.Accessor diff --git a/base/database/record/wrapper.go b/base/database/record/wrapper.go index 0f9a9e40..6d87de91 100644 --- a/base/database/record/wrapper.go +++ b/base/database/record/wrapper.go @@ -79,7 +79,21 @@ func NewWrapper(key string, meta *Meta, format uint8, data []byte) (*Wrapper, er }, nil } -// Marshal marshals the object, without the database key or metadata. +// NewWrapperFromDatabase returns a new record wrapper for the given data. +func NewWrapperFromDatabase(dbName, dbKey string, meta *Meta, format uint8, data []byte) (*Wrapper, error) { + return &Wrapper{ + Base{ + dbName: dbName, + dbKey: dbKey, + meta: meta, + }, + sync.Mutex{}, + format, + data, + }, nil +} + +// Marshal marshals the format and data. func (w *Wrapper) Marshal(r Record, format uint8) ([]byte, error) { if w.Meta() == nil { return nil, errors.New("missing meta") @@ -100,7 +114,24 @@ func (w *Wrapper) Marshal(r Record, format uint8) ([]byte, error) { return data, nil } -// MarshalRecord packs the object, including metadata, into a byte array for saving in a database. +// MarshalDataOnly marshals the data only. +func (w *Wrapper) MarshalDataOnly(self Record, format uint8) ([]byte, error) { + if w.Meta() == nil { + return nil, errors.New("missing meta") + } + + if w.Meta().Deleted > 0 { + return nil, nil + } + + if format != dsd.AUTO && format != w.Format { + return nil, errors.New("could not dump model, wrapped object format mismatch") + } + + return w.Data, nil +} + +// MarshalRecord marshals the data, format and metadata. func (w *Wrapper) MarshalRecord(r Record) ([]byte, error) { // Duplication necessary, as the version from Base would call Base.Marshal instead of Wrapper.Marshal diff --git a/base/database/storage/sqlite/bobgen.yaml b/base/database/storage/sqlite/bobgen.yaml new file mode 100644 index 00000000..c5a22cdb --- /dev/null +++ b/base/database/storage/sqlite/bobgen.yaml @@ -0,0 +1,6 @@ +sqlite: + dsn: "testdata/schema.db" + except: + migrations: + +no_factory: true diff --git a/base/database/storage/sqlite/migrations/0_settings.sql b/base/database/storage/sqlite/migrations/0_settings.sql new file mode 100644 index 00000000..4bc41739 --- /dev/null +++ b/base/database/storage/sqlite/migrations/0_settings.sql @@ -0,0 +1,7 @@ +-- +migrate Up +-- SQL in section 'Up' is executed when this migration is applied +PRAGMA auto_vacuum = INCREMENTAL; -- https://sqlite.org/pragma.html#pragma_auto_vacuum + +-- +migrate Down +-- SQL section 'Down' is executed when this migration is rolled back +PRAGMA auto_vacuum = NONE; -- https://sqlite.org/pragma.html#pragma_auto_vacuum diff --git a/base/database/storage/sqlite/migrations/1_initial.sql b/base/database/storage/sqlite/migrations/1_initial.sql new file mode 100644 index 00000000..e0c9ded7 --- /dev/null +++ b/base/database/storage/sqlite/migrations/1_initial.sql @@ -0,0 +1,19 @@ +-- +migrate Up +-- SQL in section 'Up' is executed when this migration is applied +CREATE TABLE records ( + key TEXT PRIMARY KEY, + + format SMALLINT NOT NULL, + value BLOB NOT NULL, + + created BIGINT NOT NULL, + modified BIGINT NOT NULL, + expires BIGINT DEFAULT 0 NOT NULL, + deleted BIGINT DEFAULT 0 NOT NULL, + secret BOOLEAN DEFAULT FALSE NOT NULL, + crownjewel BOOLEAN DEFAULT FALSE NOT NULL +); + +-- +migrate Down +-- SQL section 'Down' is executed when this migration is rolled back +DROP TABLE records; diff --git a/base/database/storage/sqlite/migrations_config.yml b/base/database/storage/sqlite/migrations_config.yml new file mode 100644 index 00000000..eaa65213 --- /dev/null +++ b/base/database/storage/sqlite/migrations_config.yml @@ -0,0 +1,5 @@ +development: + dialect: sqlite3 + datasource: testdata/schema.db + dir: migrations + table: migrations diff --git a/base/database/storage/sqlite/models/bob_main.bob.go b/base/database/storage/sqlite/models/bob_main.bob.go new file mode 100644 index 00000000..172e6c63 --- /dev/null +++ b/base/database/storage/sqlite/models/bob_main.bob.go @@ -0,0 +1,131 @@ +// Code generated by BobGen sqlite v0.30.0. DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package models + +import ( + "hash/maphash" + "strings" + + "github.com/stephenafamo/bob" + "github.com/stephenafamo/bob/clause" + "github.com/stephenafamo/bob/dialect/sqlite" + "github.com/stephenafamo/bob/dialect/sqlite/dialect" + sqliteDriver "modernc.org/sqlite" +) + +var TableNames = struct { + Records string +}{ + Records: "records", +} + +var ColumnNames = struct { + Records recordColumnNames +}{ + Records: recordColumnNames{ + Key: "key", + Format: "format", + Value: "value", + Created: "created", + Modified: "modified", + Expires: "expires", + Deleted: "deleted", + Secret: "secret", + Crownjewel: "crownjewel", + }, +} + +var ( + SelectWhere = Where[*dialect.SelectQuery]() + InsertWhere = Where[*dialect.InsertQuery]() + UpdateWhere = Where[*dialect.UpdateQuery]() + DeleteWhere = Where[*dialect.DeleteQuery]() +) + +func Where[Q sqlite.Filterable]() struct { + Records recordWhere[Q] +} { + return struct { + Records recordWhere[Q] + }{ + Records: buildRecordWhere[Q](RecordColumns), + } +} + +var ( + SelectJoins = getJoins[*dialect.SelectQuery] + UpdateJoins = getJoins[*dialect.UpdateQuery] +) + +type joinSet[Q interface{ aliasedAs(string) Q }] struct { + InnerJoin Q + LeftJoin Q + RightJoin Q +} + +func (j joinSet[Q]) AliasedAs(alias string) joinSet[Q] { + return joinSet[Q]{ + InnerJoin: j.InnerJoin.aliasedAs(alias), + LeftJoin: j.LeftJoin.aliasedAs(alias), + RightJoin: j.RightJoin.aliasedAs(alias), + } +} + +type joins[Q dialect.Joinable] struct{} + +func buildJoinSet[Q interface{ aliasedAs(string) Q }, C any, F func(C, string) Q](c C, f F) joinSet[Q] { + return joinSet[Q]{ + InnerJoin: f(c, clause.InnerJoin), + LeftJoin: f(c, clause.LeftJoin), + RightJoin: f(c, clause.RightJoin), + } +} + +func getJoins[Q dialect.Joinable]() joins[Q] { + return joins[Q]{} +} + +type modAs[Q any, C interface{ AliasedAs(string) C }] struct { + c C + f func(C) bob.Mod[Q] +} + +func (m modAs[Q, C]) Apply(q Q) { + m.f(m.c).Apply(q) +} + +func (m modAs[Q, C]) AliasedAs(alias string) bob.Mod[Q] { + m.c = m.c.AliasedAs(alias) + return m +} + +func randInt() int64 { + out := int64(new(maphash.Hash).Sum64()) + + if out < 0 { + return -out % 10000 + } + + return out % 10000 +} + +// ErrUniqueConstraint captures all unique constraint errors by explicitly leaving `s` empty. +var ErrUniqueConstraint = &UniqueConstraintError{s: ""} + +type UniqueConstraintError struct { + // s is a string uniquely identifying the constraint in the raw error message returned from the database. + s string +} + +func (e *UniqueConstraintError) Error() string { + return e.s +} + +func (e *UniqueConstraintError) Is(target error) bool { + err, ok := target.(*sqliteDriver.Error) + if !ok { + return false + } + return err.Code() == 2067 && strings.Contains(err.Error(), e.s) +} diff --git a/base/database/storage/sqlite/models/bob_main_test.bob.go b/base/database/storage/sqlite/models/bob_main_test.bob.go new file mode 100644 index 00000000..ab65ed33 --- /dev/null +++ b/base/database/storage/sqlite/models/bob_main_test.bob.go @@ -0,0 +1,9 @@ +// Code generated by BobGen sqlite v0.30.0. DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package models + +import "github.com/stephenafamo/bob" + +// Make sure the type Record runs hooks after queries +var _ bob.HookableType = &Record{} diff --git a/base/database/storage/sqlite/models/records.bob.go b/base/database/storage/sqlite/models/records.bob.go new file mode 100644 index 00000000..5f90d27a --- /dev/null +++ b/base/database/storage/sqlite/models/records.bob.go @@ -0,0 +1,553 @@ +// Code generated by BobGen sqlite v0.30.0. DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package models + +import ( + "context" + "io" + + "github.com/aarondl/opt/omit" + "github.com/stephenafamo/bob" + "github.com/stephenafamo/bob/dialect/sqlite" + "github.com/stephenafamo/bob/dialect/sqlite/dialect" + "github.com/stephenafamo/bob/dialect/sqlite/dm" + "github.com/stephenafamo/bob/dialect/sqlite/sm" + "github.com/stephenafamo/bob/dialect/sqlite/um" + "github.com/stephenafamo/bob/expr" +) + +// Record is an object representing the database table. +type Record struct { + Key string `db:"key,pk" ` + Format int16 `db:"format" ` + Value []byte `db:"value" ` + Created int64 `db:"created" ` + Modified int64 `db:"modified" ` + Expires int64 `db:"expires" ` + Deleted int64 `db:"deleted" ` + Secret bool `db:"secret" ` + Crownjewel bool `db:"crownjewel" ` +} + +// RecordSlice is an alias for a slice of pointers to Record. +// This should almost always be used instead of []*Record. +type RecordSlice []*Record + +// Records contains methods to work with the records table +var Records = sqlite.NewTablex[*Record, RecordSlice, *RecordSetter]("", "records") + +// RecordsQuery is a query on the records table +type RecordsQuery = *sqlite.ViewQuery[*Record, RecordSlice] + +type recordColumnNames struct { + Key string + Format string + Value string + Created string + Modified string + Expires string + Deleted string + Secret string + Crownjewel string +} + +var RecordColumns = buildRecordColumns("records") + +type recordColumns struct { + tableAlias string + Key sqlite.Expression + Format sqlite.Expression + Value sqlite.Expression + Created sqlite.Expression + Modified sqlite.Expression + Expires sqlite.Expression + Deleted sqlite.Expression + Secret sqlite.Expression + Crownjewel sqlite.Expression +} + +func (c recordColumns) Alias() string { + return c.tableAlias +} + +func (recordColumns) AliasedAs(alias string) recordColumns { + return buildRecordColumns(alias) +} + +func buildRecordColumns(alias string) recordColumns { + return recordColumns{ + tableAlias: alias, + Key: sqlite.Quote(alias, "key"), + Format: sqlite.Quote(alias, "format"), + Value: sqlite.Quote(alias, "value"), + Created: sqlite.Quote(alias, "created"), + Modified: sqlite.Quote(alias, "modified"), + Expires: sqlite.Quote(alias, "expires"), + Deleted: sqlite.Quote(alias, "deleted"), + Secret: sqlite.Quote(alias, "secret"), + Crownjewel: sqlite.Quote(alias, "crownjewel"), + } +} + +type recordWhere[Q sqlite.Filterable] struct { + Key sqlite.WhereMod[Q, string] + Format sqlite.WhereMod[Q, int16] + Value sqlite.WhereMod[Q, []byte] + Created sqlite.WhereMod[Q, int64] + Modified sqlite.WhereMod[Q, int64] + Expires sqlite.WhereMod[Q, int64] + Deleted sqlite.WhereMod[Q, int64] + Secret sqlite.WhereMod[Q, bool] + Crownjewel sqlite.WhereMod[Q, bool] +} + +func (recordWhere[Q]) AliasedAs(alias string) recordWhere[Q] { + return buildRecordWhere[Q](buildRecordColumns(alias)) +} + +func buildRecordWhere[Q sqlite.Filterable](cols recordColumns) recordWhere[Q] { + return recordWhere[Q]{ + Key: sqlite.Where[Q, string](cols.Key), + Format: sqlite.Where[Q, int16](cols.Format), + Value: sqlite.Where[Q, []byte](cols.Value), + Created: sqlite.Where[Q, int64](cols.Created), + Modified: sqlite.Where[Q, int64](cols.Modified), + Expires: sqlite.Where[Q, int64](cols.Expires), + Deleted: sqlite.Where[Q, int64](cols.Deleted), + Secret: sqlite.Where[Q, bool](cols.Secret), + Crownjewel: sqlite.Where[Q, bool](cols.Crownjewel), + } +} + +// RecordSetter is used for insert/upsert/update operations +// All values are optional, and do not have to be set +// Generated columns are not included +type RecordSetter struct { + Key omit.Val[string] `db:"key,pk" ` + Format omit.Val[int16] `db:"format" ` + Value omit.Val[[]byte] `db:"value" ` + Created omit.Val[int64] `db:"created" ` + Modified omit.Val[int64] `db:"modified" ` + Expires omit.Val[int64] `db:"expires" ` + Deleted omit.Val[int64] `db:"deleted" ` + Secret omit.Val[bool] `db:"secret" ` + Crownjewel omit.Val[bool] `db:"crownjewel" ` +} + +func (s RecordSetter) SetColumns() []string { + vals := make([]string, 0, 9) + if !s.Key.IsUnset() { + vals = append(vals, "key") + } + + if !s.Format.IsUnset() { + vals = append(vals, "format") + } + + if !s.Value.IsUnset() { + vals = append(vals, "value") + } + + if !s.Created.IsUnset() { + vals = append(vals, "created") + } + + if !s.Modified.IsUnset() { + vals = append(vals, "modified") + } + + if !s.Expires.IsUnset() { + vals = append(vals, "expires") + } + + if !s.Deleted.IsUnset() { + vals = append(vals, "deleted") + } + + if !s.Secret.IsUnset() { + vals = append(vals, "secret") + } + + if !s.Crownjewel.IsUnset() { + vals = append(vals, "crownjewel") + } + + return vals +} + +func (s RecordSetter) Overwrite(t *Record) { + if !s.Key.IsUnset() { + t.Key, _ = s.Key.Get() + } + if !s.Format.IsUnset() { + t.Format, _ = s.Format.Get() + } + if !s.Value.IsUnset() { + t.Value, _ = s.Value.Get() + } + if !s.Created.IsUnset() { + t.Created, _ = s.Created.Get() + } + if !s.Modified.IsUnset() { + t.Modified, _ = s.Modified.Get() + } + if !s.Expires.IsUnset() { + t.Expires, _ = s.Expires.Get() + } + if !s.Deleted.IsUnset() { + t.Deleted, _ = s.Deleted.Get() + } + if !s.Secret.IsUnset() { + t.Secret, _ = s.Secret.Get() + } + if !s.Crownjewel.IsUnset() { + t.Crownjewel, _ = s.Crownjewel.Get() + } +} + +func (s *RecordSetter) Apply(q *dialect.InsertQuery) { + q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) { + return Records.BeforeInsertHooks.RunHooks(ctx, exec, s) + }) + + if len(q.Table.Columns) == 0 { + q.Table.Columns = s.SetColumns() + } + + q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) { + vals := make([]bob.Expression, 0, 9) + if !s.Key.IsUnset() { + vals = append(vals, sqlite.Arg(s.Key)) + } + + if !s.Format.IsUnset() { + vals = append(vals, sqlite.Arg(s.Format)) + } + + if !s.Value.IsUnset() { + vals = append(vals, sqlite.Arg(s.Value)) + } + + if !s.Created.IsUnset() { + vals = append(vals, sqlite.Arg(s.Created)) + } + + if !s.Modified.IsUnset() { + vals = append(vals, sqlite.Arg(s.Modified)) + } + + if !s.Expires.IsUnset() { + vals = append(vals, sqlite.Arg(s.Expires)) + } + + if !s.Deleted.IsUnset() { + vals = append(vals, sqlite.Arg(s.Deleted)) + } + + if !s.Secret.IsUnset() { + vals = append(vals, sqlite.Arg(s.Secret)) + } + + if !s.Crownjewel.IsUnset() { + vals = append(vals, sqlite.Arg(s.Crownjewel)) + } + + return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "") + })) +} + +func (s RecordSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] { + return um.Set(s.Expressions()...) +} + +func (s RecordSetter) Expressions(prefix ...string) []bob.Expression { + exprs := make([]bob.Expression, 0, 9) + + if !s.Key.IsUnset() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + sqlite.Quote(append(prefix, "key")...), + sqlite.Arg(s.Key), + }}) + } + + if !s.Format.IsUnset() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + sqlite.Quote(append(prefix, "format")...), + sqlite.Arg(s.Format), + }}) + } + + if !s.Value.IsUnset() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + sqlite.Quote(append(prefix, "value")...), + sqlite.Arg(s.Value), + }}) + } + + if !s.Created.IsUnset() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + sqlite.Quote(append(prefix, "created")...), + sqlite.Arg(s.Created), + }}) + } + + if !s.Modified.IsUnset() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + sqlite.Quote(append(prefix, "modified")...), + sqlite.Arg(s.Modified), + }}) + } + + if !s.Expires.IsUnset() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + sqlite.Quote(append(prefix, "expires")...), + sqlite.Arg(s.Expires), + }}) + } + + if !s.Deleted.IsUnset() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + sqlite.Quote(append(prefix, "deleted")...), + sqlite.Arg(s.Deleted), + }}) + } + + if !s.Secret.IsUnset() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + sqlite.Quote(append(prefix, "secret")...), + sqlite.Arg(s.Secret), + }}) + } + + if !s.Crownjewel.IsUnset() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + sqlite.Quote(append(prefix, "crownjewel")...), + sqlite.Arg(s.Crownjewel), + }}) + } + + return exprs +} + +// FindRecord retrieves a single record by primary key +// If cols is empty Find will return all columns. +func FindRecord(ctx context.Context, exec bob.Executor, KeyPK string, cols ...string) (*Record, error) { + if len(cols) == 0 { + return Records.Query( + SelectWhere.Records.Key.EQ(KeyPK), + ).One(ctx, exec) + } + + return Records.Query( + SelectWhere.Records.Key.EQ(KeyPK), + sm.Columns(Records.Columns().Only(cols...)), + ).One(ctx, exec) +} + +// RecordExists checks the presence of a single record by primary key +func RecordExists(ctx context.Context, exec bob.Executor, KeyPK string) (bool, error) { + return Records.Query( + SelectWhere.Records.Key.EQ(KeyPK), + ).Exists(ctx, exec) +} + +// AfterQueryHook is called after Record is retrieved from the database +func (o *Record) AfterQueryHook(ctx context.Context, exec bob.Executor, queryType bob.QueryType) error { + var err error + + switch queryType { + case bob.QueryTypeSelect: + ctx, err = Records.AfterSelectHooks.RunHooks(ctx, exec, RecordSlice{o}) + case bob.QueryTypeInsert: + ctx, err = Records.AfterInsertHooks.RunHooks(ctx, exec, RecordSlice{o}) + case bob.QueryTypeUpdate: + ctx, err = Records.AfterUpdateHooks.RunHooks(ctx, exec, RecordSlice{o}) + case bob.QueryTypeDelete: + ctx, err = Records.AfterDeleteHooks.RunHooks(ctx, exec, RecordSlice{o}) + } + + return err +} + +// PrimaryKeyVals returns the primary key values of the Record +func (o *Record) PrimaryKeyVals() bob.Expression { + return sqlite.Arg(o.Key) +} + +func (o *Record) pkEQ() dialect.Expression { + return sqlite.Quote("records", "key").EQ(bob.ExpressionFunc(func(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) { + return o.PrimaryKeyVals().WriteSQL(ctx, w, d, start) + })) +} + +// Update uses an executor to update the Record +func (o *Record) Update(ctx context.Context, exec bob.Executor, s *RecordSetter) error { + v, err := Records.Update(s.UpdateMod(), um.Where(o.pkEQ())).One(ctx, exec) + if err != nil { + return err + } + + *o = *v + + return nil +} + +// Delete deletes a single Record record with an executor +func (o *Record) Delete(ctx context.Context, exec bob.Executor) error { + _, err := Records.Delete(dm.Where(o.pkEQ())).Exec(ctx, exec) + return err +} + +// Reload refreshes the Record using the executor +func (o *Record) Reload(ctx context.Context, exec bob.Executor) error { + o2, err := Records.Query( + SelectWhere.Records.Key.EQ(o.Key), + ).One(ctx, exec) + if err != nil { + return err + } + + *o = *o2 + + return nil +} + +// AfterQueryHook is called after RecordSlice is retrieved from the database +func (o RecordSlice) AfterQueryHook(ctx context.Context, exec bob.Executor, queryType bob.QueryType) error { + var err error + + switch queryType { + case bob.QueryTypeSelect: + ctx, err = Records.AfterSelectHooks.RunHooks(ctx, exec, o) + case bob.QueryTypeInsert: + ctx, err = Records.AfterInsertHooks.RunHooks(ctx, exec, o) + case bob.QueryTypeUpdate: + ctx, err = Records.AfterUpdateHooks.RunHooks(ctx, exec, o) + case bob.QueryTypeDelete: + ctx, err = Records.AfterDeleteHooks.RunHooks(ctx, exec, o) + } + + return err +} + +func (o RecordSlice) pkIN() dialect.Expression { + if len(o) == 0 { + return sqlite.Raw("NULL") + } + + return sqlite.Quote("records", "key").In(bob.ExpressionFunc(func(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) { + pkPairs := make([]bob.Expression, len(o)) + for i, row := range o { + pkPairs[i] = row.PrimaryKeyVals() + } + return bob.ExpressSlice(ctx, w, d, start, pkPairs, "", ", ", "") + })) +} + +// copyMatchingRows finds models in the given slice that have the same primary key +// then it first copies the existing relationships from the old model to the new model +// and then replaces the old model in the slice with the new model +func (o RecordSlice) copyMatchingRows(from ...*Record) { + for i, old := range o { + for _, new := range from { + if new.Key != old.Key { + continue + } + + o[i] = new + break + } + } +} + +// UpdateMod modifies an update query with "WHERE primary_key IN (o...)" +func (o RecordSlice) UpdateMod() bob.Mod[*dialect.UpdateQuery] { + return bob.ModFunc[*dialect.UpdateQuery](func(q *dialect.UpdateQuery) { + q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) { + return Records.BeforeUpdateHooks.RunHooks(ctx, exec, o) + }) + + q.AppendLoader(bob.LoaderFunc(func(ctx context.Context, exec bob.Executor, retrieved any) error { + var err error + switch retrieved := retrieved.(type) { + case *Record: + o.copyMatchingRows(retrieved) + case []*Record: + o.copyMatchingRows(retrieved...) + case RecordSlice: + o.copyMatchingRows(retrieved...) + default: + // If the retrieved value is not a Record or a slice of Record + // then run the AfterUpdateHooks on the slice + _, err = Records.AfterUpdateHooks.RunHooks(ctx, exec, o) + } + + return err + })) + + q.AppendWhere(o.pkIN()) + }) +} + +// DeleteMod modifies an delete query with "WHERE primary_key IN (o...)" +func (o RecordSlice) DeleteMod() bob.Mod[*dialect.DeleteQuery] { + return bob.ModFunc[*dialect.DeleteQuery](func(q *dialect.DeleteQuery) { + q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) { + return Records.BeforeDeleteHooks.RunHooks(ctx, exec, o) + }) + + q.AppendLoader(bob.LoaderFunc(func(ctx context.Context, exec bob.Executor, retrieved any) error { + var err error + switch retrieved := retrieved.(type) { + case *Record: + o.copyMatchingRows(retrieved) + case []*Record: + o.copyMatchingRows(retrieved...) + case RecordSlice: + o.copyMatchingRows(retrieved...) + default: + // If the retrieved value is not a Record or a slice of Record + // then run the AfterDeleteHooks on the slice + _, err = Records.AfterDeleteHooks.RunHooks(ctx, exec, o) + } + + return err + })) + + q.AppendWhere(o.pkIN()) + }) +} + +func (o RecordSlice) UpdateAll(ctx context.Context, exec bob.Executor, vals RecordSetter) error { + if len(o) == 0 { + return nil + } + + _, err := Records.Update(vals.UpdateMod(), o.UpdateMod()).All(ctx, exec) + return err +} + +func (o RecordSlice) DeleteAll(ctx context.Context, exec bob.Executor) error { + if len(o) == 0 { + return nil + } + + _, err := Records.Delete(o.DeleteMod()).Exec(ctx, exec) + return err +} + +func (o RecordSlice) ReloadAll(ctx context.Context, exec bob.Executor) error { + if len(o) == 0 { + return nil + } + + o2, err := Records.Query(sm.Where(o.pkIN())).All(ctx, exec) + if err != nil { + return err + } + + o.copyMatchingRows(o2...) + + return nil +} diff --git a/base/database/storage/sqlite/schema.go b/base/database/storage/sqlite/schema.go new file mode 100644 index 00000000..c87402f3 --- /dev/null +++ b/base/database/storage/sqlite/schema.go @@ -0,0 +1,59 @@ +package sqlite + +// Base command for sql-migrate: +//go:generate -command migrate go tool github.com/rubenv/sql-migrate/sql-migrate + +// Run missing migrations: +//go:generate migrate up --config=migrations_config.yml + +// Redo last migration: +// x go:generate migrate redo --config=migrations_config.yml + +// Undo all migrations: +// x go:generate migrate down --config=migrations_config.yml + +// Generate models with bob: +//go:generate go tool github.com/stephenafamo/bob/gen/bobgen-sqlite + +import ( + "embed" + + migrate "github.com/rubenv/sql-migrate" + "github.com/safing/portmaster/base/database/record" + "github.com/safing/portmaster/base/database/storage/sqlite/models" +) + +//go:embed migrations/* +var dbMigrations embed.FS + +func getMigrations() migrate.EmbedFileSystemMigrationSource { + return migrate.EmbedFileSystemMigrationSource{ + FileSystem: dbMigrations, + Root: "migrations", + } +} + +func getMeta(r *models.Record) *record.Meta { + meta := &record.Meta{ + Created: r.Created, + Modified: r.Modified, + Expires: r.Expires, + Deleted: r.Deleted, + } + if r.Secret { + meta.MakeSecret() + } + if r.Crownjewel { + meta.MakeCrownJewel() + } + return meta +} + +func setMeta(r *models.Record, m *record.Meta) { + r.Created = m.Created + r.Modified = m.Modified + r.Expires = m.Expires + r.Deleted = m.Deleted + r.Secret = m.IsSecret() + r.Crownjewel = m.IsCrownJewel() +} diff --git a/base/database/storage/sqlite/sqlite.go b/base/database/storage/sqlite/sqlite.go new file mode 100644 index 00000000..ba9b11c7 --- /dev/null +++ b/base/database/storage/sqlite/sqlite.go @@ -0,0 +1,376 @@ +package sqlite + +import ( + "context" + "database/sql" + "errors" + "fmt" + "path/filepath" + "sync" + "time" + + "github.com/aarondl/opt/omit" + migrate "github.com/rubenv/sql-migrate" + "github.com/safing/portmaster/base/database/accessor" + "github.com/safing/portmaster/base/database/iterator" + "github.com/safing/portmaster/base/database/query" + "github.com/safing/portmaster/base/database/record" + "github.com/safing/portmaster/base/database/storage" + "github.com/safing/portmaster/base/database/storage/sqlite/models" + "github.com/safing/portmaster/base/log" + "github.com/safing/structures/dsd" + "github.com/stephenafamo/bob" + "github.com/stephenafamo/bob/dialect/sqlite" + "github.com/stephenafamo/bob/dialect/sqlite/im" + "github.com/stephenafamo/bob/dialect/sqlite/um" + + _ "modernc.org/sqlite" +) + +// SQLite storage. +type SQLite struct { + name string + + db *sql.DB + bob bob.DB + lock sync.RWMutex + + ctx context.Context + cancelCtx context.CancelFunc +} + +func init() { + _ = storage.Register("sqlite", func(name, location string) (storage.Interface, error) { + return NewSQLite(name, location) + }) +} + +// NewSQLite creates a sqlite database. +func NewSQLite(name, location string) (*SQLite, error) { + dbFile := filepath.Join(location, "db.sqlite") + + // Open database file. + db, err := sql.Open("sqlite", dbFile) + if err != nil { + return nil, fmt.Errorf("open sqlite: %w", err) + } + + // Run migrations on database. + n, err := migrate.Exec(db, "sqlite3", getMigrations(), migrate.Up) + if err != nil { + return nil, fmt.Errorf("migrate sqlite: %w", err) + } + log.Debugf("database/sqlite: ran %d migrations on %s database", n, name) + + // Return as bob database. + ctx, cancelCtx := context.WithCancel(context.Background()) + return &SQLite{ + name: name, + bob: bob.NewDB(db), + ctx: ctx, + cancelCtx: cancelCtx, + }, nil +} + +// Get returns a database record. +func (db *SQLite) Get(key string) (record.Record, error) { + db.lock.RLock() + defer db.lock.RUnlock() + + // Get record from database. + r, err := models.FindRecord(db.ctx, db.bob, key) + if err != nil { + return nil, fmt.Errorf("%w: %s", storage.ErrNotFound, err) + } + + // Return data in wrapper. + return record.NewWrapperFromDatabase(db.name, key, getMeta(r), uint8(r.Format), r.Value) +} + +// GetMeta returns the metadata of a database record. +func (db *SQLite) GetMeta(key string) (*record.Meta, error) { + r, err := db.Get(key) + if err != nil { + return nil, err + } + + return r.Meta(), nil +} + +// Put stores a record in the database. +func (db *SQLite) Put(r record.Record) (record.Record, error) { + r.Lock() + defer r.Unlock() + + // Serialize to JSON. + data, err := r.MarshalDataOnly(r, dsd.JSON) + if err != nil { + return nil, err + } + + // Create structure for insert. + m := r.Meta() + setter := models.RecordSetter{ + Key: omit.From(r.DatabaseKey()), + Format: omit.From(int16(dsd.JSON)), + Value: omit.From(data), + Created: omit.From(m.Created), + Modified: omit.From(m.Modified), + Expires: omit.From(m.Expires), + Deleted: omit.From(m.Deleted), + Secret: omit.From(m.IsSecret()), + Crownjewel: omit.From(m.IsCrownJewel()), + } + + // Lock for writing. + db.lock.Lock() + defer db.lock.Unlock() + + // Simulate upsert with custom selection on conflict. + _, err = models.Records.Insert( + &setter, + im.OnConflict("key").DoUpdate( + im.SetExcluded("format", "value", "created", "modified", "expires", "deleted", "secret", "crownjewel"), + ), + ).Exec(db.ctx, db.bob) + if err != nil { + return nil, err + } + + return r, nil +} + +// PutMany stores many records in the database. +func (db *SQLite) PutMany(shadowDelete bool) (chan<- record.Record, <-chan error) { + db.lock.Lock() + defer db.lock.Unlock() + // we could lock for every record, but we want to have the same behaviour + // as the other storage backends, especially for testing. + + batch := make(chan record.Record, 100) + errs := make(chan error, 1) + + // start handler + go func() { + for r := range batch { + _, err := db.Put(r) + if err != nil { + errs <- err + return + } + } + errs <- nil + }() + + return batch, errs +} + +// Delete deletes a record from the database. +func (db *SQLite) Delete(key string) error { + // Lock for writing. + db.lock.Lock() + defer db.lock.Unlock() + + toDelete := &models.Record{Key: key} + return toDelete.Delete(db.ctx, db.bob) +} + +// Query returns a an iterator for the supplied query. +func (db *SQLite) Query(q *query.Query, local, internal bool) (*iterator.Iterator, error) { + _, err := q.Check() + if err != nil { + return nil, fmt.Errorf("invalid query: %w", err) + } + + queryIter := iterator.New() + + go db.queryExecutor(queryIter, q, local, internal) + return queryIter, nil +} + +func (db *SQLite) queryExecutor(queryIter *iterator.Iterator, q *query.Query, local, internal bool) { + // Build query. + var recordQuery *sqlite.ViewQuery[*models.Record, models.RecordSlice] + if q.DatabaseKeyPrefix() != "" { + recordQuery = models.Records.View.Query( + models.SelectWhere.Records.Key.Like(q.DatabaseKeyPrefix() + "%"), + ) + } else { + recordQuery = models.Records.View.Query() + } + + // Get all records from query. + // TODO: This will load all records into memory. While this is efficient and + // will not block others from using the datbase, this might be quite a strain + // on the system memory. Monitor and see if this is an issue. + db.lock.RLock() + records, err := models.RecordsQuery.All(recordQuery, db.ctx, db.bob) + db.lock.RUnlock() + if err != nil { + queryIter.Finish(err) + return + } + +recordsLoop: + for _, r := range records { + // Check if key matches. + if !q.MatchesKey(r.Key) { + continue recordsLoop + } + + // Check Meta. + m := getMeta(r) + if !m.CheckValidity() || + !m.CheckPermission(local, internal) { + continue recordsLoop + } + + // Check Data. + if q.HasWhereCondition() { + jsonData := string(r.Value) + jsonAccess := accessor.NewJSONAccessor(&jsonData) + if !q.MatchesAccessor(jsonAccess) { + continue recordsLoop + } + } + + // Build database record. + matched, _ := record.NewWrapperFromDatabase(db.name, r.Key, m, uint8(r.Format), r.Value) + + select { + case <-queryIter.Done: + break recordsLoop + case queryIter.Next <- matched: + default: + select { + case <-queryIter.Done: + break recordsLoop + case queryIter.Next <- matched: + case <-time.After(1 * time.Second): + err = errors.New("query timeout") + break recordsLoop + } + } + + } + + queryIter.Finish(err) +} + +// Purge deletes all records that match the given query. It returns the number of successful deletes and an error. +func (db *SQLite) Purge(ctx context.Context, q *query.Query, local, internal, shadowDelete bool) (int, error) { + // Optimize for local and internal queries without where clause. + if local && internal && !shadowDelete && !q.HasWhereCondition() { + db.lock.Lock() + defer db.lock.Unlock() + + // First count entries (SQLite does not support affected rows) + n, err := models.Records.Query( + models.SelectWhere.Records.Key.Like(q.DatabaseKeyPrefix()+"%"), + ).Count(db.ctx, db.bob) + if err != nil || n == 0 { + return int(n), err + } + + // Delete entries. + _, err = models.Records.Delete( + models.DeleteWhere.Records.Key.Like(q.DatabaseKeyPrefix()+"%"), + ).Exec(db.ctx, db.bob) + return int(n), err + } + + // Otherwise, iterate over all entries and delete matching ones. + + // Create iterator to check all matching records. + queryIter := iterator.New() + defer queryIter.Cancel() + go db.queryExecutor(queryIter, q, local, internal) + + // Delete all matching records. + var deleted int + for r := range queryIter.Next { + db.Delete(r.DatabaseKey()) + deleted++ + } + + return deleted, nil +} + +// ReadOnly returns whether the database is read only. +func (db *SQLite) ReadOnly() bool { + return false +} + +// Injected returns whether the database is injected. +func (db *SQLite) Injected() bool { + return false +} + +// MaintainRecordStates maintains records states in the database. +func (db *SQLite) MaintainRecordStates(ctx context.Context, purgeDeletedBefore time.Time, shadowDelete bool) error { + db.lock.Lock() + defer db.lock.Unlock() + + now := time.Now().Unix() + purgeThreshold := purgeDeletedBefore.Unix() + + // Option 1: Using shadow delete. + if shadowDelete { + // Mark expired records as deleted. + models.Records.Update( + um.SetCol("deleted").ToArg(now), + models.UpdateWhere.Records.Deleted.EQ(0), + models.UpdateWhere.Records.Expires.GT(0), + models.UpdateWhere.Records.Expires.LT(now), + ).Exec(db.ctx, db.bob) + + // Purge deleted records before threshold. + models.Records.Delete( + models.DeleteWhere.Records.Deleted.GT(0), + models.DeleteWhere.Records.Deleted.LT(purgeThreshold), + ).Exec(db.ctx, db.bob) + return nil + } + + // Option 2: Immediate delete. + + // Delete expired record. + models.Records.Delete( + models.DeleteWhere.Records.Expires.GT(0), + models.DeleteWhere.Records.Expires.LT(now), + ).Exec(db.ctx, db.bob) + // Delete shadow deleted records. + models.Records.Delete( + models.DeleteWhere.Records.Deleted.GT(0), + ).Exec(db.ctx, db.bob) + + return nil +} + +func (db *SQLite) Maintain(ctx context.Context) error { + db.lock.Lock() + defer db.lock.Unlock() + + // Remove up to about 100KB of SQLite pages from the freelist on every run. + // (Assuming 4KB page size.) + _, err := db.db.ExecContext(ctx, "PRAGMA incremental_vacuum(25);") + return err +} + +func (db *SQLite) MaintainThorough(ctx context.Context) error { + db.lock.Lock() + defer db.lock.Unlock() + + // Remove all pages from the freelist. + _, err := db.db.ExecContext(ctx, "PRAGMA incremental_vacuum;") + return err +} + +// Shutdown shuts down the database. +func (db *SQLite) Shutdown() error { + db.lock.Lock() + defer db.lock.Unlock() + db.cancelCtx() + + return db.bob.Close() +} diff --git a/base/database/storage/sqlite/sqlite_test.go b/base/database/storage/sqlite/sqlite_test.go new file mode 100644 index 00000000..0fc1a4a1 --- /dev/null +++ b/base/database/storage/sqlite/sqlite_test.go @@ -0,0 +1,199 @@ +package sqlite + +import ( + "context" + "os" + "sync" + "testing" + "time" + + "github.com/safing/portmaster/base/database/query" + "github.com/safing/portmaster/base/database/record" + "github.com/safing/portmaster/base/database/storage" + "github.com/stretchr/testify/assert" +) + +var ( + // Compile time interface checks. + _ storage.Interface = &SQLite{} + _ storage.Batcher = &SQLite{} + _ storage.Purger = &SQLite{} +) + +type TestRecord struct { //nolint:maligned + record.Base + sync.Mutex + S string + I int + I8 int8 + I16 int16 + I32 int32 + I64 int64 + UI uint + UI8 uint8 + UI16 uint16 + UI32 uint32 + UI64 uint64 + F32 float32 + F64 float64 + B bool +} + +func TestSQLite(t *testing.T) { + t.Parallel() + + testDir, err := os.MkdirTemp("", "testing-") + if err != nil { + t.Fatal(err) + } + defer func() { + _ = os.RemoveAll(testDir) // clean up + }() + + // start + db, err := NewSQLite("test", testDir) + if err != nil { + t.Fatal(err) + } + defer func() { + // shutdown + err = db.Shutdown() + if err != nil { + t.Fatal(err) + } + }() + + a := &TestRecord{ + S: "banana", + I: 42, + I8: 42, + I16: 42, + I32: 42, + I64: 42, + UI: 42, + UI8: 42, + UI16: 42, + UI32: 42, + UI64: 42, + F32: 42.42, + F64: 42.42, + B: true, + } + a.SetMeta(&record.Meta{}) + a.Meta().Update() + a.SetKey("test:A") + + // put record + _, err = db.Put(a) + if err != nil { + t.Fatal(err) + } + + // get and compare + r1, err := db.Get("A") + if err != nil { + t.Fatal(err) + } + + a1 := &TestRecord{} + err = record.Unwrap(r1, a1) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, a, a1, "struct must match") + + // setup query test records + qA := &TestRecord{} + qA.SetKey("test:path/to/A") + qA.CreateMeta() + qB := &TestRecord{} + qB.SetKey("test:path/to/B") + qB.CreateMeta() + qC := &TestRecord{} + qC.SetKey("test:path/to/C") + qC.CreateMeta() + qZ := &TestRecord{} + qZ.SetKey("test:z") + qZ.CreateMeta() + // put + _, err = db.Put(qA) + if err == nil { + _, err = db.Put(qB) + } + if err == nil { + _, err = db.Put(qC) + } + if err == nil { + _, err = db.Put(qZ) + } + if err != nil { + t.Fatal(err) + } + + // test query + q := query.New("test:path/to/").MustBeValid() + it, err := db.Query(q, true, true) + if err != nil { + t.Fatal(err) + } + cnt := 0 + for range it.Next { + cnt++ + } + if it.Err() != nil { + t.Fatal(it.Err()) + } + if cnt != 3 { + t.Fatalf("unexpected query result count: %d", cnt) + } + + // delete + err = db.Delete("A") + if err != nil { + t.Fatal(err) + } + + // check if its gone + _, err = db.Get("A") + if err == nil { + t.Fatal("should fail") + } + + // maintenance + err = db.MaintainRecordStates(context.TODO(), time.Now(), true) + if err != nil { + t.Fatal(err) + } + + // maintenance + err = db.MaintainRecordStates(context.TODO(), time.Now(), false) + if err != nil { + t.Fatal(err) + } + + // purging + n, err := db.Purge(context.TODO(), query.New("test:path/to/").MustBeValid(), true, true, false) + if err != nil { + t.Fatal(err) + } + if n != 3 { + t.Fatalf("unexpected purge delete count: %d", n) + } + + // test query + q = query.New("test").MustBeValid() + it, err = db.Query(q, true, true) + if err != nil { + t.Fatal(err) + } + cnt = 0 + for range it.Next { + cnt++ + } + if it.Err() != nil { + t.Fatal(it.Err()) + } + if cnt != 1 { + t.Fatalf("unexpected query result count: %d", cnt) + } +} diff --git a/base/database/storage/sqlite/testdata/.gitkeep b/base/database/storage/sqlite/testdata/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/go.mod b/go.mod index 49103f19..c72f079c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/safing/portmaster -go 1.22.0 +go 1.23 + +toolchain go1.23.5 // TODO: Remove when https://github.com/tc-hib/winres/pull/4 is released. replace github.com/tc-hib/winres => github.com/dhaavi/winres v0.2.2 @@ -8,7 +10,8 @@ replace github.com/tc-hib/winres => github.com/dhaavi/winres v0.2.2 require ( fyne.io/systray v1.11.0 github.com/VictoriaMetrics/metrics v1.35.1 - github.com/Xuanwo/go-locale v1.1.2 + github.com/Xuanwo/go-locale v1.1.1 + github.com/aarondl/opt v0.0.0-20230114172057-b91f370c41f0 github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6 github.com/agext/levenshtein v1.2.3 github.com/armon/go-radix v1.0.0 @@ -46,12 +49,14 @@ require ( github.com/oschwald/maxminddb-golang v1.13.1 github.com/r3labs/diff/v3 v3.0.1 github.com/rot256/pblind v0.0.0-20240730113005-f3275049ead5 + github.com/rubenv/sql-migrate v1.7.1 github.com/safing/jess v0.3.5 - github.com/safing/structures v1.1.0 + github.com/safing/structures v1.2.0 github.com/seehuhn/fortuna v1.0.1 github.com/shirou/gopsutil v3.21.11+incompatible github.com/spf13/cobra v1.8.1 github.com/spkg/zipfs v0.7.1 + github.com/stephenafamo/bob v0.30.0 github.com/stretchr/testify v1.9.0 github.com/tannerryan/ring v1.1.2 github.com/tc-hib/winres v0.3.1 @@ -61,68 +66,120 @@ require ( github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26 github.com/varlink/go v0.4.0 github.com/vincent-petithory/dataurl v1.0.0 - go.etcd.io/bbolt v1.3.11 - golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 - golang.org/x/image v0.23.0 - golang.org/x/net v0.34.0 + go.etcd.io/bbolt v1.3.10 + golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa + golang.org/x/image v0.19.0 + golang.org/x/net v0.28.0 golang.org/x/sync v0.10.0 golang.org/x/sys v0.29.0 gopkg.in/yaml.v3 v3.0.1 - zombiezen.com/go/sqlite v1.4.0 + modernc.org/sqlite v1.32.0 + zombiezen.com/go/sqlite v1.3.0 ) require ( al.essio.dev/pkg/shellescape v1.5.1 // indirect + filippo.io/edwards25519 v1.1.0 // indirect github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.2.0 // indirect + github.com/Masterminds/sprig/v3 v3.2.3 // indirect + github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf // indirect github.com/aead/ecdh v0.2.0 // indirect + github.com/bgentry/speakeasy v0.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/danieljoos/wincred v1.2.2 // indirect - github.com/dgraph-io/ristretto v0.2.0 // indirect + github.com/denisenkom/go-mssqldb v0.9.0 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fatih/color v1.13.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-gorp/gorp/v3 v3.1.0 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-sql-driver/mysql v1.7.2-0.20231213112541-0004702b931d // indirect + github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect github.com/godbus/dbus v4.1.0+incompatible // indirect + github.com/godror/godror v0.40.4 // indirect + github.com/godror/knownpb v0.1.1 // indirect + github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f // indirect + github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/huandu/xstrings v1.4.0 // indirect + github.com/imdario/mergo v0.3.13 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/native v1.1.0 // indirect - github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect + github.com/knadh/koanf/parsers/yaml v0.1.0 // indirect + github.com/knadh/koanf/providers/confmap v0.1.0 // indirect + github.com/knadh/koanf/providers/env v0.1.0 // indirect + github.com/knadh/koanf/providers/file v0.1.0 // indirect + github.com/knadh/koanf/v2 v2.1.0 // indirect + github.com/lib/pq v1.10.7 // indirect + github.com/mattn/go-oci8 v0.1.1 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mattn/go-sqlite3 v1.14.19 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/socket v0.5.1 // indirect + github.com/mitchellh/cli v1.1.5 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/posener/complete v1.2.3 // indirect + github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/satori/go.uuid v1.2.0 // indirect github.com/seehuhn/sha256d v1.0.0 // indirect + github.com/shopspring/decimal v1.3.1 // indirect + github.com/spf13/cast v1.5.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stephenafamo/scan v0.6.1 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect - github.com/tklauser/numcpus v0.9.0 // indirect + github.com/tklauser/numcpus v0.8.0 // indirect + github.com/urfave/cli/v2 v2.23.7 // indirect github.com/valyala/fastrand v1.1.0 // indirect github.com/valyala/histogram v1.2.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/volatiletech/inflect v0.0.1 // indirect + github.com/volatiletech/strmangle v0.0.6 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zalando/go-keyring v0.2.6 // indirect github.com/zeebo/blake3 v0.2.4 // indirect - golang.org/x/crypto v0.32.0 // indirect - golang.org/x/mod v0.22.0 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/mod v0.20.0 // indirect golang.org/x/text v0.21.0 // indirect - golang.org/x/tools v0.29.0 // indirect - google.golang.org/protobuf v1.36.2 // indirect + golang.org/x/tools v0.24.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - modernc.org/libc v1.61.7 // indirect - modernc.org/mathutil v1.7.1 // indirect - modernc.org/memory v1.8.1 // indirect - modernc.org/sqlite v1.34.4 // indirect + modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect + modernc.org/libc v1.59.9 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/strutil v1.2.0 // indirect + modernc.org/token v1.1.0 // indirect + mvdan.cc/gofumpt v0.5.0 // indirect +) + +tool ( + github.com/rubenv/sql-migrate/sql-migrate + github.com/stephenafamo/bob/gen/bobgen-sqlite ) diff --git a/go.sum b/go.sum index 1a291410..528dc251 100644 --- a/go.sum +++ b/go.sum @@ -1,34 +1,56 @@ al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg= fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/UNO-SOFT/zlog v0.8.1 h1:TEFkGJHtUfTRgMkLZiAjLSHALjwSBdw6/zByMC5GJt4= +github.com/UNO-SOFT/zlog v0.8.1/go.mod h1:yqFOjn3OhvJ4j7ArJqQNA+9V+u6t9zSAyIZdWdMweWc= github.com/VictoriaMetrics/metrics v1.35.1 h1:o84wtBKQbzLdDy14XeskkCZih6anG+veZ1SwJHFGwrU= github.com/VictoriaMetrics/metrics v1.35.1/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8= -github.com/Xuanwo/go-locale v1.1.2 h1:6H+olvrQcyVOZ+GAC2rXu4armacTT4ZrFCA0mB24XVo= -github.com/Xuanwo/go-locale v1.1.2/go.mod h1:1JBER4QV7Ji39GJ4AvVlfvqmTUqopzxQxdg2mXYOw94= +github.com/Xuanwo/go-locale v1.1.1 h1:nhvzo1phY4LRwdrwVwKWXn5iZ0pMwwsa3o29yiDRuZc= +github.com/Xuanwo/go-locale v1.1.1/go.mod h1:ldC3FzZeMYALkL3YYpwhr4iVYdOIUx42kORcnAHdKUo= +github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf h1:+edM69bH/X6JpYPmJYBRLanAMe1V5yRXYU3hHUovGcE= +github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf/go.mod h1:FZqLhJSj2tg0ZN48GB1zvj00+ZYcHPqgsC7yzcgCq6k= +github.com/aarondl/opt v0.0.0-20230114172057-b91f370c41f0 h1:vLrhbOWVPxtHao/QthU8pcpI4DbtSGnWgH7qIJf8F6k= +github.com/aarondl/opt v0.0.0-20230114172057-b91f370c41f0/go.mod h1:l4/5NZtYd/SIohsFhaJQQe+sPOTG22furpZ5FvcYOzk= github.com/aead/ecdh v0.2.0 h1:pYop54xVaq/CEREFEcukHRZfTdjiWvYIsZDXXrBapQQ= github.com/aead/ecdh v0.2.0/go.mod h1:a9HHtXuSo8J1Js1MwLQx2mBhkXMT6YwUmVVEY4tTB8U= github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6 h1:5L8Mj9Co9sJVgW3TpYk2gxGJnDjsYuboNTcRmbtGKGs= github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6/go.mod h1:3HgLJ9d18kXMLQlJvIY3+FszZYMxCz8WfE2MQ7hDY0w= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/awalterschulze/gographviz v2.0.3+incompatible h1:9sVEXJBJLwGX7EQVhLm2elIKCm7P2YHFC8v6096G09E= github.com/awalterschulze/gographviz v2.0.3+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs= +github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= github.com/bradfitz/gomemcache v0.0.0-20170208213004-1952afaa557d/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= github.com/brianvoe/gofakeit v3.18.0+incompatible h1:wDOmHc9DLG4nRjUVVaxA+CEglKOW72Y5+4WNxUIkjM8= github.com/brianvoe/gofakeit v3.18.0+incompatible/go.mod h1:kfwdRA90vvNhPutZWfH7WPaDzUjz+CZFqG+rPkOjGOc= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cilium/ebpf v0.5.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= @@ -41,20 +63,22 @@ github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNs github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= 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/denisenkom/go-mssqldb v0.9.0 h1:RSohk2RsiZqLZ0zCjtfn3S4Gp4exhpBWHyQ7D0yGjAk= +github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8= github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE= github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= -github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE= -github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= -github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dhaavi/go-notify v0.0.0-20190209221809-c404b1f22435 h1:AnwbdEI8eV3GzLM3SlrJlYmYa6OB5X8RwY4A8QJOCP0= github.com/dhaavi/go-notify v0.0.0-20190209221809-c404b1f22435/go.mod h1:EMJ8XWTopp8OLRBMUm9vHE8Wn48CNpU21HM817OKNrc= github.com/dhaavi/winres v0.2.2 h1:SUago7FwhgLSMyDdeuV6enBZ+ZQSl0KwcnbWzvlfBls= @@ -62,6 +86,9 @@ github.com/dhaavi/winres v0.2.2/go.mod h1:1NTs+/DtKP1BplIL1+XQSoq4X1PUfLczexS7gf github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/felixge/httpsnoop v1.0.0/go.mod h1:3+D9sFq0ahK/JeJPhCBUV1xlf4/eIYrUQaxulT0VzX8= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= @@ -72,30 +99,54 @@ github.com/florianl/go-nfqueue v1.3.2/go.mod h1:eSnAor2YCfMCVYrVNEhkLGN/r1L+J4uD github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/friendsofgo/errors v0.9.2 h1:X6NYxef4efCBdwI7BgS820zFaN7Cphrmb+Pljdzjtgk= +github.com/friendsofgo/errors v0.9.2/go.mod h1:yCvFW5AkDIL9qn7suHVLiI/gH228n7PC4Pn44IGoTOI= github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= +github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= +github.com/go-sql-driver/mysql v1.7.2-0.20231213112541-0004702b931d h1:QQP1nE4qh5aHTGvI1LgOFxZYVxYoGeMfbNHikogPyoA= +github.com/go-sql-driver/mysql v1.7.2-0.20231213112541-0004702b931d/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= +github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4= github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godror/godror v0.40.4 h1:X1e7hUd02GDaLWKZj40Z7L0CP0W9TrGgmPQZw6+anBg= +github.com/godror/godror v0.40.4/go.mod h1:i8YtVTHUJKfFT3wTat4A9UoqScUtZXiYB9Rf3SVARgc= +github.com/godror/knownpb v0.1.1 h1:A4J7jdx7jWBhJm18NntafzSC//iZDHkDi1+juwQ5pTI= +github.com/godror/knownpb v0.1.1/go.mod h1:4nRFbQo1dDuwKnblRXDxrfCFYeT4hjg3GjMqef58eRE= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/gddo v0.0.0-20180823221919-9d8ff1c67be5/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4= github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f h1:16RtHeWGkJMc80Etb8RPCcKevXGldr57+LOyZt8zOlg= github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f/go.mod h1:ijRvpgDJDI262hYq/IQVYgf8hd8IHUs93Ol0kvMBAx4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/lint v0.0.0-20170918230701-e5d664eb928e/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -115,8 +166,12 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= @@ -128,14 +183,25 @@ github.com/gregjones/httpcache v0.0.0-20170920190843-316c5e0ff04e/go.mod h1:Fecb github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v0.0.0-20170914154624-68e816d1c783/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb h1:PGufWXXDq9yaev6xX1YQauaO1MV90e6Mpoq1I7Lz/VM= github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb/go.mod h1:QiyDdbZLaJ/mZP4Zwc9g2QsfaEA4o7XvvgZegSci5/E= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= +github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/log15 v0.0.0-20170622235902-74a0988b5f80/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -158,8 +224,20 @@ github.com/jsimonetti/rtnetlink v0.0.0-20211022192332-93da33804786 h1:N527AHMa79 github.com/jsimonetti/rtnetlink v0.0.0-20211022192332-93da33804786/go.mod h1:v4hqbTdfQngbVSZJVWUhGE/lbTFf9jb+ygmNUDQMuOs= github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM= github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE= -github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= -github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= +github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= +github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU= +github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU= +github.com/knadh/koanf/providers/env v0.1.0 h1:LqKteXqfOWyx5Ab9VfGHmjY9BvRXi+clwyZozgVRiKg= +github.com/knadh/koanf/providers/env v0.1.0/go.mod h1:RE8K9GbACJkeEnkl8L/Qcj8p4ZyPXZIQ191HJi44ZaQ= +github.com/knadh/koanf/providers/file v0.1.0 h1:fs6U7nrV58d3CFAFh8VTde8TM262ObYf3ODrc//Lp+c= +github.com/knadh/koanf/providers/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA= +github.com/knadh/koanf/v2 v2.1.0 h1:eh4QmHHBuU8BybfIJ8mB8K8gsGCD/AUQTdwGq/GzId8= +github.com/knadh/koanf/v2 v2.1.0/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -168,6 +246,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lmittmann/tint v1.0.6 h1:vkkuDAZXc0EFGNzYjWcV0h7eEX+uujH48f/ifSkJWgc= github.com/lmittmann/tint v1.0.6/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -176,12 +256,23 @@ github.com/maruel/panicparse/v2 v2.4.0 h1:yQKMIbQ0DKfinzVkTkcUzQyQ60UCiNnYfR7PWw github.com/maruel/panicparse/v2 v2.4.0/go.mod h1:nOY2OKe8csO3F3SA5+hsxot05JLgukrF54B9x88fVp4= github.com/mat/besticon v3.12.0+incompatible h1:1KTD6wisfjfnX+fk9Kx/6VEZL+MAW1LhCkL9Q47H9Bg= github.com/mat/besticon v3.12.0+incompatible/go.mod h1:mA1auQYHt6CW5e7L9HJLmqVQC8SzNk2gVwouO0AbiEU= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-oci8 v0.1.1 h1:aEUDxNAyDG0tv8CA3TArnDQNyc4EhnWlsfxRgDHABHM= +github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= +github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo= github.com/mdlayher/ethtool v0.0.0-20211028163843-288d040e9d60/go.mod h1:aYbhishWc4Ai3I2U4Gaa2n3kHWSwzme6EsG/46HRQbE= github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc= @@ -207,6 +298,9 @@ github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= +github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= +github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -214,6 +308,7 @@ github.com/mitchellh/go-server-timing v1.0.1 h1:f00/aIe8T3MrnLhQHu3tSWvnwc5GV/p5 github.com/mitchellh/go-server-timing v1.0.1/go.mod h1:Mo6GKi9FSLwWFAMn3bqVPWe20y5ri5QGQuO9D9MCOxk= github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= @@ -222,6 +317,12 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 h1:NHrXEjTNQY7P0Zfx1aMrNhpgxHmow66XQtm0aQLY0AE= +github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8= +github.com/oklog/ulid/v2 v2.0.2 h1:r4fFzBm+bv0wNKNh5eXTwU7i85y5x+uwkxCUTNVQqLc= +github.com/oklog/ulid/v2 v2.0.2/go.mod h1:mtBL0Qe/0HAx6/a4Z30qxVIAL1eQDweXq5lxOEiwQ68= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE= github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8= github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= @@ -231,6 +332,13 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= +github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= +github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 h1:wSmWgpuccqS2IOfmYrbRiUgv+g37W5suLLLxwwniTSc= +github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494/go.mod h1:yipyliwI08eQ6XwDm1fEwKPdF/xdbkiHtrU+1Hg+vc4= github.com/r3labs/diff/v3 v3.0.1 h1:CBKqf3XmNRHXKmdU7mZP1w7TV0pDyVCis1AUHtA4Xtg= github.com/r3labs/diff/v3 v3.0.1/go.mod h1:f1S9bourRbiM66NskseyUdo0fTmEE0qKrikYJX63dgo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= @@ -239,12 +347,15 @@ github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDN github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rot256/pblind v0.0.0-20240730113005-f3275049ead5 h1:R/qQ2Hw5/BgVQS87pg/mRTep8RxqDY0rcj4pbpBvNF8= github.com/rot256/pblind v0.0.0-20240730113005-f3275049ead5/go.mod h1:NTdpGnZ/E2cKXTiAz824w1p6OIm0mBbXcyuiYPCi/Ps= +github.com/rubenv/sql-migrate v1.7.1 h1:f/o0WgfO/GqNuVg+6801K/KW3WdDSupzSjDYODmiUq4= +github.com/rubenv/sql-migrate v1.7.1/go.mod h1:Ob2Psprc0/3ggbM6wCzyYVFFuc6FyZrb2AS+ezLDFb4= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/safing/jess v0.3.5 h1:KS5elTKfWcDUow8SUoCj5QdyyGJNoExJNySerNkbxUU= github.com/safing/jess v0.3.5/go.mod h1:+B6UJnXVxi406Wk08SDnoC5NNBL7t3N0vZGokEbkVQI= -github.com/safing/structures v1.1.0 h1:QzHBQBjaZSLzw2f6PM4ibSmPcfBHAOB5CKJ+k4FYkhQ= -github.com/safing/structures v1.1.0/go.mod h1:QUrB74FcU41ahQ5oy3YNFCoSq+twE/n3+vNZc2K35II= +github.com/safing/structures v1.2.0 h1:S6EzKxxGYTO6P9P3Dkab9gisLOrfAyvy7JzFOUSkOUk= +github.com/safing/structures v1.2.0/go.mod h1:zIun7mz3xV0dJ3vXRZuU71ATzT8D/0hGJO+u+bk5Kvs= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/seehuhn/fortuna v1.0.1 h1:lu9+CHsmR0bZnx5Ay646XvCSRJ8PJTi5UYJwDBX68H0= @@ -253,12 +364,18 @@ github.com/seehuhn/sha256d v1.0.0 h1:TXTsAuEWr02QjRm153Fnvvb6fXXDo7Bmy1FizxarGYw github.com/seehuhn/sha256d v1.0.0/go.mod h1:PEuxg9faClSveVuFXacQmi+NtDI/PX8bpKjtNzf2+s4= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v0.0.0-20170901052352-ee1bd8ee15a1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.1.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= @@ -272,12 +389,21 @@ github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7Sr github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spkg/zipfs v0.7.1 h1:+2X5lvNHTybnDMQZAIHgedRXZK1WXdc+94R/P5v2XWE= github.com/spkg/zipfs v0.7.1/go.mod h1:48LW+/Rh1G7aAav1ew1PdlYn52T+LM+ARmSHfDNJvg8= +github.com/stephenafamo/bob v0.30.0 h1:pGCS3iMh1xr2xlAP20eBV6gmqun6CCH/05l7uJJfvJI= +github.com/stephenafamo/bob v0.30.0/go.mod h1:0z9AeWTOTJmGsokEtQReTEJry4iI9J+SCyKMcr40mOo= +github.com/stephenafamo/fakedb v0.0.0-20221230081958-0b86f816ed97 h1:XItoZNmhOih06TC02jK7l3wlpZ0XT/sPQYutDcGOQjg= +github.com/stephenafamo/fakedb v0.0.0-20221230081958-0b86f816ed97/go.mod h1:bM3Vmw1IakoaXocHmMIGgJFYob0vuK+CFWiJHQvz0jQ= +github.com/stephenafamo/scan v0.6.1 h1:nXokGCQwYazMuyvdNAoK0T8Z76FWcpMvDdtengpz6PU= +github.com/stephenafamo/scan v0.6.1/go.mod h1:FhIUJ8pLNyex36xGFiazDJJ5Xry0UkAi+RkWRrEcRMg= +github.com/stephenafamo/sqlparser v0.0.0-20241111104950-b04fa8a26c9c h1:JFga++XBnZG2xlnvQyHJkeBWZ9G9mGdtgvLeSRbp/BA= +github.com/stephenafamo/sqlparser v0.0.0-20241111104950-b04fa8a26c9c/go.mod h1:4iveRk8mkzQZxDuK/W0MGLrGmu/igyDYWNDD4a6v0r0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= @@ -297,11 +423,13 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= -github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo= -github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= +github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= +github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26 h1:UFHFmFfixpmfRBcxuu+LA9l8MdURWVdVNUHxO5n1d2w= github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26/go.mod h1:IGhd0qMDsUa9acVjsbsT7bu3ktadtGOHI79+idTew/M= +github.com/urfave/cli/v2 v2.23.7 h1:YHDQ46s3VghFHFf1DdF+Sh7H4RqhcM+t0TmZRJx4oJY= +github.com/urfave/cli/v2 v2.23.7/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8= github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ= @@ -315,11 +443,18 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/volatiletech/inflect v0.0.1 h1:2a6FcMQyhmPZcLa+uet3VJ8gLn/9svWhJxJYwvE8KsU= +github.com/volatiletech/inflect v0.0.1/go.mod h1:IBti31tG6phkHitLlr5j7shC5SOo//x0AjDzaJU1PLA= +github.com/volatiletech/strmangle v0.0.6 h1:AdOYE3B2ygRDq4rXDij/MMwq6KVK/pWAYxpC7CLrkKQ= +github.com/volatiletech/strmangle v0.0.6/go.mod h1:ycDvbDkjDvhC0NUU8w3fWwl5JEMTV56vTKXzR3GeR+0= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= @@ -330,26 +465,32 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= -go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= -go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= +go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= +go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= -golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= -golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= -golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= +golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ= +golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= @@ -371,13 +512,16 @@ golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -390,7 +534,9 @@ golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -405,6 +551,7 @@ golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -412,17 +559,27 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -431,52 +588,62 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= -golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.0.0-20170921000349-586095a6e407/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20170918111702-1e559d0a00ee/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU= -google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= honnef.co/go/tools v0.2.2/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= -modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0= -modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.23.10 h1:DnDZT/H6TtoJvQmVf7d8W+lVqEZpIJY/+0ENFh1LIHE= -modernc.org/ccgo/v4 v4.23.10/go.mod h1:vdN4h2WR5aEoNondUx26K7G8X+nuBscYnAEWSRmN2/0= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.20.7 h1:skrinQsjxWfvj6nbC3ztZPJy+NuwmB3hV9zX/pthNYQ= +modernc.org/ccgo/v4 v4.20.7/go.mod h1:UOkI3JSG2zT4E2ioHlncSOZsXbuDCZLvPi3uMlZT5GY= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= -modernc.org/gc/v2 v2.6.1 h1:+Qf6xdG8l7B27TQ8D8lw/iFMUj1RXRBOuMUWziJOsk8= -modernc.org/gc/v2 v2.6.1/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/libc v1.61.7 h1:exz8rasFniviSgh3dH7QBnQHqYh9lolA5hVYfsiwkfo= -modernc.org/libc v1.61.7/go.mod h1:xspSrXRNVSfWfcfqgvZDVe/Hw5kv4FVC6IRfoms5v/0= -modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= -modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= -modernc.org/memory v1.8.1 h1:HS1HRg1jEohnuONobEq2WrLEhLyw8+J42yLFTnllm2A= -modernc.org/memory v1.8.1/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= -modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= -modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= -modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= -modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8= -modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk= -modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= -modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M= +modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/libc v1.59.9 h1:k+nNDDakwipimgmJ1D9H466LhFeSkaPPycAs1OZiDmY= +modernc.org/libc v1.59.9/go.mod h1:EY/egGEU7Ju66eU6SBqCNYaFUDuc4npICkMWnU5EE3A= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s= +modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -zombiezen.com/go/sqlite v1.4.0 h1:N1s3RIljwtp4541Y8rM880qgGIgq3fTD2yks1xftnKU= -zombiezen.com/go/sqlite v1.4.0/go.mod h1:0w9F1DN9IZj9AcLS9YDKMboubCACkwYCGkzoy3eG5ik= +mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E= +mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js= +zombiezen.com/go/sqlite v1.3.0 h1:98g1gnCm+CNz6AuQHu0gqyw7gR2WU3O3PJufDOStpUs= +zombiezen.com/go/sqlite v1.3.0/go.mod h1:yRl27//s/9aXU3RWs8uFQwjkTG9gYNGEls6+6SvrclY= From 90ead7d5e557b9d2ede2c01ed3b7995ece8a9cf4 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 25 Feb 2025 11:43:29 +0100 Subject: [PATCH 07/33] Switch core and cache databases to use sqlite when bbold db is not present --- base/database/dbmodule/db.go | 5 +++++ service/core/base/databases.go | 34 +++++++++++++++++++++------------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/base/database/dbmodule/db.go b/base/database/dbmodule/db.go index 99991c88..fc36cba0 100644 --- a/base/database/dbmodule/db.go +++ b/base/database/dbmodule/db.go @@ -36,6 +36,11 @@ func SetDatabaseLocation(dirStructureRoot *utils.DirStructure) { } } +// GetDatabaseLocation returns the initialized database location. +func GetDatabaseLocation() string { + return databaseStructureRoot.Path +} + func prep() error { SetDatabaseLocation(dataroot.Root()) if databaseStructureRoot == nil { diff --git a/service/core/base/databases.go b/service/core/base/databases.go index c0e9dca4..98c587a1 100644 --- a/service/core/base/databases.go +++ b/service/core/base/databases.go @@ -1,43 +1,51 @@ package base import ( + "path/filepath" + "github.com/safing/portmaster/base/database" - _ "github.com/safing/portmaster/base/database/dbmodule" + "github.com/safing/portmaster/base/database/dbmodule" _ "github.com/safing/portmaster/base/database/storage/bbolt" + "github.com/safing/portmaster/base/utils" ) // Default Values (changeable for testing). var ( - DefaultDatabaseStorageType = "bbolt" + DefaultDatabaseStorageType = "sqlite" ) func registerDatabases() error { + // If there is an existing bbolt core database, use it instead. + coreStorageType := DefaultDatabaseStorageType + if utils.PathExists(filepath.Join(dbmodule.GetDatabaseLocation(), "core", "bbolt")) { + coreStorageType = "bbolt" + } + + // Register core database. _, err := database.Register(&database.Database{ Name: "core", Description: "Holds core data, such as settings and profiles", - StorageType: DefaultDatabaseStorageType, + StorageType: coreStorageType, }) if err != nil { return err } + // If there is an existing cache bbolt database, use it instead. + cacheStorageType := DefaultDatabaseStorageType + if utils.PathExists(filepath.Join(dbmodule.GetDatabaseLocation(), "cache", "bbolt")) { + cacheStorageType = "bbolt" + } + + // Register cache database. _, err = database.Register(&database.Database{ Name: "cache", Description: "Cached data, such as Intelligence and DNS Records", - StorageType: DefaultDatabaseStorageType, + StorageType: cacheStorageType, }) if err != nil { return err } - // _, err = database.Register(&database.Database{ - // Name: "history", - // Description: "Historic event data", - // StorageType: DefaultDatabaseStorageType, - // }) - // if err != nil { - // return err - // } - return nil } From 4721e58727657693c4d26058dd83baeb5052e7db Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Wed, 26 Feb 2025 13:03:57 +0200 Subject: [PATCH 08/33] Update Go version to 1.24 and toolchain to 1.24.0 to support the 'tool' meta-pattern introduced in go.mod with Go v1.24 --- go.mod | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index c72f079c..bdd7b59a 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/safing/portmaster -go 1.23 +go 1.24 -toolchain go1.23.5 +toolchain go1.24.0 // TODO: Remove when https://github.com/tc-hib/winres/pull/4 is released. replace github.com/tc-hib/winres => github.com/dhaavi/winres v0.2.2 From 83292d761cc69834ce0f3899f1327856d3bf30b8 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 26 Feb 2025 13:13:28 +0100 Subject: [PATCH 09/33] Fix domain list generation within public suffix --- service/intel/entity.go | 34 +++++++++++++++++++--------------- service/intel/entity_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 15 deletions(-) create mode 100644 service/intel/entity_test.go diff --git a/service/intel/entity.go b/service/intel/entity.go index 986ca140..56e0b6a5 100644 --- a/service/intel/entity.go +++ b/service/intel/entity.go @@ -397,28 +397,32 @@ func (e *Entity) getDomainLists(ctx context.Context) { } func splitDomain(domain string) []string { - domain = strings.Trim(domain, ".") - suffix, _ := publicsuffix.PublicSuffix(domain) - if suffix == domain { + // Get suffix. + d := strings.TrimSuffix(domain, ".") + suffix, icann := publicsuffix.PublicSuffix(d) + if suffix == d { return []string{domain} } - domainWithoutSuffix := domain[:len(domain)-len(suffix)] - domainWithoutSuffix = strings.Trim(domainWithoutSuffix, ".") - - splitted := strings.FieldsFunc(domainWithoutSuffix, func(r rune) bool { + // Split all subdomain into labels. + labels := strings.FieldsFunc(d[:len(d)-len(suffix)], func(r rune) bool { return r == '.' }) - domains := make([]string, 0, len(splitted)) - for idx := range splitted { - - d := strings.Join(splitted[idx:], ".") + "." + suffix - if d[len(d)-1] != '.' { - d += "." - } - domains = append(domains, d) + // Build list of all domains up to the public suffix. + domains := make([]string, 0, len(labels)+1) + for idx := range labels { + domains = append( + domains, + strings.Join(labels[idx:], ".")+"."+suffix+".", + ) } + + // If the suffix is not a real TLD, but a public suffix, add it to the list. + if !icann { + domains = append(domains, suffix+".") + } + return domains } diff --git a/service/intel/entity_test.go b/service/intel/entity_test.go new file mode 100644 index 00000000..e249851f --- /dev/null +++ b/service/intel/entity_test.go @@ -0,0 +1,31 @@ +package intel + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var splitDomainTestCases = [][]string{ + // Regular registered domains and subdomains. + {"example.com."}, + {"www.example.com.", "example.com."}, + {"sub.domain.example.com.", "domain.example.com.", "example.com."}, + {"example.co.uk."}, + {"www.example.co.uk.", "example.co.uk."}, + + // TLD or public suffix: Return as is. + {"com."}, + {"googleapis.com."}, + + // Public suffix domains: Return including public suffix. + {"test.googleapis.com.", "googleapis.com."}, + {"sub.domain.googleapis.com.", "domain.googleapis.com.", "googleapis.com."}, +} + +func TestSplitDomain(t *testing.T) { + for _, testCase := range splitDomainTestCases { + splitted := splitDomain(testCase[0]) + assert.Equal(t, testCase, splitted, "result must match") + } +} From 76c352da5adae31f0112a6ca680833ac827a9f65 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 26 Feb 2025 13:21:51 +0100 Subject: [PATCH 10/33] Make test parallel --- service/intel/entity_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/service/intel/entity_test.go b/service/intel/entity_test.go index e249851f..83a8fd7b 100644 --- a/service/intel/entity_test.go +++ b/service/intel/entity_test.go @@ -24,6 +24,8 @@ var splitDomainTestCases = [][]string{ } func TestSplitDomain(t *testing.T) { + t.Parallel() + for _, testCase := range splitDomainTestCases { splitted := splitDomain(testCase[0]) assert.Equal(t, testCase, splitted, "result must match") From 130c4a427c15dfaa5549408a68a786b81084f811 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 26 Feb 2025 13:23:38 +0100 Subject: [PATCH 11/33] Fix timing on database root path initialization --- base/database/dbmodule/db.go | 7 ------- base/database/main.go | 7 +++---- service/core/base/databases.go | 6 +++--- service/core/base/global.go | 4 ++++ 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/base/database/dbmodule/db.go b/base/database/dbmodule/db.go index fc36cba0..e975355b 100644 --- a/base/database/dbmodule/db.go +++ b/base/database/dbmodule/db.go @@ -5,7 +5,6 @@ import ( "sync/atomic" "github.com/safing/portmaster/base/database" - "github.com/safing/portmaster/base/dataroot" "github.com/safing/portmaster/base/utils" "github.com/safing/portmaster/service/mgr" ) @@ -36,13 +35,7 @@ func SetDatabaseLocation(dirStructureRoot *utils.DirStructure) { } } -// GetDatabaseLocation returns the initialized database location. -func GetDatabaseLocation() string { - return databaseStructureRoot.Path -} - func prep() error { - SetDatabaseLocation(dataroot.Root()) if databaseStructureRoot == nil { return errors.New("database location not specified") } diff --git a/base/database/main.go b/base/database/main.go index 9c9aa1ed..cbf2460c 100644 --- a/base/database/main.go +++ b/base/database/main.go @@ -9,9 +9,8 @@ import ( "github.com/safing/portmaster/base/utils" ) -const ( - databasesSubDir = "databases" -) +// DatabasesSubDir defines the sub directory where the databases are stored. +const DatabasesSubDir = "databases" var ( initialized = abool.NewBool(false) @@ -34,7 +33,7 @@ func Initialize(dirStructureRoot *utils.DirStructure) error { rootStructure = dirStructureRoot // ensure root and databases dirs - databasesStructure = rootStructure.ChildDir(databasesSubDir, utils.AdminOnlyPermission) + databasesStructure = rootStructure.ChildDir(DatabasesSubDir, utils.AdminOnlyPermission) err := databasesStructure.Ensure() if err != nil { return fmt.Errorf("could not create/open database directory (%s): %w", rootStructure.Path, err) diff --git a/service/core/base/databases.go b/service/core/base/databases.go index 98c587a1..68e7819b 100644 --- a/service/core/base/databases.go +++ b/service/core/base/databases.go @@ -4,8 +4,8 @@ import ( "path/filepath" "github.com/safing/portmaster/base/database" - "github.com/safing/portmaster/base/database/dbmodule" _ "github.com/safing/portmaster/base/database/storage/bbolt" + "github.com/safing/portmaster/base/dataroot" "github.com/safing/portmaster/base/utils" ) @@ -17,7 +17,7 @@ var ( func registerDatabases() error { // If there is an existing bbolt core database, use it instead. coreStorageType := DefaultDatabaseStorageType - if utils.PathExists(filepath.Join(dbmodule.GetDatabaseLocation(), "core", "bbolt")) { + if utils.PathExists(filepath.Join(dataroot.Root().Path, database.DatabasesSubDir, "core", "bbolt")) { coreStorageType = "bbolt" } @@ -33,7 +33,7 @@ func registerDatabases() error { // If there is an existing cache bbolt database, use it instead. cacheStorageType := DefaultDatabaseStorageType - if utils.PathExists(filepath.Join(dbmodule.GetDatabaseLocation(), "cache", "bbolt")) { + if utils.PathExists(filepath.Join(dataroot.Root().Path, database.DatabasesSubDir, "cache", "bbolt")) { cacheStorageType = "bbolt" } diff --git a/service/core/base/global.go b/service/core/base/global.go index fa67048f..fbc81dcd 100644 --- a/service/core/base/global.go +++ b/service/core/base/global.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/safing/portmaster/base/api" + "github.com/safing/portmaster/base/database/dbmodule" "github.com/safing/portmaster/base/dataroot" "github.com/safing/portmaster/base/info" "github.com/safing/portmaster/base/utils" @@ -59,6 +60,9 @@ func prep(instance instance) error { if err != nil { return err } + + // Set root dir for the databases. + dbmodule.SetDatabaseLocation(dataroot.Root()) } // set api listen address From b68646c68919489a893b376e492a60f28527d4e2 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 26 Feb 2025 16:52:34 +0100 Subject: [PATCH 12/33] Use transaction for PutMany and cursor Query --- base/database/storage/sqlite/sqlite.go | 74 ++++++++++++++++----- base/database/storage/sqlite/sqlite_test.go | 18 ++--- service/core/base/databases.go | 1 + 3 files changed, 66 insertions(+), 27 deletions(-) diff --git a/base/database/storage/sqlite/sqlite.go b/base/database/storage/sqlite/sqlite.go index ba9b11c7..53823ecd 100644 --- a/base/database/storage/sqlite/sqlite.go +++ b/base/database/storage/sqlite/sqlite.go @@ -99,8 +99,15 @@ func (db *SQLite) GetMeta(key string) (*record.Meta, error) { // Put stores a record in the database. func (db *SQLite) Put(r record.Record) (record.Record, error) { - r.Lock() - defer r.Unlock() + return db.putRecord(r, nil) +} + +func (db *SQLite) putRecord(r record.Record, tx *bob.Tx) (record.Record, error) { + // Lock record if in a transaction. + if tx != nil { + r.Lock() + defer r.Unlock() + } // Serialize to JSON. data, err := r.MarshalDataOnly(r, dsd.JSON) @@ -127,12 +134,19 @@ func (db *SQLite) Put(r record.Record) (record.Record, error) { defer db.lock.Unlock() // Simulate upsert with custom selection on conflict. - _, err = models.Records.Insert( + dbQuery := models.Records.Insert( &setter, im.OnConflict("key").DoUpdate( im.SetExcluded("format", "value", "created", "modified", "expires", "deleted", "secret", "crownjewel"), ), - ).Exec(db.ctx, db.bob) + ) + + // Execute in transaction or directly. + if tx != nil { + _, err = dbQuery.Exec(db.ctx, tx) + } else { + _, err = dbQuery.Exec(db.ctx, db.bob) + } if err != nil { return nil, err } @@ -150,16 +164,39 @@ func (db *SQLite) PutMany(shadowDelete bool) (chan<- record.Record, <-chan error batch := make(chan record.Record, 100) errs := make(chan error, 1) + tx, err := db.bob.BeginTx(db.ctx, nil) + if err != nil { + errs <- err + return batch, errs + } + // start handler go func() { - for r := range batch { - _, err := db.Put(r) - if err != nil { - errs <- err - return + // Read all put records. + writeBatch: + for { + select { + case r := <-batch: + if r != nil { + // Write record. + _, err := db.putRecord(r, &tx) + if err != nil { + errs <- err + break writeBatch + } + } else { + // Finalize transcation. + errs <- tx.Commit() + return + } + + case <-db.ctx.Done(): + break writeBatch } } - errs <- nil + + // Rollback transaction. + errs <- tx.Rollback() }() return batch, errs @@ -199,20 +236,25 @@ func (db *SQLite) queryExecutor(queryIter *iterator.Iterator, q *query.Query, lo recordQuery = models.Records.View.Query() } - // Get all records from query. - // TODO: This will load all records into memory. While this is efficient and - // will not block others from using the datbase, this might be quite a strain - // on the system memory. Monitor and see if this is an issue. + // Get cursor to go over all records in the query. db.lock.RLock() - records, err := models.RecordsQuery.All(recordQuery, db.ctx, db.bob) + cursor, err := models.RecordsQuery.Cursor(recordQuery, db.ctx, db.bob) db.lock.RUnlock() if err != nil { queryIter.Finish(err) return } + defer cursor.Close() recordsLoop: - for _, r := range records { + for cursor.Next() { + // Get next record + r, cErr := cursor.Get() + if cErr != nil { + err = fmt.Errorf("cursor error: %w", cErr) + break recordsLoop + } + // Check if key matches. if !q.MatchesKey(r.Key) { continue recordsLoop diff --git a/base/database/storage/sqlite/sqlite_test.go b/base/database/storage/sqlite/sqlite_test.go index 0fc1a4a1..188b068d 100644 --- a/base/database/storage/sqlite/sqlite_test.go +++ b/base/database/storage/sqlite/sqlite_test.go @@ -115,17 +115,13 @@ func TestSQLite(t *testing.T) { qZ := &TestRecord{} qZ.SetKey("test:z") qZ.CreateMeta() - // put - _, err = db.Put(qA) - if err == nil { - _, err = db.Put(qB) - } - if err == nil { - _, err = db.Put(qC) - } - if err == nil { - _, err = db.Put(qZ) - } + put, errs := db.PutMany(false) + put <- qA + put <- qB + put <- qC + put <- qZ + close(put) + err = <-errs if err != nil { t.Fatal(err) } diff --git a/service/core/base/databases.go b/service/core/base/databases.go index 68e7819b..c0f0f6b0 100644 --- a/service/core/base/databases.go +++ b/service/core/base/databases.go @@ -5,6 +5,7 @@ import ( "github.com/safing/portmaster/base/database" _ "github.com/safing/portmaster/base/database/storage/bbolt" + _ "github.com/safing/portmaster/base/database/storage/sqlite" "github.com/safing/portmaster/base/dataroot" "github.com/safing/portmaster/base/utils" ) From c04213219bc647021415adce5447557cf863973f Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 28 Feb 2025 10:21:59 +0100 Subject: [PATCH 13/33] Make format and value nullable and improve maintenance and purge queries --- .../storage/sqlite/migrations/1_initial.sql | 4 +- .../storage/sqlite/models/records.bob.go | 50 +++---- base/database/storage/sqlite/sqlite.go | 133 +++++++++++++++--- base/database/storage/sqlite/sqlite_test.go | 24 ++-- go.mod | 1 + go.sum | 2 + 6 files changed, 155 insertions(+), 59 deletions(-) diff --git a/base/database/storage/sqlite/migrations/1_initial.sql b/base/database/storage/sqlite/migrations/1_initial.sql index e0c9ded7..172dbded 100644 --- a/base/database/storage/sqlite/migrations/1_initial.sql +++ b/base/database/storage/sqlite/migrations/1_initial.sql @@ -3,8 +3,8 @@ CREATE TABLE records ( key TEXT PRIMARY KEY, - format SMALLINT NOT NULL, - value BLOB NOT NULL, + format SMALLINT, + value BLOB, created BIGINT NOT NULL, modified BIGINT NOT NULL, diff --git a/base/database/storage/sqlite/models/records.bob.go b/base/database/storage/sqlite/models/records.bob.go index 5f90d27a..02561c19 100644 --- a/base/database/storage/sqlite/models/records.bob.go +++ b/base/database/storage/sqlite/models/records.bob.go @@ -7,7 +7,9 @@ import ( "context" "io" + "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" + "github.com/aarondl/opt/omitnull" "github.com/stephenafamo/bob" "github.com/stephenafamo/bob/dialect/sqlite" "github.com/stephenafamo/bob/dialect/sqlite/dialect" @@ -19,15 +21,15 @@ import ( // Record is an object representing the database table. type Record struct { - Key string `db:"key,pk" ` - Format int16 `db:"format" ` - Value []byte `db:"value" ` - Created int64 `db:"created" ` - Modified int64 `db:"modified" ` - Expires int64 `db:"expires" ` - Deleted int64 `db:"deleted" ` - Secret bool `db:"secret" ` - Crownjewel bool `db:"crownjewel" ` + Key string `db:"key,pk" ` + Format null.Val[int16] `db:"format" ` + Value null.Val[[]byte] `db:"value" ` + Created int64 `db:"created" ` + Modified int64 `db:"modified" ` + Expires int64 `db:"expires" ` + Deleted int64 `db:"deleted" ` + Secret bool `db:"secret" ` + Crownjewel bool `db:"crownjewel" ` } // RecordSlice is an alias for a slice of pointers to Record. @@ -92,8 +94,8 @@ func buildRecordColumns(alias string) recordColumns { type recordWhere[Q sqlite.Filterable] struct { Key sqlite.WhereMod[Q, string] - Format sqlite.WhereMod[Q, int16] - Value sqlite.WhereMod[Q, []byte] + Format sqlite.WhereNullMod[Q, int16] + Value sqlite.WhereNullMod[Q, []byte] Created sqlite.WhereMod[Q, int64] Modified sqlite.WhereMod[Q, int64] Expires sqlite.WhereMod[Q, int64] @@ -109,8 +111,8 @@ func (recordWhere[Q]) AliasedAs(alias string) recordWhere[Q] { func buildRecordWhere[Q sqlite.Filterable](cols recordColumns) recordWhere[Q] { return recordWhere[Q]{ Key: sqlite.Where[Q, string](cols.Key), - Format: sqlite.Where[Q, int16](cols.Format), - Value: sqlite.Where[Q, []byte](cols.Value), + Format: sqlite.WhereNull[Q, int16](cols.Format), + Value: sqlite.WhereNull[Q, []byte](cols.Value), Created: sqlite.Where[Q, int64](cols.Created), Modified: sqlite.Where[Q, int64](cols.Modified), Expires: sqlite.Where[Q, int64](cols.Expires), @@ -124,15 +126,15 @@ func buildRecordWhere[Q sqlite.Filterable](cols recordColumns) recordWhere[Q] { // All values are optional, and do not have to be set // Generated columns are not included type RecordSetter struct { - Key omit.Val[string] `db:"key,pk" ` - Format omit.Val[int16] `db:"format" ` - Value omit.Val[[]byte] `db:"value" ` - Created omit.Val[int64] `db:"created" ` - Modified omit.Val[int64] `db:"modified" ` - Expires omit.Val[int64] `db:"expires" ` - Deleted omit.Val[int64] `db:"deleted" ` - Secret omit.Val[bool] `db:"secret" ` - Crownjewel omit.Val[bool] `db:"crownjewel" ` + Key omit.Val[string] `db:"key,pk" ` + Format omitnull.Val[int16] `db:"format" ` + Value omitnull.Val[[]byte] `db:"value" ` + Created omit.Val[int64] `db:"created" ` + Modified omit.Val[int64] `db:"modified" ` + Expires omit.Val[int64] `db:"expires" ` + Deleted omit.Val[int64] `db:"deleted" ` + Secret omit.Val[bool] `db:"secret" ` + Crownjewel omit.Val[bool] `db:"crownjewel" ` } func (s RecordSetter) SetColumns() []string { @@ -181,10 +183,10 @@ func (s RecordSetter) Overwrite(t *Record) { t.Key, _ = s.Key.Get() } if !s.Format.IsUnset() { - t.Format, _ = s.Format.Get() + t.Format, _ = s.Format.GetNull() } if !s.Value.IsUnset() { - t.Value, _ = s.Value.Get() + t.Value, _ = s.Value.GetNull() } if !s.Created.IsUnset() { t.Created, _ = s.Created.Get() diff --git a/base/database/storage/sqlite/sqlite.go b/base/database/storage/sqlite/sqlite.go index 53823ecd..38f17ed4 100644 --- a/base/database/storage/sqlite/sqlite.go +++ b/base/database/storage/sqlite/sqlite.go @@ -10,7 +10,15 @@ import ( "time" "github.com/aarondl/opt/omit" + "github.com/aarondl/opt/omitnull" migrate "github.com/rubenv/sql-migrate" + sqldblogger "github.com/simukti/sqldb-logger" + "github.com/stephenafamo/bob" + "github.com/stephenafamo/bob/dialect/sqlite" + "github.com/stephenafamo/bob/dialect/sqlite/im" + "github.com/stephenafamo/bob/dialect/sqlite/um" + _ "modernc.org/sqlite" + "github.com/safing/portmaster/base/database/accessor" "github.com/safing/portmaster/base/database/iterator" "github.com/safing/portmaster/base/database/query" @@ -19,12 +27,6 @@ import ( "github.com/safing/portmaster/base/database/storage/sqlite/models" "github.com/safing/portmaster/base/log" "github.com/safing/structures/dsd" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/sqlite" - "github.com/stephenafamo/bob/dialect/sqlite/im" - "github.com/stephenafamo/bob/dialect/sqlite/um" - - _ "modernc.org/sqlite" ) // SQLite storage. @@ -47,14 +49,40 @@ func init() { // NewSQLite creates a sqlite database. func NewSQLite(name, location string) (*SQLite, error) { + return openSQLite(name, location, false) +} + +// openSQLite creates a sqlite database. +func openSQLite(name, location string, printStmts bool) (*SQLite, error) { dbFile := filepath.Join(location, "db.sqlite") // Open database file. + // Default settings: + // _time_format = YYYY-MM-DDTHH:MM:SS.SSS + // _txlock = deferred db, err := sql.Open("sqlite", dbFile) if err != nil { return nil, fmt.Errorf("open sqlite: %w", err) } + // Enable statement printing. + if printStmts { + db = sqldblogger.OpenDriver(dbFile, db.Driver(), &statementLogger{}) + } + + // Set other settings. + pragmas := []string{ + "PRAGMA journal_mode=WAL;", // Corruption safe write ahead log for txs. + "PRAGMA synchronous=NORMAL;", // Best for WAL. + "PRAGMA cache_size=-10000;", // 10MB Cache. + } + for _, pragma := range pragmas { + _, err := db.Exec(pragma) + if err != nil { + return nil, fmt.Errorf("failed to init sqlite with %s: %w", pragma, err) + } + } + // Run migrations on database. n, err := migrate.Exec(db, "sqlite3", getMigrations(), migrate.Up) if err != nil { @@ -84,7 +112,13 @@ func (db *SQLite) Get(key string) (record.Record, error) { } // Return data in wrapper. - return record.NewWrapperFromDatabase(db.name, key, getMeta(r), uint8(r.Format), r.Value) + return record.NewWrapperFromDatabase( + db.name, + key, + getMeta(r), + uint8(r.Format.GetOrZero()), + r.Value.GetOrZero(), + ) } // GetMeta returns the metadata of a database record. @@ -114,13 +148,20 @@ func (db *SQLite) putRecord(r record.Record, tx *bob.Tx) (record.Record, error) if err != nil { return nil, err } + // Prepare for setter. + setFormat := omitnull.From(int16(dsd.JSON)) + setData := omitnull.From(data) + if len(data) == 0 { + setFormat.Null() + setData.Null() + } // Create structure for insert. m := r.Meta() setter := models.RecordSetter{ Key: omit.From(r.DatabaseKey()), - Format: omit.From(int16(dsd.JSON)), - Value: omit.From(data), + Format: setFormat, + Value: setData, Created: omit.From(m.Created), Modified: omit.From(m.Modified), Expires: omit.From(m.Expires), @@ -269,7 +310,11 @@ recordsLoop: // Check Data. if q.HasWhereCondition() { - jsonData := string(r.Value) + if r.Format.IsNull() || r.Value.IsNull() { + continue recordsLoop + } + + jsonData := string(r.Value.GetOrZero()) jsonAccess := accessor.NewJSONAccessor(&jsonData) if !q.MatchesAccessor(jsonAccess) { continue recordsLoop @@ -277,7 +322,13 @@ recordsLoop: } // Build database record. - matched, _ := record.NewWrapperFromDatabase(db.name, r.Key, m, uint8(r.Format), r.Value) + matched, _ := record.NewWrapperFromDatabase( + db.name, + r.Key, + m, + uint8(r.Format.GetOrZero()), + r.Value.GetOrZero(), + ) select { case <-queryIter.Done: @@ -301,7 +352,7 @@ recordsLoop: // Purge deletes all records that match the given query. It returns the number of successful deletes and an error. func (db *SQLite) Purge(ctx context.Context, q *query.Query, local, internal, shadowDelete bool) (int, error) { - // Optimize for local and internal queries without where clause. + // Optimize for local and internal queries without where clause and without shadow delete. if local && internal && !shadowDelete && !q.HasWhereCondition() { db.lock.Lock() defer db.lock.Unlock() @@ -321,21 +372,49 @@ func (db *SQLite) Purge(ctx context.Context, q *query.Query, local, internal, sh return int(n), err } - // Otherwise, iterate over all entries and delete matching ones. + // Optimize for local and internal queries without where clause, but with shadow delete. + if local && internal && shadowDelete && !q.HasWhereCondition() { + db.lock.Lock() + defer db.lock.Unlock() - // Create iterator to check all matching records. - queryIter := iterator.New() - defer queryIter.Cancel() - go db.queryExecutor(queryIter, q, local, internal) + // First count entries (SQLite does not support affected rows) + n, err := models.Records.Query( + models.SelectWhere.Records.Key.Like(q.DatabaseKeyPrefix()+"%"), + ).Count(db.ctx, db.bob) + if err != nil || n == 0 { + return int(n), err + } - // Delete all matching records. - var deleted int - for r := range queryIter.Next { - db.Delete(r.DatabaseKey()) - deleted++ + // Mark purged records as deleted. + now := time.Now().Unix() + _, err = models.Records.Update( + um.SetCol("format").ToArg(nil), + um.SetCol("value").ToArg(nil), + um.SetCol("deleted").ToArg(now), + models.UpdateWhere.Records.Key.Like(q.DatabaseKeyPrefix()+"%"), + ).Exec(db.ctx, db.bob) + return int(n), err } - return deleted, nil + // Otherwise, iterate over all entries and delete matching ones. + return 0, storage.ErrNotImplemented + + // Create iterator to check all matching records. + + // TODO: This is untested and also needs handling of shadowDelete. + // For now: Use only without where condition and with a local and internal db interface. + // queryIter := iterator.New() + // defer queryIter.Cancel() + // go db.queryExecutor(queryIter, q, local, internal) + + // // Delete all matching records. + // var deleted int + // for r := range queryIter.Next { + // db.Delete(r.DatabaseKey()) + // deleted++ + // } + + // return deleted, nil } // ReadOnly returns whether the database is read only. @@ -360,6 +439,8 @@ func (db *SQLite) MaintainRecordStates(ctx context.Context, purgeDeletedBefore t if shadowDelete { // Mark expired records as deleted. models.Records.Update( + um.SetCol("format").ToArg(nil), + um.SetCol("value").ToArg(nil), um.SetCol("deleted").ToArg(now), models.UpdateWhere.Records.Deleted.EQ(0), models.UpdateWhere.Records.Expires.GT(0), @@ -416,3 +497,9 @@ func (db *SQLite) Shutdown() error { return db.bob.Close() } + +type statementLogger struct{} + +func (sl statementLogger) Log(ctx context.Context, level sqldblogger.Level, msg string, data map[string]interface{}) { + fmt.Printf("SQL: %s --- %+v\n", msg, data) +} diff --git a/base/database/storage/sqlite/sqlite_test.go b/base/database/storage/sqlite/sqlite_test.go index 188b068d..19bb36a0 100644 --- a/base/database/storage/sqlite/sqlite_test.go +++ b/base/database/storage/sqlite/sqlite_test.go @@ -7,10 +7,11 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/safing/portmaster/base/database/query" "github.com/safing/portmaster/base/database/record" "github.com/safing/portmaster/base/database/storage" - "github.com/stretchr/testify/assert" ) var ( @@ -51,7 +52,7 @@ func TestSQLite(t *testing.T) { }() // start - db, err := NewSQLite("test", testDir) + db, err := openSQLite("test", testDir, true) if err != nil { t.Fatal(err) } @@ -105,16 +106,18 @@ func TestSQLite(t *testing.T) { // setup query test records qA := &TestRecord{} qA.SetKey("test:path/to/A") - qA.CreateMeta() + qA.UpdateMeta() qB := &TestRecord{} qB.SetKey("test:path/to/B") - qB.CreateMeta() + qB.UpdateMeta() qC := &TestRecord{} qC.SetKey("test:path/to/C") - qC.CreateMeta() + qC.UpdateMeta() + // Set expiry in the past. + qC.Meta().Expires = time.Now().Add(-time.Hour).Unix() qZ := &TestRecord{} qZ.SetKey("test:z") - qZ.CreateMeta() + qZ.UpdateMeta() put, errs := db.PutMany(false) put <- qA put <- qB @@ -139,7 +142,8 @@ func TestSQLite(t *testing.T) { if it.Err() != nil { t.Fatal(it.Err()) } - if cnt != 3 { + if cnt != 2 { + // Note: One is expired. t.Fatalf("unexpected query result count: %d", cnt) } @@ -156,7 +160,7 @@ func TestSQLite(t *testing.T) { } // maintenance - err = db.MaintainRecordStates(context.TODO(), time.Now(), true) + err = db.MaintainRecordStates(context.TODO(), time.Now().Add(-time.Minute), true) if err != nil { t.Fatal(err) } @@ -168,11 +172,11 @@ func TestSQLite(t *testing.T) { } // purging - n, err := db.Purge(context.TODO(), query.New("test:path/to/").MustBeValid(), true, true, false) + n, err := db.Purge(context.TODO(), query.New("test:path/to/").MustBeValid(), true, true, true) if err != nil { t.Fatal(err) } - if n != 3 { + if n != 2 { t.Fatalf("unexpected purge delete count: %d", n) } diff --git a/go.mod b/go.mod index bdd7b59a..2ce4d421 100644 --- a/go.mod +++ b/go.mod @@ -145,6 +145,7 @@ require ( github.com/satori/go.uuid v1.2.0 // indirect github.com/seehuhn/sha256d v1.0.0 // indirect github.com/shopspring/decimal v1.3.1 // indirect + github.com/simukti/sqldb-logger v0.0.0-20230108155151-646c1a075551 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stephenafamo/scan v0.6.1 // indirect diff --git a/go.sum b/go.sum index 528dc251..7b4cce76 100644 --- a/go.sum +++ b/go.sum @@ -367,6 +367,8 @@ github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMT github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/simukti/sqldb-logger v0.0.0-20230108155151-646c1a075551 h1:+EXKKt7RC4HyE/iE8zSeFL+7YBL8Z7vpBaEE3c7lCnk= +github.com/simukti/sqldb-logger v0.0.0-20230108155151-646c1a075551/go.mod h1:ztTX0ctjRZ1wn9OXrzhonvNmv43yjFUXJYJR95JQAJE= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v0.0.0-20170901052352-ee1bd8ee15a1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= From 71f6f093845f212890f2dfee7594b6fe12496626 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 28 Feb 2025 11:38:27 +0100 Subject: [PATCH 14/33] Use waitgroup instead of mutex for sqlite storage --- base/database/storage/sqlite/sqlite.go | 60 ++++++++++++-------------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/base/database/storage/sqlite/sqlite.go b/base/database/storage/sqlite/sqlite.go index 38f17ed4..325da138 100644 --- a/base/database/storage/sqlite/sqlite.go +++ b/base/database/storage/sqlite/sqlite.go @@ -33,9 +33,9 @@ import ( type SQLite struct { name string - db *sql.DB - bob bob.DB - lock sync.RWMutex + db *sql.DB + bob bob.DB + wg sync.WaitGroup ctx context.Context cancelCtx context.CancelFunc @@ -102,8 +102,8 @@ func openSQLite(name, location string, printStmts bool) (*SQLite, error) { // Get returns a database record. func (db *SQLite) Get(key string) (record.Record, error) { - db.lock.RLock() - defer db.lock.RUnlock() + db.wg.Add(1) + defer db.wg.Done() // Get record from database. r, err := models.FindRecord(db.ctx, db.bob, key) @@ -137,6 +137,9 @@ func (db *SQLite) Put(r record.Record) (record.Record, error) { } func (db *SQLite) putRecord(r record.Record, tx *bob.Tx) (record.Record, error) { + db.wg.Add(1) + defer db.wg.Done() + // Lock record if in a transaction. if tx != nil { r.Lock() @@ -170,10 +173,6 @@ func (db *SQLite) putRecord(r record.Record, tx *bob.Tx) (record.Record, error) Crownjewel: omit.From(m.IsCrownJewel()), } - // Lock for writing. - db.lock.Lock() - defer db.lock.Unlock() - // Simulate upsert with custom selection on conflict. dbQuery := models.Records.Insert( &setter, @@ -197,10 +196,8 @@ func (db *SQLite) putRecord(r record.Record, tx *bob.Tx) (record.Record, error) // PutMany stores many records in the database. func (db *SQLite) PutMany(shadowDelete bool) (chan<- record.Record, <-chan error) { - db.lock.Lock() - defer db.lock.Unlock() - // we could lock for every record, but we want to have the same behaviour - // as the other storage backends, especially for testing. + db.wg.Add(1) + defer db.wg.Done() batch := make(chan record.Record, 100) errs := make(chan error, 1) @@ -245,9 +242,8 @@ func (db *SQLite) PutMany(shadowDelete bool) (chan<- record.Record, <-chan error // Delete deletes a record from the database. func (db *SQLite) Delete(key string) error { - // Lock for writing. - db.lock.Lock() - defer db.lock.Unlock() + db.wg.Add(1) + defer db.wg.Done() toDelete := &models.Record{Key: key} return toDelete.Delete(db.ctx, db.bob) @@ -255,6 +251,9 @@ func (db *SQLite) Delete(key string) error { // Query returns a an iterator for the supplied query. func (db *SQLite) Query(q *query.Query, local, internal bool) (*iterator.Iterator, error) { + db.wg.Add(1) + defer db.wg.Done() + _, err := q.Check() if err != nil { return nil, fmt.Errorf("invalid query: %w", err) @@ -267,6 +266,9 @@ func (db *SQLite) Query(q *query.Query, local, internal bool) (*iterator.Iterato } func (db *SQLite) queryExecutor(queryIter *iterator.Iterator, q *query.Query, local, internal bool) { + db.wg.Add(1) + defer db.wg.Done() + // Build query. var recordQuery *sqlite.ViewQuery[*models.Record, models.RecordSlice] if q.DatabaseKeyPrefix() != "" { @@ -278,9 +280,7 @@ func (db *SQLite) queryExecutor(queryIter *iterator.Iterator, q *query.Query, lo } // Get cursor to go over all records in the query. - db.lock.RLock() cursor, err := models.RecordsQuery.Cursor(recordQuery, db.ctx, db.bob) - db.lock.RUnlock() if err != nil { queryIter.Finish(err) return @@ -352,11 +352,11 @@ recordsLoop: // Purge deletes all records that match the given query. It returns the number of successful deletes and an error. func (db *SQLite) Purge(ctx context.Context, q *query.Query, local, internal, shadowDelete bool) (int, error) { + db.wg.Add(1) + defer db.wg.Done() + // Optimize for local and internal queries without where clause and without shadow delete. if local && internal && !shadowDelete && !q.HasWhereCondition() { - db.lock.Lock() - defer db.lock.Unlock() - // First count entries (SQLite does not support affected rows) n, err := models.Records.Query( models.SelectWhere.Records.Key.Like(q.DatabaseKeyPrefix()+"%"), @@ -374,9 +374,6 @@ func (db *SQLite) Purge(ctx context.Context, q *query.Query, local, internal, sh // Optimize for local and internal queries without where clause, but with shadow delete. if local && internal && shadowDelete && !q.HasWhereCondition() { - db.lock.Lock() - defer db.lock.Unlock() - // First count entries (SQLite does not support affected rows) n, err := models.Records.Query( models.SelectWhere.Records.Key.Like(q.DatabaseKeyPrefix()+"%"), @@ -429,8 +426,8 @@ func (db *SQLite) Injected() bool { // MaintainRecordStates maintains records states in the database. func (db *SQLite) MaintainRecordStates(ctx context.Context, purgeDeletedBefore time.Time, shadowDelete bool) error { - db.lock.Lock() - defer db.lock.Unlock() + db.wg.Add(1) + defer db.wg.Done() now := time.Now().Unix() purgeThreshold := purgeDeletedBefore.Unix() @@ -471,8 +468,8 @@ func (db *SQLite) MaintainRecordStates(ctx context.Context, purgeDeletedBefore t } func (db *SQLite) Maintain(ctx context.Context) error { - db.lock.Lock() - defer db.lock.Unlock() + db.wg.Add(1) + defer db.wg.Done() // Remove up to about 100KB of SQLite pages from the freelist on every run. // (Assuming 4KB page size.) @@ -481,8 +478,8 @@ func (db *SQLite) Maintain(ctx context.Context) error { } func (db *SQLite) MaintainThorough(ctx context.Context) error { - db.lock.Lock() - defer db.lock.Unlock() + db.wg.Add(1) + defer db.wg.Done() // Remove all pages from the freelist. _, err := db.db.ExecContext(ctx, "PRAGMA incremental_vacuum;") @@ -491,8 +488,7 @@ func (db *SQLite) MaintainThorough(ctx context.Context) error { // Shutdown shuts down the database. func (db *SQLite) Shutdown() error { - db.lock.Lock() - defer db.lock.Unlock() + db.wg.Wait() db.cancelCtx() return db.bob.Close() From a13d52b68f50b5fa8a6a4409d7659d7809568521 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 28 Feb 2025 14:44:39 +0100 Subject: [PATCH 15/33] Bump Go versions in CI --- .github/workflows/go.yml | 4 ++-- Earthfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 992f0c62..16447305 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -38,9 +38,9 @@ jobs: cache: false - name: Run golangci-lint - uses: golangci/golangci-lint-action@v4 + uses: golangci/golangci-lint-action@v6 with: - version: v1.60.3 + version: v1.64.5 only-new-issues: true args: -c ./.golangci.yml --timeout 15m diff --git a/Earthfile b/Earthfile index 1bfa233e..6ac59502 100644 --- a/Earthfile +++ b/Earthfile @@ -1,6 +1,6 @@ VERSION --arg-scope-and-set --global-cache 0.8 -ARG --global go_version = 1.22 +ARG --global go_version = 1.24 ARG --global node_version = 18 ARG --global rust_version = 1.79 ARG --global golangci_lint_version = 1.57.1 From 782c07d867d40efdfaf53046634903fd898a3278 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 28 Feb 2025 14:48:09 +0100 Subject: [PATCH 16/33] Update CI Linter config --- .golangci.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 12967f60..f01395b8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -47,10 +47,7 @@ linters-settings: revive: # See https://github.com/mgechev/revive#available-rules for details. enable-all-rules: true - gci: - # put imports beginning with prefix after 3rd-party packages; - # only support one prefix - # if not set, use goimports.local-prefixes + goimports: local-prefixes: github.com/safing godox: # report any comments starting with keywords, this is useful for TODO or FIXME comments that From b8ab348095e73ae595b5756aadc07e1298463abd Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 4 Mar 2025 15:25:44 +0100 Subject: [PATCH 17/33] Fix tests and linters --- .github/workflows/go.yml | 2 +- .golangci.yml | 8 ++--- Earthfile | 2 +- base/database/storage/sqlite/schema.go | 10 +----- base/database/storage/sqlite/sqlite.go | 38 ++++++++++++++++----- base/database/storage/sqlite/sqlite_test.go | 17 +++------ go.mod | 2 +- service/intel/geoip/init_test.go | 2 ++ 8 files changed, 41 insertions(+), 40 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 16447305..7d94e54a 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -40,7 +40,7 @@ jobs: - name: Run golangci-lint uses: golangci/golangci-lint-action@v6 with: - version: v1.64.5 + version: v1.64.6 only-new-issues: true args: -c ./.golangci.yml --timeout 15m diff --git a/.golangci.yml b/.golangci.yml index f01395b8..3811a25e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -8,7 +8,6 @@ linters: - contextcheck - cyclop - depguard - - exhaustivestruct - exhaustruct - forbidigo - funlen @@ -16,12 +15,8 @@ linters: - gochecknoinits - gocognit - gocyclo - - goerr113 - - gomnd - gomoddirectives - - ifshort - interfacebloat - - interfacer - ireturn - lll - mnd @@ -32,7 +27,6 @@ linters: - noctx - nolintlint - nonamedreturns - - nosnakecase - perfsprint # TODO(ppacher): we should re-enanble this one to avoid costly fmt.* calls in the hot-path - revive - tagliatelle @@ -42,6 +36,8 @@ linters: - whitespace - wrapcheck - wsl + - gci + - tenv # Deprecated linters-settings: revive: diff --git a/Earthfile b/Earthfile index 6ac59502..67a7759b 100644 --- a/Earthfile +++ b/Earthfile @@ -3,7 +3,7 @@ VERSION --arg-scope-and-set --global-cache 0.8 ARG --global go_version = 1.24 ARG --global node_version = 18 ARG --global rust_version = 1.79 -ARG --global golangci_lint_version = 1.57.1 +ARG --global golangci_lint_version = 1.64.6 ARG --global go_builder_image = "golang:${go_version}-alpine" ARG --global node_builder_image = "node:${node_version}" diff --git a/base/database/storage/sqlite/schema.go b/base/database/storage/sqlite/schema.go index c87402f3..fcf87bcd 100644 --- a/base/database/storage/sqlite/schema.go +++ b/base/database/storage/sqlite/schema.go @@ -19,6 +19,7 @@ import ( "embed" migrate "github.com/rubenv/sql-migrate" + "github.com/safing/portmaster/base/database/record" "github.com/safing/portmaster/base/database/storage/sqlite/models" ) @@ -48,12 +49,3 @@ func getMeta(r *models.Record) *record.Meta { } return meta } - -func setMeta(r *models.Record, m *record.Meta) { - r.Created = m.Created - r.Modified = m.Modified - r.Expires = m.Expires - r.Deleted = m.Deleted - r.Secret = m.IsSecret() - r.Crownjewel = m.IsCrownJewel() -} diff --git a/base/database/storage/sqlite/sqlite.go b/base/database/storage/sqlite/sqlite.go index 325da138..454870ad 100644 --- a/base/database/storage/sqlite/sqlite.go +++ b/base/database/storage/sqlite/sqlite.go @@ -29,6 +29,11 @@ import ( "github.com/safing/structures/dsd" ) +// Errors. +var ( + ErrQueryTimeout = errors.New("query timeout") +) + // SQLite storage. type SQLite struct { name string @@ -108,7 +113,7 @@ func (db *SQLite) Get(key string) (record.Record, error) { // Get record from database. r, err := models.FindRecord(db.ctx, db.bob, key) if err != nil { - return nil, fmt.Errorf("%w: %s", storage.ErrNotFound, err) + return nil, fmt.Errorf("%w: %w", storage.ErrNotFound, err) } // Return data in wrapper. @@ -116,7 +121,7 @@ func (db *SQLite) Get(key string) (record.Record, error) { db.name, key, getMeta(r), - uint8(r.Format.GetOrZero()), + uint8(r.Format.GetOrZero()), //nolint:gosec // Values are within uint8. r.Value.GetOrZero(), ) } @@ -285,7 +290,9 @@ func (db *SQLite) queryExecutor(queryIter *iterator.Iterator, q *query.Query, lo queryIter.Finish(err) return } - defer cursor.Close() + defer func() { + _ = cursor.Close() + }() recordsLoop: for cursor.Next() { @@ -326,7 +333,7 @@ recordsLoop: db.name, r.Key, m, - uint8(r.Format.GetOrZero()), + uint8(r.Format.GetOrZero()), //nolint:gosec // Values are within uint8. r.Value.GetOrZero(), ) @@ -340,7 +347,7 @@ recordsLoop: break recordsLoop case queryIter.Next <- matched: case <-time.After(1 * time.Second): - err = errors.New("query timeout") + err = ErrQueryTimeout break recordsLoop } } @@ -435,7 +442,7 @@ func (db *SQLite) MaintainRecordStates(ctx context.Context, purgeDeletedBefore t // Option 1: Using shadow delete. if shadowDelete { // Mark expired records as deleted. - models.Records.Update( + _, err := models.Records.Update( um.SetCol("format").ToArg(nil), um.SetCol("value").ToArg(nil), um.SetCol("deleted").ToArg(now), @@ -443,26 +450,39 @@ func (db *SQLite) MaintainRecordStates(ctx context.Context, purgeDeletedBefore t models.UpdateWhere.Records.Expires.GT(0), models.UpdateWhere.Records.Expires.LT(now), ).Exec(db.ctx, db.bob) + if err != nil { + return fmt.Errorf("failed to shadow delete expired records: %w", err) + } // Purge deleted records before threshold. - models.Records.Delete( + _, err = models.Records.Delete( models.DeleteWhere.Records.Deleted.GT(0), models.DeleteWhere.Records.Deleted.LT(purgeThreshold), ).Exec(db.ctx, db.bob) + if err != nil { + return fmt.Errorf("failed to purge deleted records (before threshold): %w", err) + } return nil } // Option 2: Immediate delete. // Delete expired record. - models.Records.Delete( + _, err := models.Records.Delete( models.DeleteWhere.Records.Expires.GT(0), models.DeleteWhere.Records.Expires.LT(now), ).Exec(db.ctx, db.bob) + if err != nil { + return fmt.Errorf("failed to delete expired records: %w", err) + } + // Delete shadow deleted records. - models.Records.Delete( + _, err = models.Records.Delete( models.DeleteWhere.Records.Deleted.GT(0), ).Exec(db.ctx, db.bob) + if err != nil { + return fmt.Errorf("failed to purge deleted records: %w", err) + } return nil } diff --git a/base/database/storage/sqlite/sqlite_test.go b/base/database/storage/sqlite/sqlite_test.go index 19bb36a0..799a230b 100644 --- a/base/database/storage/sqlite/sqlite_test.go +++ b/base/database/storage/sqlite/sqlite_test.go @@ -1,8 +1,6 @@ package sqlite import ( - "context" - "os" "sync" "testing" "time" @@ -43,15 +41,8 @@ type TestRecord struct { //nolint:maligned func TestSQLite(t *testing.T) { t.Parallel() - testDir, err := os.MkdirTemp("", "testing-") - if err != nil { - t.Fatal(err) - } - defer func() { - _ = os.RemoveAll(testDir) // clean up - }() - // start + testDir := t.TempDir() db, err := openSQLite("test", testDir, true) if err != nil { t.Fatal(err) @@ -160,19 +151,19 @@ func TestSQLite(t *testing.T) { } // maintenance - err = db.MaintainRecordStates(context.TODO(), time.Now().Add(-time.Minute), true) + err = db.MaintainRecordStates(t.Context(), time.Now().Add(-time.Minute), true) if err != nil { t.Fatal(err) } // maintenance - err = db.MaintainRecordStates(context.TODO(), time.Now(), false) + err = db.MaintainRecordStates(t.Context(), time.Now(), false) if err != nil { t.Fatal(err) } // purging - n, err := db.Purge(context.TODO(), query.New("test:path/to/").MustBeValid(), true, true, true) + n, err := db.Purge(t.Context(), query.New("test:path/to/").MustBeValid(), true, true, true) if err != nil { t.Fatal(err) } diff --git a/go.mod b/go.mod index 2ce4d421..cd1831f9 100644 --- a/go.mod +++ b/go.mod @@ -54,6 +54,7 @@ require ( github.com/safing/structures v1.2.0 github.com/seehuhn/fortuna v1.0.1 github.com/shirou/gopsutil v3.21.11+incompatible + github.com/simukti/sqldb-logger v0.0.0-20230108155151-646c1a075551 github.com/spf13/cobra v1.8.1 github.com/spkg/zipfs v0.7.1 github.com/stephenafamo/bob v0.30.0 @@ -145,7 +146,6 @@ require ( github.com/satori/go.uuid v1.2.0 // indirect github.com/seehuhn/sha256d v1.0.0 // indirect github.com/shopspring/decimal v1.3.1 // indirect - github.com/simukti/sqldb-logger v0.0.0-20230108155151-646c1a075551 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stephenafamo/scan v0.6.1 // indirect diff --git a/service/intel/geoip/init_test.go b/service/intel/geoip/init_test.go index b6d722dc..c4d87b33 100644 --- a/service/intel/geoip/init_test.go +++ b/service/intel/geoip/init_test.go @@ -8,6 +8,7 @@ import ( "github.com/safing/portmaster/base/api" "github.com/safing/portmaster/base/config" "github.com/safing/portmaster/base/database/dbmodule" + "github.com/safing/portmaster/base/dataroot" "github.com/safing/portmaster/base/notifications" "github.com/safing/portmaster/service/updates" ) @@ -56,6 +57,7 @@ func runTest(m *testing.M) error { defer func() { _ = os.RemoveAll(ds) }() stub := &testInstance{} + dbmodule.SetDatabaseLocation(dataroot.Root()) stub.db, err = dbmodule.New(stub) if err != nil { return fmt.Errorf("failed to create database: %w", err) From 67cfefde9bacd2ea5f5df0041899ec63ea987219 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 4 Mar 2025 17:05:03 +0100 Subject: [PATCH 18/33] Fix tests --- base/config/main.go | 2 ++ service/intel/geoip/init_test.go | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/base/config/main.go b/base/config/main.go index c737dc2f..25eb4060 100644 --- a/base/config/main.go +++ b/base/config/main.go @@ -10,6 +10,7 @@ import ( "path/filepath" "sort" + "github.com/safing/portmaster/base/database/dbmodule" "github.com/safing/portmaster/base/dataroot" "github.com/safing/portmaster/base/utils" "github.com/safing/portmaster/base/utils/debug" @@ -150,6 +151,7 @@ func InitializeUnitTestDataroot(testName string) (string, error) { if err != nil { return "", fmt.Errorf("failed to initialize dataroot: %w", err) } + dbmodule.SetDatabaseLocation(dataroot.Root()) return basePath, nil } diff --git a/service/intel/geoip/init_test.go b/service/intel/geoip/init_test.go index c4d87b33..b6d722dc 100644 --- a/service/intel/geoip/init_test.go +++ b/service/intel/geoip/init_test.go @@ -8,7 +8,6 @@ import ( "github.com/safing/portmaster/base/api" "github.com/safing/portmaster/base/config" "github.com/safing/portmaster/base/database/dbmodule" - "github.com/safing/portmaster/base/dataroot" "github.com/safing/portmaster/base/notifications" "github.com/safing/portmaster/service/updates" ) @@ -57,7 +56,6 @@ func runTest(m *testing.M) error { defer func() { _ = os.RemoveAll(ds) }() stub := &testInstance{} - dbmodule.SetDatabaseLocation(dataroot.Root()) stub.db, err = dbmodule.New(stub) if err != nil { return fmt.Errorf("failed to create database: %w", err) From c0d8d0c2f034dd85f4f9254b7b2338bd03bc9b46 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 10 Mar 2025 10:34:57 +0100 Subject: [PATCH 19/33] Add new PurgeOlderThan interface method to SQLite Database --- base/database/controller.go | 14 +++++ base/database/interface.go | 21 +++++++ base/database/storage/interface.go | 5 ++ base/database/storage/sqlite/sqlite.go | 64 ++++++++++++++++----- base/database/storage/sqlite/sqlite_test.go | 22 ++++++- service/intel/filterlists/updater.go | 23 +++++++- 6 files changed, 130 insertions(+), 19 deletions(-) diff --git a/base/database/controller.go b/base/database/controller.go index 4d95c01e..5d23c1c8 100644 --- a/base/database/controller.go +++ b/base/database/controller.go @@ -264,6 +264,20 @@ func (c *Controller) Purge(ctx context.Context, q *query.Query, local, internal return 0, ErrNotImplemented } +// PurgeOlderThan deletes all records last updated before the given time. +// It returns the number of successful deletes and an error. +func (c *Controller) PurgeOlderThan(ctx context.Context, prefix string, purgeBefore time.Time, local, internal bool) (int, error) { + if shuttingDown.IsSet() { + return 0, ErrShuttingDown + } + + if purger, ok := c.storage.(storage.PurgeOlderThan); ok { + return purger.PurgeOlderThan(ctx, prefix, purgeBefore, local, internal, c.shadowDelete) + } + + return 0, ErrNotImplemented +} + // Shutdown shuts down the storage. func (c *Controller) Shutdown() error { return c.storage.Shutdown() diff --git a/base/database/interface.go b/base/database/interface.go index ce9b8a97..04cd46ad 100644 --- a/base/database/interface.go +++ b/base/database/interface.go @@ -562,6 +562,27 @@ func (i *Interface) Purge(ctx context.Context, q *query.Query) (int, error) { return db.Purge(ctx, q, i.options.Local, i.options.Internal) } +// PurgeOlderThan deletes all records last updated before the given time. +// It returns the number of successful deletes and an error. +func (i *Interface) PurgeOlderThan(ctx context.Context, prefix string, purgeBefore time.Time) (int, error) { + dbName, dbKeyPrefix := record.ParseKey(prefix) + if dbName == "" { + return 0, errors.New("unknown database") + } + + db, err := getController(dbName) + if err != nil { + return 0, err + } + + // Check if database is read only before we add to the cache. + if db.ReadOnly() { + return 0, ErrReadOnly + } + + return db.PurgeOlderThan(ctx, dbKeyPrefix, purgeBefore, i.options.Local, i.options.Internal) +} + // Subscribe subscribes to updates matching the given query. func (i *Interface) Subscribe(q *query.Query) (*Subscription, error) { _, err := q.Check() diff --git a/base/database/storage/interface.go b/base/database/storage/interface.go index c329a0a6..7bdd84f8 100644 --- a/base/database/storage/interface.go +++ b/base/database/storage/interface.go @@ -46,3 +46,8 @@ type Batcher interface { type Purger interface { Purge(ctx context.Context, q *query.Query, local, internal, shadowDelete bool) (int, error) } + +// PurgeOlderThan defines the database storage API for backends that support the PurgeOlderThan operation. +type PurgeOlderThan interface { + PurgeOlderThan(ctx context.Context, prefix string, purgeBefore time.Time, local, internal, shadowDelete bool) (int, error) +} diff --git a/base/database/storage/sqlite/sqlite.go b/base/database/storage/sqlite/sqlite.go index 454870ad..2cc15cb9 100644 --- a/base/database/storage/sqlite/sqlite.go +++ b/base/database/storage/sqlite/sqlite.go @@ -401,24 +401,62 @@ func (db *SQLite) Purge(ctx context.Context, q *query.Query, local, internal, sh } // Otherwise, iterate over all entries and delete matching ones. + + // TODO: Non-local, non-internal or content matching queries are not supported at the moment. return 0, storage.ErrNotImplemented +} - // Create iterator to check all matching records. +// PurgeOlderThan deletes all records last updated before the given time. It returns the number of successful deletes and an error. +func (db *SQLite) PurgeOlderThan(ctx context.Context, prefix string, purgeBefore time.Time, local, internal, shadowDelete bool) (int, error) { + db.wg.Add(1) + defer db.wg.Done() - // TODO: This is untested and also needs handling of shadowDelete. - // For now: Use only without where condition and with a local and internal db interface. - // queryIter := iterator.New() - // defer queryIter.Cancel() - // go db.queryExecutor(queryIter, q, local, internal) + purgeBeforeInt := purgeBefore.Unix() - // // Delete all matching records. - // var deleted int - // for r := range queryIter.Next { - // db.Delete(r.DatabaseKey()) - // deleted++ - // } + // Optimize for local and internal queries without where clause and without shadow delete. + if local && internal && !shadowDelete { + // First count entries (SQLite does not support affected rows) + n, err := models.Records.Query( + models.SelectWhere.Records.Key.Like(prefix+"%"), + models.SelectWhere.Records.Modified.LT(purgeBeforeInt), + ).Count(db.ctx, db.bob) + if err != nil || n == 0 { + return int(n), err + } - // return deleted, nil + // Delete entries. + _, err = models.Records.Delete( + models.DeleteWhere.Records.Key.Like(prefix+"%"), + models.DeleteWhere.Records.Modified.LT(purgeBeforeInt), + ).Exec(db.ctx, db.bob) + return int(n), err + } + + // Optimize for local and internal queries without where clause, but with shadow delete. + if local && internal && shadowDelete { + // First count entries (SQLite does not support affected rows) + n, err := models.Records.Query( + models.SelectWhere.Records.Key.Like(prefix+"%"), + models.SelectWhere.Records.Modified.LT(purgeBeforeInt), + ).Count(db.ctx, db.bob) + if err != nil || n == 0 { + return int(n), err + } + + // Mark purged records as deleted. + now := time.Now().Unix() + _, err = models.Records.Update( + um.SetCol("format").ToArg(nil), + um.SetCol("value").ToArg(nil), + um.SetCol("deleted").ToArg(now), + models.UpdateWhere.Records.Key.Like(prefix+"%"), + models.UpdateWhere.Records.Modified.LT(purgeBeforeInt), + ).Exec(db.ctx, db.bob) + return int(n), err + } + + // TODO: Non-local or non-internal queries are not supported at the moment. + return 0, storage.ErrNotImplemented } // ReadOnly returns whether the database is read only. diff --git a/base/database/storage/sqlite/sqlite_test.go b/base/database/storage/sqlite/sqlite_test.go index 799a230b..6586c8cc 100644 --- a/base/database/storage/sqlite/sqlite_test.go +++ b/base/database/storage/sqlite/sqlite_test.go @@ -98,17 +98,24 @@ func TestSQLite(t *testing.T) { qA := &TestRecord{} qA.SetKey("test:path/to/A") qA.UpdateMeta() + qB := &TestRecord{} qB.SetKey("test:path/to/B") qB.UpdateMeta() + // Set creation/modification in the past. + qB.Meta().Created = time.Now().Add(-time.Hour).Unix() + qB.Meta().Modified = time.Now().Add(-time.Hour).Unix() + qC := &TestRecord{} qC.SetKey("test:path/to/C") qC.UpdateMeta() // Set expiry in the past. qC.Meta().Expires = time.Now().Add(-time.Hour).Unix() + qZ := &TestRecord{} qZ.SetKey("test:z") qZ.UpdateMeta() + put, errs := db.PutMany(false) put <- qA put <- qB @@ -150,6 +157,15 @@ func TestSQLite(t *testing.T) { t.Fatal("should fail") } + // purge older than + n, err := db.PurgeOlderThan(t.Context(), "path/to/", time.Now().Add(-30*time.Minute), true, true, false) + if err != nil { + t.Fatal(err) + } + if n != 1 { + t.Fatalf("unexpected purge older than delete count: %d", n) + } + // maintenance err = db.MaintainRecordStates(t.Context(), time.Now().Add(-time.Minute), true) if err != nil { @@ -162,12 +178,12 @@ func TestSQLite(t *testing.T) { t.Fatal(err) } - // purging - n, err := db.Purge(t.Context(), query.New("test:path/to/").MustBeValid(), true, true, true) + // purge + n, err = db.Purge(t.Context(), query.New("test:path/to/").MustBeValid(), true, true, true) if err != nil { t.Fatal(err) } - if n != 2 { + if n != 1 { t.Fatalf("unexpected purge delete count: %d", n) } diff --git a/service/intel/filterlists/updater.go b/service/intel/filterlists/updater.go index 72f7b82e..4ffbec52 100644 --- a/service/intel/filterlists/updater.go +++ b/service/intel/filterlists/updater.go @@ -12,6 +12,7 @@ import ( "github.com/safing/portmaster/base/database" "github.com/safing/portmaster/base/database/query" + "github.com/safing/portmaster/base/database/storage" "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/updater" "github.com/safing/portmaster/service/mgr" @@ -158,9 +159,25 @@ func performUpdate(ctx context.Context) error { func removeAllObsoleteFilterEntries(wc *mgr.WorkerCtx) error { log.Debugf("intel/filterlists: cleanup task started, removing obsolete filter list entries ...") - n, err := cache.Purge(wc.Ctx(), query.New(filterListKeyPrefix).Where( - // TODO(ppacher): remember the timestamp we started the last update - // and use that rather than "one hour ago" + + // TODO: Remember the timestamp we started the last update and use that rather than "one hour ago". + + // First try to purge with PurgeOlderThan. + n, err := cache.PurgeOlderThan(wc.Ctx(), filterListKeyPrefix, time.Now().Add(-time.Hour)) + switch { + case err == nil: + // Success! + log.Debugf("intel/filterlists: successfully removed %d obsolete entries", n) + return nil + case errors.Is(err, database.ErrNotImplemented) || errors.Is(err, storage.ErrNotImplemented): + // Try next method. + default: + // Return error. + return err + } + + // Try with regular purge. + n, err = cache.Purge(wc.Ctx(), query.New(filterListKeyPrefix).Where( query.Where("UpdatedAt", query.LessThan, time.Now().Add(-time.Hour).Unix()), )) if err != nil { From 2c8ab5410436fbd7def687892a2303e36d8774f3 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 10 Mar 2025 11:44:08 +0100 Subject: [PATCH 20/33] Fix SQLite maintenance methods --- base/database/storage/sqlite/sqlite.go | 1 + base/database/storage/sqlite/sqlite_test.go | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/base/database/storage/sqlite/sqlite.go b/base/database/storage/sqlite/sqlite.go index 2cc15cb9..76e1554a 100644 --- a/base/database/storage/sqlite/sqlite.go +++ b/base/database/storage/sqlite/sqlite.go @@ -99,6 +99,7 @@ func openSQLite(name, location string, printStmts bool) (*SQLite, error) { ctx, cancelCtx := context.WithCancel(context.Background()) return &SQLite{ name: name, + db: db, bob: bob.NewDB(db), ctx: ctx, cancelCtx: cancelCtx, diff --git a/base/database/storage/sqlite/sqlite_test.go b/base/database/storage/sqlite/sqlite_test.go index 6586c8cc..43ecb114 100644 --- a/base/database/storage/sqlite/sqlite_test.go +++ b/base/database/storage/sqlite/sqlite_test.go @@ -187,6 +187,16 @@ func TestSQLite(t *testing.T) { t.Fatalf("unexpected purge delete count: %d", n) } + // Maintenance + err = db.Maintain(t.Context()) + if err != nil { + t.Fatalf("Maintain: %s", err) + } + err = db.MaintainThorough(t.Context()) + if err != nil { + t.Fatalf("MaintainThorough: %s", err) + } + // test query q = query.New("test").MustBeValid() it, err = db.Query(q, true, true) From 9b12dfffc279221d3da1e83a458124c5ca3d70b0 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 17 Mar 2025 14:13:53 +0100 Subject: [PATCH 21/33] Add option to use prepared statements for SQLite PutMany --- base/database/storage/sqlite/prepared.go | 125 ++++++++++++++++++ base/database/storage/sqlite/prepared_test.go | 86 ++++++++++++ base/database/storage/sqlite/sqlite.go | 5 + 3 files changed, 216 insertions(+) create mode 100644 base/database/storage/sqlite/prepared.go create mode 100644 base/database/storage/sqlite/prepared_test.go diff --git a/base/database/storage/sqlite/prepared.go b/base/database/storage/sqlite/prepared.go new file mode 100644 index 00000000..11136dd8 --- /dev/null +++ b/base/database/storage/sqlite/prepared.go @@ -0,0 +1,125 @@ +package sqlite + +import ( + "context" + "strconv" + + "github.com/stephenafamo/bob" + "github.com/stephenafamo/bob/dialect/sqlite/im" + "github.com/stephenafamo/bob/expr" + + "github.com/safing/portmaster/base/database/record" + "github.com/safing/portmaster/base/database/storage/sqlite/models" + "github.com/safing/structures/dsd" +) + +var UsePreparedStatements bool = true + +// PutMany stores many records in the database. +func (db *SQLite) putManyWithPreparedStmts(shadowDelete bool) (chan<- record.Record, <-chan error) { + batch := make(chan record.Record, 100) + errs := make(chan error, 1) + + // Simulate upsert with custom selection on conflict. + rawQuery, _, err := models.Records.Insert( + im.Into("records", "key", "format", "value", "created", "modified", "expires", "deleted", "secret", "crownjewel"), + im.Values(expr.Arg("key"), expr.Arg("format"), expr.Arg("value"), expr.Arg("created"), expr.Arg("modified"), expr.Arg("expires"), expr.Arg("deleted"), expr.Arg("secret"), expr.Arg("crownjewel")), + im.OnConflict("key").DoUpdate( + im.SetExcluded("format", "value", "created", "modified", "expires", "deleted", "secret", "crownjewel"), + ), + ).Build(db.ctx) + if err != nil { + errs <- err + return batch, errs + } + + // Start transaction. + tx, err := db.bob.BeginTx(db.ctx, nil) + if err != nil { + errs <- err + return batch, errs + } + + // Create prepared statement WITHIN TRANSACTION. + preparedStmt, err := tx.PrepareContext(db.ctx, rawQuery) + if err != nil { + errs <- err + return batch, errs + } + + // start handler + go func() { + // Read all put records. + writeBatch: + for { + select { + case r := <-batch: + if r != nil { + // Write record. + err := writeWithPreparedStatement(db.ctx, &preparedStmt, r) + if err != nil { + errs <- err + break writeBatch + } + } else { + // Finalize transcation. + errs <- tx.Commit() + return + } + + case <-db.ctx.Done(): + break writeBatch + } + } + + // Rollback transaction. + errs <- tx.Rollback() + }() + + return batch, errs +} + +func writeWithPreparedStatement(ctx context.Context, pStmt *bob.StdPrepared, r record.Record) error { + r.Lock() + defer r.Unlock() + + // Serialize to JSON. + data, err := r.MarshalDataOnly(r, dsd.JSON) + if err != nil { + return err + } + + // Get Meta. + m := r.Meta() + + // Insert. + if len(data) > 0 { + format := strconv.Itoa(dsd.JSON) + _, err = pStmt.ExecContext( + ctx, + r.DatabaseKey(), + format, + data, + m.Created, + m.Modified, + m.Expires, + m.Deleted, + m.IsSecret(), + m.IsCrownJewel(), + ) + } else { + _, err = pStmt.ExecContext( + ctx, + r.DatabaseKey(), + nil, + nil, + m.Created, + m.Modified, + m.Expires, + m.Deleted, + m.IsSecret(), + m.IsCrownJewel(), + ) + } + return err +} diff --git a/base/database/storage/sqlite/prepared_test.go b/base/database/storage/sqlite/prepared_test.go new file mode 100644 index 00000000..8a90bcb9 --- /dev/null +++ b/base/database/storage/sqlite/prepared_test.go @@ -0,0 +1,86 @@ +package sqlite + +import ( + "strconv" + "testing" +) + +func BenchmarkPutMany(b *testing.B) { + // Configure prepared statement usage. + origSetting := UsePreparedStatements + UsePreparedStatements = false + defer func() { + UsePreparedStatements = origSetting + }() + + // Run benchmark. + benchPutMany(b) +} + +func BenchmarkPutManyPreparedStmt(b *testing.B) { + // Configure prepared statement usage. + origSetting := UsePreparedStatements + UsePreparedStatements = true + defer func() { + UsePreparedStatements = origSetting + }() + + // Run benchmark. + benchPutMany(b) +} + +func benchPutMany(b *testing.B) { //nolint:thelper + // Start database. + testDir := b.TempDir() + db, err := openSQLite("test", testDir, false) + if err != nil { + b.Fatal(err) + } + defer func() { + // shutdown + err = db.Shutdown() + if err != nil { + b.Fatal(err) + } + }() + + // Start benchmarking. + b.ResetTimer() + + // Benchmark PutMany. + records, errs := db.PutMany(false) + for i := range b.N { + // Create test record. + newTestRecord := &TestRecord{ + S: "banana", + I: 42, + I8: 42, + I16: 42, + I32: 42, + I64: 42, + UI: 42, + UI8: 42, + UI16: 42, + UI32: 42, + UI64: 42, + F32: 42.42, + F64: 42.42, + B: true, + } + newTestRecord.UpdateMeta() + newTestRecord.SetKey("test:" + strconv.Itoa(i)) + + select { + case records <- newTestRecord: + case err := <-errs: + b.Fatal(err) + } + } + + // Finalize. + close(records) + err = <-errs + if err != nil { + b.Fatal(err) + } +} diff --git a/base/database/storage/sqlite/sqlite.go b/base/database/storage/sqlite/sqlite.go index 76e1554a..c6288322 100644 --- a/base/database/storage/sqlite/sqlite.go +++ b/base/database/storage/sqlite/sqlite.go @@ -205,6 +205,11 @@ func (db *SQLite) PutMany(shadowDelete bool) (chan<- record.Record, <-chan error db.wg.Add(1) defer db.wg.Done() + // Check if we should use prepared statement optimized inserting. + if UsePreparedStatements { + return db.putManyWithPreparedStmts(shadowDelete) + } + batch := make(chan record.Record, 100) errs := make(chan error, 1) From be133b88563e4bcab46ee3ead3023bf9030ae9cc Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 17 Mar 2025 15:43:59 +0100 Subject: [PATCH 22/33] Improve filterlist ingestion logging --- service/intel/filterlists/database.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/intel/filterlists/database.go b/service/intel/filterlists/database.go index 6de070bb..19dbb6fc 100644 --- a/service/intel/filterlists/database.go +++ b/service/intel/filterlists/database.go @@ -146,7 +146,7 @@ func persistRecords(startJob func(func() error), records <-chan record.Record) { timePerEntity := time.Since(start) / time.Duration(cnt) speed := float64(time.Second) / float64(timePerEntity) - log.Debugf("processed %d entities in %s with %s / entity (%.2f entities/second)", cnt, time.Since(start), timePerEntity, speed) + log.Debugf("intel/filterlists: processed %d entities in %s with %s / entity (%.2f entities/second)", cnt, time.Since(start), timePerEntity, speed) } batch := database.NewInterface(&database.Options{Local: true, Internal: true}) From 7b05ed82b2372b626fe0e483e60728e53b139002 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 4 Apr 2025 13:52:50 +0200 Subject: [PATCH 23/33] Add a 3s busy timeout to SQLite database backend --- base/database/storage/sqlite/sqlite.go | 1 + 1 file changed, 1 insertion(+) diff --git a/base/database/storage/sqlite/sqlite.go b/base/database/storage/sqlite/sqlite.go index c6288322..fcf52a6f 100644 --- a/base/database/storage/sqlite/sqlite.go +++ b/base/database/storage/sqlite/sqlite.go @@ -80,6 +80,7 @@ func openSQLite(name, location string, printStmts bool) (*SQLite, error) { "PRAGMA journal_mode=WAL;", // Corruption safe write ahead log for txs. "PRAGMA synchronous=NORMAL;", // Best for WAL. "PRAGMA cache_size=-10000;", // 10MB Cache. + "PRAGMA busy_timeout=3000;", // 3s (3000ms) timeout for locked tables. } for _, pragma := range pragmas { _, err := db.Exec(pragma) From f233a56eeaa2363db61d9ae08938aff171f071b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=A5=C3=A5vi?= Date: Mon, 5 May 2025 15:05:48 +0200 Subject: [PATCH 24/33] Only use stale dns cache entries when the query result was successful --- service/resolver/resolve.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/resolver/resolve.go b/service/resolver/resolve.go index 4b5b5181..e84858f5 100644 --- a/service/resolver/resolve.go +++ b/service/resolver/resolve.go @@ -184,7 +184,7 @@ func Resolve(ctx context.Context, q *Query) (rrCache *RRCache, err error) { case !rrCache.Expired(): // Return non-expired cached entry immediately. return rrCache, nil - case useStaleCache(): + case rrCache.RCode == dns.RcodeSuccess && useStaleCache(): // Return expired cache if we should use stale cache entries, // but start an async query instead. log.Tracer(ctx).Tracef( From 58ca3150e7b5d49f50d1f683b9b0383b26ef1725 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Tue, 20 May 2025 12:56:24 +0300 Subject: [PATCH 25/33] Log a warning in the UI when falling back to default connection parameters --- .../src/lib/meta-api.service.ts | 9 +++++++-- .../safing/portmaster-api/src/lib/module.ts | 10 ++++++---- .../src/environments/environment.prod.ts | 19 ++++++------------- desktop/angular/src/main.ts | 5 +++-- 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/meta-api.service.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/meta-api.service.ts index 8063a431..313ea127 100644 --- a/desktop/angular/projects/safing/portmaster-api/src/lib/meta-api.service.ts +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/meta-api.service.ts @@ -43,8 +43,13 @@ export interface AuthKeyResponse { export class MetaAPI { constructor( private http: HttpClient, - @Inject(PORTMASTER_HTTP_API_ENDPOINT) @Optional() private httpEndpoint: string = 'http://127.0.0.1:817/api', - ) { } + @Inject(PORTMASTER_HTTP_API_ENDPOINT) @Optional() private httpEndpoint: string, + ) { + if (!this.httpEndpoint) { + this.httpEndpoint = `http://localhost:817/api`; + console.warn("[portmaster-api: MetaAPI] No HTTP API endpoint provided, using default: " + this.httpEndpoint); + } + } listEndpoints(): Observable { return this.http.get(`${this.httpEndpoint}/v1/endpoints`) diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/module.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/module.ts index 076b390d..13d5e048 100644 --- a/desktop/angular/projects/safing/portmaster-api/src/lib/module.ts +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/module.ts @@ -47,11 +47,13 @@ export class PortmasterAPIModule { * @param cfg The module configuration defining the Portmaster HTTP and Websocket API endpoints. */ static forRoot(cfg: ModuleConfig = {}): ModuleWithProviders { - if (cfg.httpAPI === undefined) { - cfg.httpAPI = `http://127.0.0.1:817/api`; + if (!cfg.httpAPI) { + cfg.httpAPI = `http://${window.location.host}/api`; + console.warn("[portmaster-api] No HTTP API endpoint provided, using default: " + cfg.httpAPI); } - if (cfg.websocketAPI === undefined) { - cfg.websocketAPI = `ws://127.0.0.1:817/api/database/v1`; + if (!cfg.websocketAPI) { + cfg.websocketAPI = `ws://${window.location.host}/api/database/v1`; + console.warn("[portmaster-api] No WebSocket API endpoint provided, using default: " + cfg.websocketAPI); } return { diff --git a/desktop/angular/src/environments/environment.prod.ts b/desktop/angular/src/environments/environment.prod.ts index 8cdffedb..ecca4342 100644 --- a/desktop/angular/src/environments/environment.prod.ts +++ b/desktop/angular/src/environments/environment.prod.ts @@ -1,13 +1,6 @@ -export const environment = new class { - readonly supportHub = "https://support.safing.io" - readonly production = true; - - get httpAPI() { - return `http://${window.location.host}/api` - } - - get portAPI() { - const result = `ws://${window.location.host}/api/database/v1`; - return result; - } -} \ No newline at end of file +export const environment = { + production: true, + portAPI: `ws://${window.location.host}/api/database/v1`, + httpAPI: `http://${window.location.host}/api`, + supportHub: "https://support.safing.io" +}; \ No newline at end of file diff --git a/desktop/angular/src/main.ts b/desktop/angular/src/main.ts index c62691ce..d4a8424c 100644 --- a/desktop/angular/src/main.ts +++ b/desktop/angular/src/main.ts @@ -76,12 +76,13 @@ if (location.pathname !== "/prompt") { } else { // bootstrap the prompt interface + console.log("[INFO] Bootstrapping prompt entry point."); bootstrapApplication(PromptEntryPointComponent, { providers: [ provideHttpClient(), importProvidersFrom(PortmasterAPIModule.forRoot({ - websocketAPI: "ws://127.0.0.1:817/api/database/v1", - httpAPI: "http://127.0.0.1:817/api" + websocketAPI: "ws://localhost:817/api/database/v1", + httpAPI: "http://localhost:817/api" })), NotificationsService, { From 3e034fc33bc73234eef7c4fbde202af9eeb2d482 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Wed, 21 May 2025 14:48:59 +0300 Subject: [PATCH 26/33] (Windows) Use contrast icon for installer and UI app binary --- assets/data/icons/pm_light_contrast.ico | Bin 0 -> 420366 bytes assets/data/icons/pm_light_contrast.svg | 11 +++++++++++ desktop/tauri/src-tauri/tauri.conf.json5 | 5 +++-- 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 assets/data/icons/pm_light_contrast.ico create mode 100644 assets/data/icons/pm_light_contrast.svg diff --git a/assets/data/icons/pm_light_contrast.ico b/assets/data/icons/pm_light_contrast.ico new file mode 100644 index 0000000000000000000000000000000000000000..7d06be076f08af339f1dba4715fb261e3cec3490 GIT binary patch literal 420366 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 literal 0 HcmV?d00001 diff --git a/assets/data/icons/pm_light_contrast.svg b/assets/data/icons/pm_light_contrast.svg new file mode 100644 index 00000000..daa98bf7 --- /dev/null +++ b/assets/data/icons/pm_light_contrast.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/desktop/tauri/src-tauri/tauri.conf.json5 b/desktop/tauri/src-tauri/tauri.conf.json5 index 4d0e7e8a..28992671 100644 --- a/desktop/tauri/src-tauri/tauri.conf.json5 +++ b/desktop/tauri/src-tauri/tauri.conf.json5 @@ -124,7 +124,7 @@ "nsis": { "installMode": "perMachine", "installerHooks": "templates/nsis/install_hooks.nsh", - "installerIcon": "../../../assets/data/icons/pm_light.ico" + "installerIcon": "../../../assets/data/icons/pm_light_contrast.ico" }, "wix": { "fragmentPaths": [ @@ -141,8 +141,9 @@ "rpm", "nsis" //, "msi" ], - "icon": [ + "icon": [ "../../../assets/data/icons/pm_dark_512.png", + "../../../assets/data/icons/pm_light_contrast.ico", "../../../assets/data/icons/pm_dark.ico", "../../../assets/data/icons/pm_light_512.png", "../../../assets/data/icons/pm_light.ico" From 11c4ae39d238056e67a3435ba8c48b6c59855e38 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Wed, 21 May 2025 18:08:04 +0300 Subject: [PATCH 27/33] (Windows) Fix false-positive detection of Portmaster UI processes Problem: System browsers launched from the Portmaster UI (e.g., when a user clicks a link) may be incorrectly detected as Portmaster UI child processes. Solution: The Tauri UI app now sets the PORTMASTER_UI_WEBVIEW_PROCESS environment variable for all child WebView processes. Portmaster-core uses this variable to accurately determine if a process is truly related to the Portmaster UI. --- desktop/tauri/src-tauri/src/window.rs | 29 +++++++++++++++++++++++++++ service/process/profile.go | 29 ++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/desktop/tauri/src-tauri/src/window.rs b/desktop/tauri/src-tauri/src/window.rs index 43c03d46..50bfb4af 100644 --- a/desktop/tauri/src-tauri/src/window.rs +++ b/desktop/tauri/src-tauri/src/window.rs @@ -9,6 +9,8 @@ use crate::{portmaster::PortmasterExt, traymenu}; const LIGHT_PM_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_light_512.png"); const DARK_PM_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_dark_512.png"); +const CUSTOM_ENVVAR_FOR_WEBVIEW_PROCESS: &str = "PORTMASTER_UI_WEBVIEW_PROCESS"; + /// Either returns the existing "main" window or creates a new one. /// /// The window is not automatically shown (i.e it starts hidden). @@ -24,6 +26,7 @@ pub fn create_main_window(app: &AppHandle) -> Result { } else { debug!("[tauri] creating main window"); + do_before_window_create(); // required operations before window creation let res = WebviewWindowBuilder::new(app, "main", WebviewUrl::App("index.html".into())) .title("Portmaster") .visible(false) @@ -31,6 +34,7 @@ pub fn create_main_window(app: &AppHandle) -> Result { .min_inner_size(800.0, 600.0) .theme(Some(Theme::Dark)) .build(); + do_after_window_create(); // required operations after window creation match res { Ok(win) => { @@ -69,6 +73,8 @@ pub fn create_splash_window(app: &AppHandle) -> Result { let _ = window.show(); Ok(window) } else { + + do_before_window_create(); // required operations before window creation let window = WebviewWindowBuilder::new(app, "splash", WebviewUrl::App("index.html".into())) .center() .closable(false) @@ -78,6 +84,7 @@ pub fn create_splash_window(app: &AppHandle) -> Result { .title("Portmaster") .inner_size(600.0, 250.0) .build()?; + do_after_window_create(); // required operations after window creation set_window_icon(&window); let _ = window.request_user_attention(Some(UserAttentionType::Informational)); @@ -118,6 +125,28 @@ pub fn set_window_icon(window: &WebviewWindow) { }; } +/// This function must be called before the window is created. +/// +/// Temporarily sets the environment variable `PORTMASTER_WEBVIEW_UI_PROCESS` to "true". +/// This ensures that any child process (i.e., the WebView process) spawned during window creation +/// will inherit this environment variable. This allows portmaster-core to detect that the process +/// is a child WebView of the main process. +/// +/// IMPORTANT: After the window is created, you must call `do_after_window_create()` to remove +/// the environment variable from the main process environment. +pub fn do_before_window_create() { + std::env::set_var(CUSTOM_ENVVAR_FOR_WEBVIEW_PROCESS, "true"); +} + +/// This function must be called after the window is created. +/// +/// Removes the `PORTMASTER_WEBVIEW_UI_PROCESS` environment variable from the main process. +/// This ensures that only the child WebView process has the variable set, and the main process +/// does not retain it. +pub fn do_after_window_create() { + std::env::remove_var(CUSTOM_ENVVAR_FOR_WEBVIEW_PROCESS); +} + /// Opens a window for the tauri application. /// /// If the main window has already been created, it is instructed to diff --git a/service/process/profile.go b/service/process/profile.go index 7653bd72..8217aa9b 100644 --- a/service/process/profile.go +++ b/service/process/profile.go @@ -118,6 +118,8 @@ func (p *Process) IsPortmasterUi(ctx context.Context) bool { var previousPid int proc := p + hasPmWebviewEnvVar := false + for i := 0; i < checkLevels; i++ { if proc.Pid == UnidentifiedProcessID || proc.Pid == SystemProcessID { break @@ -125,7 +127,32 @@ func (p *Process) IsPortmasterUi(ctx context.Context) bool { realPath, err := filepath.EvalSymlinks(proc.Path) if err == nil && realPath == module.portmasterUIPath { - return true + if runtime.GOOS != "windows" { + return true + } + + // On Windows, avoid false positive detection of the Portmaster UI. + // For example: + // There may be cases where a system browser is launched from the Portmaster UI, + // making it a child of the Portmaster UI process (e.g., user clicked a link in the UI). + // To ensure that 'p' is the actual Portmaster UI process, we check for the presence + // of the 'PORTMASTER_UI_WEBVIEW_PROCESS' environment variable in the process and its parents. + // If the env var is set, we are a child (WebView window) of the Portmaster UI process. + // Otherwise, the process was launched by the Portmaster UI, but should not be trusted as the Portmaster UI process. + if i == 0 { + return true // We are the main Portmaster UI process. + } + if hasPmWebviewEnvVar { + return true // We are a WebView window of the Portmaster UI process. + } + // The process was launched by the Portmaster UI, but should not be trusted as the Portmaster UI process. + log.Tracer(ctx).Warning(fmt.Sprintf("process: %d '%s' is a child of the Portmaster UI, but does not have the PORTMASTER_UI_WEBVIEW_PROCESS environment variable set. Ignoring.", p.Pid, p.Path)) + return false + } + + // Check if the process has the environment variable set. + if _, ok := proc.Env["PORTMASTER_UI_WEBVIEW_PROCESS"]; ok { + hasPmWebviewEnvVar = true } if i < checkLevels-1 { // no need to check parent if we are at the last level From 3b91aa06ba2eae78943b92a21c3a57d87a4c1c28 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Wed, 21 May 2025 18:12:50 +0300 Subject: [PATCH 28/33] Enhance default connection settings for Portmaster UI profile to block all connections, ensuring only necessary connections to Portmaster Core are allowed. --- service/profile/special.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/service/profile/special.go b/service/profile/special.go index 55b466ec..b7674668 100644 --- a/service/profile/special.go +++ b/service/profile/special.go @@ -237,14 +237,20 @@ func createSpecialProfile(profileID string, path string) *Profile { Source: SourceLocal, PresentationPath: path, Config: map[string]interface{}{ + // Block all connections by default for the Portmaster UI profile, + // since the only required connections are to the Portmaster Core, + // which are fast-tracked. + // + // This ensures that any unexpected connections — + // possibly made by the internal WebView implementation — + // are blocked. CfgOptionDefaultActionKey: DefaultActionBlockValue, - CfgOptionBlockScopeInternetKey: false, - CfgOptionBlockScopeLANKey: false, - CfgOptionBlockScopeLocalKey: false, - CfgOptionBlockP2PKey: false, + CfgOptionBlockScopeInternetKey: true, + CfgOptionBlockScopeLANKey: true, + CfgOptionBlockScopeLocalKey: true, + CfgOptionBlockP2PKey: true, CfgOptionBlockInboundKey: true, CfgOptionEndpointsKey: []string{ - "+ Localhost", "+ .safing.io", }, }, From fbc93cc09fbcadf4290bf0fa289b274af053a5c8 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Thu, 22 May 2025 15:30:05 +0300 Subject: [PATCH 29/33] Add more descriptive comments + minor improvements --- service/process/profile.go | 13 ++++++++++++- service/profile/special.go | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/service/process/profile.go b/service/process/profile.go index 8217aa9b..4ade9a0d 100644 --- a/service/process/profile.go +++ b/service/process/profile.go @@ -135,6 +135,12 @@ func (p *Process) IsPortmasterUi(ctx context.Context) bool { // For example: // There may be cases where a system browser is launched from the Portmaster UI, // making it a child of the Portmaster UI process (e.g., user clicked a link in the UI). + // In this case, the parent process tree may look like this: + // Portmaster.exe + // ├─ WebView (PM UI) + // │ └─ WebView (PM UI child) + // └─ System Web Browser ... + // // To ensure that 'p' is the actual Portmaster UI process, we check for the presence // of the 'PORTMASTER_UI_WEBVIEW_PROCESS' environment variable in the process and its parents. // If the env var is set, we are a child (WebView window) of the Portmaster UI process. @@ -146,11 +152,16 @@ func (p *Process) IsPortmasterUi(ctx context.Context) bool { return true // We are a WebView window of the Portmaster UI process. } // The process was launched by the Portmaster UI, but should not be trusted as the Portmaster UI process. - log.Tracer(ctx).Warning(fmt.Sprintf("process: %d '%s' is a child of the Portmaster UI, but does not have the PORTMASTER_UI_WEBVIEW_PROCESS environment variable set. Ignoring.", p.Pid, p.Path)) + log.Tracer(ctx).Warningf("process: %d '%s' is a child of the Portmaster UI, but does not have the PORTMASTER_UI_WEBVIEW_PROCESS environment variable set. Ignoring.", p.Pid, p.Path) return false } // Check if the process has the environment variable set. + // + // It is OK to check for the existence of the environment variable in all + // processes in the parent chain (on all loop iterations). This increases the + // chance of correct detection, even if a child or grandchild WebView process + // did not inherit the environment variable for some reason. if _, ok := proc.Env["PORTMASTER_UI_WEBVIEW_PROCESS"]; ok { hasPmWebviewEnvVar = true } diff --git a/service/profile/special.go b/service/profile/special.go index b7674668..cfff90bc 100644 --- a/service/profile/special.go +++ b/service/profile/special.go @@ -245,7 +245,7 @@ func createSpecialProfile(profileID string, path string) *Profile { // possibly made by the internal WebView implementation — // are blocked. CfgOptionDefaultActionKey: DefaultActionBlockValue, - CfgOptionBlockScopeInternetKey: true, + CfgOptionBlockScopeInternetKey: false, // This is stronger than the rules, and thus must be false in order to access safing.io. CfgOptionBlockScopeLANKey: true, CfgOptionBlockScopeLocalKey: true, CfgOptionBlockP2PKey: true, From 531d147936a264d57dd52c60c3e4e5f54823e9a3 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Thu, 22 May 2025 15:40:06 +0300 Subject: [PATCH 30/33] Improve logging message format --- service/process/profile.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/process/profile.go b/service/process/profile.go index 4ade9a0d..5b3de094 100644 --- a/service/process/profile.go +++ b/service/process/profile.go @@ -152,7 +152,7 @@ func (p *Process) IsPortmasterUi(ctx context.Context) bool { return true // We are a WebView window of the Portmaster UI process. } // The process was launched by the Portmaster UI, but should not be trusted as the Portmaster UI process. - log.Tracer(ctx).Warningf("process: %d '%s' is a child of the Portmaster UI, but does not have the PORTMASTER_UI_WEBVIEW_PROCESS environment variable set. Ignoring.", p.Pid, p.Path) + log.Tracer(ctx).Warningf("process: %d %q is a child of the Portmaster UI, but does not have the PORTMASTER_UI_WEBVIEW_PROCESS environment variable set. Ignoring.", p.Pid, p.Path) return false } From d3f6cb1504bbfa107f689e4a3912650ac5ef835f Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Thu, 22 May 2025 15:49:58 +0300 Subject: [PATCH 31/33] Bump version 2.0.16 --- desktop/angular/package-lock.json | 4 ++-- desktop/angular/package.json | 2 +- desktop/tauri/src-tauri/Cargo.lock | 2 +- desktop/tauri/src-tauri/Cargo.toml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/desktop/angular/package-lock.json b/desktop/angular/package-lock.json index b43cf93b..0022ace7 100644 --- a/desktop/angular/package-lock.json +++ b/desktop/angular/package-lock.json @@ -1,12 +1,12 @@ { "name": "portmaster", - "version": "2.0.14", + "version": "2.0.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "portmaster", - "version": "2.0.14", + "version": "2.0.16", "dependencies": { "@angular/animations": "^16.0.1", "@angular/cdk": "^16.0.1", diff --git a/desktop/angular/package.json b/desktop/angular/package.json index 57aed849..f09fe8d8 100644 --- a/desktop/angular/package.json +++ b/desktop/angular/package.json @@ -1,6 +1,6 @@ { "name": "portmaster", - "version": "2.0.14", + "version": "2.0.16", "scripts": { "ng": "ng", "start": "npm install && npm run build-libs:dev && ng serve --proxy-config ./proxy.json", diff --git a/desktop/tauri/src-tauri/Cargo.lock b/desktop/tauri/src-tauri/Cargo.lock index 09a481d4..ff7fd78d 100644 --- a/desktop/tauri/src-tauri/Cargo.lock +++ b/desktop/tauri/src-tauri/Cargo.lock @@ -3945,7 +3945,7 @@ dependencies = [ [[package]] name = "portmaster" -version = "2.0.14" +version = "2.0.16" dependencies = [ "assert_matches", "cached", diff --git a/desktop/tauri/src-tauri/Cargo.toml b/desktop/tauri/src-tauri/Cargo.toml index 2d8c1711..e92dcabb 100644 --- a/desktop/tauri/src-tauri/Cargo.toml +++ b/desktop/tauri/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "portmaster" -version = "2.0.14" +version = "2.0.16" description = "Portmaster UI" authors = ["Safing"] license = "" From 9d345f1941881a7b31caf5a206213803596a611c Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Wed, 28 May 2025 11:53:38 +0300 Subject: [PATCH 32/33] go mod tidy --- go.mod | 22 +--------------------- go.sum | 55 ++++++++++++------------------------------------------- 2 files changed, 13 insertions(+), 64 deletions(-) diff --git a/go.mod b/go.mod index d3b7f5ad..7f15ca30 100644 --- a/go.mod +++ b/go.mod @@ -66,9 +66,6 @@ require ( golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa golang.org/x/image v0.19.0 golang.org/x/net v0.28.0 - golang.org/x/sync v0.8.0 - golang.org/x/sys v0.24.0 - gopkg.in/yaml.v3 v3.0.1 golang.org/x/sync v0.10.0 golang.org/x/sys v0.29.0 gopkg.in/yaml.v3 v3.0.1 @@ -79,7 +76,6 @@ require ( require github.com/sergeymakinen/go-bmp v1.0.0 // indirect require ( - al.essio.dev/pkg/shellescape v1.5.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect github.com/Masterminds/goutils v1.1.1 // indirect @@ -88,8 +84,6 @@ require ( github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf // indirect github.com/aead/ecdh v0.2.0 // indirect github.com/alessio/shellescape v1.4.2 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/danieljoos/wincred v1.2.1 // indirect github.com/bgentry/speakeasy v0.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect @@ -104,18 +98,14 @@ require ( github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect - github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f // indirect - github.com/golang/glog v1.2.1 // indirect github.com/go-sql-driver/mysql v1.7.2-0.20231213112541-0004702b931d // indirect github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect - github.com/godbus/dbus v4.1.0+incompatible // indirect github.com/godror/godror v0.40.4 // indirect github.com/godror/knownpb v0.1.1 // indirect github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f // indirect - github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect + github.com/golang/glog v1.2.1 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/uuid v1.6.0 // indirect @@ -172,16 +162,6 @@ require ( github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zalando/go-keyring v0.2.5 // indirect github.com/zeebo/blake3 v0.2.4 // indirect - golang.org/x/crypto v0.26.0 // indirect - golang.org/x/mod v0.20.0 // indirect - golang.org/x/text v0.17.0 // indirect - golang.org/x/tools v0.24.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - modernc.org/libc v1.59.9 // indirect - modernc.org/mathutil v1.6.0 // indirect - modernc.org/memory v1.8.0 // indirect - modernc.org/sqlite v1.32.0 // indirect golang.org/x/crypto v0.31.0 // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/text v0.21.0 // indirect diff --git a/go.sum b/go.sum index 45abfb59..0ed9ffca 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,6 @@ cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg= -fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -65,8 +63,8 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs= -github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps= +github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= +github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= 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= @@ -79,10 +77,6 @@ github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWa github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dhaavi/go-notify v0.0.0-20190209221809-c404b1f22435 h1:AnwbdEI8eV3GzLM3SlrJlYmYa6OB5X8RwY4A8QJOCP0= -github.com/dhaavi/go-notify v0.0.0-20190209221809-c404b1f22435/go.mod h1:EMJ8XWTopp8OLRBMUm9vHE8Wn48CNpU21HM817OKNrc= -github.com/dhaavi/winres v0.2.2 h1:SUago7FwhgLSMyDdeuV6enBZ+ZQSl0KwcnbWzvlfBls= -github.com/dhaavi/winres v0.2.2/go.mod h1:1NTs+/DtKP1BplIL1+XQSoq4X1PUfLczexS7gf3x9T4= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= @@ -126,12 +120,10 @@ github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZs github.com/go-sql-driver/mysql v1.7.2-0.20231213112541-0004702b931d h1:QQP1nE4qh5aHTGvI1LgOFxZYVxYoGeMfbNHikogPyoA= github.com/go-sql-driver/mysql v1.7.2-0.20231213112541-0004702b931d/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4= -github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godror/godror v0.40.4 h1:X1e7hUd02GDaLWKZj40Z7L0CP0W9TrGgmPQZw6+anBg= @@ -150,8 +142,6 @@ github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f/go.mod h1:ijRvpgDJDI26 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4= github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/lint v0.0.0-20170918230701-e5d664eb928e/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -251,38 +241,25 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lmittmann/tint v1.0.5 h1:NQclAutOfYsqs2F1Lenue6OoWCajs5wJcP3DfWVpePw= github.com/lmittmann/tint v1.0.5/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/maruel/panicparse/v2 v2.3.1 h1:NtJavmbMn0DyzmmSStE8yUsmPZrZmudPH7kplxBinOA= github.com/maruel/panicparse/v2 v2.3.1/go.mod h1:s3UmQB9Fm/n7n/prcD2xBGDkwXD6y2LeZnhbEXvs9Dg= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= -github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lmittmann/tint v1.0.6 h1:vkkuDAZXc0EFGNzYjWcV0h7eEX+uujH48f/ifSkJWgc= -github.com/lmittmann/tint v1.0.6/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= -github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/maruel/panicparse/v2 v2.4.0 h1:yQKMIbQ0DKfinzVkTkcUzQyQ60UCiNnYfR7PWwTs2VI= -github.com/maruel/panicparse/v2 v2.4.0/go.mod h1:nOY2OKe8csO3F3SA5+hsxot05JLgukrF54B9x88fVp4= -github.com/mat/besticon v3.12.0+incompatible h1:1KTD6wisfjfnX+fk9Kx/6VEZL+MAW1LhCkL9Q47H9Bg= -github.com/mat/besticon v3.12.0+incompatible/go.mod h1:mA1auQYHt6CW5e7L9HJLmqVQC8SzNk2gVwouO0AbiEU= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-oci8 v0.1.1 h1:aEUDxNAyDG0tv8CA3TArnDQNyc4EhnWlsfxRgDHABHM= @@ -501,8 +478,6 @@ golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= @@ -550,8 +525,6 @@ golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= @@ -591,19 +564,17 @@ golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -614,8 +585,6 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= From 774a83dc33f5343e5f436f4c2253d0847849ca21 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Wed, 28 May 2025 13:20:05 +0300 Subject: [PATCH 33/33] Bump version 2.0.17 --- desktop/angular/package-lock.json | 4 ++-- desktop/angular/package.json | 2 +- desktop/tauri/src-tauri/Cargo.lock | 2 +- desktop/tauri/src-tauri/Cargo.toml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/desktop/angular/package-lock.json b/desktop/angular/package-lock.json index 0022ace7..8e3f4b5d 100644 --- a/desktop/angular/package-lock.json +++ b/desktop/angular/package-lock.json @@ -1,12 +1,12 @@ { "name": "portmaster", - "version": "2.0.16", + "version": "2.0.17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "portmaster", - "version": "2.0.16", + "version": "2.0.17", "dependencies": { "@angular/animations": "^16.0.1", "@angular/cdk": "^16.0.1", diff --git a/desktop/angular/package.json b/desktop/angular/package.json index f09fe8d8..d89a30a0 100644 --- a/desktop/angular/package.json +++ b/desktop/angular/package.json @@ -1,6 +1,6 @@ { "name": "portmaster", - "version": "2.0.16", + "version": "2.0.17", "scripts": { "ng": "ng", "start": "npm install && npm run build-libs:dev && ng serve --proxy-config ./proxy.json", diff --git a/desktop/tauri/src-tauri/Cargo.lock b/desktop/tauri/src-tauri/Cargo.lock index ff7fd78d..a6b75bec 100644 --- a/desktop/tauri/src-tauri/Cargo.lock +++ b/desktop/tauri/src-tauri/Cargo.lock @@ -3945,7 +3945,7 @@ dependencies = [ [[package]] name = "portmaster" -version = "2.0.16" +version = "2.0.17" dependencies = [ "assert_matches", "cached", diff --git a/desktop/tauri/src-tauri/Cargo.toml b/desktop/tauri/src-tauri/Cargo.toml index e92dcabb..f8eb93ef 100644 --- a/desktop/tauri/src-tauri/Cargo.toml +++ b/desktop/tauri/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "portmaster" -version = "2.0.16" +version = "2.0.17" description = "Portmaster UI" authors = ["Safing"] license = ""