diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..47ec750 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,57 @@ +name: CI + +on: + push: + pull_request: + +defaults: + run: + shell: bash + +env: + LIBDAVE_VERSION: v1.1.0 + +jobs: + libdave: + strategy: + matrix: + runner: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, macos-15-intel, windows-latest] + fail-fast: false + + runs-on: ${{matrix.runner}} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: 1.24 + + - name: Setup go.work + run: | + go work init . ./libdave ./golibdave + + - name: "[Windows Only] Install pkgconfiglite" + if: runner.os == 'Windows' + run: choco install pkgconfiglite + + - name: Install libdave + run: | + if [ "$RUNNER_OS" = "Windows" ]; then + pwsh ./scripts/libdave_install.ps1 "$LIBDAVE_VERSION" + echo "$LOCALAPPDATA/libdave/bin" >> "$GITHUB_PATH" + else + ./scripts/libdave_install.sh "$LIBDAVE_VERSION" + fi + + - name: Test libdave + run: | + if [ "$RUNNER_OS" == "Windows" ]; then + export PKG_CONFIG_PATH="$LOCALAPPDATA/pkgconfig;$PKG_CONFIG_PATH" + else + export PKG_CONFIG_PATH="$HOME/.local/lib/pkgconfig:$PKG_CONFIG_PATH" + fi + + go test ./libdave diff --git a/.github/workflows/update_libdave.yml b/.github/workflows/update_libdave.yml deleted file mode 100644 index 34e187a..0000000 --- a/.github/workflows/update_libdave.yml +++ /dev/null @@ -1,199 +0,0 @@ -name: Update libdave - -on: - workflow_dispatch: -# schedule: -# # Runs at 00:00 UTC every day -# - cron: '0 0 * * *' - -permissions: - contents: write - pull-requests: write - -env: - LIBDAVE_REPO: "https://github.com/discord/libdave" - -jobs: - check-version: - runs-on: ubuntu-latest - outputs: - should_update: ${{ steps.check.outputs.should_update }} - new_sha: ${{ steps.check.outputs.new_sha }} - steps: - - uses: actions/checkout@v4 - - - name: Check Remote vs Local SHA - id: check - run: | - # Get the latest commit hash from the remote repository - REMOTE_SHA=$(git ls-remote https://github.com/discord/libdave HEAD | awk '{ print $1 }') - echo "Remote SHA: $REMOTE_SHA" - - LOCAL_FILE="libdave/lib/sha.txt" - LOCAL_SHA="" - - if [ -f "$LOCAL_FILE" ]; then - LOCAL_SHA=$(cat "$LOCAL_FILE" | tr -d '[:space:]') - echo "Local SHA: $LOCAL_SHA" - else - echo "Local SHA file not found." - fi - - if [ "$REMOTE_SHA" != "$LOCAL_SHA" ]; then - echo "Mismatch detected. Triggering build." - echo "should_update=true" >> "$GITHUB_OUTPUT" - echo "new_sha=$REMOTE_SHA" >> "$GITHUB_OUTPUT" - else - echo "Hashes match. No update needed." - echo "should_update=false" >> "$GITHUB_OUTPUT" - fi - - build-artifacts: - needs: check-version - if: needs.check-version.outputs.should_update == 'true' - strategy: - fail-fast: false - matrix: - include: - # Linux -# - os: ubuntu-latest -# arch: x86 -# platform: linux -# install_cmd: sudo apt-get update && sudo apt-get install -y gcc-multilib g++-multilib -# extra_flags: "-m32" - - os: ubuntu-latest - arch: x86-64 - platform: linux - - os: ubuntu-24.04-arm - arch: aarch64 - platform: linux - - # MacOS - - os: macos-15-intel - arch: x86-64 - platform: darwin - - os: macos-latest - arch: aarch64 - platform: darwin - - # Windows -# - os: windows-latest -# arch: x86 -# platform: windows -# extra_flags: "-m32" -# - os: windows-latest -# arch: amd64 -# platform: windows -# - os: windows-11-arm -# arch: aarch64 -# platform: linux - - runs-on: ${{ matrix.os }} - env: - CFLAGS: "-O2 ${{ matrix.extra_flags }}" - CXXFLAGS: "-O2 ${{ matrix.extra_flags }}" - defaults: - run: - shell: bash - - steps: - - name: Install Dependencies - if: matrix.install_cmd != '' - run: ${{ matrix.install_cmd }} - - - name: Setup libdave repo - run: | - git clone "$LIBDAVE_REPO" libdave - cd libdave/cpp - - git submodule update --init --recursive - - ./vcpkg/bootstrap-vcpkg.sh -disableMetrics - - - name: Build libdave - working-directory: libdave/cpp - run: | - make shared - - - name: Rename and Prepare Artifact - working-directory: libdave/cpp - run: | - mkdir -p staging - - case "${{ matrix.platform }}" in - linux) - EXTENSION="so" - ;; - windows) - EXTENSION="dll" - ;; - darwin) - EXTENSION="dylib" - ;; - *) - echo "Unknown architecture" - exit 1 - ;; - esac - - cp "build/libdave.$EXTENSION" "staging/libdave_${{ matrix.arch }}.$EXTENSION" - - if [ "${{ matrix.os }}" == "ubuntu-latest" ] && [ "${{ matrix.arch }}" == "x86-64" ]; then - cp includes/dave.h staging/dave.h - fi - - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: artifact-${{ matrix.platform }}-${{ matrix.arch }} - path: libdave/cpp/staging/* - if-no-files-found: error - - - create-pr: - needs: [check-version, build-artifacts] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: downloaded_artifacts - - - name: Organize Files & Generate Hashes - run: | - mkdir -p libdave/lib/build/linux - mkdir -p libdave/lib/build/darwin - mkdir -p libdave/lib/build/windows - mkdir -p libdave/cpp/includes - - process_artifacts() { - platform=$1 - dest_dir="libdave/lib/$platform" - - # Find and move files for this platform - find downloaded_artifacts -name "*$platform*" -type f \( -name "*.so" -o -name "*.dylib" -o -name "*.dll" \) -exec cp {} "$dest_dir/" \; - } - - process_artifacts "linux" - process_artifacts "darwin" - process_artifacts "windows" - - echo "${{ needs.check_version.outputs.latest_sha }}" > ${{ env.TARGET_SHA_FILE }} - - echo "Generating SHA256 checksums..." - find libdave/lib -type f \( -name "*.so" -o -name "*.dylib" -o -name "*.dll" \) -exec sha256sum {} + > libdave/lib/sha256.txt - - rm -rf downloaded_artifacts - - - name: Create Pull Request - uses: peter-evans/create-pull-request@v7 - with: - token: ${{ secrets.GITHUB_TOKEN }} - commit-message: "chore: update libdave to ${{ needs.check-version.outputs.new_sha }}" - branch: update-libdave-${{ needs.check_version.outputs.latest_sha }} - delete-branch: true - title: "chore: update libdave to ${{ needs.check-version.outputs.new_sha }}" - body: | - Update libdave prebuilt libraries to use ${{ needs.check-version.outputs.new_sha }}. diff --git a/README.md b/README.md index c058a6c..f8e5055 100644 --- a/README.md +++ b/README.md @@ -9,47 +9,68 @@ # GoDave -GoDave is a library that provides Go bindings for [libdave](https://github.com/discord/libdave) and provides a generic DAVE interface allowing for different implementations in the future. +GoDave is a library that provides Go bindings for [libdave](https://github.com/discord/libdave) and provides a generic DAVE interface allowing for +different implementations in the future. ## Summary + 1. [Libdave Installation](#libdave-installation) -2. [Installation Script (Recommended)](#installation-script-recommended) -3. [Manual Build](#manual-build) -4. [Example Usage](#example-usage) -5. [License](#license) + 1. [Windows Installation](#windows-instructions) + 2. [Installing manually](#manual-installation) +2. [Example Usage](#example-usage) +3. [License](#license) ## Libdave Installation -This library uses CGO and dynamic linking to use libdave. As such, it needs to be installed in the system beforehand -to build this library. +# FIXME: Needs rewriting + +This library uses CGO and dynamic linking to use libdave. We automatically pull the latest libdave version and link +against the [shared libraries published by Discord](https://github.com/discord/libdave/releases). + +We provide helpful scripts under [scripts/](https://github.com/disgoorg/godave/tree/master/scripts) to allow you to +download pre-built binaries of build them yourself, depending on your needs. Please audit them before executing! > [!NOTE] > Due to the nature of this project, it might be necessary to re-install libdave when updating to a new GoDave version. -> -> Versions requiring this will be denoted with a bump in the major version (for reference: major.minor.patch). +> The version that require this may be indicated with a minor bump (for reference: `mayor.minor.patch`). +> +> You can see what version is required by checking [this file](https://github.com/disgoorg/godave/tree/master/libdave/release.txt) -### Installation Script (Recommended) +### Linux/MacOS/WSL instructions -We provide helpful scripts in [scripts/](https://github.com/disgoorg/godave/tree/master/scripts) to simplify installing -a compatible libdave version. Grab whichever one is applicable to your OS (`.sh` for Linux and MacOS; `ps1` for -Windows PowerShell) and (after auditing its contents) run it and follow any instructions it might output. +Open a terminal and execute the following commands: -Once that step is complete, you can continue with the installation of GoDave. +```bash +# Set CC/CXX variables to change the compiler used (ie, for clang +#export CC=/usr/bin/clang CXX=/usr/bin/clang -### Manual Build +./libdave_install.sh v1.1.0 +``` -For a manual build, please clone https://github.com/discord/libdave and use revision -`74979cb33febf4ddef0c2b66e57520b339550c17`. +### MUSL Linux -> [!NOTE] -> We provide no guarantees for this version of GoDave to run for other revisions other than that the one mentioned above. -> -> As the library evolves and new versions of libdave are released, the above revision will be updated to match the -> GoDave version - -Once checked out, please follow the -[build instructions](https://github.com/discord/libdave/tree/74979cb33febf4ddef0c2b66e57520b339550c17/cpp#building) and -setup the appropriate `pkg-config` file and configuration to allow for discovery at compilation time. +If you want to build a MUSL version of libdave, you can execute the following commands: + +```bash +export VCPKG_FORCE_SYSTEM_BINARIES=1 +export CC=/usr/bin/gcc CXX=/usr/bin/g++ +export CXXFLAGS="-Wno-error=maybe-uninitialized" + +# Install necessary packages +apk add build-base cmake ninja zip unzip curl git pkgconfig perl nams go + +# FORCE_BUILD=1 as Discord do not provide pre-built binaries +FORCE_BUILD=1 ./libdave_install.sh v1.1.0 +``` + +### Windows instructions + +Open Powershell and execute the following commands: + +```ps1 +Set-ExecutionPolicy RemoteSigned –Scope Process +.\libdave_install.ps1 v1.1.0 +``` ## Example Usage diff --git a/libdave/commit_result.go b/libdave/commit_result.go index db2758e..4ae4a05 100644 --- a/libdave/commit_result.go +++ b/libdave/commit_result.go @@ -15,9 +15,9 @@ func newCommitResult(handle commitResultHandle) *CommitResult { handle: handle, } - runtime.AddCleanup(commitResult, func(handle commitResultHandle) { - C.daveCommitResultDestroy(handle) - }, commitResult.handle) + runtime.SetFinalizer(commitResult, func(c *CommitResult) { + C.daveCommitResultDestroy(c.handle) + }) return commitResult } @@ -37,7 +37,7 @@ func (r *CommitResult) GetRosterMemberIDs() []uint64 { ) C.daveCommitResultGetRosterMemberIds(r.handle, &rosterIDs, &rosterIDsLength) - return newCUint64MemoryView(rosterIDs, rosterIDsLength) + return newUint64Slice(rosterIDs, rosterIDsLength) } func (r *CommitResult) GetRosterMemberSignature(rosterID uint64) []byte { @@ -47,5 +47,5 @@ func (r *CommitResult) GetRosterMemberSignature(rosterID uint64) []byte { ) C.daveCommitResultGetRosterMemberSignature(r.handle, C.uint64_t(rosterID), &rosterMemberSignature, &rosterMemberSignatureLength) - return newCBytesMemoryView(rosterMemberSignature, rosterMemberSignatureLength) + return newByteSlice(rosterMemberSignature, rosterMemberSignatureLength) } diff --git a/libdave/decryptor.go b/libdave/decryptor.go index 24cbe64..2b898c5 100644 --- a/libdave/decryptor.go +++ b/libdave/decryptor.go @@ -19,8 +19,8 @@ const ( func (r decryptorResultCode) ToError() error { switch r { - case decryptorResultCodeDecryptionFailure: - return ErrDecryptionFailure + case decryptorResultCodeSuccess: + return nil case decryptorResultCodeMissingKeyRatchet: return ErrMissingKeyRatchet case decryptorResultCodeInvalidNonce: @@ -28,7 +28,7 @@ func (r decryptorResultCode) ToError() error { case decryptorResultCodeMissingCryptor: return ErrMissingCryptor default: - return nil + return ErrGenericDecryptionFailure } } @@ -53,9 +53,9 @@ func NewDecryptor() *Decryptor { handle: C.daveDecryptorCreate(), } - runtime.AddCleanup(decryptor, func(handle decryptorHandle) { - C.daveDecryptorDestroy(handle) - }, decryptor.handle) + runtime.SetFinalizer(decryptor, func(d *Decryptor) { + C.daveDecryptorDestroy(decryptor.handle) + }) return decryptor } diff --git a/libdave/encryptor.go b/libdave/encryptor.go index a172405..9ed2830 100644 --- a/libdave/encryptor.go +++ b/libdave/encryptor.go @@ -1,10 +1,14 @@ package libdave // #include "dave.h" +// extern void godaveProtocolVersionChangedCallback(void* userData); import "C" import ( + "log/slog" "runtime" + "runtime/cgo" "unsafe" + "weak" ) type encryptorResultCode int @@ -12,17 +16,38 @@ type encryptorResultCode int const ( encryptorResultCodeSuccess encryptorResultCode = iota encryptorResultCodeEncryptionFailure + encryptorResultCodeMissingKeyRatchet + encryptionResultCodeMissingCryptor + encryptionResultCodeTooManyAttempts ) func (r encryptorResultCode) ToError() error { switch r { - case encryptorResultCodeEncryptionFailure: - return ErrEncryptionFailure - default: + case encryptorResultCodeSuccess: return nil + case encryptorResultCodeMissingKeyRatchet: + return ErrMissingKeyRatchet + case encryptionResultCodeMissingCryptor: + return ErrMissingCryptor + case encryptionResultCodeTooManyAttempts: + return ErrTooManyAttempts + default: + return ErrGenericEncryptionFailure } } +//export godaveProtocolVersionChangedCallback +func godaveProtocolVersionChangedCallback(userData unsafe.Pointer) { + h := *(*cgo.Handle)(userData) + encryptor := h.Value().(weak.Pointer[Encryptor]).Value() + + if encryptor == nil { + return + } + + defaultLogger.Load().Debug("protocol version changed", slog.Int("newVersion", int(encryptor.GetProtocolVersion()))) +} + type EncryptorStats struct { PassthroughCount uint64 EncryptSuccessCount uint64 @@ -36,7 +61,9 @@ type EncryptorStats struct { type encryptionHandle = C.DAVEEncryptorHandle type Encryptor struct { - handle encryptionHandle + handle encryptionHandle + pinner runtime.Pinner + cgoHandle cgo.Handle } func NewEncryptor() *Encryptor { @@ -44,13 +71,31 @@ func NewEncryptor() *Encryptor { handle: C.daveEncryptorCreate(), } - runtime.AddCleanup(encryptor, func(handle encryptionHandle) { - C.daveEncryptorDestroy(handle) - }, encryptor.handle) + // A weak pointer is necessary here to avoid circular refs + encryptor.cgoHandle = cgo.NewHandle(weak.Make(encryptor)) + + C.daveEncryptorSetProtocolVersionChangedCallback( + encryptor.handle, + C.DAVEEncryptorProtocolVersionChangedCallback(C.godaveProtocolVersionChangedCallback), + unsafe.Pointer(&encryptor.cgoHandle), + ) + + runtime.SetFinalizer(encryptor, func(e *Encryptor) { + C.daveEncryptorDestroy(e.handle) + e.cgoHandle.Delete() + }) return encryptor } +func (e *Encryptor) HasKeyRatchet() bool { + return bool(C.daveEncryptorHasKeyRatchet(e.handle)) +} + +func (e *Encryptor) IsPassthroughMode() bool { + return bool(C.daveEncryptorIsPassthroughMode(e.handle)) +} + func (e *Encryptor) SetKeyRatchet(keyRatchet *KeyRatchet) { C.daveEncryptorSetKeyRatchet(e.handle, keyRatchet.handle) } @@ -87,11 +132,6 @@ func (e *Encryptor) Encrypt(mediaType MediaType, ssrc uint32, frame []byte, encr return int(bytesWritten), res.ToError() } -// FIXME: Implement -// func (e *Encryptor) SetProtocolVersionChangedCallback() { -// panic("TODO") -// } - func (e *Encryptor) GetStats(mediaType MediaType) *EncryptorStats { var cStats C.DAVEEncryptorStats C.daveEncryptorGetStats(e.handle, C.DAVEMediaType(mediaType), &cStats) diff --git a/libdave/errors.go b/libdave/errors.go index f46d0bd..8870c7d 100644 --- a/libdave/errors.go +++ b/libdave/errors.go @@ -3,9 +3,10 @@ package libdave import "errors" var ( - ErrEncryptionFailure = errors.New("failed to encrypt frame") - ErrDecryptionFailure = errors.New("failed to decrypt frame") - ErrMissingKeyRatchet = errors.New("missing key ratchet") - ErrInvalidNonce = errors.New("invalid nonce") - ErrMissingCryptor = errors.New("missing cryptor") + ErrGenericEncryptionFailure = errors.New("failed to encrypt frame") + ErrGenericDecryptionFailure = errors.New("failed to decrypt frame") + ErrMissingKeyRatchet = errors.New("missing key ratchet") + ErrInvalidNonce = errors.New("invalid nonce") + ErrMissingCryptor = errors.New("missing cryptor") + ErrTooManyAttempts = errors.New("too many attempts to encrypt the frame failed") ) diff --git a/libdave/key_ratchet.go b/libdave/key_ratchet.go index 81ea784..c8d3b5f 100644 --- a/libdave/key_ratchet.go +++ b/libdave/key_ratchet.go @@ -13,9 +13,9 @@ type KeyRatchet struct { func newKeyRatchet(handle keyRatchetHandle) *KeyRatchet { keyRatchet := &KeyRatchet{handle: handle} - runtime.AddCleanup(keyRatchet, func(handle keyRatchetHandle) { - C.daveKeyRatchetDestroy(handle) - }, keyRatchet.handle) + runtime.SetFinalizer(keyRatchet, func(k *KeyRatchet) { + C.daveKeyRatchetDestroy(k.handle) + }) return keyRatchet } diff --git a/libdave/lib.go b/libdave/lib.go index fe82003..418f186 100644 --- a/libdave/lib.go +++ b/libdave/lib.go @@ -1,97 +1,12 @@ package libdave +// FIXME: Consider https://pkg.go.dev/cmd/cgo#hdr-Optimizing_calls_of_C_code + // #cgo pkg-config: dave // #include "dave.h" -// extern void godaveGlobalLogCallback(DAVELoggingSeverity severity, char* file, int line, char* message); import "C" -import ( - "context" - "log/slog" - "sync/atomic" - "unsafe" -) - -var ( - logLoggerLevel slog.LevelVar - defaultLogger atomic.Pointer[slog.Logger] -) - -func init() { - SetDefaultLogLoggerLevel(slog.LevelError) - SetDefaultLogger(slog.New(newLogWrapper(slog.Default().Handler())). - With(slog.String("name", "libdave")), - ) - - C.daveSetLogSinkCallback(C.DAVELogSinkCallback(unsafe.Pointer(C.godaveGlobalLogCallback))) -} // MaxSupportedProtocolVersion returns the maximum supported libdave protocol version. func MaxSupportedProtocolVersion() uint16 { return uint16(C.daveMaxSupportedProtocolVersion()) } - -// SetDefaultLogger sets the default logger used by libdave. -func SetDefaultLogger(logger *slog.Logger) { - defaultLogger.Store(logger) -} - -// SetDefaultLogLoggerLevel sets the log level for libdave logs. -// By default, the level is set to slog.LevelError. -// It returns the previous log level. -func SetDefaultLogLoggerLevel(level slog.Level) (oldLevel slog.Level) { - oldLevel = logLoggerLevel.Level() - logLoggerLevel.Set(level) - return -} - -//export godaveGlobalLogCallback -func godaveGlobalLogCallback(severity C.DAVELoggingSeverity, file *C.char, line C.int, message *C.char) { - var slogSeverity slog.Level - switch severity { - case C.DAVE_LOGGING_SEVERITY_VERBOSE: - slogSeverity = slog.LevelDebug - case C.DAVE_LOGGING_SEVERITY_INFO: - slogSeverity = slog.LevelInfo - case C.DAVE_LOGGING_SEVERITY_WARNING: - slogSeverity = slog.LevelWarn - case C.DAVE_LOGGING_SEVERITY_ERROR: - slogSeverity = slog.LevelError - case C.DAVE_LOGGING_SEVERITY_NONE: - return - } - - defaultLogger.Load().Log(context.Background(), slogSeverity, C.GoString(message), slog.String("file", C.GoString(file)), slog.Int("line", int(line))) -} - -var _ slog.Handler = (*logWrapper)(nil) - -// newLogWrapper wraps the default slog.Handler and only enables logs at or above the given level. -func newLogWrapper(handler slog.Handler) *logWrapper { - return &logWrapper{ - handler: handler, - } -} - -type logWrapper struct { - handler slog.Handler -} - -func (l *logWrapper) Enabled(_ context.Context, level slog.Level) bool { - return level >= logLoggerLevel.Level() -} - -func (l *logWrapper) Handle(ctx context.Context, record slog.Record) error { - return l.handler.Handle(ctx, record) -} - -func (l logWrapper) WithAttrs(attrs []slog.Attr) slog.Handler { - return &logWrapper{ - handler: l.handler.WithAttrs(attrs), - } -} - -func (l logWrapper) WithGroup(name string) slog.Handler { - return &logWrapper{ - handler: l.handler.WithGroup(name), - } -} diff --git a/libdave/lib_test.go b/libdave/lib_test.go new file mode 100644 index 0000000..6e53b01 --- /dev/null +++ b/libdave/lib_test.go @@ -0,0 +1,13 @@ +package libdave + +import ( + "testing" +) + +func TestMaxSupportedProtocolVersion(t *testing.T) { + maxSupportedProtocolVersion := MaxSupportedProtocolVersion() + + if maxSupportedProtocolVersion != 1 { + t.Errorf("expected 1, got %d", maxSupportedProtocolVersion) + } +} diff --git a/libdave/logging.go b/libdave/logging.go new file mode 100644 index 0000000..26bf029 --- /dev/null +++ b/libdave/logging.go @@ -0,0 +1,91 @@ +package libdave + +// #include "dave.h" +// extern void godaveGlobalLogCallback(DAVELoggingSeverity severity, char* file, int line, char* message); +import "C" +import ( + "context" + "log/slog" + "sync/atomic" + "unsafe" +) + +var ( + logLoggerLevel slog.LevelVar + defaultLogger atomic.Pointer[slog.Logger] +) + +func init() { + SetDefaultLogLoggerLevel(slog.LevelError) + SetDefaultLogger(slog.New(newLogWrapper(slog.Default().Handler())). + With(slog.String("name", "libdave")), + ) + + C.daveSetLogSinkCallback(C.DAVELogSinkCallback(unsafe.Pointer(C.godaveGlobalLogCallback))) +} + +//export godaveGlobalLogCallback +func godaveGlobalLogCallback(severity C.DAVELoggingSeverity, file *C.char, line C.int, message *C.char) { + var slogSeverity slog.Level + switch severity { + case C.DAVE_LOGGING_SEVERITY_VERBOSE: + slogSeverity = slog.LevelDebug + case C.DAVE_LOGGING_SEVERITY_INFO: + slogSeverity = slog.LevelInfo + case C.DAVE_LOGGING_SEVERITY_WARNING: + slogSeverity = slog.LevelWarn + case C.DAVE_LOGGING_SEVERITY_ERROR: + slogSeverity = slog.LevelError + case C.DAVE_LOGGING_SEVERITY_NONE: + return + } + + defaultLogger.Load().Log(context.Background(), slogSeverity, C.GoString(message), slog.String("file", C.GoString(file)), slog.Int("line", int(line))) +} + +// SetDefaultLogger sets the default logger used by libdave. +func SetDefaultLogger(logger *slog.Logger) { + defaultLogger.Store(logger) +} + +// SetDefaultLogLoggerLevel sets the log level for libdave logs. +// By default, the level is set to slog.LevelError. +// It returns the previous log level. +func SetDefaultLogLoggerLevel(level slog.Level) (oldLevel slog.Level) { + oldLevel = logLoggerLevel.Level() + logLoggerLevel.Set(level) + return +} + +var _ slog.Handler = (*logWrapper)(nil) + +// newLogWrapper wraps the default slog.Handler and only enables logs at or above the given level. +func newLogWrapper(handler slog.Handler) *logWrapper { + return &logWrapper{ + handler: handler, + } +} + +type logWrapper struct { + handler slog.Handler +} + +func (l *logWrapper) Enabled(_ context.Context, level slog.Level) bool { + return level >= logLoggerLevel.Level() +} + +func (l *logWrapper) Handle(ctx context.Context, record slog.Record) error { + return l.handler.Handle(ctx, record) +} + +func (l logWrapper) WithAttrs(attrs []slog.Attr) slog.Handler { + return &logWrapper{ + handler: l.handler.WithAttrs(attrs), + } +} + +func (l logWrapper) WithGroup(name string) slog.Handler { + return &logWrapper{ + handler: l.handler.WithGroup(name), + } +} diff --git a/libdave/release.txt b/libdave/release.txt new file mode 100644 index 0000000..795460f --- /dev/null +++ b/libdave/release.txt @@ -0,0 +1 @@ +v1.1.0 diff --git a/libdave/session.go b/libdave/session.go index 1f10cc7..108183b 100644 --- a/libdave/session.go +++ b/libdave/session.go @@ -2,11 +2,13 @@ package libdave // #include // #include "dave.h" -// extern void libdaveGlobalFailureCallback(char* source, char* reason); +// extern void godaveGlobalFailureCallback(char* source, char* reason, void* userData); +// extern void godavePairwiseFingerprintCallback(uint8_t* fingerpint, size_t length, void* userData); import "C" import ( "log/slog" "runtime" + "runtime/cgo" "unsafe" ) @@ -16,9 +18,30 @@ type Session struct { handle sessionHandle } -//export libdaveGlobalFailureCallback -func libdaveGlobalFailureCallback(source *C.char, reason *C.char) { - defaultLogger.Load().Error(C.GoString(reason), slog.String("source", C.GoString(source))) +//export godaveGlobalFailureCallback +func godaveGlobalFailureCallback(source *C.char, reason *C.char, userData unsafe.Pointer) { + h := *(*cgo.Handle)(userData) + authSessionID := h.Value().(string) + + defaultLogger.Load().Error( + C.GoString(reason), + slog.String("source", C.GoString(source)), + slog.String("authSessionID", authSessionID), + ) +} + +//export godavePairwiseFingerprintCallback +func godavePairwiseFingerprintCallback(fingerprint *C.uint8_t, length C.size_t, userData unsafe.Pointer) { + h := *(*cgo.Handle)(userData) + retChan := h.Value().(chan []byte) + + // Copy the data over into Go land + // No need to free the C array, as the library will do it for us + view := unsafe.Slice((*byte)(fingerprint), length) + slice := make([]byte, length) + copy(slice, view) + + retChan <- slice } func NewSession(context string, authSessionID string) *Session { @@ -28,17 +51,21 @@ func NewSession(context string, authSessionID string) *Session { cAuthSessionID := C.CString(authSessionID) defer C.free(unsafe.Pointer(cAuthSessionID)) + authSessionIDHandler := cgo.NewHandle(authSessionID) + session := &Session{ handle: C.daveSessionCreate( unsafe.Pointer(cContext), cAuthSessionID, - C.DAVEMLSFailureCallback(unsafe.Pointer(C.libdaveGlobalFailureCallback)), + C.DAVEMLSFailureCallback(unsafe.Pointer(C.godaveGlobalFailureCallback)), + unsafe.Pointer(&authSessionIDHandler), ), } - runtime.AddCleanup(session, func(handle sessionHandle) { - C.daveSessionDestroy(handle) - }, session.handle) + runtime.SetFinalizer(session, func(s *Session) { + C.daveSessionDestroy(s.handle) + authSessionIDHandler.Delete() + }) return session } @@ -69,7 +96,7 @@ func (s *Session) GetLastEpochAuthenticator() []byte { ) C.daveSessionGetLastEpochAuthenticator(s.handle, &authenticator, &authenticatorLen) - return newCBytesMemoryView(authenticator, authenticatorLen) + return newByteSlice(authenticator, authenticatorLen) } func (s *Session) SetExternalSender(externalSender []byte) { @@ -94,7 +121,7 @@ func (s *Session) ProcessProposals(proposals []byte, recognizedUserIDs []string) &welcomeBytesLen, ) - return newCBytesMemoryView(welcomeBytes, welcomeBytesLen) + return newByteSlice(welcomeBytes, welcomeBytesLen) } func (s *Session) ProcessCommit(commit []byte) *CommitResult { @@ -121,7 +148,7 @@ func (s *Session) GetMarshalledKeyPackage() []byte { ) C.daveSessionGetMarshalledKeyPackage(s.handle, &keyPackage, &keyPackageLen) - return newCBytesMemoryView(keyPackage, keyPackageLen) + return newByteSlice(keyPackage, keyPackageLen) } func (s *Session) GetKeyRatchet(userID string) *KeyRatchet { @@ -131,20 +158,21 @@ func (s *Session) GetKeyRatchet(userID string) *KeyRatchet { return newKeyRatchet(C.daveSessionGetKeyRatchet(s.handle, cUserID)) } -// FIXME: Implement using trampoline when https://github.com/discord/libdave/issues/10 is implemented -// An alternative is to use a global cgo.Handle, but it will prevent concurrent calls to GetPairwiseFingerprint -// func (session *Session) GetPairwiseFingerprint(version uint16, userID string) []byte { -// cUserID := C.CString(userID) -// defer C.free(unsafe.Pointer(cUserID)) -// -// ch := make(chan []byte) -// callback := func(fingerprint *C.uint8_t, length C.size_t) { -// ch <- newCBytesMemoryView(fingerprint, length) -// } -// -// fHandle := cgo.NewHandle(callback) -// defer fHandle.Delete() -// C.daveSessionGetPairwiseFingerprint(session.handle, C.uint16_t(version), cUserID, (C.DAVEPairwiseFingerprintCallback)(unsafe.Pointer(fHandle))) -// -// return <-ch -// } +func (s *Session) GetPairwiseFingerprint(version uint16, userID string) []byte { + cUserID := C.CString(userID) + defer C.free(unsafe.Pointer(cUserID)) + + ch := make(chan []byte) + handler := cgo.NewHandle(ch) + defer handler.Delete() + + C.daveSessionGetPairwiseFingerprint( + s.handle, + C.uint16_t(version), + cUserID, + (C.DAVEPairwiseFingerprintCallback)(unsafe.Pointer(C.godavePairwiseFingerprintCallback)), + unsafe.Pointer(&handler), + ) + + return <-ch +} diff --git a/libdave/utils.go b/libdave/utils.go index ef126c8..e011c6d 100644 --- a/libdave/utils.go +++ b/libdave/utils.go @@ -4,7 +4,6 @@ package libdave // #include import "C" import ( - "runtime" "unsafe" ) @@ -23,36 +22,28 @@ func stringSliceToC(strings []string) (**C.char, func()) { return &cArray[0], freeFunc } -// IMPORTANT: The cArray pointer passed here should not be used after this function to prevent a use-after-free -func newCBytesMemoryView(cArray *C.uint8_t, length C.size_t) []byte { - // A bit of a hacky solution, but this allows tracking the underlying C allocated - // memory with the Go slice, and cleaning it all up when it falls out of scope - if length == 0 { - return nil - } +// IMPORTANT: This function will free the underlying C memory, so cArray becomes unsafe to use +// after this function call +func newByteSlice(cArray *C.uint8_t, length C.size_t) []byte { + view := unsafe.Slice((*byte)(cArray), length) - slice := unsafe.Slice((*byte)(cArray), length) + slice := make([]byte, length) + copy(slice, view) - runtime.AddCleanup(&slice, func(cArray *C.uint8_t) { - C.free(unsafe.Pointer(cArray)) - }, cArray) + C.free(unsafe.Pointer(cArray)) return slice } -// IMPORTANT: The cArray pointer passed here should not be used after this function to prevent a use-after-free -func newCUint64MemoryView(cArray *C.uint64_t, length C.size_t) []uint64 { - // A bit of a hacky solution, but this allows tracking the underlying C allocated - // memory with the Go slice, and cleaning it all up when it falls out of scope - if length == 0 { - return nil - } +// IMPORTANT: This function will free the underlying C memory, so cArray becomes unsafe to use +// after this function call +func newUint64Slice(cArray *C.uint64_t, length C.size_t) []uint64 { + view := unsafe.Slice((*uint64)(cArray), length) - slice := unsafe.Slice((*uint64)(cArray), length) + slice := make([]uint64, length) + copy(slice, view) - runtime.AddCleanup(&slice, func(cArray *C.uint64_t) { - C.free(unsafe.Pointer(cArray)) - }, cArray) + C.free(unsafe.Pointer(cArray)) return slice } diff --git a/libdave/welcome_result.go b/libdave/welcome_result.go index 23ee6f3..59c37e0 100644 --- a/libdave/welcome_result.go +++ b/libdave/welcome_result.go @@ -19,9 +19,9 @@ func newWelcomeResult(handle welcomeResultHandle) *WelcomeResult { handle: handle, } - runtime.AddCleanup(welcomeResult, func(handle welcomeResultHandle) { - C.daveWelcomeResultDestroy(handle) - }, welcomeResult.handle) + runtime.SetFinalizer(welcomeResult, func(s *WelcomeResult) { + C.daveWelcomeResultDestroy(s.handle) + }) return welcomeResult } @@ -33,7 +33,7 @@ func (w *WelcomeResult) GetRosterMemberIDs() []uint64 { ) C.daveWelcomeResultGetRosterMemberIds(w.handle, &rosterIDs, &rosterIDsLength) - return newCUint64MemoryView(rosterIDs, rosterIDsLength) + return newUint64Slice(rosterIDs, rosterIDsLength) } func (w *WelcomeResult) GetRosterMemberSignature(rosterID uint64) []byte { @@ -43,5 +43,5 @@ func (w *WelcomeResult) GetRosterMemberSignature(rosterID uint64) []byte { ) C.daveWelcomeResultGetRosterMemberSignature(w.handle, C.uint64_t(rosterID), &rosterMemberSignature, &rosterMemberSignatureLength) - return newCBytesMemoryView(rosterMemberSignature, rosterMemberSignatureLength) + return newByteSlice(rosterMemberSignature, rosterMemberSignatureLength) } diff --git a/scripts/libdave_install.ps1 b/scripts/libdave_install.ps1 index c16801c..731ef83 100644 --- a/scripts/libdave_install.ps1 +++ b/scripts/libdave_install.ps1 @@ -1,68 +1,174 @@ +# libdave-install.ps1 +# Usage: .\libdave-install.ps1 -Version "v0.0.1" + +[CmdletBinding(PositionalBinding=$false)] +param ( + [Parameter(Mandatory=$true, Position=0)] + [string]$Version, + [switch]$ForceBuild, + [string]$SslFlavour = "boringssl" +) + $ErrorActionPreference = "Stop" -# Check Dependencies -$requiredCmds = @("git", "make", "cmake") -foreach ($cmd in $requiredCmds) { - if (-not (Get-Command $cmd -ErrorAction SilentlyContinue)) { - Write-Error "Error: $cmd is not installed. Please install it and try again." - exit 1 +# --- Configuration --- +$RepoOwner = "discord" +$RepoName = "libdave" +$LibDaveRepo = "https://github.com/$RepoOwner/$RepoName" + +$InstallBase = Join-Path $env:LOCALAPPDATA "libdave" +$BinDir = Join-Path $InstallBase "bin" +$LibDir = Join-Path $InstallBase "lib" +$IncDir = Join-Path $InstallBase "include" +$PcDir = Join-Path $env:LOCALAPPDATA "pkgconfig" +$PcFile = Join-Path $PcDir "dave.pc" + +function Log-Info ([string]$Msg) { Write-Host "-> $Msg" -ForegroundColor Cyan } + +function Check-Dependencies { + $deps = @("git", "make", "cmake") + foreach ($cmd in $deps) { + if (-not (Get-Command $cmd -ErrorAction SilentlyContinue)) { + Write-Error "Missing dependency: $cmd. Please install it via winget or choco." + } } } -$LIBDAVE_REPO = "https://github.com/discord/libdave" -$LIBDAVE_SHA = "74979cb33febf4ddef0c2b66e57520b339550c17" +function Get-Environment { + $arch = switch ($env:PROCESSOR_ARCHITECTURE) { + "AMD64" { "X64" } + "ARM64" { "ARM64" } + Default { $_ } + } + return @{ Arch = $arch } +} + +function Install-Prebuilt { + param($Tag, $Env) + $AssetPattern = "libdave-Windows-$($Env.Arch)-$SslFlavour.zip" + $DownloadUrl = "$LibDaveRepo/releases/download/$Tag/$AssetPattern" + $TempZip = Join-Path $env:TEMP "libdave_prebuilt.zip" + + Log-Info "Checking for prebuilt asset at: $DownloadUrl" + + try { + Invoke-WebRequest -Uri $DownloadUrl -OutFile $TempZip -UseBasicParsing + } catch { + Log-Info "No prebuilt asset found. Falling back to build." + return $false + } + + Log-Info "Found prebuilt asset. Extracting..." + + if (-not (Test-Path $InstallBase)) { New-Item -ItemType Directory -Path $InstallBase } + + Expand-Archive -Path $TempZip -DestinationPath "$env:TEMP\libdave_stage" -Force + + # Copy specific files to the install directories + Remove-Item $InstallBase -Recurse + New-Item -ItemType Directory -Path $BinDir, $LibDir, $IncDir -Force | Out-Null + Copy-Item "$env:TEMP\libdave_stage\include\dave\dave.h" -Destination $IncDir -Recurse + Copy-Item "$env:TEMP\libdave_stage\bin\libdave.dll" -Destination $BinDir + Copy-Item "$env:TEMP\libdave_stage\lib\libdave.lib" -Destination $LibDir + + Remove-Item $TempZip -Force + return $true +} + +function Build-Manual { + param($Ref) + Log-Info "Starting manual build process for ref: $Ref ($SslFlavour)" + Check-Dependencies + + $WorkDir = Join-Path $env:TEMP "libdave_build_$(New-Guid)" + New-Item -ItemType Directory -Path $WorkDir | Out-Null + + git clone $LibDaveRepo $WorkDir + $CurrentDir = Get-Location + Set-Location (Join-Path $WorkDir "cpp") -$INSTALL_ROOT = Join-Path $HOME ".local" -$LIB_DIR = Join-Path $INSTALL_ROOT "lib" -$INC_DIR = Join-Path $INSTALL_ROOT "include" -$PC_DIR = Join-Path $LIB_DIR "pkgconfig" -$PC_FILE = Join-Path $PC_DIR "dave.pc" + git checkout $Ref + git submodule update --init --recursive -$TEMP_DIR = Join-Path $env:TEMP "libdave_build" -if (Test-Path $TEMP_DIR) { Remove-Item -Recurse -Force $TEMP_DIR } -New-Item -ItemType Directory -Path $TEMP_DIR | Out-Null + Log-Info "Bootstrapping vcpkg..." + .\vcpkg\bootstrap-vcpkg.bat -disableMetrics -Write-Host "-> Cloning repository" -Set-Location $TEMP_DIR -git clone $LIBDAVE_REPO libdave -Set-Location libdave/cpp -git checkout $LIBDAVE_SHA + Log-Info "Compiling shared library..." + make shared "SSL=$SslFlavour" BUILD_TYPE=Release -git submodule update --init --recursive -.\vcpkg\bootstrap-vcpkg.bat -disableMetrics + Log-Info "Installing..." -Write-Host "-> Building shared library" -make shared + Remove-Item $InstallBase -Recurse + New-Item -ItemType Directory -Path $BinDir, $LibDir, $IncDir -Force | Out-Null + Copy-Item "includes\dave\dave.h" -Destination $IncDir + Copy-Item "build\Release\libdave.dll" -Destination $BinDir + Copy-Item "build\Release\libdave.lib" -Destination $LibDir + + Set-Location $CurrentDir + Remove-Item $WorkDir -Recurse -Force +} -Write-Host "-> Installing files" -if (-not (Test-Path $LIB_DIR)) { New-Item -ItemType Directory -Path $LIB_DIR } -if (-not (Test-Path $INC_DIR)) { New-Item -ItemType Directory -Path $INC_DIR } -if (-not (Test-Path $PC_DIR)) { New-Item -ItemType Directory -Path $PC_DIR } +function Generate-PkgConfig { + Log-Info "Generating pkg-config metadata..." -Copy-Item "build\Release\dave.dll" -Destination $LIB_DIR -Copy-Item "build\Release\dave.lib" -Destination $LIB_DIR -Copy-Item "includes\dave.h" -Destination $INC_DIR + if (-not (Test-Path $PcDir)) { New-Item -ItemType Directory -Path $PcDir -Force | Out-Null } -Write-Host "-> Generating pkg-config metadata" -$PC_CONTENT = @" -prefix=$($INSTALL_ROOT.Replace('\', '/')) -exec_prefix=\${prefix} -libdir=\${prefix}/lib -includedir=\${prefix}/include + # We use forward slashes for the .pc file as many pkg-config tools + # on Windows (like those in MSYS2/Cygwin) prefer them. + $Prefix = $InstallBase.Replace('\', '/') + + # For some reason, pkgconfiglite doesnt't like variables, so always expand $Prefix for now until a fix is found + $PcContent = @" +prefix=$Prefix +exec_prefix=$Prefix/bin +libdir=$Prefix/lib +includedir=$Prefix/include Name: dave Description: Discord Audio & Video End-to-End Encryption (DAVE) Protocol -Version: $LIBDAVE_SHA -URL: $LIBDAVE_REPO -Libs: -L\${libdir} -ldave -Cflags: -I\${includedir} +Version: $Version +URL: $LibDaveRepo +Libs: -L`${libdir} -ldave +Cflags: -I`${includedir} "@ -$PC_CONTENT | Out-File -Encoding ascii $PC_FILE -Write-Host "-> Cleaning up" -Set-Location $HOME -Remove-Item -Recurse -Force $TEMP_DIR + Out-File -FilePath $PcFile -InputObject $PcContent -Encoding UTF8 + Log-Info "Created $PcFile" +} + +function Update-EnvironmentVariables { + Log-Info "Updating User PATH..." + $CurrentPath = [Environment]::GetEnvironmentVariable("Path", "User") + if ($CurrentPath -notlike "*$BinDir*") { + [Environment]::SetEnvironmentVariable("Path", "$BinDir;$CurrentPath", "User") + } -Write-Host "--- Installation Complete ---" -Write-Host "Add $LIB_DIR to your PATH environment variable." -Write-Host "Set PKG_CONFIG_PATH to $PC_DIR" \ No newline at end of file + Log-Info "Updating PKG_CONFIG_PATH..." + $CurrentPkgPath = [Environment]::GetEnvironmentVariable("PKG_CONFIG_PATH", "User") + if ($CurrentPkgPath -notlike "*$PcDir*") { + $NewPkgPath = if ([string]::IsNullOrEmpty($CurrentPkgPath)) { $PcDir } else { "$PcDir;$CurrentPkgPath" } + [Environment]::SetEnvironmentVariable("PKG_CONFIG_PATH", $NewPkgPath, "User") + } +} + + +# --- Main Logic --- +$CurrentDir = Get-Location +try { + $EnvInfo = Get-Environment + $IsSha = $Version -match "^[0-9a-fA-F]{7,40}$" + $BuildRef = if ($IsSha) { $Version } else { "$($Version.Replace('/cpp',''))/cpp" } + + if ($IsSha -or $ForceBuild) { + Build-Manual -Ref $BuildRef + } else { + $Success = Install-Prebuilt -Tag $BuildRef -Env $EnvInfo + if (-not $Success) { Build-Manual -Ref $BuildRef } + } + + Generate-PkgConfig + Update-EnvironmentVariables + Log-Info "Installation successful: libdave $Version ($($EnvInfo.Arch))" +} finally { + Set-Location $CurrentDir +} diff --git a/scripts/libdave_install.sh b/scripts/libdave_install.sh index 7d712d2..d104900 100755 --- a/scripts/libdave_install.sh +++ b/scripts/libdave_install.sh @@ -1,77 +1,131 @@ -#!/bin/bash -set -euo pipefail -IFS=$'\n\t' - -LIBDAVE_REPO=https://github.com/discord/libdave -LIBDAVE_SHA=74979cb33febf4ddef0c2b66e57520b339550c17 - -# Determine OS -if [[ "$(uname -s)" == "darwin"* ]]; then - PLATFORM=macos -else - PLATFORM=linux -fi - -# Dependencies -REQUIRED_CMDS=("git" "make" "cmake" "curl" "zip") -for cmd in "${REQUIRED_CMDS[@]}"; do - if ! command -v "$cmd" &> /dev/null; then - echo "Error: $cmd is not installed." - if [ "$PLATFORM" == "macos" ]; then - echo "Please run: brew install $cmd" - else - echo "Please install it using your package manager (apt, dnf, etc.)" - fi - exit 1 - fi -done - - -# Installation paths +#!/bin/sh +set -u + +# --- Configuration & Globals --- +REPO_OWNER="discord" +REPO_NAME="libdave" +VERSION="${1:-}" +SSL_FLAVOUR="${SSL_FLAVOUR:-boringssl}" +NON_INTERACTIVE=${NON_INTERACTIVE:-} +FORCE_BUILD=${FORCE_BUILD:-} + +LIBDAVE_REPO="https://github.com/$REPO_OWNER/$REPO_NAME" LIB_DIR="$HOME/.local/lib" +BIN_DIR="$HOME/.local/bin" INC_DIR="$HOME/.local/include" PC_DIR="$LIB_DIR/pkgconfig" PC_FILE="$PC_DIR/dave.pc" -# OS Specific extensions -if [ "$PLATFORM" == "macos" ]; then - LIB_EXT="dylib" - LIB_VAR="DYLD_LIBRARY_PATH" -else - LIB_EXT="so" - LIB_VAR="LD_LIBRARY_PATH" -fi - -echo "-> Cloning repository" -WORK_DIR=$(mktemp -d) -cd "$WORK_DIR" - -git clone "$LIBDAVE_REPO" libdave -cd libdave/cpp -git checkout "$LIBDAVE_SHA" - -git submodule update --init --recursive -./vcpkg/bootstrap-vcpkg.sh -disableMetrics - -echo "-> Building shared library for $PLATFORM" -make shared - -echo "-> Installing to $LIB_DIR" -mkdir -p "$LIB_DIR" "$INC_DIR" "$PC_DIR" - -cp includes/dave.h "$INC_DIR/" - -# Handle potential naming variations in build output -if [ -f "build/libdave.$LIB_EXT" ]; then - cp "build/libdave.$LIB_EXT" "$LIB_DIR/" -elif [ -f "build/libdave.so" ] && [ "$PLATFORM" == "macos" ]; then - cp "build/libdave.so" "$LIB_DIR/libdave.dylib" -else - cp build/libdave.* "$LIB_DIR/" 2>/dev/null || echo "Warning: Could not find build artifacts" -fi - -echo "-> Generating pkg-config metadata" -cat < "$PC_FILE" +log() { echo "-> $*"; } +error_exit() { echo "Error: $1" >&2; exit 1; } + +check_dependencies() { + deps="$1" + for cmd in $deps; do + if ! command -v "$cmd" >/dev/null 2>&1; then + error_exit "Missing dependency: $cmd." + fi + done +} + +detect_environment() { + PLATFORM=$(uname -s) + ARCH=$(uname -m) + + case "${PLATFORM}" in + Linux) + OS="linux"; LIB_EXT="so"; LIB_VAR="LD_LIBRARY_PATH"; GITHUB_OS="Linux" ;; + Darwin) + OS="macos"; LIB_EXT="dylib"; LIB_VAR="DYLD_LIBRARY_PATH"; GITHUB_OS="macOS" ;; + MSYS*|MINGW*|CYGWIN*) + OS="win"; LIB_EXT="lib"; LIB_VAR="PATH"; GITHUB_OS="Windows" ;; + *) error_exit "Unsupported OS: $PLATFORM" ;; + esac + + case "${ARCH}" in + x86_64) GITHUB_ARCH="X64" ;; + arm64|aarch64) GITHUB_ARCH="ARM64" ;; + *) GITHUB_ARCH="$ARCH" ;; + esac + + # Check if stdin is a terminal for interactivity + if [ ! -t 0 ]; then NON_INTERACTIVE=1; fi +} + +try_download_prebuilt() { + tag="$1" + asset_pattern="libdave-$GITHUB_OS-$GITHUB_ARCH-$SSL_FLAVOUR.zip" + + check_dependencies "curl unzip" + + # Construct direct download URL (GitHub standard format) + download_url="https://github.com/$REPO_OWNER/$REPO_NAME/releases/download/$tag/$asset_pattern" + + log "Checking for prebuilt asset at: $download_url" + + tmp_zip="/tmp/libdave_prebuilt.zip" + if curl -fsL "$download_url" -o "$tmp_zip"; then + log "Found prebuilt asset. Extracting..." + mkdir -p "$LIB_DIR" "$INC_DIR" + + unzip -j -o "$tmp_zip" "include/dave/dave.h" -d "$INC_DIR" + if [ "$OS" = "win" ]; then + unzip -j -o "$tmp_zip" "bin/libdave.dll" -d "$BIN_DIR" + unzip -j -o "$tmp_zip" "lib/libdave.lib" -d "$LIB_DIR" + else + unzip -j -o "$tmp_zip" "lib/libdave.$LIB_EXT" -d "$LIB_DIR" + fi + rm -f "$tmp_zip" + return 0 + else + log "No prebuilt asset found. Falling back to build." + return 1 + fi +} + +manual_build() { + checkout_ref="$1" + log "Starting manual build process for ref: $checkout_ref" + + check_dependencies "git make cmake" + + WORK_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'libdave') + + git clone "$LIBDAVE_REPO" "$WORK_DIR/libdave" + cd "$WORK_DIR/libdave/cpp" || exit 1 + + git checkout "$checkout_ref" + git submodule update --init --recursive + + if [ "$OS" = "win" ]; then + ./vcpkg/bootstrap-vcpkg.bat -disableMetrics + else + ./vcpkg/bootstrap-vcpkg.sh -disableMetrics + fi + + log "Compiling shared library..." + make shared SSL="$SSL_FLAVOUR" BUILD_TYPE=Release + + log "Installing..." + mkdir -p "$LIB_DIR" "$INC_DIR" "$PC_DIR" + cp includes/dave/dave.h "$INC_DIR/" + + if [ "$OS" = "win" ]; then + mkdir -p "$BIN_DIR" + cp "build/Release/libdave.dll" "$BIN_DIR/" + cp "build/Release/libdave.lib" "$LIB_DIR/" + else + cp "build/libdave.$LIB_EXT" "$LIB_DIR/" + fi + + # Cleanup + rm -rf "$WORK_DIR" +} + +generate_pkg_config() { + log "Generating pkg-config metadata..." + mkdir -p "$PC_DIR" + cat < "$PC_FILE" prefix=$HOME/.local exec_prefix=\${prefix} libdir=\${exec_prefix}/lib @@ -79,19 +133,85 @@ includedir=\${prefix}/include Name: dave Description: Discord Audio & Video End-to-End Encryption (DAVE) Protocol -Version: $LIBDAVE_SHA +Version: $VERSION URL: $LIBDAVE_REPO Libs: -L\${libdir} -ldave -Wl,-rpath,\${libdir} Cflags: -I\${includedir} EOF +} + +update_shell_profile() { + case "$SHELL" in + *zsh*) profile="$HOME/.zshrc" ;; + *) profile="$HOME/.bashrc" ;; + esac + + pc_line="export PKG_CONFIG_PATH=\"\$HOME/.local/lib/pkgconfig:\$PKG_CONFIG_PATH\"" + path_line="export PATH=\"\$HOME/.local/path:\$PATH\"" + + # Use grep -F for fixed string matching + needs_pc=0; + needs_path=0; + if [ -f "$profile" ]; then + grep -qF "$pc_line" "$profile" || needs_pc=1 + if [ "$OS" = "win" ]; then + grep -qF "$path_line" "$profile" || needs_path=1 + fi + else + needs_pc=1 + if [ "$OS" = "win" ]; then + needs_path=1 + fi + fi + + if [ "$needs_pc" -eq 1 ] || [ "$needs_path" -eq 1 ]; then + printf "\n--- Action Required ---\n" + printf "Add these to %s:\n" "$profile" + [ "$needs_pc" -eq 1 ] && printf " %s\n" "$pc_line" + [ "$needs_path" -eq 1 ] && printf " %s\n" "$path_line" + + if [ -z "$NON_INTERACTIVE" ]; then + printf "Apply changes automatically? (y/n): " + read -r reply + case "$reply" in + [Yy]*) + { + [ "$needs_pc" -eq 1 ] && printf "%s\n" "$pc_line" + [ "$needs_path" -eq 1 ] && printf "%s\n" "$path_line" + } >> "$profile" + log "Profile updated." + ;; + esac + fi + fi +} + +main() { + if [ -z "$VERSION" ]; then + error_exit "Usage: $0 (e.g., v0.0.1 or GIT SHA)" + fi + + detect_environment + + is_sha=$(echo "$VERSION" | grep -E "^[0-9a-fA-F]{7,40}$" >/dev/null 2>&1; echo $?) + if [ "$is_sha" -eq 0 ]; then + version="$VERSION" + else + # Normalize the version to add the '/cpp' extension if its missing + stripped_version=$(echo "$VERSION" | sed 's/\/cpp$//') + version="${stripped_version}/cpp" + fi + + if [ "$is_sha" -eq 0 ] || [ -n "$FORCE_BUILD" ]; then + manual_build "$version" + else + try_download_prebuilt "$version" || manual_build "$version" + fi + + generate_pkg_config + update_shell_profile -echo "-> Cleaning up" -rm -rf "$WORK_DIR" + log "Installation successful: libdave $VERSION ($ARCH)" +} -echo "--- Installation Complete ---" -echo "libdave revision installed: $LIBDAVE_SHA" -echo -echo "Please update your shell profile (.bashrc, .zshrc, etc) with the following lines:" -echo -echo "export PKG_CONFIG_PATH=\"$PC_DIR:\$PKG_CONFIG_PATH\"" -echo "export $LIB_VAR=\"$LIB_DIR:\$$LIB_VAR\"" +main "$@"