diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 823617e..7d9f201 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -16,20 +16,18 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Check out code
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
 
     - name: Setup Go
-      uses: actions/setup-go@v4
+      uses: actions/setup-go@v5
       with:
         go-version: '^1.21'
-
-    - name: Get dependencies
-      run: go mod download
+        cache: false
 
     - name: Run golangci-lint
-      uses: golangci/golangci-lint-action@v3
+      uses: golangci/golangci-lint-action@v4
       with:
-        version: v1.52.2
+        version: v1.57.1
         only-new-issues: true
         args: -c ./.golangci.yml --timeout 15m
 
@@ -41,12 +39,13 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Check out code
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
 
     - name: Setup Go
-      uses: actions/setup-go@v4
+      uses: actions/setup-go@v5
       with:
         go-version: '^1.21'
+        cache: false
 
     - name: Get dependencies
       run: go mod download
diff --git a/container/container.go b/container/container.go
index 3f3292d..d30e961 100644
--- a/container/container.go
+++ b/container/container.go
@@ -4,7 +4,7 @@ import (
 	"errors"
 	"io"
 
-	"github.com/safing/portbase/formats/varint"
+	"github.com/safing/structures/varint"
 )
 
 // Container is []byte sclie on steroids, allowing for quick data appending, prepending and fetching.
diff --git a/container/container_test.go b/container/container_test.go
index 0c8e636..21c3745 100644
--- a/container/container_test.go
+++ b/container/container_test.go
@@ -3,8 +3,6 @@ package container
 import (
 	"bytes"
 	"testing"
-
-	"github.com/safing/portbase/utils"
 )
 
 var (
@@ -25,7 +23,7 @@ var (
 func TestContainerDataHandling(t *testing.T) {
 	t.Parallel()
 
-	c1 := New(utils.DuplicateBytes(testData))
+	c1 := New(duplicateBytes(testData))
 	c1c := c1.carbonCopy()
 
 	c2 := New()
@@ -87,7 +85,7 @@ func compareMany(t *testing.T, reference []byte, other ...[]byte) {
 func TestDataFetching(t *testing.T) {
 	t.Parallel()
 
-	c1 := New(utils.DuplicateBytes(testData))
+	c1 := New(duplicateBytes(testData))
 	data := c1.GetMax(1)
 	if string(data[0]) != "T" {
 		t.Errorf("failed to GetMax(1), got %s, expected %s", string(data), "T")
@@ -107,7 +105,7 @@ func TestDataFetching(t *testing.T) {
 func TestBlocks(t *testing.T) {
 	t.Parallel()
 
-	c1 := New(utils.DuplicateBytes(testData))
+	c1 := New(duplicateBytes(testData))
 	c1.PrependLength()
 
 	n, err := c1.GetNextN8()
@@ -149,7 +147,7 @@ func TestBlocks(t *testing.T) {
 func TestContainerBlockHandling(t *testing.T) {
 	t.Parallel()
 
-	c1 := New(utils.DuplicateBytes(testData))
+	c1 := New(duplicateBytes(testData))
 	c1.PrependLength()
 	c1.AppendAsBlock(testData)
 	c1c := c1.carbonCopy()
@@ -204,5 +202,12 @@ func TestContainerMisc(t *testing.T) {
 func TestDeprecated(t *testing.T) {
 	t.Parallel()
 
-	NewContainer(utils.DuplicateBytes(testData))
+	NewContainer(duplicateBytes(testData))
+}
+
+// duplicateBytes returns a new copy of the given byte slice.
+func duplicateBytes(a []byte) []byte {
+	b := make([]byte, len(a))
+	copy(b, a)
+	return b
 }
diff --git a/dsd/compression.go b/dsd/compression.go
index ebaa11c..0c699af 100644
--- a/dsd/compression.go
+++ b/dsd/compression.go
@@ -5,7 +5,7 @@ import (
 	"compress/gzip"
 	"errors"
 
-	"github.com/safing/portbase/formats/varint"
+	"github.com/safing/structures/varint"
 )
 
 // DumpAndCompress stores the interface as a dsd formatted data structure and compresses the resulting data.
diff --git a/dsd/dsd.go b/dsd/dsd.go
index 2664877..f04a7c9 100644
--- a/dsd/dsd.go
+++ b/dsd/dsd.go
@@ -13,8 +13,7 @@ import (
 	"github.com/ghodss/yaml"
 	"github.com/vmihailenco/msgpack/v5"
 
-	"github.com/safing/portbase/formats/varint"
-	"github.com/safing/portbase/utils"
+	"github.com/safing/structures/varint"
 )
 
 // Load loads an dsd structured data blob into the given interface.
@@ -39,25 +38,25 @@ func LoadAsFormat(data []byte, format uint8, t interface{}) (err error) {
 	case JSON:
 		err = json.Unmarshal(data, t)
 		if err != nil {
-			return fmt.Errorf("dsd: failed to unpack json: %w, data: %s", err, utils.SafeFirst16Bytes(data))
+			return fmt.Errorf("dsd: failed to unpack json: %w, data: %s", err, safeFirst16Bytes(data))
 		}
 		return nil
 	case YAML:
 		err = yaml.Unmarshal(data, t)
 		if err != nil {
-			return fmt.Errorf("dsd: failed to unpack yaml: %w, data: %s", err, utils.SafeFirst16Bytes(data))
+			return fmt.Errorf("dsd: failed to unpack yaml: %w, data: %s", err, safeFirst16Bytes(data))
 		}
 		return nil
 	case CBOR:
 		err = cbor.Unmarshal(data, t)
 		if err != nil {
-			return fmt.Errorf("dsd: failed to unpack cbor: %w, data: %s", err, utils.SafeFirst16Bytes(data))
+			return fmt.Errorf("dsd: failed to unpack cbor: %w, data: %s", err, safeFirst16Bytes(data))
 		}
 		return nil
 	case MsgPack:
 		err = msgpack.Unmarshal(data, t)
 		if err != nil {
-			return fmt.Errorf("dsd: failed to unpack msgpack: %w, data: %s", err, utils.SafeFirst16Bytes(data))
+			return fmt.Errorf("dsd: failed to unpack msgpack: %w, data: %s", err, safeFirst16Bytes(data))
 		}
 		return nil
 	case GenCode:
@@ -67,7 +66,7 @@ func LoadAsFormat(data []byte, format uint8, t interface{}) (err error) {
 		}
 		_, err = genCodeStruct.GenCodeUnmarshal(data)
 		if err != nil {
-			return fmt.Errorf("dsd: failed to unpack gencode: %w, data: %s", err, utils.SafeFirst16Bytes(data))
+			return fmt.Errorf("dsd: failed to unpack gencode: %w, data: %s", err, safeFirst16Bytes(data))
 		}
 		return nil
 	default:
diff --git a/dsd/http_test.go b/dsd/http_test.go
index 32651ac..ed6f39b 100644
--- a/dsd/http_test.go
+++ b/dsd/http_test.go
@@ -5,6 +5,7 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestMimeTypes(t *testing.T) {
@@ -13,12 +14,12 @@ func TestMimeTypes(t *testing.T) {
 	// Test static maps.
 	for _, mimeType := range FormatToMimeType {
 		cleaned, _, err := mime.ParseMediaType(mimeType)
-		assert.NoError(t, err, "mime type must be parse-able")
+		require.NoError(t, err, "mime type must be parse-able")
 		assert.Equal(t, mimeType, cleaned, "mime type should be clean in map already")
 	}
 	for mimeType := range MimeTypeToFormat {
 		cleaned, _, err := mime.ParseMediaType(mimeType)
-		assert.NoError(t, err, "mime type must be parse-able")
+		require.NoError(t, err, "mime type must be parse-able")
 		assert.Equal(t, mimeType, cleaned, "mime type should be clean in map already")
 	}
 
diff --git a/dsd/safe.go b/dsd/safe.go
new file mode 100644
index 0000000..e4a47f9
--- /dev/null
+++ b/dsd/safe.go
@@ -0,0 +1,23 @@
+package dsd
+
+import (
+	"encoding/hex"
+	"strings"
+)
+
+// safeFirst16Bytes return the first 16 bytes of the given data in safe form.
+func safeFirst16Bytes(data []byte) string {
+	if len(data) == 0 {
+		return "<empty>"
+	}
+
+	return strings.TrimPrefix(
+		strings.SplitN(hex.Dump(data), "\n", 2)[0],
+		"00000000  ",
+	)
+}
+
+// safeFirst16Chars return the first 16 characters of the given data in safe form.
+func safeFirst16Chars(s string) string {
+	return safeFirst16Bytes([]byte(s))
+}
diff --git a/dsd/safe_test.go b/dsd/safe_test.go
new file mode 100644
index 0000000..2fa0c33
--- /dev/null
+++ b/dsd/safe_test.go
@@ -0,0 +1,29 @@
+package dsd
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestSafeFirst16(t *testing.T) {
+	t.Parallel()
+
+	assert.Equal(t,
+		"47 6f 20 69 73 20 61 6e  20 6f 70 65 6e 20 73 6f  |Go is an open so|",
+		safeFirst16Bytes([]byte("Go is an open source programming language.")),
+	)
+	assert.Equal(t,
+		"47 6f 20 69 73 20 61 6e  20 6f 70 65 6e 20 73 6f  |Go is an open so|",
+		safeFirst16Chars("Go is an open source programming language."),
+	)
+
+	assert.Equal(t,
+		"<empty>",
+		safeFirst16Bytes(nil),
+	)
+	assert.Equal(t,
+		"<empty>",
+		safeFirst16Chars(""),
+	)
+}
diff --git a/go.mod b/go.mod
index 0f288f2..ff17c55 100644
--- a/go.mod
+++ b/go.mod
@@ -7,17 +7,14 @@ toolchain go1.21.2
 require (
 	github.com/fxamacker/cbor/v2 v2.7.0
 	github.com/ghodss/yaml v1.0.0
-	github.com/safing/portbase v0.19.5
 	github.com/stretchr/testify v1.8.4
 	github.com/vmihailenco/msgpack/v5 v5.4.1
 )
 
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/gofrs/uuid v4.4.0+incompatible // indirect
 	github.com/kr/pretty v0.2.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/tevino/abool v1.2.0 // indirect
 	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
 	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
diff --git a/go.sum b/go.sum
index 1033679..fc4b73e 100644
--- a/go.sum
+++ b/go.sum
@@ -4,8 +4,6 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv
 github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
 github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-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/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
 github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -13,12 +11,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 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/safing/portbase v0.19.5 h1:3/8odzlvb629tHPwdj/sthSeJcwZHYrqA6YuvNUZzNc=
-github.com/safing/portbase v0.19.5/go.mod h1:Qrh3ck+7VZloFmnozCs9Hj8godhJAi55cmiDiC7BwTc=
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA=
-github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
 github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
 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=
diff --git a/test b/test
new file mode 100755
index 0000000..82dd86b
--- /dev/null
+++ b/test
@@ -0,0 +1,168 @@
+#!/bin/bash
+
+warnings=0
+errors=0
+scripted=0
+goUp="\\e[1A"
+fullTestFlags="-short"
+install=0
+testonly=0
+
+function help {
+  echo "usage: $0 [command] [options]"
+  echo ""
+  echo "commands:"
+  echo "  <none>        run baseline tests"
+  echo "  full          run full tests (ie. not short)"
+  echo "  install       install deps for running tests"
+  echo ""
+  echo "options:"
+  echo "  --scripted    don't jump console lines (still use colors)"
+  echo "  --test-only   run tests only, no linters"
+  echo "  [package]     run only on this package"
+}
+
+function run {
+  if [[ $scripted -eq 0 ]]; then
+    echo "[......] $*"
+  fi
+
+  # create tmpfile
+  tmpfile=$(mktemp)
+  # execute
+  $* >$tmpfile 2>&1
+  rc=$?
+  output=$(cat $tmpfile)
+
+  # check return code
+  if [[ $rc -eq 0 ]]; then
+    if [[ $output == *"[no test files]"* ]]; then
+      echo -e "${goUp}[\e[01;33mNOTEST\e[00m] $*"
+      warnings=$((warnings+1))
+    else
+      echo -ne "${goUp}[\e[01;32m  OK  \e[00m] "
+      if [[ $2 == "test" ]]; then
+        echo -n $*
+        echo -n ": "
+        echo $output | cut -f "3-" -d " "
+      else
+        echo $*
+      fi
+    fi
+  else
+    if [[ $output == *"build constraints exclude all Go files"* ]]; then
+      echo -e "${goUp}[ !=OS ] $*"
+    else
+      echo -e "${goUp}[\e[01;31m FAIL \e[00m] $*"
+      cat $tmpfile
+      errors=$((errors+1))
+    fi
+  fi
+
+  rm -f $tmpfile
+}
+
+# get and switch to script dir
+baseDir="$( cd "$(dirname "$0")" && pwd )"
+cd "$baseDir"
+
+# args
+while true; do
+  case "$1" in
+  "-h"|"help"|"--help")
+    help
+    exit 0
+    ;;
+  "--scripted")
+    scripted=1
+    goUp=""
+    shift 1
+    ;;
+  "--test-only")
+    testonly=1
+    shift 1
+    ;;
+  "install")
+    install=1
+    shift 1
+    ;;
+  "full")
+    fullTestFlags=""
+    shift 1
+    ;;
+  *)
+    break
+    ;;
+  esac
+done
+
+# check if $GOPATH/bin is in $PATH
+if [[ $PATH != *"$GOPATH/bin"* ]]; then
+  export PATH=$GOPATH/bin:$PATH
+fi
+
+# install
+if [[ $install -eq 1 ]]; then
+  echo "installing dependencies..."
+  # TODO: update golangci-lint version regularly
+  echo "$ curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.44.0"
+  curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.44.0
+  exit 0
+fi
+
+# check dependencies
+if [[ $(which go) == "" ]]; then
+  echo "go command not found"
+  exit 1
+fi
+if [[ $testonly -eq 0 ]]; then
+  if [[ $(which gofmt) == "" ]]; then
+    echo "gofmt command not found"
+    exit 1
+  fi
+  if [[ $(which golangci-lint) == "" ]]; then
+    echo "golangci-lint command not found"
+    echo "install with: curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin vX.Y.Z"
+    echo "don't forget to specify the version you want"
+    echo "or run: ./test install"
+    echo ""
+    echo "alternatively, install the current dev version with: go get -u github.com/golangci/golangci-lint/cmd/golangci-lint"
+    exit 1
+  fi
+fi
+
+# target selection
+if [[ "$1" == "" ]]; then
+  # get all packages
+  packages=$(go list -e ./...)
+else
+  # single package testing
+  packages=$(go list -e)/$1
+  echo "note: only running tests for package $packages"
+fi
+
+# platform info
+platformInfo=$(go env GOOS GOARCH)
+echo "running tests for ${platformInfo//$'\n'/ }:"
+
+# run vet/test on packages
+for package in $packages; do
+  packagename=${package#github.com/safing/structures} #TODO: could be queried with `go list .`
+  packagename=${packagename#/}
+  echo ""
+  echo $package
+  if [[ $testonly -eq 0 ]]; then
+    run go vet $package
+    run golangci-lint run $packagename
+  fi
+  run go test -cover $fullTestFlags $package
+done
+
+echo ""
+if [[ $errors -gt 0 ]]; then
+  echo "failed with $errors errors and $warnings warnings"
+  exit 1
+else
+  echo "succeeded with $warnings warnings"
+  exit 0
+fi