diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index f2a61bc..0000000 --- a/.dockerignore +++ /dev/null @@ -1,12 +0,0 @@ -.git -.github -.vscode -.gcrypt-state -gcrypt.exe -*.log -*.db -tokens.enc -config.yaml -node_modules -bin -obj diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..17daa4b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,16 @@ +# gcrypt builds on Windows and its CI lints on a Windows runner. gofmt requires +# LF line endings, but Git's autocrlf would otherwise check files out as CRLF on +# Windows, making every file fail the gofmt check. Force LF for source and text +# config files so checkouts are consistent on every platform. +*.go text eol=lf +go.mod text eol=lf +go.sum text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.json text eol=lf +*.md text eol=lf + +# Binary assets must never be line-ending normalised. +*.png binary +*.ico binary +*.icns binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db6b537..c144f44 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,98 +9,80 @@ on: jobs: lint-and-test: name: Lint and Test - runs-on: ubuntu-latest + # gcrypt is a Windows-only application: several packages import + # golang.org/x/sys/windows and the GUI uses cgo (Fyne/GLFW). It therefore + # only compiles on Windows, so linting, vetting, security scanning and tests + # all run on a Windows runner (which ships a cgo-capable gcc). + runs-on: windows-latest + env: + CGO_ENABLED: "1" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: - go-version: '1.22' + go-version-file: 'go.mod' cache: true - name: Verify dependencies run: go mod verify - name: golangci-lint - uses: golangci/golangci-lint-action@v4 + uses: golangci/golangci-lint-action@v9 with: version: latest - name: Run Gosec Security Scanner - uses: securego/gosec@master - with: - args: ./... + run: | + go install github.com/securego/gosec/v2/cmd/gosec@latest + gosec ./... - name: Run Govulncheck run: | go install golang.org/x/vuln/cmd/govulncheck@latest govulncheck ./... + - name: Run Tests + run: go test -v ./... + + secret-scan: + name: Secret Scan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Gitleaks Scan uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Run Tests - run: go test -v ./... - build-windows: name: Build Windows Binary runs-on: windows-latest needs: lint-and-test steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: - go-version: '1.22' + go-version-file: 'go.mod' cache: true - name: Build - run: go build -o gcrypt.exe ./cmd/gcrypt/main.go + # Build the whole package, not just main.go — the package is split across + # several files (console/opengl/single-instance helpers) that main.go needs. + run: go build -o gcrypt.exe ./cmd/gcrypt + env: + CGO_ENABLED: "1" - name: Upload Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: gcrypt-windows path: gcrypt.exe - - publish-docker: - name: Build and Push Docker Image - runs-on: ubuntu-latest - needs: lint-and-test - if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') - permissions: - contents: read - packages: write - steps: - - uses: actions/checkout@v4 - - - name: Log in to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=sha - type=ref,event=branch - latest - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 4973df3..8737909 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Initialize CodeQL uses: github/codeql-action/init@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 1454906..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Release - -on: - push: - tags: - - 'v*' - -permissions: - contents: write - -jobs: - goreleaser: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.22' - cache: true - - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v5 - with: - distribution: goreleaser - version: latest - args: release --clean - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.golangci.yml b/.golangci.yml index aeb5649..b89122d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,21 +1,23 @@ +# golangci-lint v2 configuration. +# `default: standard` enables errcheck, govet, ineffassign, staticcheck and +# unused (staticcheck now subsumes the old gosimple). gofmt moved to the +# separate `formatters` section in v2. +version: "2" + linters: + # gosec is intentionally NOT enabled here: it runs as its own dedicated CI step + # (securego/gosec), which honours gosec's native `//nosec` audit comments — + # golangci-lint's embedded gosec ignores those, so running it here too would + # double-report every audited line. golangci-lint covers everything else. + default: standard enable: - - gofmt - - govet - - errcheck - - staticcheck - - unused - - gosimple - - ineffassign - - typecheck - bodyclose - noctx - - gosec issues: - exclude-use-default: false max-issues-per-linter: 0 max-same-issues: 0 -run: - timeout: 5m +formatters: + enable: + - gofmt diff --git a/.goreleaser.yaml b/.goreleaser.yaml deleted file mode 100644 index adc8844..0000000 --- a/.goreleaser.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# This is a sample .goreleaser.yaml file with some sensible defaults. -# For more info, check out https://goreleaser.com -version: 2 - -before: - hooks: - - go mod tidy - -builds: - - env: - - CGO_ENABLED=0 - goos: - - linux - - windows - - darwin - goarch: - - amd64 - - arm64 - main: ./cmd/gcrypt/main.go - binary: gcrypt - ldflags: - - -s -w -H=windowsgui -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} - -archives: - - name_template: >- - {{ .ProjectName }}_ - {{- title .Os }}_ - {{- if eq .Arch "amd64" }}x86_64 - {{- else if eq .Arch "386" }}i386 - {{- else }}{{ .Arch }}{{ end }} - {{- if .Arm }}v{{ .Arm }}{{ end }} - format_overrides: - - goos: windows - format: zip - -checksum: - name_template: 'checksums.txt' - -snapshot: - name_template: "{{ incpatch .Version }}-next" - -changelog: - sort: asc - filters: - exclude: - - '^docs:' - - '^test:' diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index a5b1bb7..0000000 --- a/Dockerfile +++ /dev/null @@ -1,39 +0,0 @@ -# Multi-stage build for gcrypt -FROM golang:1.22-alpine AS builder - -# Install build dependencies -RUN apk add --no-cache git gcc musl-dev - -WORKDIR /app - -# Copy go mod and sum files -COPY go.mod go.sum ./ -RUN go mod download - -# Copy source code -COPY . . - -# Build the application -# We use CGO_ENABLED=0 for a static binary if possible, -# but systray/sqlite might need CGO on some platforms. -# For a headless Linux container, we might skip the tray. -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o gcrypt ./cmd/gcrypt/main.go - -# Final stage -FROM alpine:latest - -RUN apk add --no-cache ca-certificates tzdata - -WORKDIR /root/ - -# Copy the binary from the builder -COPY --from=builder /app/gcrypt . - -# Create a directory for the sync folder -RUN mkdir -p /data/sync - -# Set environment variables -ENV GCRYPT_CONFIG_PATH=/root/.config/gcrypt/config.yaml - -# Command to run -ENTRYPOINT ["./gcrypt"] diff --git a/cmd/gcrypt/console_windows.go b/cmd/gcrypt/console_windows.go index 0e6f9d2..b507d71 100644 --- a/cmd/gcrypt/console_windows.go +++ b/cmd/gcrypt/console_windows.go @@ -32,7 +32,7 @@ func hideConsoleIfOwned() { // If only one process is attached to this console, it's ours alone and safe // to hide. More than one means we share a parent terminal — leave it. var pids [2]uint32 - n, _, _ := getConsoleProcessList.Call(uintptr(unsafe.Pointer(&pids[0])), uintptr(len(pids))) + n, _, _ := getConsoleProcessList.Call(uintptr(unsafe.Pointer(&pids[0])), uintptr(len(pids))) // #nosec G103 -- required Win32 syscall pointer marshalling if int(n) == 1 { const swHide = 0 _, _, _ = showWindow.Call(hwnd, swHide) diff --git a/cmd/gcrypt/opengl_windows.go b/cmd/gcrypt/opengl_windows.go index a7e38c2..8a1cec8 100644 --- a/cmd/gcrypt/opengl_windows.go +++ b/cmd/gcrypt/opengl_windows.go @@ -3,6 +3,7 @@ package main import ( + "context" "io" "os" "os/exec" @@ -112,7 +113,7 @@ func ensureWorkingOpenGL(log func(string, ...map[string]interface{})) (reexeced // Re-launch: the fresh process will load the just-staged Mesa opengl32.dll at // startup and the GUI will render in software. - cmd := exec.Command(exe, os.Args[1:]...) + cmd := exec.CommandContext(context.Background(), exe, os.Args[1:]...) // #nosec G204 G702 -- re-launches this same executable with its own args cmd.Env = os.Environ() cmd.Stdout, cmd.Stderr, cmd.Stdin = os.Stdout, os.Stderr, os.Stdin if err := cmd.Start(); err != nil { @@ -143,17 +144,17 @@ func stageMesaBesideExe(mesaDir, exeDir string) error { } func copyFile(src, dst string) error { - in, err := os.Open(src) + in, err := os.Open(src) // #nosec G304 -- src is a bundled Mesa DLL path under the app dir, not user input if err != nil { return err } - defer in.Close() - out, err := os.Create(dst) + defer func() { _ = in.Close() }() + out, err := os.Create(dst) // #nosec G304 -- dst is beside the executable, an app-controlled path if err != nil { return err } if _, err := io.Copy(out, in); err != nil { - out.Close() + _ = out.Close() return err } return out.Close() @@ -245,7 +246,7 @@ func probeHardwareOpenGL() (ok bool, known bool) { const wsOverlapped = 0x00000000 hwnd, _, _ := createWindowExW.Call( 0, - uintptr(unsafe.Pointer(classStatic)), + uintptr(unsafe.Pointer(classStatic)), // #nosec G103 -- required Win32 syscall pointer marshalling 0, wsOverlapped, 0, 0, 1, 1, @@ -254,13 +255,13 @@ func probeHardwareOpenGL() (ok bool, known bool) { if hwnd == 0 { return false, false } - defer destroyWindow.Call(hwnd) + defer func() { _, _, _ = destroyWindow.Call(hwnd) }() hdc, _, _ := getDC.Call(hwnd) if hdc == 0 { return false, false } - defer releaseDC.Call(hwnd, hdc) + defer func() { _, _, _ = releaseDC.Call(hwnd, hdc) }() const ( pfdDrawToWindow = 0x00000004 @@ -276,7 +277,7 @@ func probeHardwareOpenGL() (ok bool, known bool) { cDepthBits: 24, iLayerType: 0, // PFD_MAIN_PLANE } - pf, _, _ := choosePixelFormat.Call(hdc, uintptr(unsafe.Pointer(&pfd))) + pf, _, _ := choosePixelFormat.Call(hdc, uintptr(unsafe.Pointer(&pfd))) // #nosec G103 -- required Win32 syscall pointer marshalling if pf == 0 { // No OpenGL-capable pixel format at all -> definitely inadequate. return false, true @@ -287,7 +288,7 @@ func probeHardwareOpenGL() (ok bool, known bool) { // which only supports OpenGL 1.1 — exactly what RDP exposes and far too old // for Fyne. A hardware ICD reports neither flag. var desc pixelFormatDescriptor - r, _, _ := describePixelFormat.Call(hdc, pf, unsafe.Sizeof(desc), uintptr(unsafe.Pointer(&desc))) + r, _, _ := describePixelFormat.Call(hdc, pf, unsafe.Sizeof(desc), uintptr(unsafe.Pointer(&desc))) // #nosec G103 -- required Win32 syscall pointer marshalling if r == 0 { return false, false // couldn't determine } diff --git a/cmd/gcrypt/singleinstance_windows.go b/cmd/gcrypt/singleinstance_windows.go index 0c0411e..cb52adf 100644 --- a/cmd/gcrypt/singleinstance_windows.go +++ b/cmd/gcrypt/singleinstance_windows.go @@ -8,6 +8,7 @@ import "golang.org/x/sys/windows" // process lifetime and released automatically by the OS on exit. const singleInstanceMutex = `gcrypt-single-instance-9c1f` +//nolint:unused // intentionally held for the process lifetime; the OS releases it on exit var singleInstanceHandle windows.Handle // acquireSingleInstance reports whether this process may run. It returns false diff --git a/go.mod b/go.mod index 523c7ee..cd88f79 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( golang.org/x/sys v0.46.0 golang.org/x/term v0.44.0 golang.org/x/text v0.38.0 - google.golang.org/api v0.284.0 + google.golang.org/api v0.285.0 gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.52.0 ) @@ -20,53 +20,53 @@ require ( cloud.google.com/go/auth v0.20.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect - fyne.io/systray v1.12.1 // indirect - github.com/BurntSushi/toml v1.5.0 // indirect + fyne.io/systray v1.12.2 // indirect + github.com/BurntSushi/toml v1.6.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/felixge/httpsnoop v1.1.0 // indirect github.com/fredbi/uri v1.1.1 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect github.com/fyne-io/gl-js v0.2.0 // indirect github.com/fyne-io/glfw-js v0.3.0 // indirect github.com/fyne-io/image v0.1.1 // indirect github.com/fyne-io/oksvg v0.2.0 // indirect - github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect - github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect + github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 // indirect + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20260406072232-3ac4aa2bb164 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-text/render v0.2.1 // indirect github.com/go-text/typesetting v0.3.4 // indirect - github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.16 // indirect github.com/googleapis/gax-go/v2 v2.22.0 // indirect github.com/hack-pad/go-indexeddb v0.3.2 // indirect - github.com/hack-pad/safejs v0.1.0 // indirect + github.com/hack-pad/safejs v0.1.1 // indirect github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect github.com/mattn/go-isatty v0.0.22 // indirect - github.com/mattn/go-runewidth v0.0.17 // indirect + github.com/mattn/go-runewidth v0.0.24 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect - github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect + github.com/nicksnyder/go-i18n/v2 v2.6.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/rivo/uniseg v0.2.0 // indirect github.com/rymdport/portal v0.4.2 // indirect github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect github.com/stretchr/testify v1.11.1 // indirect - github.com/yuin/goldmark v1.7.8 // indirect + github.com/yuin/goldmark v1.8.2 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect go.opentelemetry.io/otel v1.44.0 // indirect go.opentelemetry.io/otel/metric v1.44.0 // indirect go.opentelemetry.io/otel/trace v1.44.0 // indirect - golang.org/x/image v0.24.0 // indirect + golang.org/x/image v0.42.0 // indirect golang.org/x/net v0.56.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260618152121-87f3d3e198d3 // indirect google.golang.org/grpc v1.81.1 // indirect google.golang.org/protobuf v1.36.11 // indirect modernc.org/libc v1.73.4 // indirect diff --git a/go.sum b/go.sum index aba85e6..e25637e 100644 --- a/go.sum +++ b/go.sum @@ -6,12 +6,14 @@ cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdB cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= fyne.io/fyne/v2 v2.7.4 h1:OVCI5mT+Onb2kA4wlmGA5pLCqKik9f4NDb5jiR1OMTc= fyne.io/fyne/v2 v2.7.4/go.mod h1:ZD1mmhBY75mSa97IXl3MPlICd1uNHfCXYh5hKIlVOII= -fyne.io/systray v1.12.1 h1:ygBD6aZXwiOmZoY5N+ukbH9pih0Kq6fYgVeMYbr5skQ= -fyne.io/systray v1.12.1/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= -github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= -github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +fyne.io/systray v1.12.2 h1:Y8DZxgLHsVQt6rY9Zrkkg+j67S7vv/1F2viOWKPpVeA= +fyne.io/systray v1.12.2/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 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/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= 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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -22,8 +24,8 @@ github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeO github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko= github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs= github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI= github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk= @@ -32,10 +34,10 @@ github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA= github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM= github.com/fyne-io/oksvg v0.2.0 h1:mxcGU2dx6nwjJsSA9PCYZDuoAcsZ/OuJlvg/Q9Njfo8= github.com/fyne-io/oksvg v0.2.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI= -github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA= -github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 h1:IO5P06Pcj9K04d+l4nrf3c2U56+dAotIFG6u4P1wAHI= +github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20260406072232-3ac4aa2bb164 h1:ddtotcEXIT7dFhSeXIWjK8YMFYl5bB2NdYQwgv6Uqjk= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20260406072232-3ac4aa2bb164/go.mod h1:SyRD8YfuKk+ZXlDqYiqe1qMSqjNgtHzBTG810KUagMc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -47,8 +49,8 @@ github.com/go-text/typesetting v0.3.4 h1:YYurUOtEb9kGSOz4uE3k4OpBGsp1dDL8+fjCeaF github.com/go-text/typesetting v0.3.4/go.mod h1:4qZCQphq4KSgGTAeI0uMEkVbROgfah8BuyF5LRYr7XY= github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3 h1:drBZzMgdYPbmyXqOto4YhhJGrFIQCX94FpR4MzTCsos= github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o= -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/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= +github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -65,8 +67,8 @@ github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY= github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A= github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0= -github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8= -github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= +github.com/hack-pad/safejs v0.1.1 h1:d5qPO0iQ7h2oVtpzGnLExE+Wn9AtytxIfltcS2b9KD8= +github.com/hack-pad/safejs v0.1.1/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= 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/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE= @@ -79,22 +81,20 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= -github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ= -github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= +github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/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/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk= -github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ= +github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ= +github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA= github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= 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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU= @@ -105,8 +105,8 @@ github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqd github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= -github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= +github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= @@ -121,10 +121,12 @@ go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRk go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= -golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= -golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= +golang.org/x/image v0.42.0 h1:1gSs6ehNWXLbkHBIPcWztk3D/6aIA/8hauiAYtlodVY= +golang.org/x/image v0.42.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q= golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= @@ -143,14 +145,14 @@ golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/api v0.284.0 h1:i+cKTgeQRcRySkP7QTl5PDO7/pAm8EcMFIUMlNbk4Vc= -google.golang.org/api v0.284.0/go.mod h1:AU44fU+XVZOCcd8uLaBIa/ZgzgPf/0qqY3+m7lQaado= +google.golang.org/api v0.285.0 h1:B7eHHoKGAX/LrPkQvhQqnGwjgWxofbdGwCTQvpm8FkM= +google.golang.org/api v0.285.0/go.mod h1:NlOlUIr8MPoIhT9Bb/oUnRuHbJOLwxb6JSYJM8Yz+jQ= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI= google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad h1:45WmJvIV6C2+O/jjLkPUH+F3aOj/1miDoU2DD0+NWbg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260618152121-87f3d3e198d3 h1:phvBWCAQMGN1945mp5fjCXP6jEF0+a0+4TjokS4sxNY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260618152121-87f3d3e198d3/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/internal/config/config.go b/internal/config/config.go index 968871c..a700c32 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -58,9 +58,9 @@ type SyncDirection string const ( SyncDirTwoWay SyncDirection = "two_way" // full bidirectional (default) - SyncDirUploadOnly SyncDirection = "upload_only" // local → remote only - SyncDirDownloadOnly SyncDirection = "download_only" // remote → local only - SyncDirMirror SyncDirection = "mirror" // local → remote, no remote deletes + SyncDirUploadOnly SyncDirection = "upload_only" // local → remote only + SyncDirDownloadOnly SyncDirection = "download_only" // remote → local only + SyncDirMirror SyncDirection = "mirror" // local → remote, no remote deletes ) // ConflictPolicy determines how a sync conflict is resolved. @@ -83,11 +83,11 @@ type SyncPair struct { IgnorePatterns []string `yaml:"ignore_patterns"` SyncInterval int `yaml:"sync_interval"` SelectedFolders []string `yaml:"selected_folders"` - ForwardOnly bool `yaml:"forward_only"` // DEPRECATED: migrated to SyncDirection - SyncDirection SyncDirection `yaml:"sync_direction"` // data-flow policy - ConflictPolicy ConflictPolicy `yaml:"conflict_policy"` // conflict resolution strategy - OnlineOnly bool `yaml:"online_only"` // create placeholders instead of downloading - PadFilenames bool `yaml:"pad_filenames"` // pad encrypted names to hide length (set before first sync) + ForwardOnly bool `yaml:"forward_only"` // DEPRECATED: migrated to SyncDirection + SyncDirection SyncDirection `yaml:"sync_direction"` // data-flow policy + ConflictPolicy ConflictPolicy `yaml:"conflict_policy"` // conflict resolution strategy + OnlineOnly bool `yaml:"online_only"` // create placeholders instead of downloading + PadFilenames bool `yaml:"pad_filenames"` // pad encrypted names to hide length (set before first sync) } // EncryptionConfig holds encryption-related settings. @@ -161,12 +161,12 @@ func DefaultConfig() *Config { PassphraseHash: "", }, App: AppConfig{ - AutoStart: true, - LogLevel: "info", - LogPath: filepath.Join(gcryptDir, "gcrypt.log"), - LogMaxSize: 10, - LogMaxBackups: 3, - MaxFileSize: 0, + AutoStart: true, + LogLevel: "info", + LogPath: filepath.Join(gcryptDir, "gcrypt.log"), + LogMaxSize: 10, + LogMaxBackups: 3, + MaxFileSize: 0, }, } } @@ -248,7 +248,7 @@ func (c *Config) CanAddPair(localDir, driveFolderID string) error { return fmt.Errorf("local folder %q overlaps an existing sync folder %q", localDir, p.LocalDir) } if driveFolderID != "" && driveFolderID == p.DriveFolderID { - return fmt.Errorf("Drive folder %q is already used by another sync pair", driveFolderID) + return fmt.Errorf("drive folder %q is already used by another sync pair", driveFolderID) } } return nil @@ -489,7 +489,7 @@ func ConfigPath() string { // If the file contains a V1 format (no "version" key or version=1), it is // automatically migrated to V2 and saved back. func Load(path string) (*Config, error) { - data, err := os.ReadFile(path) + data, err := os.ReadFile(path) // #nosec G304 -- path is the app's config file location (flag/default), not untrusted input if err != nil { return nil, err } @@ -528,7 +528,7 @@ func migrateV1(path string, data []byte) (*Config, error) { // Create backup of the original V1 file. backupPath := path + ".v1.bak" - if err := os.WriteFile(backupPath, data, 0600); err != nil { + if err := os.WriteFile(backupPath, data, 0600); err != nil { // #nosec G703 -- backupPath is derived from the app's own config path return nil, fmt.Errorf("creating V1 backup: %w", err) } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 1a5e199..21e2dd0 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -144,7 +144,7 @@ func TestV1Migration(t *testing.T) { backupPath := configPath + ".v1.bak" // Write a V1 config file (no version key). - v1 := v1Config{ + v1 := v1Config{ // #nosec G101 -- test fixture; PassphraseHash is a dummy, not a real credential SyncDir: "/home/user/GcryptDrive", DriveFolderID: "1aBcDeFgHiJkLmNoPqRsTuVwXyZ", PassphraseHash: "argon2id$v=19$m=262144,t=4,p=4$hash", @@ -211,7 +211,7 @@ func TestV1Migration(t *testing.T) { } // Verify the file on disk is now V2. - raw, err := os.ReadFile(configPath) + raw, err := os.ReadFile(configPath) // #nosec G304 -- configPath is a test temp file if err != nil { t.Fatalf("read migrated config: %v", err) } diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go index 9d793b0..752666c 100644 --- a/internal/crypto/crypto.go +++ b/internal/crypto/crypto.go @@ -474,7 +474,7 @@ func LockMemory(b []byte) error { return nil } err := windows.VirtualLock( - uintptr(unsafe.Pointer(&b[0])), + uintptr(unsafe.Pointer(&b[0])), // #nosec G103 -- required Win32 syscall pointer marshalling uintptr(len(b)), ) if err != nil { @@ -490,7 +490,7 @@ func UnlockMemory(b []byte) error { return nil } err := windows.VirtualUnlock( - uintptr(unsafe.Pointer(&b[0])), + uintptr(unsafe.Pointer(&b[0])), // #nosec G103 -- required Win32 syscall pointer marshalling uintptr(len(b)), ) if err != nil { diff --git a/internal/crypto/filename.go b/internal/crypto/filename.go index e9fa1ca..a24d106 100644 --- a/internal/crypto/filename.go +++ b/internal/crypto/filename.go @@ -110,7 +110,7 @@ func encryptFilename(plaintext string, masterKey []byte, pad bool) (string, erro return "", fmt.Errorf("crypto: filename too long to pad (%d bytes, max %d)", len(nb), 0xFFFF) } padded := make([]byte, roundUpFilename(2+len(nb))) - binary.LittleEndian.PutUint16(padded[0:2], uint16(len(nb))) + binary.LittleEndian.PutUint16(padded[0:2], uint16(len(nb))) // #nosec G115 -- len(nb) is bounded to <= 0xFFFF by the guard above copy(padded[2:], nb) payload = padded } diff --git a/internal/crypto/strength.go b/internal/crypto/strength.go index 238c174..f4bbbc1 100644 --- a/internal/crypto/strength.go +++ b/internal/crypto/strength.go @@ -16,12 +16,12 @@ const MinPassphraseLength = 12 // the length bar but offer little protection. It is not exhaustive — it just // catches the most common mistakes. var commonWeakPassphrases = map[string]struct{}{ - "password1234": {}, + "password1234": {}, "passwordpassword": {}, - "123456789012": {}, - "qwertyuiopas": {}, - "letmeinletmein": {}, - "gcryptgcrypt": {}, + "123456789012": {}, + "qwertyuiopas": {}, + "letmeinletmein": {}, + "gcryptgcrypt": {}, } // CheckPassphraseStrength validates a candidate master passphrase and returns a diff --git a/internal/drive/auth.go b/internal/drive/auth.go index 250cf57..febfbf2 100644 --- a/internal/drive/auth.go +++ b/internal/drive/auth.go @@ -74,7 +74,8 @@ func GetTokenFromWebBrowser(config *oauth2.Config) (*oauth2.Token, error) { openBrowser(authURL) // Listen on port 8089 and wait for the callback. - listener, err := net.Listen("tcp", "localhost:8089") + var lc net.ListenConfig + listener, err := lc.Listen(context.Background(), "tcp", "localhost:8089") if err != nil { return nil, fmt.Errorf("drive: failed to listen on localhost:8089: %w", err) } @@ -82,7 +83,9 @@ func GetTokenFromWebBrowser(config *oauth2.Config) (*oauth2.Token, error) { codeCh := make(chan string, 1) errCh := make(chan error, 1) - srv := &http.Server{} + // ReadHeaderTimeout is set in the struct literal (not assigned afterwards) so + // static analysers can see this server is not vulnerable to Slowloris. + srv := &http.Server{ReadHeaderTimeout: 10 * time.Second} srv.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/callback" { http.NotFound(w, r) @@ -107,8 +110,6 @@ func GetTokenFromWebBrowser(config *oauth2.Config) (*oauth2.Token, error) { codeCh <- code }) - srv.ReadHeaderTimeout = 10 * time.Second - go func() { if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed { errCh <- fmt.Errorf("drive: callback server error: %w", err) @@ -147,14 +148,17 @@ func GetTokenFromWebBrowser(config *oauth2.Config) (*oauth2.Token, error) { // implementation varies by OS; on Windows it uses rundll32, on Darwin it uses // the open command, and on Linux/other it uses xdg-open. func openBrowser(url string) { + // The command names are fixed constants and the only variable argument is the + // OAuth authorization URL this package just built — not attacker input — so + // the G204 "subprocess from variable" warnings are audited and suppressed. var cmd *exec.Cmd switch runtime.GOOS { case "windows": - cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url) + cmd = exec.CommandContext(context.Background(), "rundll32", "url.dll,FileProtocolHandler", url) // #nosec G204 -- fixed command; url is our own OAuth URL case "darwin": - cmd = exec.Command("open", url) + cmd = exec.CommandContext(context.Background(), "open", url) // #nosec G204 -- fixed command; url is our own OAuth URL default: - cmd = exec.Command("xdg-open", url) + cmd = exec.CommandContext(context.Background(), "xdg-open", url) // #nosec G204 -- fixed command; url is our own OAuth URL } // Best-effort: ignore errors if the browser cannot be opened. _ = cmd.Run() @@ -165,7 +169,10 @@ func openBrowser(url string) { // JSON-marshalled and then encrypted using crypto.EncryptBytes with the path // "gcrypt://oauth-token" as AAD. func SaveToken(path string, token *oauth2.Token, masterKey []byte) error { - data, err := json.Marshal(token) + // Marshalling the token deliberately includes the access/refresh tokens — the + // JSON is immediately encrypted with the master key below and never written in + // the clear, so the G117 "secret in marshalled struct" warning is by design. + data, err := json.Marshal(token) // #nosec G117 -- token JSON is encrypted before being persisted if err != nil { return fmt.Errorf("drive: failed to marshal token: %w", err) } @@ -192,7 +199,7 @@ func SaveToken(path string, token *oauth2.Token, masterKey []byte) error { // master key using crypto.DecryptBytes with the path "gcrypt://oauth-token" // as AAD, and unmarshals the JSON into an *oauth2.Token. func LoadToken(path string, masterKey []byte) (*oauth2.Token, error) { - data, err := os.ReadFile(path) + data, err := os.ReadFile(path) // #nosec G304 -- path is the app's own token file location, not user input if err != nil { return nil, fmt.Errorf("drive: failed to read token file: %w", err) } diff --git a/internal/drive/client.go b/internal/drive/client.go index 315b3cf..3daf5a0 100644 --- a/internal/drive/client.go +++ b/internal/drive/client.go @@ -281,7 +281,7 @@ func (c *Client) UploadFile(ctx context.Context, name string, parentID string, c func (c *Client) DownloadFile(ctx context.Context, fileID string) (io.ReadCloser, error) { resp, err := c.svc.Files.Get(fileID). Context(ctx). - Download() + Download() //nolint:bodyclose // resp.Body is wrapped and returned to the caller, which closes it if err != nil { return nil, wrapAPIError(err) } diff --git a/internal/drive/store.go b/internal/drive/store.go index 76396a1..bb29033 100644 --- a/internal/drive/store.go +++ b/internal/drive/store.go @@ -1,6 +1,7 @@ package drive import ( + "context" "database/sql" "fmt" "log/slog" @@ -69,6 +70,11 @@ CREATE INDEX IF NOT EXISTS idx_sync_map_root_status ON sync_map(sync_root_id, sy // defaultRootID is used as the sync root ID during V1→V2 migration. If empty, // a new UUID is generated. For fresh databases the parameter is unused. func NewStore(dbPath string, defaultRootID string) (*Store, error) { + // One-shot startup work (schema apply + V1→V2 migration) runs on a background + // context: there is no caller request to tie it to, and it must not be + // cancellable mid-migration or the database could be left half-converted. + ctx := context.Background() + db, err := openStore(dbPath) if err != nil { // The database could not be opened or failed its integrity check. The @@ -88,7 +94,7 @@ func NewStore(dbPath string, defaultRootID string) (*Store, error) { } // Apply V2 schema (CREATE IF NOT EXISTS is idempotent for new databases). - if _, err := db.Exec(schemaV2); err != nil { + if _, err := db.ExecContext(ctx, schemaV2); err != nil { _ = db.Close() return nil, fmt.Errorf("store: failed to apply schema: %w", err) } @@ -96,10 +102,10 @@ func NewStore(dbPath string, defaultRootID string) (*Store, error) { // Check if migration from V1 is needed. var needsMigration bool var count int - err = db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='sync_roots'").Scan(&count) + err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='sync_roots'").Scan(&count) if err == nil && count == 0 { // sync_roots doesn't exist — check if old sync_map exists (V1 schema). - err = db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='sync_map'").Scan(&count) + err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='sync_map'").Scan(&count) if err == nil && count > 0 { needsMigration = true } @@ -109,7 +115,7 @@ func NewStore(dbPath string, defaultRootID string) (*Store, error) { if defaultRootID == "" { defaultRootID = uuid.New().String() } - if err := migrateV1ToV2(db, defaultRootID); err != nil { + if err := migrateV1ToV2(ctx, db, defaultRootID); err != nil { _ = db.Close() return nil, fmt.Errorf("store: migrating database: %w", err) } @@ -121,15 +127,15 @@ func NewStore(dbPath string, defaultRootID string) (*Store, error) { // migrateV1ToV2 migrates a V1 database (single PK on local_path) to the V2 // schema (composite PK on sync_root_id, local_path). The migration runs in a // single transaction so it is atomic. -func migrateV1ToV2(db *sql.DB, defaultRootID string) error { - tx, err := db.Begin() +func migrateV1ToV2(ctx context.Context, db *sql.DB, defaultRootID string) error { + tx, err := db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("begin migration transaction: %w", err) } defer func() { _ = tx.Rollback() }() // no-op if committed // 1. Create schema_version and sync_roots tables (idempotent). - if _, err := tx.Exec(` + if _, err := tx.ExecContext(ctx, ` CREATE TABLE IF NOT EXISTS schema_version ( version INTEGER PRIMARY KEY, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP @@ -146,7 +152,7 @@ func migrateV1ToV2(db *sql.DB, defaultRootID string) error { } // 2. Create new sync_map_v2 table with composite PK. - if _, err := tx.Exec(` + if _, err := tx.ExecContext(ctx, ` CREATE TABLE IF NOT EXISTS sync_map_v2 ( sync_root_id TEXT NOT NULL, local_path TEXT NOT NULL, @@ -170,7 +176,7 @@ func migrateV1ToV2(db *sql.DB, defaultRootID string) error { } // 3. Copy all existing rows from sync_map to sync_map_v2, adding the default sync_root_id. - if _, err := tx.Exec(` + if _, err := tx.ExecContext(ctx, ` INSERT INTO sync_map_v2 (sync_root_id, local_path, remote_id, local_hash, remote_hash, version, encrypted_dek, dek_nonce, content_nonce, @@ -184,17 +190,17 @@ func migrateV1ToV2(db *sql.DB, defaultRootID string) error { } // 4. Drop old sync_map table. - if _, err := tx.Exec(`DROP TABLE sync_map`); err != nil { + if _, err := tx.ExecContext(ctx, `DROP TABLE sync_map`); err != nil { return fmt.Errorf("drop old sync_map: %w", err) } // 5. Rename sync_map_v2 to sync_map. - if _, err := tx.Exec(`ALTER TABLE sync_map_v2 RENAME TO sync_map`); err != nil { + if _, err := tx.ExecContext(ctx, `ALTER TABLE sync_map_v2 RENAME TO sync_map`); err != nil { return fmt.Errorf("rename sync_map_v2: %w", err) } // 6. Recreate indexes. - if _, err := tx.Exec(` + if _, err := tx.ExecContext(ctx, ` CREATE INDEX IF NOT EXISTS idx_sync_map_remote_id ON sync_map(remote_id); CREATE INDEX IF NOT EXISTS idx_sync_map_status ON sync_map(sync_status); CREATE INDEX IF NOT EXISTS idx_sync_map_root_status ON sync_map(sync_root_id, sync_status); @@ -206,7 +212,7 @@ func migrateV1ToV2(db *sql.DB, defaultRootID string) error { // We don't know the local_dir or drive_folder_id from the DB alone, // so we insert placeholder values. The caller should update them // via UpsertSyncRoot after migration. - if _, err := tx.Exec(` + if _, err := tx.ExecContext(ctx, ` INSERT OR IGNORE INTO sync_roots (id, local_dir, drive_folder_id) VALUES (?, '', ''); `, defaultRootID); err != nil { @@ -214,7 +220,7 @@ func migrateV1ToV2(db *sql.DB, defaultRootID string) error { } // 8. Insert schema version 2. - if _, err := tx.Exec(` + if _, err := tx.ExecContext(ctx, ` INSERT OR IGNORE INTO schema_version (version) VALUES (2); `); err != nil { return fmt.Errorf("insert schema version: %w", err) @@ -233,6 +239,10 @@ func migrateV1ToV2(db *sql.DB, defaultRootID string) error { // then quarantine the file and retry with a fresh one). A new/empty database // passes the check. func openStore(dbPath string) (*sql.DB, error) { + // Connection setup is one-shot startup work with no caller request to bind + // to, so it uses a background context. + ctx := context.Background() + db, err := sql.Open("sqlite", dbPath) if err != nil { return nil, fmt.Errorf("open: %w", err) @@ -242,7 +252,7 @@ func openStore(dbPath string) (*sql.DB, error) { "PRAGMA journal_mode = WAL", "PRAGMA synchronous = NORMAL", } { - if _, err := db.Exec(pragma); err != nil { + if _, err := db.ExecContext(ctx, pragma); err != nil { _ = db.Close() return nil, fmt.Errorf("pragma %q: %w", pragma, err) } @@ -250,7 +260,7 @@ func openStore(dbPath string) (*sql.DB, error) { // quick_check is far cheaper than integrity_check and still catches the // structural corruption that would otherwise crash queries at runtime. var res string - if err := db.QueryRow("PRAGMA quick_check").Scan(&res); err != nil { + if err := db.QueryRowContext(ctx, "PRAGMA quick_check").Scan(&res); err != nil { _ = db.Close() return nil, fmt.Errorf("integrity check: %w", err) } @@ -294,7 +304,7 @@ func (s *Store) Close() error { // GetSyncFile looks up sync metadata by sync root ID and local file path. // Returns sql.ErrNoRows if no matching row exists. -func (s *Store) GetSyncFile(syncRootID, localPath string) (*models.SyncFile, error) { +func (s *Store) GetSyncFile(ctx context.Context, syncRootID, localPath string) (*models.SyncFile, error) { s.mu.Lock() defer s.mu.Unlock() @@ -306,7 +316,7 @@ func (s *Store) GetSyncFile(syncRootID, localPath string) (*models.SyncFile, err WHERE sync_root_id = ? AND local_path = ? ` - row := s.db.QueryRow(query, syncRootID, localPath) + row := s.db.QueryRowContext(ctx, query, syncRootID, localPath) sf, err := scanSyncFile(row) if err != nil { return nil, fmt.Errorf("store: get sync file by path: %w", err) @@ -316,7 +326,7 @@ func (s *Store) GetSyncFile(syncRootID, localPath string) (*models.SyncFile, err // GetSyncFileByRemoteID looks up sync metadata by sync root ID and the remote // Google Drive file ID. Returns sql.ErrNoRows if no matching row exists. -func (s *Store) GetSyncFileByRemoteID(syncRootID, remoteID string) (*models.SyncFile, error) { +func (s *Store) GetSyncFileByRemoteID(ctx context.Context, syncRootID, remoteID string) (*models.SyncFile, error) { s.mu.Lock() defer s.mu.Unlock() @@ -328,7 +338,7 @@ func (s *Store) GetSyncFileByRemoteID(syncRootID, remoteID string) (*models.Sync WHERE sync_root_id = ? AND remote_id = ? ` - row := s.db.QueryRow(query, syncRootID, remoteID) + row := s.db.QueryRowContext(ctx, query, syncRootID, remoteID) sf, err := scanSyncFile(row) if err != nil { return nil, fmt.Errorf("store: get sync file by remote ID: %w", err) @@ -338,7 +348,7 @@ func (s *Store) GetSyncFileByRemoteID(syncRootID, remoteID string) (*models.Sync // PutSyncFile inserts or replaces a sync metadata record. If a row with the // same (sync_root_id, local_path) already exists it is fully replaced. -func (s *Store) PutSyncFile(sf *models.SyncFile) error { +func (s *Store) PutSyncFile(ctx context.Context, sf *models.SyncFile) error { s.mu.Lock() defer s.mu.Unlock() @@ -350,7 +360,7 @@ func (s *Store) PutSyncFile(sf *models.SyncFile) error { VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ` - _, err := s.db.Exec(query, + _, err := s.db.ExecContext(ctx, query, sf.SyncRootID, sf.LocalPath, sf.RemoteID, @@ -373,7 +383,7 @@ func (s *Store) PutSyncFile(sf *models.SyncFile) error { // DeleteSyncFile removes the sync metadata row for the given sync root and // local path. -func (s *Store) DeleteSyncFile(syncRootID, localPath string) error { +func (s *Store) DeleteSyncFile(ctx context.Context, syncRootID, localPath string) error { s.mu.Lock() defer s.mu.Unlock() @@ -383,7 +393,7 @@ func (s *Store) DeleteSyncFile(syncRootID, localPath string) error { // that was never tracked (e.g. an ignored or never-synced file) can still // generate a delete operation, and that should be a no-op rather than an // error that retries forever. - if _, err := s.db.Exec(query, syncRootID, localPath); err != nil { + if _, err := s.db.ExecContext(ctx, query, syncRootID, localPath); err != nil { return fmt.Errorf("store: delete sync file: %w", err) } @@ -392,7 +402,7 @@ func (s *Store) DeleteSyncFile(syncRootID, localPath string) error { // ListPending returns all tracked files in the given sync root whose // sync_status is not "synced". -func (s *Store) ListPending(syncRootID string) ([]*models.SyncFile, error) { +func (s *Store) ListPending(ctx context.Context, syncRootID string) ([]*models.SyncFile, error) { s.mu.Lock() defer s.mu.Unlock() @@ -404,7 +414,7 @@ func (s *Store) ListPending(syncRootID string) ([]*models.SyncFile, error) { WHERE sync_root_id = ? AND sync_status != 'synced' ` - rows, err := s.db.Query(query, syncRootID) + rows, err := s.db.QueryContext(ctx, query, syncRootID) if err != nil { return nil, fmt.Errorf("store: list pending: %w", err) } @@ -414,7 +424,7 @@ func (s *Store) ListPending(syncRootID string) ([]*models.SyncFile, error) { } // ListAll returns all tracked files in the given sync root. -func (s *Store) ListAll(syncRootID string) ([]*models.SyncFile, error) { +func (s *Store) ListAll(ctx context.Context, syncRootID string) ([]*models.SyncFile, error) { s.mu.Lock() defer s.mu.Unlock() @@ -426,7 +436,7 @@ func (s *Store) ListAll(syncRootID string) ([]*models.SyncFile, error) { WHERE sync_root_id = ? ` - rows, err := s.db.Query(query, syncRootID) + rows, err := s.db.QueryContext(ctx, query, syncRootID) if err != nil { return nil, fmt.Errorf("store: list all: %w", err) } @@ -437,7 +447,7 @@ func (s *Store) ListAll(syncRootID string) ([]*models.SyncFile, error) { // UpdateStatus changes the sync_status column for the given sync root and // local path. -func (s *Store) UpdateStatus(syncRootID, localPath string, status models.SyncStatus) error { +func (s *Store) UpdateStatus(ctx context.Context, syncRootID, localPath string, status models.SyncStatus) error { s.mu.Lock() defer s.mu.Unlock() @@ -447,7 +457,7 @@ func (s *Store) UpdateStatus(syncRootID, localPath string, status models.SyncSta WHERE sync_root_id = ? AND local_path = ? ` - result, err := s.db.Exec(query, string(status), syncRootID, localPath) + result, err := s.db.ExecContext(ctx, query, string(status), syncRootID, localPath) if err != nil { return fmt.Errorf("store: update status: %w", err) } @@ -462,7 +472,7 @@ func (s *Store) UpdateStatus(syncRootID, localPath string, status models.SyncSta // UpdateRemoteInfo updates the remote metadata fields after a successful // upload: the remote file ID, the remote hash, and the file size. -func (s *Store) UpdateRemoteInfo(syncRootID, localPath, remoteID, remoteHash string, size int64) error { +func (s *Store) UpdateRemoteInfo(ctx context.Context, syncRootID, localPath, remoteID, remoteHash string, size int64) error { s.mu.Lock() defer s.mu.Unlock() @@ -472,7 +482,7 @@ func (s *Store) UpdateRemoteInfo(syncRootID, localPath, remoteID, remoteHash str WHERE sync_root_id = ? AND local_path = ? ` - result, err := s.db.Exec(query, remoteID, remoteHash, size, syncRootID, localPath) + result, err := s.db.ExecContext(ctx, query, remoteID, remoteHash, size, syncRootID, localPath) if err != nil { return fmt.Errorf("store: update remote info: %w", err) } @@ -491,7 +501,7 @@ func (s *Store) UpdateRemoteInfo(syncRootID, localPath, remoteID, remoteHash str // UpsertSyncRoot inserts a new sync root or updates an existing one (matched // by ID). -func (s *Store) UpsertSyncRoot(root *models.SyncRoot) error { +func (s *Store) UpsertSyncRoot(ctx context.Context, root *models.SyncRoot) error { s.mu.Lock() defer s.mu.Unlock() @@ -500,7 +510,7 @@ func (s *Store) UpsertSyncRoot(root *models.SyncRoot) error { VALUES (?, ?, ?, CURRENT_TIMESTAMP) ` - _, err := s.db.Exec(query, root.ID, root.LocalDir, root.DriveFolderID) + _, err := s.db.ExecContext(ctx, query, root.ID, root.LocalDir, root.DriveFolderID) if err != nil { return fmt.Errorf("store: upsert sync root: %w", err) } @@ -509,7 +519,7 @@ func (s *Store) UpsertSyncRoot(root *models.SyncRoot) error { } // GetSyncRoot retrieves a sync root by ID. Returns sql.ErrNoRows if not found. -func (s *Store) GetSyncRoot(id string) (*models.SyncRoot, error) { +func (s *Store) GetSyncRoot(ctx context.Context, id string) (*models.SyncRoot, error) { s.mu.Lock() defer s.mu.Unlock() @@ -519,7 +529,7 @@ func (s *Store) GetSyncRoot(id string) (*models.SyncRoot, error) { WHERE id = ? ` - row := s.db.QueryRow(query, id) + row := s.db.QueryRowContext(ctx, query, id) root := &models.SyncRoot{} err := row.Scan(&root.ID, &root.LocalDir, &root.DriveFolderID, &root.CreatedAt, &root.UpdatedAt) if err != nil { @@ -529,7 +539,7 @@ func (s *Store) GetSyncRoot(id string) (*models.SyncRoot, error) { } // ListSyncRoots returns all configured sync roots. -func (s *Store) ListSyncRoots() ([]*models.SyncRoot, error) { +func (s *Store) ListSyncRoots(ctx context.Context) ([]*models.SyncRoot, error) { s.mu.Lock() defer s.mu.Unlock() @@ -538,7 +548,7 @@ func (s *Store) ListSyncRoots() ([]*models.SyncRoot, error) { FROM sync_roots ` - rows, err := s.db.Query(query) + rows, err := s.db.QueryContext(ctx, query) if err != nil { return nil, fmt.Errorf("store: list sync roots: %w", err) } @@ -561,13 +571,13 @@ func (s *Store) ListSyncRoots() ([]*models.SyncRoot, error) { // DeleteSyncRoot removes a sync root by ID. Associated sync_map rows are // automatically deleted via the ON DELETE CASCADE foreign key constraint. -func (s *Store) DeleteSyncRoot(id string) error { +func (s *Store) DeleteSyncRoot(ctx context.Context, id string) error { s.mu.Lock() defer s.mu.Unlock() const query = `DELETE FROM sync_roots WHERE id = ?` - result, err := s.db.Exec(query, id) + result, err := s.db.ExecContext(ctx, query, id) if err != nil { return fmt.Errorf("store: delete sync root: %w", err) } diff --git a/internal/service/appcontroller.go b/internal/service/appcontroller.go index d64e842..3a8b6d4 100644 --- a/internal/service/appcontroller.go +++ b/internal/service/appcontroller.go @@ -247,7 +247,7 @@ func (ac *AppController) HandlePassphrase() error { saltPath := filepath.Join(appDir, "gcrypt", "salt.bin") var salt []byte - saltData, err := os.ReadFile(saltPath) + saltData, err := os.ReadFile(saltPath) // #nosec G304 -- saltPath is the app's own salt.bin under %APPDATA%, not user input if err == nil { salt = saltData if len(salt) != 16 { @@ -386,7 +386,7 @@ func (ac *AppController) TryAutoUnlock() bool { // Load the salt (kept alongside the key; needed for later crypto ops). saltPath := filepath.Join(appDataDir(), "gcrypt", "salt.bin") - salt, err := os.ReadFile(saltPath) + salt, err := os.ReadFile(saltPath) // #nosec G304 -- saltPath is the app's own salt.bin under %APPDATA%, not user input if err != nil { if ac.logger != nil { ac.logger.Warn("auto-unlock failed to read salt", map[string]interface{}{ @@ -637,7 +637,7 @@ func (ac *AppController) StartSync() error { LocalDir: pair.LocalDir, DriveFolderID: pair.DriveFolderID, } - if err := store.UpsertSyncRoot(root); err != nil { + if err := store.UpsertSyncRoot(context.Background(), root); err != nil { return fmt.Errorf("service: upsert sync root %s: %w", pair.ID, err) } } diff --git a/internal/service/backup.go b/internal/service/backup.go index 0b136d7..73d823d 100644 --- a/internal/service/backup.go +++ b/internal/service/backup.go @@ -49,13 +49,13 @@ func ExportBackup(destDir string) ([]string, error) { // copyFileSecure copies src to dst, creating dst with owner-only permissions. func copyFileSecure(src, dst string) error { - in, err := os.Open(src) + in, err := os.Open(src) // #nosec G304 -- src/dst are app-controlled config/backup paths, not user input if err != nil { return err } defer func() { _ = in.Close() }() - out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) + out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) // #nosec G304 -- src/dst are app-controlled config/backup paths, not user input if err != nil { return err } diff --git a/internal/service/dialogs_windows.go b/internal/service/dialogs_windows.go index a3ac101..b563fc1 100644 --- a/internal/service/dialogs_windows.go +++ b/internal/service/dialogs_windows.go @@ -122,7 +122,7 @@ type browseInfo struct { func messageBox(title, text string, flags uint32) int { t, _ := syscall.UTF16PtrFromString(title) m, _ := syscall.UTF16PtrFromString(text) - r, _, _ := pMessageBoxDlg.Call(0, uintptr(unsafe.Pointer(m)), uintptr(unsafe.Pointer(t)), uintptr(flags)) + r, _, _ := pMessageBoxDlg.Call(0, uintptr(unsafe.Pointer(m)), uintptr(unsafe.Pointer(t)), uintptr(flags)) // #nosec G103 -- required Win32 syscall pointer marshalling return int(r) } @@ -146,14 +146,14 @@ func pickFolder(title string) (string, bool) { }(), } - pidl, _, _ := pSHBrowseForFolder.Call(uintptr(unsafe.Pointer(&bi))) + pidl, _, _ := pSHBrowseForFolder.Call(uintptr(unsafe.Pointer(&bi))) // #nosec G103 -- required Win32 syscall pointer marshalling if pidl == 0 { return "", false } defer func() { _, _, _ = pCoTaskMemFreeDlg.Call(pidl) }() pathBuf := make([]uint16, 260) - ret, _, _ := pSHGetPathFromIDLst.Call(pidl, uintptr(unsafe.Pointer(&pathBuf[0]))) + ret, _, _ := pSHGetPathFromIDLst.Call(pidl, uintptr(unsafe.Pointer(&pathBuf[0]))) // #nosec G103 -- required Win32 syscall pointer marshalling if ret == 0 { return "", false } @@ -195,7 +195,7 @@ func registerTextDlgClass() { lpszClassName: textDlgClassName, } wc.cbSize = uint32(unsafe.Sizeof(wc)) - _, _, _ = pRegisterClassExDlg.Call(uintptr(unsafe.Pointer(&wc))) + _, _, _ = pRegisterClassExDlg.Call(uintptr(unsafe.Pointer(&wc))) // #nosec G103 -- required Win32 syscall pointer marshalling }) } @@ -235,7 +235,7 @@ func getControlText(hwnd uintptr) string { return "" } buf := make([]uint16, length+1) - _, _, _ = pGetWindowTextDlg.Call(hwnd, uintptr(unsafe.Pointer(&buf[0])), uintptr(length+1)) + _, _, _ = pGetWindowTextDlg.Call(hwnd, uintptr(unsafe.Pointer(&buf[0])), uintptr(length+1)) // #nosec G103 -- required Win32 syscall pointer marshalling return syscall.UTF16ToString(buf) } @@ -244,8 +244,8 @@ func createControl(class, text string, style uint32, x, y, w, h int, parent uint textPtr, _ := syscall.UTF16PtrFromString(text) hwnd, _, _ := pCreateWindowExDlg.Call( 0, - uintptr(unsafe.Pointer(classPtr)), - uintptr(unsafe.Pointer(textPtr)), + uintptr(unsafe.Pointer(classPtr)), // #nosec G103 -- required Win32 syscall pointer marshalling + uintptr(unsafe.Pointer(textPtr)), // #nosec G103 -- required Win32 syscall pointer marshalling uintptr(style), uintptr(x), uintptr(y), uintptr(w), uintptr(h), parent, @@ -278,8 +278,8 @@ func promptText(title, label, initial string, password bool) (string, bool) { hwnd, _, _ := pCreateWindowExDlg.Call( wsExTopmost|wsExDlgFrame, - uintptr(unsafe.Pointer(textDlgClassName)), - uintptr(unsafe.Pointer(titlePtr)), + uintptr(unsafe.Pointer(textDlgClassName)), // #nosec G103 -- required Win32 syscall pointer marshalling + uintptr(unsafe.Pointer(titlePtr)), // #nosec G103 -- required Win32 syscall pointer marshalling wsOverlapped|wsCaption|wsSysMenu, cwUseDefault, cwUseDefault, width, height, 0, 0, 0, 0, @@ -313,15 +313,15 @@ func promptText(title, label, initial string, password bool) (string, bool) { // Modal message loop. var msg msgDlg for { - r, _, _ := pGetMessageDlg.Call(uintptr(unsafe.Pointer(&msg)), 0, 0, 0) - if int32(r) <= 0 { + r, _, _ := pGetMessageDlg.Call(uintptr(unsafe.Pointer(&msg)), 0, 0, 0) // #nosec G103 -- required Win32 syscall pointer marshalling + if int32(r) <= 0 { // #nosec G115 -- GetMessage's documented return is -1/0/>0; int32 is the correct contract break } // Let the dialog manager handle Tab/Enter/Esc navigation. - handled, _, _ := pIsDialogMessageDlg.Call(hwnd, uintptr(unsafe.Pointer(&msg))) + handled, _, _ := pIsDialogMessageDlg.Call(hwnd, uintptr(unsafe.Pointer(&msg))) // #nosec G103 -- required Win32 syscall pointer marshalling if handled == 0 { - _, _, _ = pTranslateMsgDlg.Call(uintptr(unsafe.Pointer(&msg))) - _, _, _ = pDispatchMsgDlg.Call(uintptr(unsafe.Pointer(&msg))) + _, _, _ = pTranslateMsgDlg.Call(uintptr(unsafe.Pointer(&msg))) // #nosec G103 -- required Win32 syscall pointer marshalling + _, _, _ = pDispatchMsgDlg.Call(uintptr(unsafe.Pointer(&msg))) // #nosec G103 -- required Win32 syscall pointer marshalling } } diff --git a/internal/service/dpapi_windows.go b/internal/service/dpapi_windows.go index b4f07ee..167ca88 100644 --- a/internal/service/dpapi_windows.go +++ b/internal/service/dpapi_windows.go @@ -36,7 +36,7 @@ func newBlob(d []byte) dataBlob { if len(d) == 0 { return dataBlob{} } - return dataBlob{cbData: uint32(len(d)), pbData: &d[0]} + return dataBlob{cbData: uint32(len(d)), pbData: &d[0]} // #nosec G115 -- len(d) is a small in-memory secret blob, far below uint32 max } // bytes copies the blob's contents into a Go-managed slice. @@ -45,7 +45,7 @@ func (b dataBlob) bytes() []byte { return nil } out := make([]byte, b.cbData) - copy(out, unsafe.Slice(b.pbData, b.cbData)) + copy(out, unsafe.Slice(b.pbData, b.cbData)) // #nosec G103 -- required to copy the DPAPI-allocated blob into Go memory return out } @@ -55,18 +55,18 @@ func protectData(plaintext []byte) ([]byte, error) { var out dataBlob ret, _, err := procProtect.Call( - uintptr(unsafe.Pointer(&in)), - 0, // szDataDescr - 0, // pOptionalEntropy - 0, // pvReserved - 0, // pPromptStruct + uintptr(unsafe.Pointer(&in)), // #nosec G103 -- required Win32 DPAPI syscall pointer marshalling + 0, // szDataDescr + 0, // pOptionalEntropy + 0, // pvReserved + 0, // pPromptStruct cryptProtectUIForbidden, - uintptr(unsafe.Pointer(&out)), + uintptr(unsafe.Pointer(&out)), // #nosec G103 -- required Win32 DPAPI syscall pointer marshalling ) if ret == 0 { return nil, fmt.Errorf("service: CryptProtectData failed: %w", err) } - defer func() { _, _, _ = procLocalFree.Call(uintptr(unsafe.Pointer(out.pbData))) }() + defer func() { _, _, _ = procLocalFree.Call(uintptr(unsafe.Pointer(out.pbData))) }() // #nosec G103 -- required Win32 syscall pointer marshalling return out.bytes(), nil } @@ -78,18 +78,18 @@ func unprotectData(ciphertext []byte) ([]byte, error) { var out dataBlob ret, _, err := procUnprotect.Call( - uintptr(unsafe.Pointer(&in)), - 0, // ppszDataDescr - 0, // pOptionalEntropy - 0, // pvReserved - 0, // pPromptStruct + uintptr(unsafe.Pointer(&in)), // #nosec G103 -- required Win32 DPAPI syscall pointer marshalling + 0, // ppszDataDescr + 0, // pOptionalEntropy + 0, // pvReserved + 0, // pPromptStruct cryptProtectUIForbidden, - uintptr(unsafe.Pointer(&out)), + uintptr(unsafe.Pointer(&out)), // #nosec G103 -- required Win32 DPAPI syscall pointer marshalling ) if ret == 0 { return nil, fmt.Errorf("service: CryptUnprotectData failed: %w", err) } - defer func() { _, _, _ = procLocalFree.Call(uintptr(unsafe.Pointer(out.pbData))) }() + defer func() { _, _, _ = procLocalFree.Call(uintptr(unsafe.Pointer(out.pbData))) }() // #nosec G103 -- required Win32 syscall pointer marshalling return out.bytes(), nil } diff --git a/internal/service/fyneui.go b/internal/service/fyneui.go index 9b103d1..6689ed3 100644 --- a/internal/service/fyneui.go +++ b/internal/service/fyneui.go @@ -108,9 +108,9 @@ type FyneApp struct { // Header widgets. statusDot *canvas.Circle statusLabel *widget.Label - summary *widget.Label // sync folder path - metrics *widget.Label // cumulative counts + transfer rate - liveLabel *widget.Label // current in-flight file(s) + pending + summary *widget.Label // sync folder path + metrics *widget.Label // cumulative counts + transfer rate + liveLabel *widget.Label // current in-flight file(s) + pending progress *widget.ProgressBar // overall sync progress (done / total) // Account / Drive-quota widgets (Settings tab; updated by a background poll). @@ -140,8 +140,8 @@ type FyneApp struct { // Live "now transferring" panel (top of Activity tab) + its last-rendered // signature, so it's only rebuilt when the in-flight set actually changes. - liveTransfers *fyne.Container - lastLiveSig string + liveTransfers *fyne.Container + lastLiveSig string // Search/filter text for the Activity and Folders tabs (lower-cased). activityFilter string @@ -1071,7 +1071,7 @@ func (f *FyneApp) openSyncFolder() { if dir == "" { return } - if err := exec.Command("explorer", dir).Start(); err != nil { + if err := exec.CommandContext(context.Background(), "explorer", dir).Start(); err != nil { // #nosec G204 -- fixed command; dir is the app's configured sync folder fmt.Fprintf(os.Stderr, "service: open sync folder: %v\n", err) } } @@ -1089,7 +1089,7 @@ func (f *FyneApp) viewLog() { if path == "" { return } - if err := exec.Command("notepad", path).Start(); err != nil { + if err := exec.CommandContext(context.Background(), "notepad", path).Start(); err != nil { // #nosec G204 -- fixed command; path is the app's own log file fmt.Fprintf(os.Stderr, "service: view log: %v\n", err) } } diff --git a/internal/service/fyneui_settings.go b/internal/service/fyneui_settings.go index 6d3770e..23e7e85 100644 --- a/internal/service/fyneui_settings.go +++ b/internal/service/fyneui_settings.go @@ -1,6 +1,7 @@ package service import ( + "context" "fmt" "os" "os/exec" @@ -563,7 +564,7 @@ func (f *FyneApp) buildPairCard(pairID string) fyne.CanvasObject { }) openBtn := widget.NewButtonWithIcon("Open", theme.FolderOpenIcon(), func() { if p := cfg.GetSyncPair(pairID); p != nil { - if err := exec.Command("explorer", p.LocalDir).Start(); err != nil { + if err := exec.CommandContext(context.Background(), "explorer", p.LocalDir).Start(); err != nil { // #nosec G204 -- fixed command; LocalDir is the pair's configured sync folder fmt.Fprintf(os.Stderr, "service: open pair folder: %v\n", err) } } diff --git a/internal/service/logger.go b/internal/service/logger.go index fa565d3..ff805ca 100644 --- a/internal/service/logger.go +++ b/internal/service/logger.go @@ -71,7 +71,7 @@ func NewLogger(path string) (*Logger, error) { return nil, fmt.Errorf("service: create log directory: %w", err) } - f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) // #nosec G304 -- path is the app's configured log file location if err != nil { return nil, fmt.Errorf("service: open log file: %w", err) } diff --git a/internal/service/logger_test.go b/internal/service/logger_test.go index f2adbfd..05db07b 100644 --- a/internal/service/logger_test.go +++ b/internal/service/logger_test.go @@ -31,7 +31,7 @@ func TestLoggerRotationKeepsCorrectBackups(t *testing.T) { } read := func(p string) string { - b, err := os.ReadFile(p) + b, err := os.ReadFile(p) // #nosec G304 -- p is a test temp file if err != nil { t.Fatalf("read %s: %v", p, err) } @@ -73,7 +73,7 @@ func TestLoggerLevelFiltering(t *testing.T) { logger.Warn("wrn-message") logger.Error("err-message") - b, err := os.ReadFile(path) + b, err := os.ReadFile(path) // #nosec G304 -- path is a test temp file if err != nil { t.Fatalf("read log: %v", err) } diff --git a/internal/service/setup.go b/internal/service/setup.go index bad45f9..89e6c21 100644 --- a/internal/service/setup.go +++ b/internal/service/setup.go @@ -44,7 +44,7 @@ func RunSetup(ctrl *AppController, logger *Logger) error { // Fall back to a sensible default if the picker was dismissed. syncDir = filepath.Join(os.Getenv("USERPROFILE"), "gcrypt") } - if err := os.MkdirAll(syncDir, 0750); err != nil { + if err := os.MkdirAll(syncDir, 0750); err != nil { // #nosec G703 -- syncDir is user-chosen via the folder picker or a default under USERPROFILE messageBox("gcrypt setup", fmt.Sprintf("Could not create sync folder:\n%v", err), mbOK|mbIconError) return fmt.Errorf("service: creating sync dir: %w", err) } @@ -216,7 +216,7 @@ func runConnectExistingSetup(ctrl *AppController, logger *Logger) error { } // Read and validate the exported salt. - importedSalt, err := os.ReadFile(filepath.Join(srcDir, "salt.bin")) + importedSalt, err := os.ReadFile(filepath.Join(srcDir, "salt.bin")) // #nosec G304 -- srcDir is the import folder the user selected during setup if err != nil { messageBox("gcrypt setup", "Could not read salt.bin from the selected folder.\nMake sure you copied it from the other PC.", @@ -280,7 +280,7 @@ func runConnectExistingSetup(ctrl *AppController, logger *Logger) error { if err := os.MkdirAll(filepath.Dir(saltPath), 0750); err != nil { return fmt.Errorf("service: creating salt dir: %w", err) } - if err := os.WriteFile(saltPath, importedSalt, 0600); err != nil { + if err := os.WriteFile(saltPath, importedSalt, 0600); err != nil { // #nosec G703 -- saltPath is the app's own salt.bin under %APPDATA% return fmt.Errorf("service: saving salt: %w", err) } @@ -308,7 +308,7 @@ func runConnectExistingSetup(ctrl *AppController, logger *Logger) error { if !ok || strings.TrimSpace(syncDir) == "" { syncDir = filepath.Join(os.Getenv("USERPROFILE"), "gcrypt") } - if err := os.MkdirAll(syncDir, 0750); err != nil { + if err := os.MkdirAll(syncDir, 0750); err != nil { // #nosec G703 -- syncDir is user-chosen via the folder picker or a default under USERPROFILE messageBox("gcrypt setup", fmt.Sprintf("Could not create sync folder:\n%v", err), mbOK|mbIconError) return fmt.Errorf("service: creating sync dir: %w", err) } diff --git a/internal/sync/engine.go b/internal/sync/engine.go index 45e63ba..7012a4d 100644 --- a/internal/sync/engine.go +++ b/internal/sync/engine.go @@ -304,21 +304,21 @@ func NewEngine(pair *config.SyncPair, appCfg *config.AppConfig, driveClient *dri } e := &Engine{ - pair: pair, - appCfg: appCfg, - driveClient: driveClient, - store: store, - watcher: watcher, - scanner: scanner, - masterKey: masterKey, - state: StateIdle, - activeOps: make(map[int]string), - folderCache: make(map[string]string), - workQueue: make(chan *models.SyncOperation, 1000), - errorCh: make(chan error, 256), - stateChangeCh: make(chan SyncState, 64), - syncNowCh: make(chan struct{}, 1), - OnError: make(chan error, 64), + pair: pair, + appCfg: appCfg, + driveClient: driveClient, + store: store, + watcher: watcher, + scanner: scanner, + masterKey: masterKey, + state: StateIdle, + activeOps: make(map[int]string), + folderCache: make(map[string]string), + workQueue: make(chan *models.SyncOperation, 1000), + errorCh: make(chan error, 256), + stateChangeCh: make(chan SyncState, 64), + syncNowCh: make(chan struct{}, 1), + OnError: make(chan error, 64), maxRetries: 5, workers: workers, rateLimiter: time.NewTicker(time.Second / time.Duration(reqPerSec)), @@ -538,7 +538,7 @@ func (e *Engine) notifyAppStateChange(oldState, newState appstate.State) { // to the rate at which workers drain the queue, keeping memory bounded even for // very large sync roots. func (e *Engine) scanAndEnqueue() error { - previousFiles, err := e.store.ListAll(e.pair.ID) + previousFiles, err := e.store.ListAll(e.ctx, e.pair.ID) if err != nil { return fmt.Errorf("list store: %w", err) } @@ -849,7 +849,7 @@ type ErroredFile struct { // ListErrored returns the pair's tracked files currently in the error state // (max retries exhausted). Best-effort: returns nil if the store can't be read. func (e *Engine) ListErrored() []ErroredFile { - files, err := e.store.ListAll(e.pair.ID) + files, err := e.store.ListAll(e.ctx, e.pair.ID) if err != nil { return nil } @@ -867,7 +867,7 @@ func (e *Engine) ListErrored() []ErroredFile { // the number of operations re-enqueued. It may block briefly applying queue // backpressure, so callers should not hold locks across it. func (e *Engine) RetryFailed() int { - files, err := e.store.ListAll(e.pair.ID) + files, err := e.store.ListAll(e.ctx, e.pair.ID) if err != nil { return 0 } @@ -903,7 +903,7 @@ func (e *Engine) ResolveConflictAction(localPath string, action config.ConflictP return fmt.Errorf("sync: no pending conflict for %s", localPath) } - existing, err := e.store.GetSyncFile(e.pair.ID, localPath) + existing, err := e.store.GetSyncFile(e.ctx, e.pair.ID, localPath) if err != nil || existing == nil { return fmt.Errorf("sync: no store record for %s", localPath) } @@ -939,7 +939,7 @@ func (e *Engine) backupLocalConflictCopy(localPath string) { // OnlineOnlyCount returns how many online-only placeholder files this pair has // (tracked remote files that have not been downloaded). func (e *Engine) OnlineOnlyCount() int { - files, err := e.store.ListAll(e.pair.ID) + files, err := e.store.ListAll(e.ctx, e.pair.ID) if err != nil { return 0 } @@ -956,7 +956,7 @@ func (e *Engine) OnlineOnlyCount() int { // this pair, materialising them on disk. Returns the number queued. May block // briefly on queue backpressure, so callers should not hold locks across it. func (e *Engine) MakeAvailableOffline() int { - files, err := e.store.ListAll(e.pair.ID) + files, err := e.store.ListAll(e.ctx, e.pair.ID) if err != nil { return 0 } @@ -1137,7 +1137,7 @@ func (e *Engine) processEvents() { continue } // Try to preserve existing remote ID from the store. - existing, err := e.store.GetSyncFile(e.pair.ID, ev.Path) + existing, err := e.store.GetSyncFile(e.ctx, e.pair.ID, ev.Path) if err == nil && existing != nil { // If the file is already recorded as synced with this exact // content, the event carries no real change — most commonly @@ -1156,7 +1156,7 @@ func (e *Engine) processEvents() { LocalPath: ev.Path, } // Try to get the remote ID from the store. - existing, err := e.store.GetSyncFile(e.pair.ID, ev.Path) + existing, err := e.store.GetSyncFile(e.ctx, e.pair.ID, ev.Path) if err == nil && existing != nil { sf.RemoteID = existing.RemoteID } @@ -1167,7 +1167,7 @@ func (e *Engine) processEvents() { oldSF := &models.SyncFile{ LocalPath: ev.OldPath, } - existing, err := e.store.GetSyncFile(e.pair.ID, ev.OldPath) + existing, err := e.store.GetSyncFile(e.ctx, e.pair.ID, ev.OldPath) if err == nil && existing != nil { oldSF.RemoteID = existing.RemoteID } @@ -1342,7 +1342,7 @@ func (e *Engine) runWorker(id int) { // Max retries exceeded — terminal failure. Mark the file as // errored and clear it from the backlog. if op.File != nil { - _ = e.store.UpdateStatus(e.pair.ID, op.File.LocalPath, models.SyncStatusError) + _ = e.store.UpdateStatus(e.ctx, e.pair.ID, op.File.LocalPath, models.SyncStatusError) } e.sendError(fmt.Errorf("sync: max retries exceeded for %s %s: %w", op.OpType, op.File.LocalPath, err)) @@ -1487,7 +1487,7 @@ func (e *Engine) uploadFile(sf *models.SyncFile) error { errCh <- err return } - defer f.Close() + defer func() { _ = f.Close() }() err = crypto.EncryptStream(f, pw, e.masterKey, sf.LocalPath) _ = pw.CloseWithError(err) // nil → clean EOF; non-nil → reader aborts @@ -1580,7 +1580,7 @@ func (e *Engine) uploadFile(sf *models.SyncFile) error { if sf.Version == 0 { sf.Version = 1 } - if err := e.store.PutSyncFile(sf); err != nil { + if err := e.store.PutSyncFile(e.ctx, sf); err != nil { return fmt.Errorf("sync: record uploaded file %s: %w", sf.LocalPath, err) } @@ -1703,7 +1703,7 @@ func (e *Engine) downloadFile(sf *models.SyncFile) error { if sf.Version == 0 { sf.Version = 1 } - if err := e.store.PutSyncFile(sf); err != nil { + if err := e.store.PutSyncFile(e.ctx, sf); err != nil { return fmt.Errorf("sync: record downloaded file %s: %w", sf.LocalPath, err) } @@ -1752,7 +1752,7 @@ func (e *Engine) deleteRemote(sf *models.SyncFile) error { } // 2. Delete from store. - if err := e.store.DeleteSyncFile(e.pair.ID, sf.LocalPath); err != nil { + if err := e.store.DeleteSyncFile(e.ctx, e.pair.ID, sf.LocalPath); err != nil { return fmt.Errorf("sync: delete store record %s: %w", sf.LocalPath, err) } @@ -1801,7 +1801,7 @@ func (e *Engine) deleteLocal(sf *models.SyncFile) error { } // 2. Delete from store. - if err := e.store.DeleteSyncFile(e.pair.ID, sf.LocalPath); err != nil { + if err := e.store.DeleteSyncFile(e.ctx, e.pair.ID, sf.LocalPath); err != nil { return fmt.Errorf("sync: delete store record %s: %w", sf.LocalPath, err) } @@ -1873,7 +1873,7 @@ func (e *Engine) resolveConflict(sf *models.SyncFile) error { RemoteHash: sf.RemoteHash, }) // Mark as conflict in the store so the Issues view picks it up. - _ = e.store.UpdateStatus(e.pair.ID, sf.LocalPath, models.SyncStatusConflict) + _ = e.store.UpdateStatus(e.ctx, e.pair.ID, sf.LocalPath, models.SyncStatusConflict) return nil default: // auto (last-write-wins) @@ -2013,7 +2013,7 @@ func (e *Engine) pollChanges() (relevant bool, newToken string, err error) { folderIDs := e.knownFolderIDs() tracked := make(map[string]struct{}) - if files, lerr := e.store.ListAll(e.pair.ID); lerr == nil { + if files, lerr := e.store.ListAll(e.ctx, e.pair.ID); lerr == nil { for _, sf := range files { if sf.RemoteID != "" { tracked[sf.RemoteID] = struct{}{} @@ -2077,7 +2077,7 @@ func (e *Engine) reconcileRemote() error { } // Build a set of remote IDs currently tracked in the store. - trackedFiles, err := e.store.ListAll(e.pair.ID) + trackedFiles, err := e.store.ListAll(e.ctx, e.pair.ID) if err != nil { return fmt.Errorf("list store files: %w", err) } @@ -2121,7 +2121,7 @@ func (e *Engine) reconcileRemote() error { } if e.pair.OnlineOnly { sf.SyncStatus = models.SyncStatusOnlineOnly - if perr := e.store.PutSyncFile(sf); perr != nil { + if perr := e.store.PutSyncFile(e.ctx, sf); perr != nil { e.sendError(fmt.Errorf("sync: record online-only file %s: %w", sf.LocalPath, perr)) } continue @@ -2139,7 +2139,7 @@ func (e *Engine) reconcileRemote() error { tracked.RemoteHash = rf.RemoteHash tracked.Size = rf.Size tracked.ModTime = rf.ModTime - if perr := e.store.PutSyncFile(tracked); perr != nil { + if perr := e.store.PutSyncFile(e.ctx, tracked); perr != nil { e.sendError(fmt.Errorf("sync: update online-only file %s: %w", tracked.LocalPath, perr)) } continue diff --git a/internal/sync/ignore.go b/internal/sync/ignore.go index a8be603..7537cf6 100644 --- a/internal/sync/ignore.go +++ b/internal/sync/ignore.go @@ -225,7 +225,7 @@ func (im *IgnoreMatcher) matchDoubleStar(rel, pattern string, anchored, dirOnly // LoadIgnoreFile reads a .gcrypt-ignore file and returns the list of // patterns. Empty lines and lines starting with # are skipped. func LoadIgnoreFile(path string) ([]string, error) { - f, err := os.Open(path) + f, err := os.Open(path) // #nosec G304 -- path is the .gcrypt-ignore file inside the configured sync root if err != nil { return nil, fmt.Errorf("open ignore file: %w", err) } diff --git a/internal/sync/manager.go b/internal/sync/manager.go index d2f4533..87ba7bc 100644 --- a/internal/sync/manager.go +++ b/internal/sync/manager.go @@ -246,7 +246,7 @@ func (m *SyncManager) addPairLocked(pair *config.SyncPair, async bool) error { m.engines[pair.ID] = engine // Start per-engine error forwarding. - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(context.Background()) // #nosec G118 -- cancel is stored in m.engineCancels and invoked on stop/remove m.engineCancels[pair.ID] = cancel go m.forwardEngineErrors(ctx, pair.ID, engine) diff --git a/internal/sync/metered_windows.go b/internal/sync/metered_windows.go index c0f467e..cc91d3d 100644 --- a/internal/sync/metered_windows.go +++ b/internal/sync/metered_windows.go @@ -8,13 +8,6 @@ import ( "golang.org/x/sys/windows" ) -// NLM_CONNECTIVITY flags (subset). -const ( - nlmConnectivityDisconnected = 0x0000 - nlmConnectivityIPV4Internet = 0x0040 - nlmConnectivityIPV6Internet = 0x0400 -) - // NLM_CONNECTION_COST flags. const ( nlmConnectionCostUnknown = 0x0000 @@ -26,8 +19,8 @@ const ( ) var ( - modOle32 = windows.NewLazySystemDLL("ole32.dll") - procCoCreateInstance = modOle32.NewProc("CoCreateInstance") + modOle32 = windows.NewLazySystemDLL("ole32.dll") + procCoCreateInstance = modOle32.NewProc("CoCreateInstance") modNlmAPI = windows.NewLazySystemDLL("nlmapi.dll") //nolint:unused // referenced via CLSID ) @@ -77,11 +70,11 @@ func IsMeteredNetwork() bool { var pUnk unsafe.Pointer hr, _, _ = procCoCreateInstance.Call( - uintptr(unsafe.Pointer(&clsidNetworkListManager)), + uintptr(unsafe.Pointer(&clsidNetworkListManager)), // #nosec G103 -- required Win32/COM syscall pointer marshalling 0, 1|4, // CLSCTX_INPROC_SERVER|CLSCTX_LOCAL_SERVER - uintptr(unsafe.Pointer(&iidNetworkCostManager)), - uintptr(unsafe.Pointer(&pUnk)), + uintptr(unsafe.Pointer(&iidNetworkCostManager)), // #nosec G103 -- required Win32/COM syscall pointer marshalling + uintptr(unsafe.Pointer(&pUnk)), // #nosec G103 -- required Win32/COM syscall pointer marshalling ) if hr != 0 || pUnk == nil { slog.Debug("metered: CoCreateInstance failed", "hr", hr) @@ -95,7 +88,7 @@ func IsMeteredNetwork() bool { }() var cost uint32 - hr, _, _ = syscall.SyscallN(mgr.vtbl.GetCost, uintptr(pUnk), uintptr(unsafe.Pointer(&cost)), 0) + hr, _, _ = syscall.SyscallN(mgr.vtbl.GetCost, uintptr(pUnk), uintptr(unsafe.Pointer(&cost)), 0) // #nosec G103 -- required Win32/COM syscall pointer marshalling if hr != 0 { slog.Debug("metered: GetCost failed", "hr", hr) return false diff --git a/internal/sync/scanner.go b/internal/sync/scanner.go index 79e7119..19699a4 100644 --- a/internal/sync/scanner.go +++ b/internal/sync/scanner.go @@ -19,7 +19,6 @@ import ( "golang.org/x/sync/errgroup" ) - // hashCacheEntry is a cached content hash together with the file stamp it was // computed from, so the cache can be invalidated when the file changes. type hashCacheEntry struct { @@ -44,7 +43,7 @@ type persistedHashEntry struct { // against the live file stamp on use, so a stale entry can never return a wrong // hash — at worst it forces a recompute. func (s *Scanner) LoadHashCache(path string) error { - data, err := os.ReadFile(path) + data, err := os.ReadFile(path) // #nosec G304 -- path is the app's own hash-cache file under its data dir if err != nil { return err } @@ -399,7 +398,7 @@ func (s *Scanner) ComputeHash(path string) (string, error) { } } - f, err := os.Open(absPath) + f, err := os.Open(absPath) // #nosec G304 -- absPath is a file within the configured sync root being hashed if err != nil { return "", fmt.Errorf("open for hash: %w", err) } diff --git a/internal/sync/watcher.go b/internal/sync/watcher.go index b2422e1..6a807b7 100644 --- a/internal/sync/watcher.go +++ b/internal/sync/watcher.go @@ -218,8 +218,8 @@ func (w *Watcher) readDirectoryChanges(dir string) { err := windows.ReadDirectoryChanges( handle, &buf[0], - uint32(len(buf)), - true, // watch subtree + uint32(len(buf)), // #nosec G115 -- buf is a fixed small buffer; its length always fits in uint32 + true, // watch subtree notifyFilter, &bytesReturned, &overlapped, @@ -291,15 +291,12 @@ func (w *Watcher) parseNotifyBuffer(buf []byte, watchDir string) { var renameOldPath string offset := 0 - for { - if offset+12 > len(buf) { - break - } + for offset+12 <= len(buf) { // Read the fixed header fields. - nextEntryOffset := *(*uint32)(unsafe.Pointer(&buf[offset])) - action := *(*uint32)(unsafe.Pointer(&buf[offset+4])) - fileNameLength := *(*uint32)(unsafe.Pointer(&buf[offset+8])) + nextEntryOffset := *(*uint32)(unsafe.Pointer(&buf[offset])) // #nosec G103 -- parsing the FILE_NOTIFY_INFORMATION buffer returned by Win32 + action := *(*uint32)(unsafe.Pointer(&buf[offset+4])) // #nosec G103 -- parsing the FILE_NOTIFY_INFORMATION buffer returned by Win32 + fileNameLength := *(*uint32)(unsafe.Pointer(&buf[offset+8])) // #nosec G103 -- parsing the FILE_NOTIFY_INFORMATION buffer returned by Win32 if offset+12+int(fileNameLength) > len(buf) { break @@ -309,7 +306,7 @@ func (w *Watcher) parseNotifyBuffer(buf []byte, watchDir string) { fileNameBytes := buf[offset+12 : offset+12+int(fileNameLength)] fileNameUTF16 := make([]uint16, fileNameLength/2) for i := range fileNameUTF16 { - fileNameUTF16[i] = *(*uint16)(unsafe.Pointer(&fileNameBytes[i*2])) + fileNameUTF16[i] = *(*uint16)(unsafe.Pointer(&fileNameBytes[i*2])) // #nosec G103 -- decoding the UTF-16 filename from the Win32 notify buffer } fileName := string(utf16.Decode(fileNameUTF16)) @@ -470,4 +467,3 @@ func (w *Watcher) shouldIgnore(relPath string) bool { return false } - diff --git a/tools/gccwrap/main.go b/tools/gccwrap/main.go index f4f74b2..80638ed 100644 --- a/tools/gccwrap/main.go +++ b/tools/gccwrap/main.go @@ -12,6 +12,7 @@ package main import ( + "context" "os" "os/exec" "strings" @@ -43,20 +44,20 @@ func main() { if real == "" { p, err := exec.LookPath("gcc") if err != nil { - os.Stderr.WriteString("gccwrap: cannot find real gcc on PATH: " + err.Error() + "\n") + _, _ = os.Stderr.WriteString("gccwrap: cannot find real gcc on PATH: " + err.Error() + "\n") os.Exit(1) } // Guard against recursion: if the wrapper was named gcc.exe and placed on // PATH, LookPath("gcc") could resolve back to this binary. Refuse rather // than fork-bomb. if self, serr := os.Executable(); serr == nil && sameFile(self, p) { - os.Stderr.WriteString("gccwrap: refusing to invoke self (resolved gcc is this wrapper at " + p + "); set REAL_CC to the real gcc\n") + _, _ = os.Stderr.WriteString("gccwrap: refusing to invoke self (resolved gcc is this wrapper at " + p + "); set REAL_CC to the real gcc\n") os.Exit(1) } real = p } - cmd := exec.Command(real, args...) + cmd := exec.CommandContext(context.Background(), real, args...) // #nosec G204 G702 -- this wrapper exists precisely to forward args to the real gcc (REAL_CC/PATH) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -64,7 +65,7 @@ func main() { if ee, ok := err.(*exec.ExitError); ok { os.Exit(ee.ExitCode()) } - os.Stderr.WriteString("gccwrap: " + err.Error() + "\n") + _, _ = os.Stderr.WriteString("gccwrap: " + err.Error() + "\n") os.Exit(1) } }